Skip to content

cardinalby/js-eval-action

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

test build publish-context-types

Eval JS expression as a workflow step

It's a GitHub Action for evaluating small pieces of JavaScript code passed as expression input or placed to a separate file.

Can be handy for implementing easy logic, math and string manipulation instead of using bash scripts.

Also, you can make own composite actions on base of js-eval-action by creating a single action.yml file. If you want to extract JS code to a file and get code completion and type checks you can install js-eval-action-expression-context package. Look at example dir to see this approach.

Examples

Do simple math:

- id: getNextAttemptNumber
  uses: cardinalby/js-eval-action@v1
  env:
    STEP_SIZE: 2
  with:
    data: '8'
    expression: "parseInt(inputs.data) + parseInt(env.STEP_SIZE)"

# steps.getNextAttemptNumber.outputs.result == "10"

Compare the new version with the old one

- id: checkNewVersion
  uses: cardinalby/js-eval-action@v1
  env:
    OLD_VERSION: 1.2.3
    NEW_VERSION: 1.3.0
  with:
    expression: |
      ({ 
        greater: semver.gte(env.NEW_VERSION, env.OLD_VERSION), 
        compatible: semver.major(env.NEW_VERSION) === semver.major(env.OLD_VERSION)
      })
    extractOutputs: 'true'  

# steps.checkNewVersion.outputs.greater == "true"
# steps.checkNewVersion.outputs.compatible == "true"

Export env variables from .env file to a job env

- name: Export env variables
  uses: ./
  env:
    ENV_FILE: 'constants.env'
  with:
    jsFile: 'exportEnvs.js'

exportEnvs.js:

Object.entries(
    dotenv.parse(fs.readFileSync(env.ENV_FILE).toString())
).forEach(
    e => core.exportVariable(e[0], e[1])
)

Look at example dir to see the same code extracted as a composite action.

Validate dispatched_workflow inputs

- name: Validate workflow_dispatch inputs
  uses: ./
  env:
    attempt: ${{ github.event.inputs.attemptNumber }}
    max: ${{ github.event.inputs.maxAttempts }}
  with:
    expression: |
      {
        const attempt = parseInt(env.attempt), max = parseInt(env.max);
        assert(attempt && max && max >= attempt);
      } 

# Will fail if github.event.inputs are invalid

Read JSON data

- id: jsonExample
  uses: cardinalby/js-eval-action@v1
  env:
    MY_VAR: '{"a": "hello", "b": "hell"}'
  with:
    jsonEnvs: MY_VAR
    expression: "env.MY_VAR.a.indexOf(env.MY_VAR.b) !== -1"

# steps.jsonExample.outputs.result == "true"

Get default branch by octokit request

- id: getDefaultBranch
  uses: cardinalby/js-eval-action@v1
  env:
    # Required to use octokit
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 
  with:
    expression: | 
      (await octokit.rest.repos.get({
        owner: context.repo.owner, 
        repo: context.repo.repo
      })).data.default_branch

# steps.getDefaultBranch.outputs.result == "master"

For this particular case you can use a dedicated octokit/request-action.

Parse yaml

- id: readYamlExample
  uses: cardinalby/js-eval-action@v1  
  with:
    expression: 'yaml.parse((await fs.readFile("fileInRepo.yml")).toString()).myProperty'

# steps.readYamlExample.outputs.result == "property value"

More examples

Actual self-test functional examples are in .github/workflows/test.yml

Inputs

expression

JS expression that returns a value.

  • You have to set either expression input or jsFile
  • By default, (if extractOutputs input is false) the value will be serialized to string and put to result output. If extractOutputs iss true, the value has to be an object. Each property of it will be considered as a separate output and serialized to string.
  • Expression will be put to async () => %EXPRESSION% wrapper.
  • You can use await in the expression and return a Promise. When it is fulfilled, it's result will be taken.
  • You can use curly braces with return statement: { let x = 5; x += 2; return x; }
  • To return the object directly wrap it into parentheses: ({out1: 5, out2: 10});
  • Do not pass untrusted string to expression!

jsFile

Path to a file containing JS code that will be evaluated. Use instead of expression if you want to extract JavaScript code to the separate file.

  • You have to set either expression input or jsFile
  • This code is not wrapped like the expression input code, but evaluated directly instead and value is parsed according to the same rules. It's recommended to wrap it by yourself: (async () => 2 * 2)(). Replace 2 * 2 by your expression.
  • Look at js-eval-action-expression-context package for type declarations if you want to enable type checking and autocompletion in your JS file.

extractOutputs Default: false

Requires the value returned by the expression to be an object. Each property of it will be considered as a separate output and serialized to string. To return the object directly in the expression wrap it into parentheses

jsonInputs Default: empty

Parse listed inputs as JSON (if you access them via inputs.NAME in expression).

  • Format: input names separated by | sign. Example: input1|input2.
  • Use asterisk * to parse all inputs as JSON.

jsonEnvs Default: empty

Parse listed env variables as JSON (if you access them via env.NAME in expression).

  • Format: input names separated by | sign. Example: input1|input2.
  • Use asterisk * to parse all inputs as JSON.

timeoutMs Default: empty

Timeout of JS evaluation in milliseconds. No timeout if empty. If timeout reached, action fails with error and timedOut output set to true.

data Default: empty

Arbitrary data to be accessed inside JS expression. Doesn't have any other meaning.

Any other inputs

You can also set other not documented inputs and access them in JS expression using inputs.NAME, but in this case GitHub runner will produce warnings about unknown inputs. To avoid it you can:

  • Set env variables for the step and access them as env.NAME in the expression
  • Set env variables with INPUT_ prefixes. For example, INPUT_XYZ (upper case!) env variable is considered xyz input and doesn't produce a warning.

Outputs

result

If extractOutputs input is false contains a result of the expression evaluation. undefined otherwise.

timedOut

If timeoutMs input is set and execution was timed out, this output contains true. false otherwise.

Any other outputs

If extractOutputs input is true, each property of the object returned by the expression will be serialized and set as a separate output.

Output serialization

Value returned by the JS expression is serialized to string that will be set to output(s).

  • If extractOutputs input is false (default), expression result will be serialized and put to result output.
  • If extractOutputs input is true, expression result has to be an object. Each property of it will be serialized and set as a separate output.

Serialization rules:

JS value String
true true
false false
123 123
"abc" abc
undefined undefined
{a: 3, b: "c"} {"a":3,"b":"c"}
["a", "b"] ["a","b"]

JavaScript evaluation context

In the expression you can access the following objects:

inputs

Allows you to read inputs in form of inputs.inputName. Note, that input names are not case-sensitive.
Normally all inputs are of string type. But if the input you read is marked as JSON input by jsonInputs, it will be parsed (at the moment of access) and result value will be returned.

env

Allows you to read env variables in form of env.varName. Note, that env variables names are case-sensitive.
Normally all env variables are of string type. But if the env variable you read is marked as JSON input by jsonEnvs, it will be parsed (at the moment of access) and result value will be returned.

context

GitHub Actions context object.

octokit

Contains an instance of octokit if GITHUB_TOKEN env variable is set. Usage example:

(await octokit.rest.repos.get({owner: context.repo.owner, repo: context.repo.repo})).data.name

core

Contains @actions/core library.

semver

Contains semver library.

yaml

Contains yaml library.

wildstring

Contains wildstring library.

dotenv

Contains dotenv library.

dotenvExpand

Contains dotenv-expand exported function.

fs

Contains fs-extra library.

path

Contains NodeJS path module.

assert

Contains NodeJS assert module.
Note: console.assert() doesn't cause an Error in NodeJS since version 10. It's the reason to use assert(value), assert.deepStrictEqual(actual, expected), etc. instead.

buffer, Buffer

Contains NodeJS buffer module.
and Buffer constructor available separately from module.