UNPKG

@im-sampm/act-js

Version:

nodejs wrapper for nektos/act

327 lines (326 loc) 12.4 kB
"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;