├── .gitattributes
├── .github
└── workflows
│ ├── bat.yml
│ └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.yml
├── README.md
├── SECURITY.md
├── action.yml
├── devel
└── contributing.md
├── jest.config.js
├── license.txt
├── package-lock.json
├── package.json
├── scripts
└── setupdeps.sh
├── src
├── index.ts
├── matlab.ts
├── matlab.unit.test.ts
├── script.ts
└── script.unit.test.ts
└── tsconfig.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | dist/* binary
2 | lib/* binary
3 |
--------------------------------------------------------------------------------
/.github/workflows/bat.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 | on: [push]
3 |
4 | jobs:
5 | bat:
6 | name: Build and Test
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions/setup-node@v4
11 | with:
12 | node-version: 20
13 | - name: Perform npm tasks
14 | run: npm run ci
15 |
16 | - name: Perform 'setup-matlab'
17 | uses: matlab-actions/setup-matlab@v2
18 |
19 | - name: Greet the world in style
20 | uses: ./
21 | with:
22 | command: "disp('hello world');"
23 |
24 | - name: Run MATLAB statement
25 | uses: ./
26 | with:
27 | command: f = fopen('myscript.m', 'w'); fwrite(f, 'assert(true)'); fclose(f);
28 |
29 | - name: Run MATLAB script
30 | uses: ./
31 | with:
32 | command: myscript
33 |
34 | - name: Run MATLAB statement with quotes 1
35 | uses: ./
36 | with:
37 | command: "eval(\"a = 1+2\"), assert(a == 3); eval('b = 3+4'), assert(b == 7);"
38 |
39 | - name: Run MATLAB statement with quotes 2
40 | uses: ./
41 | with:
42 | command: 'eval("a = 1+2"), assert(a == 3); eval(''b = 3+4''), assert(b == 7);'
43 |
44 | - name: Run MATLAB statement with quotes 3
45 | uses: ./
46 | with:
47 | command: a = """hello world""", b = '"hello world"', assert(strcmp(a,b));
48 |
49 | - name: Run MATLAB statement with symbols
50 | uses: ./
51 | with:
52 | command: a = " !""#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~", b = char([32:126]), assert(strcmp(a, b), a+b);
53 |
54 | - name: Run MATLAB statement in working directory
55 | uses: ./
56 | with:
57 | command: exp = getenv('GITHUB_WORKSPACE'), act = pwd, assert(strcmp(act, exp), strjoin({act exp}, '\n'));
58 |
59 | - name: Run MATLAB statement with arguments
60 | uses: ./
61 | with:
62 | command: disp("Hello world!!")
63 | startup-options: -nojvm -nodesktop -logfile mylog.log
64 |
65 | - name: Validate that previous command ran with arguments
66 | uses: ./
67 | with:
68 | command: assert(isfile("mylog.log"));
69 |
70 | - run: echo 'onetyone = 11' > startup.m
71 | shell: bash
72 |
73 | - name: MATLAB runs startup.m automatically
74 | uses: ./
75 | with:
76 | command: assert(onetyone==11, 'the variable `onetyone` was not set as expected by startup.m')
77 |
78 | - run: |
79 | mkdir subdir
80 | echo 'onetyonetyone = 111' > subdir/startup.m
81 | shell: bash
82 |
83 | - name: MATLAB sd startup option is not overwritten
84 | uses: ./
85 | with:
86 | command: >
87 | assert(onetyonetyone==111);
88 | [~, f] = fileparts(pwd);
89 | assert(strcmp(f, 'subdir'));
90 | startup-options: -sd subdir
91 |
92 | - name: Verify environment variables make it to MATLAB
93 | uses: ./
94 | with:
95 | command: exp = 'my_value', act = getenv('MY_VAR'), assert(strcmp(act, exp), strjoin({act exp}, '\n'));
96 | env:
97 | MY_VAR: my_value
98 |
99 | # Disabled while we work out online licensing kinks
100 | # # Remove when online batch licensing is the default
101 | # - name: Verify MW_BATCH_LICENSING_ONLINE variable set
102 | # uses: ./
103 | # with:
104 | # command: exp = 'true', act = getenv('MW_BATCH_LICENSING_ONLINE'), assert(strcmp(act, exp), strjoin({act exp}, '\n'));
105 | # env:
106 | # MY_VAR: my_value
107 |
108 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | release:
5 | types: published
6 |
7 | jobs:
8 | build:
9 | name: Build
10 | runs-on: ubuntu-latest
11 | outputs:
12 | tag: ${{ steps.update-package-version.outputs.version }}
13 | steps:
14 | # Configure runner with the right stuff
15 | - uses: actions/checkout@v4
16 | with:
17 | token: ${{ secrets.GITHUB_TOKEN }}
18 | - name: Configure git
19 | run: |
20 | git config user.name 'Release Action'
21 | git config user.email '<>'
22 | - uses: actions/setup-node@v4
23 | with:
24 | node-version: 20
25 |
26 | # Call `npm version`. It increments the version and commits the changes.
27 | # We'll save the output (new version string) for use in the following
28 | # steps
29 | - name: Update package version
30 | id: update-package-version
31 | run: |
32 | git tag -d "${{ github.event.release.tag_name }}"
33 | VERSION=$(npm version "${{ github.event.release.tag_name }}" --no-git-tag-version)
34 | echo "::set-output name=version::$VERSION"
35 | git add package.json package-lock.json
36 | git commit -m "[skip ci] Bump $VERSION"
37 | git push origin HEAD:main
38 |
39 | # Now carry on, business as usual
40 | - name: Perform npm tasks
41 | run: npm run ci
42 |
43 | # Finally, create a detached commit containing the built artifacts and tag
44 | # it with the release. Note: the fact that the branch is locally updated
45 | # will not be relayed (pushed) to origin
46 | - name: Commit to release branch
47 | id: release_info
48 | run: |
49 | # Check for semantic versioning
50 | echo "Preparing release for version $longVersion"
51 | longVersion="${{github.event.release.tag_name}}"
52 | [[ $longVersion == v[0-9]*.[0-9]*.[0-9]* ]] || (echo "must follow semantic versioning" && exit 1)
53 | majorVersion=$(echo ${longVersion%.*.*})
54 | minorVersion=$(echo ${longVersion%.*})
55 |
56 | # Add the built artifacts. Using --force because dist/lib should be in
57 | # .gitignore
58 | git add --force dist lib
59 |
60 | # Make the commit
61 | MESSAGE="Build for $(git rev-parse --short HEAD)"
62 | git commit --allow-empty -m "$MESSAGE"
63 | git tag -f -a -m "Release $longVersion" $longVersion
64 |
65 | # Get the commit of the tag you just released
66 | commitHash=$(git rev-list -n 1 $longVersion)
67 |
68 | # Delete the old major and minor version tags locally
69 | git tag -d $majorVersion || true
70 | git tag -d $minorVersion || true
71 |
72 | # Make new major and minor version tags locally that point to the commit you got from the "git rev-list" above
73 | git tag -f $majorVersion $commitHash
74 | git tag -f $minorVersion $commitHash
75 |
76 | # Force push the new minor version tag to overwrite the old tag remotely
77 | echo "Pushing new tags"
78 | git push -f origin $longVersion
79 | git push -f origin $majorVersion
80 | git push -f origin $minorVersion
81 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## For development environment
2 | .vscode
3 |
4 | # Don't include intermediate TypeScript compilation;
5 | # it should be forcibly added on release
6 | lib
7 |
8 | # Leave out dist; it should be forcibly added on release
9 | dist
10 |
11 | ## https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore
12 | # Logs
13 | logs
14 | *.log
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 | lerna-debug.log*
19 |
20 | # Diagnostic reports (https://nodejs.org/api/report.html)
21 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
22 |
23 | # Runtime data
24 | pids
25 | *.pid
26 | *.seed
27 | *.pid.lock
28 |
29 | # Directory for instrumented libs generated by jscoverage/JSCover
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 | coverage
34 | *.lcov
35 |
36 | # nyc test coverage
37 | .nyc_output
38 |
39 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
40 | .grunt
41 |
42 | # Bower dependency directory (https://bower.io/)
43 | bower_components
44 |
45 | # node-waf configuration
46 | .lock-wscript
47 |
48 | # Compiled binary addons (https://nodejs.org/api/addons.html)
49 | build/Release
50 |
51 | # Dependency directories
52 | node_modules/
53 | jspm_packages/
54 |
55 | # Snowpack dependency directory (https://snowpack.dev/)
56 | web_modules/
57 |
58 | # TypeScript cache
59 | *.tsbuildinfo
60 |
61 | # Optional npm cache directory
62 | .npm
63 |
64 | # Optional eslint cache
65 | .eslintcache
66 |
67 | # Microbundle cache
68 | .rpt2_cache/
69 | .rts2_cache_cjs/
70 | .rts2_cache_es/
71 | .rts2_cache_umd/
72 |
73 | # Optional REPL history
74 | .node_repl_history
75 |
76 | # Output of 'npm pack'
77 | *.tgz
78 |
79 | # Yarn Integrity file
80 | .yarn-integrity
81 |
82 | # dotenv environment variables file
83 | .env
84 | .env.test
85 |
86 | # parcel-bundler cache (https://parceljs.org/)
87 | .cache
88 | .parcel-cache
89 |
90 | # Next.js build output
91 | .next
92 | out
93 |
94 | # Nuxt.js build / generate output
95 | .nuxt
96 | # dist
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and not Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # Serverless directories
108 | .serverless/
109 |
110 | # FuseBox cache
111 | .fusebox/
112 |
113 | # DynamoDB Local files
114 | .dynamodb/
115 |
116 | # TernJS port file
117 | .tern-port
118 |
119 | # Stores VSCode versions used for testing VSCode extensions
120 | .vscode-test
121 |
122 | # yarn v2
123 | .yarn/cache
124 | .yarn/unplugged
125 | .yarn/build-state.yml
126 | .yarn/install-state.gz
127 | .pnp.*
128 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | ## No need to work on final dist
2 | dist
3 |
4 | ## Ignore external node_modules
5 | node_modules
6 |
7 | ## Don't include intermediate TypeScript compilation
8 | tsout
9 |
10 | ## Ignore code coverage
11 | coverage
12 |
13 | ## Editor related
14 | .vscode
15 |
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | tabWidth: 4
2 | printWidth: 100
3 | overrides:
4 | - files: "*.yml"
5 | options:
6 | tabWidth: 2
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Action for Running MATLAB Commands
2 |
3 | The [Run MATLAB Command](#run-matlab-command) action enables you to execute MATLAB® scripts, functions, and statements on a [GitHub®-hosted](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners) or [self-hosted](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners) runner:
4 | - To use a GitHub-hosted runner, include the [Setup MATLAB](https://github.com/matlab-actions/setup-matlab/) action in your workflow to set up your preferred MATLAB release (R2021a or later) on the runner.
5 | - To use a self-hosted runner, set up a computer with MATLAB on its path and register the runner with GitHub Actions. (On self-hosted UNIX® runners, you can also use the **Setup MATLAB** action instead of having MATLAB already installed.) The runner uses the topmost MATLAB release on the system path to execute your workflow.
6 |
7 | ## Examples
8 | Use the **Run MATLAB Command** action to run MATLAB scripts, functions, and statements. You can use this action to flexibly customize your test run or add a step in MATLAB to your workflow.
9 |
10 | ### Run MATLAB Script
11 | On a self-hosted runner that has MATLAB installed, run a script named `myscript.m` in the root of your repository. To run the script, specify the **Run MATLAB Command** action in your workflow.
12 |
13 | ```yaml
14 | name: Run MATLAB Script
15 | on: [push]
16 | jobs:
17 | my-job:
18 | name: Run MATLAB Script
19 | runs-on: self-hosted
20 | steps:
21 | - name: Check out repository
22 | uses: actions/checkout@v4
23 | - name: Run script
24 | uses: matlab-actions/run-command@v2
25 | with:
26 | command: myscript
27 | ```
28 |
29 | ### Run MATLAB Statements
30 | Using the latest release of MATLAB on a GitHub-hosted runner, run your MATLAB statements. To set up the latest release of MATLAB on the runner, specify the [Setup MATLAB](https://github.com/matlab-actions/setup-matlab/) action in your workflow. To run the statements, specify the **Run MATLAB Command** action.
31 |
32 | ```yaml
33 | name: Run MATLAB Statements
34 | on: [push]
35 | jobs:
36 | my-job:
37 | name: Run MATLAB Statements
38 | runs-on: ubuntu-latest
39 | steps:
40 | - name: Check out repository
41 | uses: actions/checkout@v4
42 | - name: Set up MATLAB
43 | uses: matlab-actions/setup-matlab@v2
44 | - name: Run statements
45 | uses: matlab-actions/run-command@v2
46 | with:
47 | command: results = runtests, assertSuccess(results);
48 | ```
49 |
50 |
51 | ### Use MATLAB Batch Licensing Token
52 | When you define a workflow using the [Setup MATLAB](https://github.com/matlab-actions/setup-matlab/) action, you need a [MATLAB batch licensing token](https://github.com/mathworks-ref-arch/matlab-dockerfile/blob/main/alternates/non-interactive/MATLAB-BATCH.md#matlab-batch-licensing-token) if your project is private or if your workflow uses transformation products, such as MATLAB Coder™ and MATLAB Compiler™. Batch licensing tokens are strings that enable MATLAB to start in noninteractive environments. You can request a token by submitting the [MATLAB Batch Licensing Pilot](https://www.mathworks.com/support/batch-tokens.html) form.
53 |
54 | To use a MATLAB batch licensing token:
55 |
56 | 1. Set the token as a secret. For more information about secrets, see [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).
57 | 2. Map the secret to an environment variable named `MLM_LICENSE_TOKEN` in your workflow.
58 |
59 | For example, use the latest release of MATLAB on a GitHub-hosted runner to run a script named `myscript.m` in your private project. To set up the latest release of MATLAB on the runner, specify the **Setup MATLAB** action in your workflow. To run the script, specify the **Run MATLAB Command** action. In this example, `MyToken` is the name of the secret that holds the batch licensing token.
60 |
61 | ```YAML
62 | name: Use MATLAB Batch Licensing Token
63 | on: [push]
64 | env:
65 | MLM_LICENSE_TOKEN: ${{ secrets.MyToken }}
66 | jobs:
67 | my-job:
68 | name: Run MATLAB Script in Private Project
69 | runs-on: ubuntu-latest
70 | steps:
71 | - name: Check out repository
72 | uses: actions/checkout@v4
73 | - name: Set up MATLAB
74 | uses: matlab-actions/setup-matlab@v2
75 | - name: Run script
76 | uses: matlab-actions/run-command@v2
77 | with:
78 | command: myscript
79 | ```
80 |
81 | ## Run MATLAB Command
82 | When you define your workflow in the `.github/workflows` directory of your repository, specify the **Run MATLAB Command** action as `matlab-actions/run-command@v2`. The action requires an input and also accepts an optional input.
83 |
84 | Input | Description
85 | ------------------------- | ---------------
86 | `command` |
(Required) Script, function, or statement to execute. If the value of `command` is the name of a MATLAB script or function, do not specify the file extension. If you specify more than one script, function, or statement, use a comma or semicolon to separate them.
MATLAB exits with exit code 0 if the specified script, function, or statement executes successfully without error. Otherwise, MATLAB terminates with a nonzero exit code, which causes the action to fail. To fail the action in certain conditions, use the [`assert`](https://www.mathworks.com/help/matlab/ref/assert.html) or [`error`](https://www.mathworks.com/help/matlab/ref/error.html) function.
**Example:** `command: myscript`
**Example:** `command: results = runtests, assertSuccess(results);`
87 | `startup-options` | (Optional) MATLAB startup options, specified as a list of options separated by spaces. For more information about startup options, see [Commonly Used Startup Options](https://www.mathworks.com/help/matlab/matlab_env/commonly-used-startup-options.html).
Using this input to specify the `-batch` or `-r` option is not supported.
**Example:** `startup-options: -nojvm`
**Example:** `startup-options: -nojvm -logfile output.log`
88 |
89 | When you use this action, all of the required files must be on the MATLAB search path. If your script or function is not in the root of your repository, you can use the [`addpath`](https://www.mathworks.com/help/matlab/ref/addpath.html), [`cd`](https://www.mathworks.com/help/matlab/ref/cd.html), or [`run`](https://www.mathworks.com/help/matlab/ref/run.html) function to put it on the path. For example, to run `myscript.m` in a folder named `myfolder` located in the root of the repository, you can specify `command` like this:
90 |
91 | `command: addpath("myfolder"), myscript`
92 |
93 | ## Notes
94 | * By default, when you use the **Run MATLAB Command** action, the root of your repository serves as the MATLAB startup folder. To run your MATLAB commands using a different folder, specify the `-sd` startup option or the `cd` command in the action.
95 | * In MATLAB R2019a and later, the **Run MATLAB Command** action uses the `-batch` option to start MATLAB noninteractively. Preferences do not persist across different MATLAB sessions launched with the `-batch` option. To run code that requires the same preferences, use a single action.
96 | * When you use the **Run MATLAB Command** action, you execute third-party code that is licensed under separate terms.
97 |
98 | ## See Also
99 | - [Action for Running MATLAB Builds](https://github.com/matlab-actions/run-build/)
100 | - [Action for Running MATLAB Tests](https://github.com/matlab-actions/run-tests/)
101 | - [Action for Setting Up MATLAB](https://github.com/matlab-actions/setup-matlab/)
102 | - [Continuous Integration with MATLAB and Simulink](https://www.mathworks.com/solutions/continuous-integration.html)
103 |
104 | ## Contact Us
105 | If you have any questions or suggestions, contact MathWorks® at [continuous-integration@mathworks.com](mailto:continuous-integration@mathworks.com).
106 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Reporting Security Vulnerabilities
2 |
3 | If you believe you have discovered a security vulnerability, please report it to
4 | [security@mathworks.com](mailto:security@mathworks.com). Please see
5 | [MathWorks Vulnerability Disclosure Policy for Security Researchers](https://www.mathworks.com/company/aboutus/policies_statements/vulnerability-disclosure-policy.html)
6 | for additional information.
7 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2020-2024 The MathWorks, Inc.
2 |
3 | name: Run MATLAB Command
4 | description: >-
5 | Run MATLAB scripts, functions, and statements
6 | inputs:
7 | command:
8 | description: >-
9 | Script, function, or statement to execute
10 | required: true
11 | startup-options:
12 | description: >-
13 | Startup options for MATLAB
14 | required: false
15 | default: ""
16 | runs:
17 | using: node20
18 | main: dist/index.js
19 |
--------------------------------------------------------------------------------
/devel/contributing.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | Verify changes by running tests and building locally with the following command:
4 |
5 | ```
6 | npm run ci
7 | ```
8 |
9 | ## Creating a New Release
10 |
11 | Familiarize yourself with the best practices for [releasing and maintaining GitHub actions](https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions).
12 |
13 | Changes should be made on a new branch. The new branch should be merged to the main branch via a pull request. Ensure that all of the CI pipeline checks and tests have passed for your changes.
14 |
15 | After the pull request has been approved and merged to main, follow the Github process for [creating a new release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository). The release must follow semantic versioning (ex: vX.Y.Z). This will kick off a new pipeline execution, and the action will automatically be published to the GitHub Actions Marketplace if the pipeline finishes successfully. Check the [GitHub Marketplace](https://github.com/marketplace/actions/setup-matlab) and check the major version in the repository (ex: v1 for v1.0.0) to ensure that the new semantically versioned tag is available.
16 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "ts-jest",
3 | testEnvironment: "node",
4 | testRunner: "jest-circus/runner",
5 | collectCoverage: true,
6 | };
7 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Copyright 2022 The MathWorks, Inc.
2 |
3 | Redistribution and use in source and binary forms, with or without modification,
4 | are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this
7 | list of conditions and the following disclaimer.
8 |
9 | 2. Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation and/or
11 | other materials provided with the distribution.
12 |
13 | 3. Neither the name of the copyright holder nor the names of its contributors
14 | may be used to endorse or promote products derived from this software without
15 | specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "run-matlab-command-action",
3 | "author": "The MathWorks, Inc.",
4 | "version": "2.2.1",
5 | "description": "",
6 | "main": "lib/index.js",
7 | "scripts": {
8 | "clean": "rm -rf dist lib",
9 | "format": "prettier --write .",
10 | "format-check": "prettier --check .",
11 | "build": "tsc",
12 | "deps": "bash ./scripts/setupdeps.sh",
13 | "package": "ncc build --minify",
14 | "test": "jest",
15 | "all": "npm test && npm run build && npm run package",
16 | "ci": "npm run clean && npm run deps && npm ci && npm run all"
17 | },
18 | "files": [
19 | "lib/"
20 | ],
21 | "dependencies": {
22 | "@actions/core": "^1.10.0",
23 | "@actions/exec": "^1.1.0",
24 | "uuid": "^8.3.2"
25 | },
26 | "devDependencies": {
27 | "@types/jest": "^29.1.1",
28 | "@types/node": "^20.11.16",
29 | "@types/uuid": "^8.3.0",
30 | "@vercel/ncc": "^0.38.0",
31 | "jest": "^29.1.2",
32 | "jest-circus": "^29.1.2",
33 | "prettier": "2.3.1",
34 | "ts-jest": "^29.0.3",
35 | "typescript": "^4.3.4"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/scripts/setupdeps.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | RMC_BASE_URL='https://ssd.mathworks.com/supportfiles/ci/run-matlab-command/v2'
4 | SUPPORTED_OS=('win64' 'maci64' 'maca64' 'glnxa64')
5 |
6 | # Create dist directory if it doesn't already exist
7 | DISTDIR="$(pwd)/dist/bin"
8 | mkdir -p $DISTDIR
9 |
10 | # Download and extract in a temporary directory
11 | WORKINGDIR=$(mktemp -d -t rmc_build.XXXXXX)
12 | cd $WORKINGDIR
13 |
14 | wget -O "$WORKINGDIR/license.txt" "$RMC_BASE_URL/license.txt"
15 | wget -O "$WORKINGDIR/thirdpartylicenses.txt" "$RMC_BASE_URL/thirdpartylicenses.txt"
16 |
17 | for os in ${SUPPORTED_OS[@]}
18 | do
19 | if [[ $os == 'win64' ]] ; then
20 | bin_ext='.exe'
21 | else
22 | bin_ext=''
23 | fi
24 | mkdir -p "$WORKINGDIR/$os"
25 | wget -O "$WORKINGDIR/$os/run-matlab-command$bin_ext" "$RMC_BASE_URL/$os/run-matlab-command$bin_ext"
26 | done
27 |
28 | mv -f ./* "$DISTDIR/"
29 | rm -rf $WORKINGDIR
30 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2020-2023 The MathWorks, Inc.
2 |
3 | import * as core from "@actions/core";
4 | import * as exec from "@actions/exec";
5 | import * as matlab from "./matlab";
6 |
7 | export { matlab };
8 |
9 | /**
10 | * Gather action inputs and then run action.
11 | */
12 | async function run() {
13 | const platform = process.platform;
14 | const architecture = process.arch;
15 | const workspaceDir = process.cwd();
16 | const command = core.getInput("command");
17 | const startupOpts = core.getInput("startup-options").split(" ");
18 |
19 | const helperScript = await matlab.generateScript(workspaceDir, command);
20 | const execOpts = {
21 | // env: {...process.env, MW_BATCH_LICENSING_ONLINE:'true'} // Disabled while we work out online licensing kinks
22 | };
23 | await matlab.runCommand(helperScript, platform, architecture, (cmd,args)=>exec.exec(cmd,args,execOpts), startupOpts);
24 | }
25 |
26 | // Only run this action if it is invoked directly. Do not run if this node
27 | // module is required by another action such as run-tests.
28 | if (require.main === module) {
29 | run().catch((e) => {
30 | core.setFailed(e);
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/matlab.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2020-2025 The MathWorks, Inc.
2 |
3 | import { promises as fs } from "fs";
4 | import * as os from "os";
5 | import * as path from "path";
6 | import { v4 as uuid } from "uuid";
7 | import * as script from "./script";
8 |
9 | /**
10 | * Helper interface to represent a MATLAB script.
11 | */
12 | export interface HelperScript {
13 | dir: string;
14 | command: string;
15 | }
16 |
17 | /**
18 | * Type of a function that executes a command on a runner and returns the error
19 | * code.
20 | */
21 | export type ExecFn = (command: string, args?: string[]) => Promise;
22 |
23 | /**
24 | * Generate a MATLAB script in the temporary directory that runs a command in
25 | * the workspace.
26 | *
27 | * @param workspaceDir CI job workspace directory
28 | * @param command MATLAB command to run
29 | */
30 | export async function generateScript(workspaceDir: string, command: string): Promise {
31 | const scriptName = script.safeName(`command_${uuid()}`);
32 |
33 | const temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "run_matlab_command-"));
34 |
35 | const scriptPath = path.join(temporaryDirectory, scriptName + ".m");
36 | await fs.writeFile(scriptPath, script.cdAndCall(command), {
37 | encoding: "utf8",
38 | });
39 |
40 | return {
41 | dir: temporaryDirectory,
42 | command: scriptName
43 | };
44 | }
45 |
46 | /**
47 | * Run a HelperScript in MATLAB.
48 | *
49 | * Create the HelperScript using `generateScript`.
50 | *
51 | * @param hs HelperScript pointing to the script containing the command
52 | * @param platform Operating system of the runner (e.g., "win32" or "linux")
53 | * @param architecture Architecture of the runner (e.g., "x64")
54 | * @param fn ExecFn that will execute a command on the runner
55 | */
56 | export async function runCommand(hs: HelperScript, platform: string, architecture: string, fn: ExecFn, args?: string[]): Promise {
57 | const rmcPath = getRunMATLABCommandScriptPath(platform, architecture);
58 | await fs.chmod(rmcPath, 0o777);
59 |
60 | const rmcArg = `setenv('MW_ORIG_WORKING_FOLDER',cd('${script.pathToCharVec(hs.dir)}')); ${hs.command}`;
61 |
62 | let execArgs = [rmcArg];
63 |
64 | if (args) {
65 | execArgs = execArgs.concat(args);
66 | }
67 |
68 | const exitCode = await fn(rmcPath, execArgs);
69 | if (exitCode !== 0) {
70 | return Promise.reject(Error(`Exited with non-zero code ${exitCode}`));
71 | }
72 | }
73 |
74 | /**
75 | * Get the path of the script containing RunMATLABCommand for the host OS.
76 | *
77 | * @param platform Operating system of the runner (e.g., "win32" or "linux")
78 | * @param architecture Architecture of the runner (e.g., "x64")
79 | */
80 | export function getRunMATLABCommandScriptPath(platform: string, architecture: string): string {
81 | if (architecture != "x64" && !(platform == "darwin" && architecture == "arm64")) {
82 | throw new Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`);
83 | }
84 | let ext;
85 | let platformDir;
86 | switch (platform) {
87 | case "win32":
88 | ext = ".exe";
89 | platformDir = "win64";
90 | break;
91 | case "darwin":
92 | ext = "";
93 | if (architecture == "x64") {
94 | platformDir = "maci64";
95 | } else {
96 | platformDir = "maca64";
97 | }
98 | break;
99 | case "linux":
100 | ext = "";
101 | platformDir = "glnxa64";
102 | break;
103 | default:
104 | throw new Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`);
105 | }
106 | const rmcPath = path.join(__dirname, "bin", platformDir, `run-matlab-command${ext}`);
107 | return rmcPath;
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/matlab.unit.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2020-2023 The MathWorks, Inc.
2 |
3 | import { promises as fs } from "fs";
4 | import * as path from "path";
5 | import * as matlab from "./matlab";
6 |
7 | afterEach(() => {
8 | jest.resetAllMocks();
9 | });
10 |
11 | describe("script generation", () => {
12 | const mockPath = "/tmp/run-matlab-command-ABC123";
13 | const workspaceDir = "/home/sweet/home";
14 | const command = "disp('hello, world!');";
15 |
16 | const rejection = "yeah, no";
17 |
18 | it("properly generates a script", async () => {
19 | const mkdtemp = jest.spyOn(fs, "mkdtemp");
20 | const writeFile = jest.spyOn(fs, "writeFile");
21 |
22 | mkdtemp.mockResolvedValue(mockPath);
23 | writeFile.mockResolvedValue(undefined);
24 |
25 | const actual = matlab.generateScript(workspaceDir, command);
26 | await expect(actual).resolves.toBeDefined();
27 | expect(mkdtemp).toHaveBeenCalledTimes(1);
28 | expect(writeFile).toHaveBeenCalledTimes(1);
29 | });
30 |
31 | it("fails when the temporary directory cannot be made", async () => {
32 | const mkdtemp = jest.spyOn(fs, "mkdtemp");
33 | const writeFile = jest.spyOn(fs, "writeFile");
34 |
35 | mkdtemp.mockRejectedValue(rejection);
36 | writeFile.mockRejectedValue("this should not have been called");
37 |
38 | const actual = matlab.generateScript(workspaceDir, command);
39 | await expect(actual).rejects.toBeDefined();
40 | expect(mkdtemp).toHaveBeenCalledTimes(1);
41 | expect(writeFile).not.toHaveBeenCalled();
42 | });
43 |
44 | it("fails when there's an error writing to the file", async () => {
45 | const mkdtemp = jest.spyOn(fs, "mkdtemp");
46 | const writeFile = jest.spyOn(fs, "writeFile");
47 |
48 | mkdtemp.mockResolvedValue(mockPath);
49 | writeFile.mockRejectedValue(rejection);
50 |
51 | const actual = matlab.generateScript(workspaceDir, command);
52 | await expect(actual).rejects.toBeDefined();
53 | expect(mkdtemp).toHaveBeenCalledTimes(1);
54 | expect(writeFile).toHaveBeenCalledTimes(1);
55 | });
56 | });
57 |
58 | describe("run command", () => {
59 | const helperScript = { dir: "/home/sweet/home", command: "disp('hello, world');" };
60 | const platform = "win32";
61 | const architecture = "x64"
62 |
63 | it("ideally works", async () => {
64 | const chmod = jest.spyOn(fs, "chmod");
65 | const execFn = jest.fn();
66 |
67 | chmod.mockResolvedValue(undefined);
68 | execFn.mockResolvedValue(0);
69 |
70 | const actual = matlab.runCommand(helperScript, platform, architecture, execFn);
71 | await expect(actual).resolves.toBeUndefined();
72 | });
73 |
74 | it("ideally works with arguments", async () => {
75 | const chmod = jest.spyOn(fs, "chmod");
76 | const execFn = jest.fn();
77 |
78 | chmod.mockResolvedValue(undefined);
79 | execFn.mockResolvedValue(0);
80 |
81 | const actual = matlab.runCommand(helperScript, platform, architecture, execFn, ["-nojvm", "-logfile", "file"]);
82 | await expect(actual).resolves.toBeUndefined();
83 | expect(execFn.mock.calls[0][1][1]).toBe("-nojvm");
84 | expect(execFn.mock.calls[0][1][2]).toBe("-logfile");
85 | expect(execFn.mock.calls[0][1][3]).toBe("file");
86 | });
87 |
88 | it("fails when chmod fails", async () => {
89 | const chmod = jest.spyOn(fs, "chmod");
90 | const execFn = jest.fn();
91 |
92 | chmod.mockRejectedValue(null);
93 |
94 | const actual = matlab.runCommand(helperScript, platform, architecture, execFn);
95 | await expect(actual).rejects.toBeDefined();
96 | expect(chmod).toHaveBeenCalledTimes(1);
97 | expect(execFn).not.toHaveBeenCalled();
98 | });
99 |
100 | it("fails when the execFn fails", async () => {
101 | const chmod = jest.spyOn(fs, "chmod");
102 | const execFn = jest.fn();
103 |
104 | chmod.mockResolvedValue(undefined);
105 | execFn.mockRejectedValue(null);
106 |
107 | const actual = matlab.runCommand(helperScript, platform, architecture, execFn);
108 | await expect(actual).rejects.toBeDefined();
109 | expect(chmod).toHaveBeenCalledTimes(1);
110 | expect(execFn).toHaveBeenCalledTimes(1);
111 | });
112 |
113 | it("fails when the execFn has a non-zero exit code", async () => {
114 | const chmod = jest.spyOn(fs, "chmod");
115 | const execFn = jest.fn();
116 |
117 | chmod.mockResolvedValue(undefined);
118 | execFn.mockResolvedValue(1);
119 |
120 | const actual = matlab.runCommand(helperScript, platform, architecture, execFn);
121 | await expect(actual).rejects.toBeDefined();
122 | expect(chmod).toHaveBeenCalledTimes(1);
123 | expect(execFn).toHaveBeenCalledTimes(1);
124 | });
125 | });
126 |
127 | describe("ci helper path", () => {
128 | const platform = "linux"
129 | const architecture = "x64"
130 | const testExtension = (platform: string, ext: string) => {
131 | it(`considers the appropriate script on ${platform}`, () => {
132 | const actualPath = matlab.getRunMATLABCommandScriptPath(platform, architecture);
133 | const actualExt = path.extname(actualPath);
134 | expect(actualExt).toMatch(ext);
135 | });
136 | };
137 |
138 | const testDirectory = (platform: string, architecture: string, subdirectory: string) => {
139 | it(`considers the appropriate script on ${platform}`, () => {
140 | const actualPath = matlab.getRunMATLABCommandScriptPath(platform, architecture);
141 | expect(actualPath).toContain(subdirectory);
142 | });
143 | };
144 |
145 | testExtension("win32", "exe");
146 | testExtension("darwin", "");
147 | testExtension("linux", "");
148 |
149 | testDirectory("win32", "x64", "win64");
150 | testDirectory("darwin", "x64", "maci64");
151 | testDirectory("darwin", "arm64", "maca64");
152 | testDirectory("linux", "x64", "glnxa64");
153 |
154 | it("errors on unsupported platform", () => {
155 | expect(() => matlab.getRunMATLABCommandScriptPath('sunos',architecture)).toThrow();
156 | })
157 |
158 | it("errors on unsupported architecture", () => {
159 | expect(() => matlab.getRunMATLABCommandScriptPath(platform, 'x86')).toThrow();
160 | })
161 | });
162 |
--------------------------------------------------------------------------------
/src/script.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2020-2025 The MathWorks, Inc.
2 |
3 | /**
4 | * Generate MATLAB command for changing directories and calling a command in it.
5 | *
6 | * @param dir Directory to change to.
7 | * @param command Command to run in directory.
8 | * @returns MATLAB command.
9 | */
10 | export function cdAndCall(command: string): string {
11 | return `cd(getenv('MW_ORIG_WORKING_FOLDER')); ${command}`;
12 | }
13 |
14 | /**
15 | * Convert a path-like string to MATLAB character vector literal.
16 | *
17 | * @param s Input string.
18 | * @returns Input string in MATLAB character vector literal.
19 | */
20 | export function pathToCharVec(s: string): string {
21 | return s.replace(/'/g, "''");
22 | }
23 |
24 | /**
25 | * Convert an identifier (i.e., a script name) to one that is callable by MATLAB.
26 | *
27 | * @param s Input identifier.
28 | */
29 | export function safeName(s: string): string {
30 | return s.replace(/-/g, "_");
31 | }
32 |
--------------------------------------------------------------------------------
/src/script.unit.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2020-2025 The MathWorks, Inc.
2 |
3 | import * as script from "./script";
4 |
5 | describe("call generation", () => {
6 | it("ideally works", () => {
7 | // I know what your thinking
8 | const testCommand = "disp('hello world')";
9 | const expectedString = `cd(getenv('MW_ORIG_WORKING_FOLDER')); ${testCommand}`;
10 |
11 | expect(script.cdAndCall(testCommand)).toMatch(expectedString);
12 | });
13 | });
14 |
15 | describe("safe names", () => {
16 | it("turns dashes into underscores", () => {
17 | const testString = "__this-is-a-kebab---";
18 | const expectedString = "__this_is_a_kebab___";
19 |
20 | expect(script.safeName(testString)).toMatch(expectedString);
21 | });
22 | });
23 |
24 | describe("path to character vector", () => {
25 | it("duplicates single quotes", () => {
26 | // I know what your thinking
27 | const testString = String.raw`C:\Users\you\You're Documents`;
28 | const expectedString = String.raw`C:\Users\you\You''re Documents`;
29 |
30 | expect(script.pathToCharVec(testString)).toMatch(expectedString);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "CommonJS",
5 | "declaration": true,
6 | "sourceMap": true,
7 | "rootDir": "src",
8 | "outDir": "lib",
9 | "strict": true,
10 | "noImplicitAny": true,
11 | "resolveJsonModule": true,
12 | "esModuleInterop": true
13 | },
14 |
15 | "exclude": ["node_modules", "lib", "**/*.test.ts"]
16 | }
17 |
--------------------------------------------------------------------------------