feat: CISandbox v1.0.0 — Interactive CI Pipeline Debugger with Local Env Sync
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=cli.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { parseWorkflow, pickJob } from './parser.js';
|
||||
import { runInteractive } from './runner.js';
|
||||
const program = new Command();
|
||||
program
|
||||
.name('cisandbox')
|
||||
.description('Interactive CI Pipeline Debugger with Local Env Sync')
|
||||
.version('1.0.0');
|
||||
program
|
||||
.argument('<workflow-file>', 'Path to GitHub Actions workflow YAML')
|
||||
.option('-j, --job <name>', 'Job name to run (required if multiple jobs)')
|
||||
.option('-w, --workspace <path>', 'Workspace directory to mount', process.cwd())
|
||||
.option('-e, --env-file <path>', '.env file to load into container')
|
||||
.option('-n, --dry-run', 'Preview steps without running containers')
|
||||
.action(async (workflowFile, options) => {
|
||||
try {
|
||||
const workflow = parseWorkflow(workflowFile);
|
||||
const { name: jobName, job } = pickJob(workflow, options.job);
|
||||
const ctx = {
|
||||
workflow,
|
||||
jobName,
|
||||
job,
|
||||
stepResults: [],
|
||||
containerId: null,
|
||||
env: {},
|
||||
};
|
||||
await runInteractive(ctx, {
|
||||
workflowFile,
|
||||
jobName,
|
||||
dryRun: options.dryRun,
|
||||
workspace: options.workspace,
|
||||
envFile: options.envFile,
|
||||
});
|
||||
const failed = ctx.stepResults.filter(r => r.status === 'failure').length;
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
program.parse();
|
||||
//# sourceMappingURL=cli.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,WAAW,CAAC;KACjB,WAAW,CAAC,sDAAsD,CAAC;KACnE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,QAAQ,CAAC,iBAAiB,EAAE,sCAAsC,CAAC;KACnE,MAAM,CAAC,kBAAkB,EAAE,6CAA6C,CAAC;KACzE,MAAM,CAAC,wBAAwB,EAAE,8BAA8B,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;KAC/E,MAAM,CAAC,uBAAuB,EAAE,kCAAkC,CAAC;KACnE,MAAM,CAAC,eAAe,EAAE,0CAA0C,CAAC;KACnE,MAAM,CAAC,KAAK,EAAE,YAAoB,EAAE,OAAiF,EAAE,EAAE;IACxH,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;QAC7C,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAE9D,MAAM,GAAG,GAAe;YACtB,QAAQ;YACR,OAAO;YACP,GAAG;YACH,WAAW,EAAE,EAAE;YACf,WAAW,EAAE,IAAI;YACjB,GAAG,EAAE,EAAE;SACR,CAAC;QAEF,MAAM,cAAc,CAAC,GAAG,EAAE;YACxB,YAAY;YACZ,OAAO;YACP,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;QAC1E,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACvF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
import Docker from 'dockerode';
|
||||
import type { Step, DockerExecResult } from './types.js';
|
||||
export declare class DockerManager {
|
||||
docker: Docker;
|
||||
constructor();
|
||||
pullImage(image: string): Promise<void>;
|
||||
createContainer(image: string, options: {
|
||||
workspace: string;
|
||||
env: Record<string, string>;
|
||||
workingDir?: string;
|
||||
}): Promise<string>;
|
||||
execInContainer(containerId: string, step: Step, modifiedCommand?: string): Promise<DockerExecResult>;
|
||||
private runShell;
|
||||
stopContainer(containerId: string): Promise<void>;
|
||||
imageExists(image: string): Promise<boolean>;
|
||||
}
|
||||
//# sourceMappingURL=docker.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../src/docker.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,WAAW,CAAC;AAE/B,OAAO,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEzD,qBAAa,aAAa;IACxB,MAAM,EAAE,MAAM,CAAC;;IAMT,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvC,eAAe,CACnB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;QAClB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GACA,OAAO,CAAC,MAAM,CAAC;IAiBZ,eAAe,CACnB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,IAAI,EACV,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,gBAAgB,CAAC;YAed,QAAQ;IA6ChB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASjD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAQnD"}
|
||||
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
import Docker from 'dockerode';
|
||||
import { resolve } from 'path';
|
||||
export class DockerManager {
|
||||
docker;
|
||||
constructor() {
|
||||
this.docker = new Docker();
|
||||
}
|
||||
async pullImage(image) {
|
||||
const stream = await this.docker.pull(image);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.docker.modem.followProgress(stream, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
async createContainer(image, options) {
|
||||
const absWorkspace = resolve(options.workspace);
|
||||
const container = await this.docker.createContainer({
|
||||
Image: image,
|
||||
Cmd: ['sleep', '3600'],
|
||||
WorkingDir: options.workingDir || '/github/workspace',
|
||||
HostConfig: {
|
||||
Binds: [`${absWorkspace}:/github/workspace`],
|
||||
AutoRemove: true,
|
||||
},
|
||||
Env: Object.entries(options.env).map(([k, v]) => `${k}=${v}`),
|
||||
Entrypoint: ['sh', '-c', 'while true; do sleep 3600; done'],
|
||||
});
|
||||
await container.start();
|
||||
return container.id;
|
||||
}
|
||||
async execInContainer(containerId, step, modifiedCommand) {
|
||||
const container = this.docker.getContainer(containerId);
|
||||
const cmd = modifiedCommand ?? step.run ?? 'true';
|
||||
const shell = step.shell ?? 'bash';
|
||||
const workingDir = step.workingDirectory ?? '/github/workspace';
|
||||
// Handle 'uses' steps by converting to a comment
|
||||
if (!modifiedCommand && step.uses) {
|
||||
const usesCmd = `echo "[CISandbox] Action step: ${step.uses}" && echo "Note: 'uses' actions are not yet supported in CISandbox. Convert to 'run' or modify the step to debug."`;
|
||||
return this.runShell(container, usesCmd, workingDir, shell, step.env);
|
||||
}
|
||||
return this.runShell(container, cmd, workingDir, shell, step.env);
|
||||
}
|
||||
async runShell(container, cmd, workingDir, shell, extraEnv) {
|
||||
const exec = await container.exec({
|
||||
Cmd: [shell, '-c', cmd],
|
||||
WorkingDir: workingDir,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Env: extraEnv ? Object.entries(extraEnv).map(([k, v]) => `${k}=${v}`) : undefined,
|
||||
});
|
||||
const stream = await exec.start({ hijack: true, stdin: false });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => {
|
||||
// Docker multiplexed stream: first byte is stream type
|
||||
const type = chunk[0];
|
||||
const payload = chunk.slice(8).toString('utf-8');
|
||||
if (type === 1)
|
||||
stdout += payload;
|
||||
else
|
||||
stderr += payload;
|
||||
});
|
||||
stream.on('end', async () => {
|
||||
try {
|
||||
const inspect = await exec.inspect();
|
||||
resolve({
|
||||
exitCode: inspect.ExitCode ?? 0,
|
||||
stdout: stdout.trimEnd(),
|
||||
stderr: stderr.trimEnd(),
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
async stopContainer(containerId) {
|
||||
try {
|
||||
const container = this.docker.getContainer(containerId);
|
||||
await container.stop({ t: 2 });
|
||||
}
|
||||
catch {
|
||||
// Already stopped or doesn't exist
|
||||
}
|
||||
}
|
||||
async imageExists(image) {
|
||||
try {
|
||||
await this.docker.getImage(image).inspect();
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=docker.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"docker.js","sourceRoot":"","sources":["../src/docker.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,MAAM,OAAO,aAAa;IACxB,MAAM,CAAS;IAEf;QACE,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,KAAa;QAC3B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE;gBAC/C,IAAI,GAAG;oBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;oBAChB,OAAO,EAAE,CAAC;YACjB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,KAAa,EACb,OAIC;QAED,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAChD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;YAClD,KAAK,EAAE,KAAK;YACZ,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC;YACtB,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,mBAAmB;YACrD,UAAU,EAAE;gBACV,KAAK,EAAE,CAAC,GAAG,YAAY,oBAAoB,CAAC;gBAC5C,UAAU,EAAE,IAAI;aACjB;YACD,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7D,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,iCAAiC,CAAC;SAC5D,CAAC,CAAC;QACH,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC,EAAE,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,WAAmB,EACnB,IAAU,EACV,eAAwB;QAExB,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;QACxD,MAAM,GAAG,GAAG,eAAe,IAAI,IAAI,CAAC,GAAG,IAAI,MAAM,CAAC;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;QACnC,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,IAAI,mBAAmB,CAAC;QAEhE,iDAAiD;QACjD,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,kCAAkC,IAAI,CAAC,IAAI,oHAAoH,CAAC;YAChL,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACxE,CAAC;QAED,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACpE,CAAC;IAEO,KAAK,CAAC,QAAQ,CACpB,SAA2B,EAC3B,GAAW,EACX,UAAkB,EAClB,KAAa,EACb,QAAiC;QAEjC,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC;YAChC,GAAG,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC;YACvB,UAAU,EAAE,UAAU;YACtB,YAAY,EAAE,IAAI;YAClB,YAAY,EAAE,IAAI;YAClB,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;SAClF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBAClC,uDAAuD;gBACvD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBACtB,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;gBACjD,IAAI,IAAI,KAAK,CAAC;oBAAE,MAAM,IAAI,OAAO,CAAC;;oBAC7B,MAAM,IAAI,OAAO,CAAC;YACzB,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;gBAC1B,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;oBACrC,OAAO,CAAC;wBACN,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC;wBAC/B,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE;wBACxB,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE;qBACzB,CAAC,CAAC;gBACL,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,CAAC,GAAG,CAAC,CAAC;gBACd,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,WAAmB;QACrC,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;YACxD,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;QACrC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAa;QAC7B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=docker.test.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"docker.test.d.ts","sourceRoot":"","sources":["../src/docker.test.ts"],"names":[],"mappings":""}
|
||||
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { DockerManager } from '../src/docker.js';
|
||||
// Mock dockerode
|
||||
vi.mock('dockerode', () => {
|
||||
return {
|
||||
default: class MockDocker {
|
||||
getContainer = vi.fn((id) => ({
|
||||
exec: vi.fn(async (opts) => {
|
||||
const cmd = opts.Cmd.join(' ');
|
||||
const isUsesWarning = cmd.includes('Action step');
|
||||
const stdoutText = isUsesWarning
|
||||
? '[CISandbox] Action step: actions/checkout@v4\nNote: \'uses\' actions are not yet supported in CISandbox. Convert to \'run\' or modify the step to debug.'
|
||||
: 'hello';
|
||||
return {
|
||||
start: vi.fn(async () => {
|
||||
const { EventEmitter } = await import('events');
|
||||
const ee = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
const buf = Buffer.alloc(8 + stdoutText.length);
|
||||
buf[0] = 1;
|
||||
buf.write(stdoutText, 8);
|
||||
ee.emit('data', buf);
|
||||
ee.emit('end');
|
||||
});
|
||||
return ee;
|
||||
}),
|
||||
inspect: vi.fn(async () => ({ ExitCode: 0 })),
|
||||
};
|
||||
}),
|
||||
stop: vi.fn(async () => { }),
|
||||
}));
|
||||
getImage = vi.fn(() => ({
|
||||
inspect: vi.fn(async () => ({})),
|
||||
}));
|
||||
createContainer = vi.fn(async () => ({ id: 'abc123', start: vi.fn(async () => { }) }));
|
||||
pull = vi.fn(async () => {
|
||||
const { EventEmitter } = await import('events');
|
||||
const ee = new EventEmitter();
|
||||
process.nextTick(() => ee.emit('end'));
|
||||
return ee;
|
||||
});
|
||||
modem = { followProgress: vi.fn((_, cb) => cb()) };
|
||||
},
|
||||
};
|
||||
});
|
||||
describe('DockerManager', () => {
|
||||
it('checks image existence', async () => {
|
||||
const dm = new DockerManager();
|
||||
const exists = await dm.imageExists('node:20-slim');
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
it('creates a container', async () => {
|
||||
const dm = new DockerManager();
|
||||
const id = await dm.createContainer('node:20-slim', {
|
||||
workspace: '/tmp',
|
||||
env: { FOO: 'bar' },
|
||||
});
|
||||
expect(id).toBe('abc123');
|
||||
});
|
||||
it('executes a step', async () => {
|
||||
const dm = new DockerManager();
|
||||
const result = await dm.execInContainer('abc123', { run: 'echo hello' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('hello');
|
||||
});
|
||||
it('handles uses steps with warning', async () => {
|
||||
const dm = new DockerManager();
|
||||
const result = await dm.execInContainer('abc123', { uses: 'actions/checkout@v4' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('not yet supported');
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=docker.test.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"docker.test.js","sourceRoot":"","sources":["../src/docker.test.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export { parseWorkflow, listJobs, pickJob, normalizeRunsOn } from './parser.js';
|
||||
export { DockerManager } from './docker.js';
|
||||
export { runInteractive } from './runner.js';
|
||||
export type { Workflow, Job, Step, StepResult, RunContext, RunnerOptions, DockerExecResult, } from './types.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EACV,QAAQ,EACR,GAAG,EACH,IAAI,EACJ,UAAU,EACV,UAAU,EACV,aAAa,EACb,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export { parseWorkflow, listJobs, pickJob, normalizeRunsOn } from './parser.js';
|
||||
export { DockerManager } from './docker.js';
|
||||
export { runInteractive } from './runner.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import type { Workflow, Job } from './types.js';
|
||||
export declare function parseWorkflow(filePath: string): Workflow;
|
||||
export declare function listJobs(workflow: Workflow): {
|
||||
name: string;
|
||||
job: Job;
|
||||
}[];
|
||||
export declare function pickJob(workflow: Workflow, jobName?: string): {
|
||||
name: string;
|
||||
job: Job;
|
||||
};
|
||||
export declare function normalizeRunsOn(runsOn: string): string;
|
||||
//# sourceMappingURL=parser.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAaxD;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EAAE,CAEzE;AAED,wBAAgB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,CAsBxF;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWtD"}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import yaml from 'js-yaml';
|
||||
export function parseWorkflow(filePath) {
|
||||
const fullPath = resolve(filePath);
|
||||
const content = readFileSync(fullPath, 'utf-8');
|
||||
const parsed = yaml.load(content);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error(`Invalid YAML in ${filePath}: not an object`);
|
||||
}
|
||||
if (!parsed.jobs || typeof parsed.jobs !== 'object') {
|
||||
throw new Error(`Invalid workflow in ${filePath}: missing 'jobs' field`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
export function listJobs(workflow) {
|
||||
return Object.entries(workflow.jobs).map(([name, job]) => ({ name, job }));
|
||||
}
|
||||
export function pickJob(workflow, jobName) {
|
||||
const jobs = listJobs(workflow);
|
||||
if (jobs.length === 0) {
|
||||
throw new Error('Workflow has no jobs');
|
||||
}
|
||||
if (jobName) {
|
||||
const found = jobs.find(j => j.name === jobName);
|
||||
if (!found) {
|
||||
throw new Error(`Job "${jobName}" not found. Available: ${jobs.map(j => j.name).join(', ')}`);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
// Return first job if only one exists
|
||||
if (jobs.length === 1) {
|
||||
return jobs[0];
|
||||
}
|
||||
throw new Error(`Multiple jobs found: ${jobs.map(j => j.name).join(', ')}. Use --job to specify one.`);
|
||||
}
|
||||
export function normalizeRunsOn(runsOn) {
|
||||
const mapping = {
|
||||
'ubuntu-latest': 'node:20-slim',
|
||||
'ubuntu-24.04': 'node:20-slim',
|
||||
'ubuntu-22.04': 'node:20-slim',
|
||||
'ubuntu-20.04': 'node:18-slim',
|
||||
'node-latest': 'node:22-slim',
|
||||
'node-20': 'node:20-slim',
|
||||
'node-18': 'node:18-slim',
|
||||
};
|
||||
return mapping[runsOn] ?? runsOn;
|
||||
}
|
||||
//# sourceMappingURL=parser.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,IAAI,MAAM,SAAS,CAAC;AAG3B,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAa,CAAC;IAE9C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,mBAAmB,QAAQ,iBAAiB,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,wBAAwB,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,QAAkB;IACzC,OAAO,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,QAAkB,EAAE,OAAgB;IAC1D,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;QACjD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,QAAQ,OAAO,2BAA2B,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChG,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,sCAAsC;IACtC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IAED,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,6BAA6B,CACtF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,MAAM,OAAO,GAA2B;QACtC,eAAe,EAAE,cAAc;QAC/B,cAAc,EAAE,cAAc;QAC9B,cAAc,EAAE,cAAc;QAC9B,cAAc,EAAE,cAAc;QAC9B,aAAa,EAAE,cAAc;QAC7B,SAAS,EAAE,cAAc;QACzB,SAAS,EAAE,cAAc;KAC1B,CAAC;IACF,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC;AACnC,CAAC"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=parser.test.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"parser.test.d.ts","sourceRoot":"","sources":["../src/parser.test.ts"],"names":[],"mappings":""}
|
||||
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { parseWorkflow, listJobs, pickJob, normalizeRunsOn } from '../src/parser.js';
|
||||
const TMP = join(process.cwd(), 'tmp-test');
|
||||
describe('parser', () => {
|
||||
beforeEach(() => {
|
||||
mkdirSync(TMP, { recursive: true });
|
||||
});
|
||||
afterEach(() => {
|
||||
rmSync(TMP, { recursive: true, force: true });
|
||||
});
|
||||
it('parses a basic workflow', () => {
|
||||
const path = join(TMP, 'workflow.yml');
|
||||
writeFileSync(path, `
|
||||
name: Test Workflow
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hello
|
||||
`);
|
||||
const wf = parseWorkflow(path);
|
||||
expect(wf.name).toBe('Test Workflow');
|
||||
expect(wf.jobs.build['runs-on']).toBe('ubuntu-latest');
|
||||
expect(wf.jobs.build.steps).toHaveLength(1);
|
||||
});
|
||||
it('lists all jobs', () => {
|
||||
const path = join(TMP, 'workflow.yml');
|
||||
writeFileSync(path, `
|
||||
name: Multi Job
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo build
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo test
|
||||
`);
|
||||
const wf = parseWorkflow(path);
|
||||
const jobs = listJobs(wf);
|
||||
expect(jobs).toHaveLength(2);
|
||||
expect(jobs.map(j => j.name)).toEqual(['build', 'test']);
|
||||
});
|
||||
it('picks specified job', () => {
|
||||
const path = join(TMP, 'workflow.yml');
|
||||
writeFileSync(path, `
|
||||
name: Multi Job
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo build
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo test
|
||||
`);
|
||||
const wf = parseWorkflow(path);
|
||||
const picked = pickJob(wf, 'test');
|
||||
expect(picked.name).toBe('test');
|
||||
});
|
||||
it('picks first job when only one exists', () => {
|
||||
const path = join(TMP, 'workflow.yml');
|
||||
writeFileSync(path, `
|
||||
name: Single Job
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo build
|
||||
`);
|
||||
const wf = parseWorkflow(path);
|
||||
const picked = pickJob(wf);
|
||||
expect(picked.name).toBe('build');
|
||||
});
|
||||
it('throws when multiple jobs and no name given', () => {
|
||||
const path = join(TMP, 'workflow.yml');
|
||||
writeFileSync(path, `
|
||||
name: Multi Job
|
||||
on: push
|
||||
jobs:
|
||||
a:
|
||||
runs-on: ubuntu-latest
|
||||
steps: []
|
||||
b:
|
||||
runs-on: ubuntu-latest
|
||||
steps: []
|
||||
`);
|
||||
const wf = parseWorkflow(path);
|
||||
expect(() => pickJob(wf)).toThrow('Use --job');
|
||||
});
|
||||
it('throws on missing file', () => {
|
||||
expect(() => parseWorkflow(join(TMP, 'nonexistent.yml'))).toThrow();
|
||||
});
|
||||
it('normalizes runs-on values', () => {
|
||||
expect(normalizeRunsOn('ubuntu-latest')).toBe('node:20-slim');
|
||||
expect(normalizeRunsOn('ubuntu-22.04')).toBe('node:20-slim');
|
||||
expect(normalizeRunsOn('custom-image')).toBe('custom-image');
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=parser.test.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"parser.test.js","sourceRoot":"","sources":["../src/parser.test.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { RunContext, RunnerOptions } from './types.js';
|
||||
export declare function runInteractive(ctx: RunContext, options: RunnerOptions): Promise<void>;
|
||||
//# sourceMappingURL=runner.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAc,aAAa,EAAQ,MAAM,YAAY,CAAC;AAQ9E,wBAAsB,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAkI3F"}
|
||||
Vendored
+198
@@ -0,0 +1,198 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { createInterface } from 'readline';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { DockerManager } from './docker.js';
|
||||
import { normalizeRunsOn } from './parser.js';
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
function ask(q) {
|
||||
return new Promise(resolve => rl.question(q, resolve));
|
||||
}
|
||||
export async function runInteractive(ctx, options) {
|
||||
const docker = new DockerManager();
|
||||
const spinner = ora();
|
||||
// Load env file if provided
|
||||
let extraEnv = {};
|
||||
if (options.envFile) {
|
||||
const envContent = readFileSync(options.envFile, 'utf-8');
|
||||
for (const line of envContent.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#'))
|
||||
continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq > 0) {
|
||||
extraEnv[trimmed.slice(0, eq)] = trimmed.slice(eq + 1).replace(/^"|"$/g, '').replace(/^'|'$/g, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
const workspace = options.workspace ?? process.cwd();
|
||||
const image = normalizeRunsOn(ctx.job['runs-on']);
|
||||
const env = { ...ctx.workflow.env, ...ctx.job.env, ...extraEnv };
|
||||
console.log(chalk.bold.blue('\n⚡ CISandbox — Interactive CI Debugger\n'));
|
||||
console.log(chalk.gray(`Workflow: ${ctx.workflow.name}`));
|
||||
console.log(chalk.gray(`Job: ${ctx.jobName}`));
|
||||
console.log(chalk.gray(`Image: ${image}`));
|
||||
console.log(chalk.gray(`Workspace: ${workspace}\n`));
|
||||
if (options.dryRun) {
|
||||
console.log(chalk.yellow('DRY RUN mode — no containers will be created\n'));
|
||||
}
|
||||
// Show step preview
|
||||
console.log(chalk.bold('Steps:'));
|
||||
ctx.job.steps.forEach((step, i) => {
|
||||
const label = step.name ?? step.run ?? step.uses ?? `step ${i + 1}`;
|
||||
console.log(chalk.gray(` ${i + 1}. ${label}`));
|
||||
});
|
||||
console.log('');
|
||||
if (options.dryRun) {
|
||||
console.log(chalk.yellow('Dry run complete. Re-run without --dry-run to execute.\n'));
|
||||
return;
|
||||
}
|
||||
// Ensure image is available
|
||||
spinner.start(`Checking image: ${image}`);
|
||||
const exists = await docker.imageExists(image);
|
||||
if (!exists) {
|
||||
spinner.text = `Pulling image: ${image}`;
|
||||
try {
|
||||
await docker.pullImage(image);
|
||||
spinner.succeed(`Pulled image: ${image}`);
|
||||
}
|
||||
catch (err) {
|
||||
spinner.fail(`Failed to pull image: ${image}`);
|
||||
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
spinner.succeed(`Image ready: ${image}`);
|
||||
}
|
||||
// Create container
|
||||
spinner.start('Creating debug container');
|
||||
const containerId = await docker.createContainer(image, { workspace, env });
|
||||
ctx.containerId = containerId;
|
||||
spinner.succeed(`Container created: ${containerId.slice(0, 12)}`);
|
||||
console.log(chalk.bold.green('\n▶ Starting interactive execution\n'));
|
||||
console.log(chalk.gray('Controls: [Enter]=continue | [s]=skip | [m]=modify command | [e]=edit env | [q]=quit\n'));
|
||||
try {
|
||||
for (let i = 0; i < ctx.job.steps.length; i++) {
|
||||
const step = ctx.job.steps[i];
|
||||
const result = await runStepInteractive(docker, containerId, step, i, ctx);
|
||||
ctx.stepResults.push(result);
|
||||
if (result.status === 'failure') {
|
||||
console.log(chalk.bold.red(`\n✖ Step ${i + 1} FAILED (exit ${result.exitCode})`));
|
||||
const choice = await ask(chalk.yellow('Retry (r), modify+retry (m), skip (s), or quit (q)? [r/m/s/q]: '));
|
||||
if (choice.trim().toLowerCase() === 'r') {
|
||||
i--; // retry same step
|
||||
ctx.stepResults.pop();
|
||||
continue;
|
||||
}
|
||||
else if (choice.trim().toLowerCase() === 'm') {
|
||||
const newCmd = await ask(chalk.cyan('New command: '));
|
||||
const retryResult = await runStepInteractive(docker, containerId, step, i, ctx, newCmd);
|
||||
ctx.stepResults[ctx.stepResults.length - 1] = retryResult;
|
||||
if (retryResult.status === 'failure') {
|
||||
console.log(chalk.red('Modified command also failed. Moving on...\n'));
|
||||
}
|
||||
}
|
||||
else if (choice.trim().toLowerCase() === 's') {
|
||||
result.status = 'skipped';
|
||||
console.log(chalk.yellow('Skipped.\n'));
|
||||
}
|
||||
else {
|
||||
console.log(chalk.gray('Quitting...'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
spinner.start('Stopping container');
|
||||
await docker.stopContainer(containerId);
|
||||
spinner.succeed('Container stopped');
|
||||
}
|
||||
// Summary
|
||||
console.log(chalk.bold.blue('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
||||
console.log(chalk.bold('Run Summary'));
|
||||
console.log(chalk.bold.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
||||
for (const r of ctx.stepResults) {
|
||||
const icon = r.status === 'success' ? chalk.green('✔') : r.status === 'failure' ? chalk.red('✖') : chalk.yellow('○');
|
||||
const label = r.step.name ?? r.step.run ?? r.step.uses ?? `step ${r.index + 1}`;
|
||||
const dur = `${r.durationMs}ms`;
|
||||
console.log(`${icon} Step ${r.index + 1}: ${label} ${chalk.gray(`(${dur})`)}`);
|
||||
if (r.modifiedCommand) {
|
||||
console.log(chalk.cyan(` modified: ${r.modifiedCommand}`));
|
||||
}
|
||||
}
|
||||
const passed = ctx.stepResults.filter(r => r.status === 'success').length;
|
||||
const failed = ctx.stepResults.filter(r => r.status === 'failure').length;
|
||||
const skipped = ctx.stepResults.filter(r => r.status === 'skipped').length;
|
||||
console.log(chalk.bold.blue('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
||||
console.log(`${chalk.green(`${passed} passed`)} | ${chalk.red(`${failed} failed`)} | ${chalk.yellow(`${skipped} skipped`)}`);
|
||||
console.log(chalk.bold.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
||||
rl.close();
|
||||
}
|
||||
async function runStepInteractive(docker, containerId, step, index, ctx, overrideCommand) {
|
||||
const label = step.name ?? step.run ?? step.uses ?? `step ${index + 1}`;
|
||||
console.log(chalk.bold(`\n▶ Step ${index + 1}: ${label}`));
|
||||
if (step.if) {
|
||||
console.log(chalk.gray(` condition: ${step.if}`));
|
||||
}
|
||||
if (step.uses) {
|
||||
console.log(chalk.gray(` uses: ${step.uses}`));
|
||||
}
|
||||
if (step.run) {
|
||||
console.log(chalk.gray(` run: ${step.run.slice(0, 120)}${step.run.length > 120 ? '...' : ''}`));
|
||||
}
|
||||
const action = await ask(chalk.gray(' [Enter]=run | s=skip | m=modify | q=quit: '));
|
||||
const choice = action.trim().toLowerCase();
|
||||
if (choice === 'q') {
|
||||
return { step, index, status: 'skipped', exitCode: null, stdout: '', stderr: '', durationMs: 0 };
|
||||
}
|
||||
if (choice === 's') {
|
||||
console.log(chalk.yellow(' → skipped'));
|
||||
return { step, index, status: 'skipped', exitCode: null, stdout: '', stderr: '', durationMs: 0 };
|
||||
}
|
||||
let cmd = overrideCommand;
|
||||
if (choice === 'm') {
|
||||
cmd = await ask(chalk.cyan(' New command: '));
|
||||
}
|
||||
const start = Date.now();
|
||||
const spinner = ora(' Running...').start();
|
||||
try {
|
||||
const result = await docker.execInContainer(containerId, step, cmd);
|
||||
const durationMs = Date.now() - start;
|
||||
spinner.stop();
|
||||
if (result.stdout) {
|
||||
console.log(chalk.gray(result.stdout.split('\n').map(l => ` │ ${l}`).join('\n')));
|
||||
}
|
||||
if (result.stderr) {
|
||||
console.log(chalk.red(result.stderr.split('\n').map(l => ` ✕ ${l}`).join('\n')));
|
||||
}
|
||||
if (result.exitCode === 0) {
|
||||
console.log(chalk.green(` ✔ success (${durationMs}ms)`));
|
||||
return {
|
||||
step, index, status: 'success',
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout, stderr: result.stderr,
|
||||
durationMs,
|
||||
modifiedCommand: cmd,
|
||||
};
|
||||
}
|
||||
else {
|
||||
console.log(chalk.red(` ✖ failed (exit ${result.exitCode}, ${durationMs}ms)`));
|
||||
return {
|
||||
step, index, status: 'failure',
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout, stderr: result.stderr,
|
||||
durationMs,
|
||||
modifiedCommand: cmd,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
spinner.stop();
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.log(chalk.red(` ✖ error: ${msg}`));
|
||||
return { step, index, status: 'failure', exitCode: 1, stdout: '', stderr: msg, durationMs: Date.now() - start, modifiedCommand: cmd };
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=runner.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
export interface Workflow {
|
||||
name: string;
|
||||
on: string | string[] | Record<string, unknown>;
|
||||
jobs: Record<string, Job>;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
export interface Job {
|
||||
name?: string;
|
||||
'runs-on': string;
|
||||
steps: Step[];
|
||||
env?: Record<string, string>;
|
||||
needs?: string | string[];
|
||||
if?: string;
|
||||
}
|
||||
export interface Step {
|
||||
name?: string;
|
||||
uses?: string;
|
||||
run?: string;
|
||||
with?: Record<string, string | number | boolean>;
|
||||
env?: Record<string, string>;
|
||||
if?: string;
|
||||
workingDirectory?: string;
|
||||
shell?: string;
|
||||
id?: string;
|
||||
}
|
||||
export interface StepResult {
|
||||
step: Step;
|
||||
index: number;
|
||||
status: 'pending' | 'running' | 'success' | 'failure' | 'skipped' | 'modified';
|
||||
exitCode: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
durationMs: number;
|
||||
modifiedCommand?: string;
|
||||
}
|
||||
export interface RunContext {
|
||||
workflow: Workflow;
|
||||
jobName: string;
|
||||
job: Job;
|
||||
stepResults: StepResult[];
|
||||
containerId: string | null;
|
||||
env: Record<string, string>;
|
||||
}
|
||||
export interface DockerExecResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
export interface RunnerOptions {
|
||||
workflowFile: string;
|
||||
jobName?: string;
|
||||
dryRun?: boolean;
|
||||
workspace?: string;
|
||||
envFile?: string;
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,GAAG;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACjD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAC/E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,GAAG,CAAC;IACT,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
||||
Reference in New Issue
Block a user