@im-sampm/act-js
Version:
nodejs wrapper for nektos/act
327 lines (326 loc) • 12.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Act = void 0;
const child_process_1 = require("child_process");
const act_constants_1 = require("../act/act.constants");
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const path_1 = __importDefault(require("path"));
const os_1 = require("os");
const proxy_1 = require("../proxy/proxy");
const argument_map_1 = require("../map/argument-map");
const step_mocker_1 = require("../step-mocker/step-mocker");
const action_event_1 = require("../action-event/action-event");
const output_parser_1 = require("../output-parser/output-parser");
const action_input_1 = require("../action-input/action-input");
class Act {
constructor(cwd, workflowFile, defaultImageSize) {
this.secrets = new argument_map_1.ArgumentMap("-s");
this.cwd = cwd ?? process.cwd();
this.workflowFile = workflowFile ?? this.cwd;
this.vars = new argument_map_1.ArgumentMap("--var");
this.env = new argument_map_1.ArgumentMap("--env");
this.matrix = new argument_map_1.ArgumentMap("--matrix", ":");
this.platforms = new argument_map_1.ArgumentMap("--platform");
this.event = new action_event_1.ActionEvent();
this.input = new action_input_1.ActionInput(this.event);
this.containerOpts = {};
this.setDefaultImage(defaultImageSize);
this.setGithubStepSummary("/dev/stdout");
}
setCwd(cwd) {
this.cwd = cwd;
return this;
}
setWorkflowFile(workflowFile) {
this.workflowFile = workflowFile;
return this;
}
setSecret(key, val) {
this.secrets.map.set(key, val);
return this;
}
deleteSecret(key) {
this.secrets.map.delete(key);
return this;
}
clearSecret() {
this.secrets.map.clear();
return this;
}
setVar(key, val) {
this.vars.map.set(key, val);
return this;
}
deleteVar(key) {
this.vars.map.delete(key);
return this;
}
clearVar() {
this.vars.map.clear();
return this;
}
setEnv(key, val) {
this.env.map.set(key, val);
return this;
}
deleteEnv(key) {
this.env.map.delete(key);
return this;
}
clearEnv() {
this.env.map.clear();
this.setGithubStepSummary("/dev/stdout");
return this;
}
setGithubToken(token) {
this.setSecret("GITHUB_TOKEN", token);
return this;
}
setGithubStepSummary(file) {
this.setEnv("GITHUB_STEP_SUMMARY", file);
return this;
}
setEvent(event) {
this.event.event = event;
return this;
}
setInput(key, val) {
this.input.map.set(key, val);
return this;
}
deleteInput(key) {
this.input.map.delete(key);
return this;
}
clearInput() {
this.input.map.clear();
return this;
}
setMatrix(key, val) {
this.matrix.map.set(key, val);
return this;
}
deleteMatrix(key) {
this.matrix.map.delete(key);
return this;
}
clearMatrix() {
this.matrix.map.clear();
return this;
}
setPlatforms(key, val) {
this.platforms.map.set(key, val);
return this;
}
deletePlatforms(key) {
this.platforms.map.delete(key);
return this;
}
clearPlatforms() {
this.platforms.map.clear();
return this;
}
setContainerArchitecture(val) {
this.containerOpts.containerArchitecture = val;
return this;
}
setContainerDaemonSocket(val) {
this.containerOpts.containerDaemonSocket = val;
return this;
}
setCustomContainerOpts(val) {
this.containerOpts.containerOptions = val;
return this;
}
clearAllContainerOpts() {
this.containerOpts = {};
return this;
}
/**
* List available workflows.
* If working directory is not specified then node's current working directory is used
* You can also list workflows specific to an event by passing the event name
* @param cwd
* @param workflowFile
* @param event
*/
async list(event, cwd = this.cwd, workflowFile = this.workflowFile) {
const args = ["-W", workflowFile, "-l"];
const data = await this.act(cwd, undefined, ...(event ? [event, ...args] : args));
return data
.split("\n")
.slice(1, -1) // remove first (title columns) and last column
.filter(element => element !== "" && element.split(" ").length > 1) // remove empty strings and warnings
.map(element => {
const splitElement = element.split(" ").filter(val => val !== ""); // remove emoty strings
return {
jobId: splitElement[1].trim(),
jobName: splitElement[2].trim(),
workflowName: splitElement[3].trim(),
workflowFile: splitElement[4].trim(),
events: splitElement[5].trim(),
};
});
}
async runJob(jobId, opts) {
await this.handleStepMocking(workflow => workflow.jobId === jobId, opts);
return this.run(["-j", jobId], opts);
}
async runEvent(event, opts) {
await this.handleStepMocking(workflow => workflow.events.includes(event), opts);
return this.run([event], opts);
}
async runEventAndJob(event, jobId, opts) {
await this.handleStepMocking(workflow => workflow.events.includes(event) && workflow.jobId === jobId, opts);
return this.run([event, "-j", jobId], opts);
}
async handleStepMocking(filter, opts) {
if (opts?.mockSteps) {
// there could multiple workflow files with same event triggers or job names. Act executes them all
let workflowFiles = [];
const cwd = opts.cwd ?? this.cwd;
// if workflow file was defined then no need to consider all possible options
if (opts.workflowFile) {
workflowFiles = [path_1.default.basename(opts.workflowFile)];
}
else if (this.workflowFile !== cwd) {
workflowFiles = [path_1.default.basename(this.workflowFile)];
}
else {
workflowFiles = (await this.list(undefined, opts.cwd)).filter(filter).map(l => l.workflowFile);
}
return Promise.all(workflowFiles.map(workflowFile => {
const stepMocker = new step_mocker_1.StepMocker(workflowFile, opts.cwd ?? this.cwd);
return stepMocker.mock(opts.mockSteps);
}));
}
}
// wrapper around the act cli command
async act(cwd, logFile, ...args) {
const fsStream = await this.logRawOutput(logFile);
return new Promise((resolve, reject) => {
// do not use spawnSync. will cause a deadlock when used with proxy settings
const childProcess = (0, child_process_1.spawn)(act_constants_1.ACT_BINARY, args, { cwd });
let data = "";
childProcess.stdout.on("data", chunk => {
const output = chunk.toString();
data += output;
fsStream?.write(output);
});
childProcess.stderr.on("data", chunk => {
const output = chunk.toString();
data += output;
fsStream?.write(output);
});
childProcess.on("close", code => {
fsStream?.close();
if (code === null ||
/Cannot connect to the Docker daemon at .+/.test(data)) {
reject(data);
}
else {
resolve(data);
}
});
});
}
async parseRunOpts(opts) {
const actArguments = [];
const cwd = opts?.cwd ?? this.cwd;
const workflowFile = opts?.workflowFile ?? this.workflowFile;
let proxy = undefined;
if (opts?.mockApi && opts.mockApi.length > 0) {
proxy = new proxy_1.ForwardProxy(opts.mockApi, opts?.verbose);
const address = await proxy.start();
this.setEnv("http_proxy", `http://${address}`);
this.setEnv("https_proxy", `http://${address}`);
this.setEnv("HTTP_PROXY", `http://${address}`);
this.setEnv("HTTPS_PROXY", `http://${address}`);
}
if (opts?.artifactServer) {
actArguments.push("--artifact-server-path", opts?.artifactServer.path);
if (opts.artifactServer.port) {
actArguments.push("--artifact-server-port", opts?.artifactServer.port);
}
}
if (opts?.bind) {
actArguments.push("--bind");
}
if (opts?.verbose) {
actArguments.push("--verbose");
}
if (this.containerOpts.containerArchitecture) {
actArguments.push("--container-architecture", this.containerOpts.containerArchitecture);
}
if (this.containerOpts.containerDaemonSocket) {
actArguments.push("--container-daemon-socket", this.containerOpts.containerDaemonSocket);
}
if (this.containerOpts.containerOptions) {
actArguments.push("--container-options", this.containerOpts.containerOptions);
}
actArguments.push("-W", workflowFile);
return { cwd, proxy, actArguments };
}
/**
* Run the actual act binary. Pass any necessary env or secrets formatted according to the cli's requirements
* @param cmd
* @param opts
* @returns
*/
async run(cmd, opts) {
const { cwd, actArguments, proxy } = await this.parseRunOpts(opts);
const vars = this.vars.toActArguments();
const env = this.env.toActArguments();
const secrets = this.secrets.toActArguments();
const input = this.input.toActArguments();
const event = await this.event.toActArguments();
const matrix = this.matrix.toActArguments();
const platforms = this.platforms.toActArguments();
const data = await this.act(cwd, opts?.logFile, ...cmd, ...secrets, ...vars, ...env, ...input, ...event, ...matrix, ...platforms, ...actArguments);
const promises = [
this.event.removeEvent(),
...(proxy ? [proxy.stop()] : [])
];
const result = new output_parser_1.OutputParser(data).parseOutput();
await Promise.all(promises);
return result;
}
/**
* Produce a .actrc file in the home directory of the user if it does not exist
* @param defaultImageSize
*/
setDefaultImage(defaultImageSize) {
const actrcPath = path_1.default.join((0, os_1.homedir)(), ".actrc");
const ubuntuLatest = "-P ubuntu-latest=";
const ubuntu2004 = "-P ubuntu-20.04=";
const ubuntu1804 = "-P ubuntu-18.04=";
const ubuntu2204 = "-P ubuntu-22.04=";
const catthehacker = "ghcr.io/catthehacker/";
if (!(0, fs_1.existsSync)(actrcPath)) {
let actrc = "";
switch (defaultImageSize ?? "medium") {
case "micro":
actrc = `${ubuntuLatest}node:16-buster-slim\n${ubuntu2004}node:16-buster-slim\n${ubuntu1804}node:16-buster-slim\n${ubuntu2204}node:16-bullseye-slim`;
break;
case "medium":
actrc = `${ubuntuLatest}${catthehacker}ubuntu:act-latest\n${ubuntu2004}${catthehacker}ubuntu:act-20.04\n${ubuntu1804}${catthehacker}ubuntu:act-18.04\n${ubuntu2204}${catthehacker}ubuntu:act-22.04`;
break;
case "large":
actrc = `${ubuntuLatest}${catthehacker}ubuntu:full-latest\n${ubuntu2004}${catthehacker}ubuntu:full-20.04\n${ubuntu1804}${catthehacker}ubuntu:full-18.04`;
break;
}
(0, fs_1.writeFileSync)(actrcPath, actrc);
}
}
async logRawOutput(logFile) {
if (logFile) {
const filehandle = await (0, promises_1.open)(logFile, "w");
return filehandle.createWriteStream();
}
}
}
exports.Act = Act;