├── .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 | --------------------------------------------------------------------------------