Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,17 @@ jobs:
- name: Run unit tests
run: bun run test:unit

- name: Check Docker availability
id: docker-check
run: |
if docker version >/dev/null 2>&1; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
fi

- name: Run full test suite (Docker)
if: steps.docker-check.outputs.available == 'true'
run: bun run test

release:
Expand Down Expand Up @@ -85,3 +95,54 @@ jobs:
generate_release_notes: true
draft: false
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}

publish-npm:
name: Publish to npm
runs-on: ubuntu-latest
timeout-minutes: 15
needs: release
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
id-token: write
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The publish-npm job grants id-token: write, but the workflow uses NODE_AUTH_TOKEN and does not appear to use OIDC/provenance publishing. Consider removing id-token: write to follow least-privilege, or switch to provenance/OIDC publishing so the permission is justified.

Suggested change
id-token: write

Copilot uses AI. Check for mistakes.
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build packages
run: bun run build

- name: Publish @ignite/shared
run: npm publish
working-directory: packages/shared
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @ignite/core
run: npm publish
working-directory: packages/core
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @ignite/http
run: npm publish
working-directory: packages/http
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @ignite/cli
run: npm publish
working-directory: packages/cli
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ignite",
"version": "0.8.0",
"version": "0.9.0",
"private": true,
"description": "Secure JS/TS code execution in Docker with sandboxing for AI agents, untrusted code, and microservices",
"workspaces": [
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"name": "@ignite/cli",
"version": "0.8.0",
"version": "0.9.0",
"type": "module",
"bin": {
"ignite": "./dist/index.js"
},
"main": "./dist/index.js",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun run tsc",
"dev": "bun run tsc --watch",
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface RunOptions {
audit?: boolean;
runtime?: string;
auditOutput?: string;
stream?: boolean;
}

export async function runCommand(servicePath: string, options: RunOptions): Promise<void> {
Expand Down Expand Up @@ -52,7 +53,14 @@ export async function runCommand(servicePath: string, options: RunOptions): Prom
? (await loadPolicyFile(service.servicePath)) ?? DEFAULT_POLICY
: undefined;

const metrics = await executeService(service, { input, skipBuild: true, audit: options.audit, policy });
const metrics = await executeService(service, {
input,
skipBuild: true,
audit: options.audit,
policy,
onStdout: options.stream ? (chunk) => process.stdout.write(chunk) : undefined,
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--stream currently writes container output directly to stdout/stderr while --json later prints a JSON payload to stdout. If a user passes both flags, the streamed output will corrupt the JSON output (invalid JSON). Consider making --stream mutually exclusive with --json (validation in the CLI) or streaming to stderr when --json is enabled.

Suggested change
onStdout: options.stream ? (chunk) => process.stdout.write(chunk) : undefined,
onStdout: options.stream
? (chunk) => {
if (options.json) {
// When JSON output is requested, send streamed stdout to stderr
// so that stdout remains valid JSON.
process.stderr.write(chunk);
} else {
process.stdout.write(chunk);
}
}
: undefined,

Copilot uses AI. Check for mistakes.
onStderr: options.stream ? (chunk) => process.stderr.write(chunk) : undefined,
});
Comment on lines +60 to +63
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With --stream enabled, stdout/stderr is printed in real time via the callbacks, but the subsequent human-readable report output (via formatReportAsText) includes a STDOUT: section that prints the captured stdout again. This will duplicate potentially large outputs and make the CLI output hard to read. Consider omitting stdout from the text report when streaming is enabled (e.g., add an option to the formatter, or clear/replace the displayed stdout while still keeping the raw stdout for audit parsing/JSON output).

Copilot uses AI. Check for mistakes.

const report = createReport(preflightResult, metrics);
const audit = options.audit && policy
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const program = new Command();
program
.name('ignite')
.description('Secure sandbox for AI-generated code, untrusted scripts, and JS/TS execution')
.version('0.8.0');
.version('0.9.0');

program
.command('init <name>')
Expand All @@ -31,6 +31,7 @@ program
.option('--json', 'Output results as JSON')
.option('--audit', 'Run with security audit (blocks network, read-only filesystem)')
.option('--audit-output <file>', 'Write security audit to a JSON file')
.option('--stream', 'Stream container output to the terminal in real time')
.action(runCommand);

program
Expand Down
5 changes: 4 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ignite/core",
"version": "0.8.0",
"version": "0.9.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -10,6 +10,9 @@
"types": "./dist/index.d.ts"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun run tsc",
"dev": "bun run tsc --watch",
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/__tests__/docker-execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ describe('Docker Execution', () => {
expect(metrics.stdout).toContain('Ignite');
}, 120000);

it('streams stdout in real time when onStdout callback is provided', async () => {
if (!dockerAvailable) return;

const service = await loadService(HELLO_BUN_PATH);
const chunks: string[] = [];

const metrics = await executeService(service, {
onStdout: (chunk) => chunks.push(chunk),
});

expect(metrics.exitCode).toBe(0);
expect(chunks.length).toBeGreaterThan(0);
expect(chunks.join('')).toBe(metrics.stdout);
}, 120000);

it('returns correct cold start detection', async () => {
if (!dockerAvailable) return;

Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/__tests__/docker-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,25 @@ describe('buildDockerRunArgs', () => {
expect(args).toContain('--name');
expect(args).toContain('ignite-test-container');
});

it('produces the same args regardless of whether streaming callbacks are provided', () => {
const baseOptions = {
imageName: 'ignite-test',
containerName: 'ignite-test-container',
memoryLimitMb: 64,
timeoutMs: 1000,
workDir: '/app',
env: {},
volumes: [],
};

const argsWithoutStreaming = buildDockerRunArgs(baseOptions);
const argsWithStreaming = buildDockerRunArgs({
...baseOptions,
onStdout: () => {},
onStderr: () => {},
});

expect(argsWithStreaming).toEqual(argsWithoutStreaming);
});
});
6 changes: 6 additions & 0 deletions packages/core/src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export interface ExecuteOptions {
skipBuild?: boolean;
audit?: boolean;
policy?: SecurityPolicy;
/** Called with each stdout chunk as it arrives from the container in real time */
onStdout?: (chunk: string) => void;
/** Called with each stderr chunk as it arrives from the container in real time */
onStderr?: (chunk: string) => void;
}

interface ExecutionState {
Expand Down Expand Up @@ -76,6 +80,8 @@ export async function executeService(
NODE_ENV: 'production',
},
security: options.audit ? securityOptions : undefined,
onStdout: options.onStdout,
onStderr: options.onStderr,
});

const metrics = parseMetrics(runResult, isColdStart);
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/runtime/docker-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export async function dockerRun(options: DockerRunOptions): Promise<DockerRunRes
logger.debug(`Running: docker ${args.join(' ')}`);

const startTime = Date.now();
const result = await execDockerWithTimeout(args, options.timeoutMs);
const result = await execDockerWithTimeout(args, options.timeoutMs, options.onStdout, options.onStderr);
const durationMs = Date.now() - startTime;

const inspectResult = await inspectExitedContainer(options.containerName).catch(() => null);
Expand Down Expand Up @@ -188,7 +188,7 @@ function execDocker(args: string[]): Promise<ExecResult> {
});
}

function execDockerWithTimeout(args: string[], timeoutMs: number): Promise<ExecResult> {
function execDockerWithTimeout(args: string[], timeoutMs: number, onStdout?: (chunk: string) => void, onStderr?: (chunk: string) => void): Promise<ExecResult> {
return new Promise((resolve) => {
const proc = spawn('docker', args, { stdio: ['ignore', 'pipe', 'pipe'] });

Expand All @@ -202,11 +202,15 @@ function execDockerWithTimeout(args: string[], timeoutMs: number): Promise<ExecR
}, timeoutMs);

proc.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
const chunk = data.toString();
stdout += chunk;
onStdout?.(chunk);
});

proc.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
const chunk = data.toString();
stderr += chunk;
onStderr?.(chunk);
});
Comment on lines 204 to 214
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onStdout/onStderr are user-provided callbacks invoked from the data event handlers. If a callback throws, it will raise an unhandled exception from the event emitter and can crash the process (or cause the Promise to never resolve). Wrap callback invocations in a try/catch (and optionally log) so streaming cannot destabilize execution.

Copilot uses AI. Check for mistakes.

proc.on('close', (code) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/runtime/runtime.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface DockerRunOptions {
env: Record<string, string>;
command?: string[];
security?: SecurityOptions;
/** Called with each stdout chunk as it arrives from the container */
onStdout?: (chunk: string) => void;
/** Called with each stderr chunk as it arrives from the container */
onStderr?: (chunk: string) => void;
}

export interface SecurityOptions {
Expand Down
5 changes: 4 additions & 1 deletion packages/http/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ignite/http",
"version": "0.8.0",
"version": "0.9.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -10,6 +10,9 @@
"types": "./dist/index.d.ts"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun run tsc",
"dev": "bun run tsc --watch",
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-bun/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ignite/runtime-bun",
"version": "0.8.0",
"version": "0.9.0",
"private": true,
"description": "Bun runtime adapter for Ignite"
}
5 changes: 4 additions & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ignite/shared",
"version": "0.8.0",
"version": "0.9.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -10,6 +10,9 @@
"types": "./dist/index.d.ts"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun run tsc",
"dev": "bun run tsc --watch",
Expand Down
Loading