feat: CISandbox v1.0.0 — Interactive CI Pipeline Debugger with Local Env Sync
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user