TypeScript API for GitHub Action execution and integration/functional testing
Purpose
🔶 Execute your GitHub action locally (or at any other environment).
🔶 Write integration and functional tests for an action, run them locally and on CI.
🔶 Have a short feedback loop without pushing and checking an action behaviour at real GitHub runners every time you change it.
Features
✅ Supports executing JavaScript and Docker actions.
✅ Tested under Windows, Linux and macOS (Intel + Apple Silicon), NodeJS >= 12 locally and on GitHub hosted runners.
✅ Works well with Docker Desktop under Windows and macOS.
✅ Can be used together with any JavaScript test frameworks or alone.
✅ Can execute an explicitly specified JS file or main, pre, post script from action.yml
.
✅ Can execute a separate sync or async JS function, isolating its environment (process env, exitCode and working dir), intercepting stdout and stderr output for effective dependencies mocking.
✅ Has a clear JavaScript API with TypeScript declarations and reasonable defaults
✅ Produces warnings about deprecated Actions commands
Setting up an action run option includes:
- Inputs. Can read default input values from
action.yml
- Saved state
- Custom environment variables
- GitHub context
- GitHub service environment variables
- Faking GitHub service files (file commands, event payload file)
- Faking GitHub dirs (workflow, workspace, temp)
Reading results of an action run includes:
- Reading exit code, stdout and stderr
- Reading outputs, saved state, warnings, errors, notices and secrets from intercepted stdout
- Reading exported vars, added paths from faked file commands
Installation
Install for use in tests
npm i github-action-ts-run-api --save-dev
Documentation
Other information:
Quick examples
Test JS action in a child node process
action.yml
name: 'test'
runs:
using: 'node16'
main: 'main.js'
main.js:
const core = require("@actions/core");
const context = require('@actions/github').context;
const fs = require('fs');
core.addPath('newPath');
fs.writeFileSync(
path.join(process.env.RUNNER_TEMP, 'f.txt'),
context.payload.pull_request.number.toString()
);
action.test.ts:
import {RunOptions, RunTarget} from 'github-action-ts-run-api';
const target = RunTarget.mainJsScript('action.yml');
const options = RunOptions.create()
.setGithubContext({payload: {pull_request: {number: 123}}})
.setFakeFsOptions({rmFakedTempDirAfterRun: false});
const res = await target.run(options);
try {
assert(res.commands.addedPaths === ['newPath']);
const pathOfCreatedFile = path.join(res.tempDirPath, 'f.txt');
assert(fs.readFileSync(pathOfCreatedFile).toString() === '123');
} finally {
res.cleanUpFakedDirs();
}
Test JavaScript function in isolated Action environment
main.js
const core = require("@actions/core");
export async function actionMainFn() {
core.setOutput('out1', core.getInput('in1'));
core.setOutput('out2', process.env.ENV2);
core.exportVariable('v3', core.getState('my_state'));
return new Promise(resolve => setTimeout(() => {
core.setFailed('err1');
resolve();
}, 1000));
}
main.test.ts:
import {RunOptions, RunTarget} from 'github-action-ts-run-api';
import {actionMainFn} from './main.js';
const target = RunTarget.asyncFn(actionMainFn);
const options = RunOptions.create()
.setInputs({in1: 'abc'})
.setEnv({ENV2: 'def'})
.setState({my_state: 'ghi'});
const result = await target.run(options);
assert(result.durationMs >= 1000);
assert(result.commands.outputs === {out1: 'abc', out2: 'def'});
assert(result.commands.exportedVars === {v3: 'ghi'});
assert(result.exitCode === 1);
assert(result.runnerWarnings.length === 0);
assert(process.exitCode !== 1);
assert(result.commands.errors === ['err1']);
Test Docker action
import {RunOptions, RunTarget} from 'github-action-ts-run-api';
const target = RunTarget.dockerAction('action.yml');
const options = RunOptions.create()
.setInputs({input1: 'val1', input2: 'val2'})
.setEnv({ENV1: 'val3'})
.setWorkingDir('/dir/inside/container')
.setTimeoutMs(5000)
const res = await target.run(options);
console.log(
res.commands.outputs,
res.commands.exportedVars,
res.isSuccessBuild,
res.isSuccess,
res.isTimedOut
);
Integration tests examples:
You can find examples for the complicated cases in the library integration tests:
Also, check out real actions integration tests: