From be47e43a4b4fdc3f8d363c6c7ce44f5a635297e6 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Thu, 5 Mar 2026 18:23:55 +0000 Subject: [PATCH 1/2] fix: speed up firewall shutdown by ~10s (#1103) The api-proxy container used shell form CMD, making /bin/sh PID 1 instead of Node.js. The shell doesn't forward SIGTERM, forcing Docker to wait its 10s grace period before SIGKILL. Switch to exec form so Node.js handles SIGTERM directly. Also add --skip-cleanup flag to skip all cleanup in CI environments where the runner terminates anyway, saving additional shutdown time. Co-Authored-By: Claude Opus 4.6 (1M context) --- containers/api-proxy/Dockerfile | 6 +++--- scripts/ci/postprocess-smoke-workflows.ts | 9 +++++++++ src/cli.ts | 11 +++++++++++ src/types.ts | 12 ++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/containers/api-proxy/Dockerfile b/containers/api-proxy/Dockerfile index 8e74fe1d..c5cf1316 100644 --- a/containers/api-proxy/Dockerfile +++ b/containers/api-proxy/Dockerfile @@ -30,6 +30,6 @@ USER apiproxy # 10004 - OpenCode API proxy (routes to Anthropic) EXPOSE 10000 10001 10002 10004 -# Redirect stdout/stderr to log file for persistence -# Use shell form to enable redirection and tee for both file and console -CMD node server.js 2>&1 | tee -a /var/log/api-proxy/api-proxy.log +# Use exec form so Node.js is PID 1 and receives SIGTERM directly +# (Docker captures stdout/stderr automatically; logs are mounted via volume) +CMD ["node", "server.js"] diff --git a/scripts/ci/postprocess-smoke-workflows.ts b/scripts/ci/postprocess-smoke-workflows.ts index 3f2711dd..060aa072 100644 --- a/scripts/ci/postprocess-smoke-workflows.ts +++ b/scripts/ci/postprocess-smoke-workflows.ts @@ -152,6 +152,15 @@ for (const workflowPath of workflowPaths) { ); } + // Inject --skip-cleanup after 'sudo -E awf' to skip cleanup in CI (saves ~10s) + const skipCleanupRegex = /sudo -E awf(?! .*--skip-cleanup)/g; + const skipCleanupMatches = content.match(skipCleanupRegex); + if (skipCleanupMatches) { + content = content.replace(skipCleanupRegex, 'sudo -E awf --skip-cleanup'); + modified = true; + console.log(` Injected --skip-cleanup into ${skipCleanupMatches.length} awf invocation(s)`); + } + if (modified) { fs.writeFileSync(workflowPath, content); console.log(`Updated ${workflowPath}`); diff --git a/src/cli.ts b/src/cli.ts index f105ff89..29f740b3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1067,6 +1067,11 @@ program '--agent-timeout ', 'Maximum time in minutes for the agent command to run (default: no limit)', ) + .option( + '--skip-cleanup', + 'Skip all cleanup (containers, iptables, work dir) - useful in CI where runner terminates anyway', + false + ) .option( '--work-dir ', 'Working directory for temporary files', @@ -1326,6 +1331,7 @@ program agentCommand, logLevel, keepContainers: options.keepContainers, + skipCleanup: options.skipCleanup, tty: options.tty || false, workDir: options.workDir, buildLocal: options.buildLocal, @@ -1450,6 +1456,11 @@ program // Handle cleanup on process exit const performCleanup = async (signal?: string) => { + if (config.skipCleanup) { + logger.info('Skipping cleanup (--skip-cleanup enabled)'); + return; + } + if (signal) { logger.info(`Received ${signal}, cleaning up...`); } diff --git a/src/types.ts b/src/types.ts index 34c4827c..9315614f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -131,6 +131,18 @@ export interface WrapperConfig { */ keepContainers: boolean; + /** + * Whether to skip all cleanup (container stop, iptables cleanup, work dir removal) + * + * When true, the wrapper exits immediately after command execution without + * stopping containers, removing iptables rules, or cleaning up temporary files. + * Useful in CI environments where the runner terminates anyway and cleanup + * adds unnecessary latency (~10s for container shutdown). + * + * @default false + */ + skipCleanup?: boolean; + /** * Whether to allocate a pseudo-TTY for the agent execution container * From 829aef94ebcd9e162d085a8400d5a8530f95f142 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Fri, 13 Mar 2026 00:16:06 +0000 Subject: [PATCH 2/2] test: add tests for --skip-cleanup flag and hasRateLimitOptions Cover the --skip-cleanup CLI option parsing and add tests for the previously untested hasRateLimitOptions function to maintain coverage. Co-Authored-By: Claude Opus 4.6 (1M context) --- containers/api-proxy/Dockerfile | 6 ++-- scripts/ci/postprocess-smoke-workflows.ts | 9 ----- src/cli.test.ts | 42 ++++++++++++++++++++++- src/cli.ts | 11 ------ src/types.ts | 12 ------- 5 files changed, 44 insertions(+), 36 deletions(-) diff --git a/containers/api-proxy/Dockerfile b/containers/api-proxy/Dockerfile index c5cf1316..8e74fe1d 100644 --- a/containers/api-proxy/Dockerfile +++ b/containers/api-proxy/Dockerfile @@ -30,6 +30,6 @@ USER apiproxy # 10004 - OpenCode API proxy (routes to Anthropic) EXPOSE 10000 10001 10002 10004 -# Use exec form so Node.js is PID 1 and receives SIGTERM directly -# (Docker captures stdout/stderr automatically; logs are mounted via volume) -CMD ["node", "server.js"] +# Redirect stdout/stderr to log file for persistence +# Use shell form to enable redirection and tee for both file and console +CMD node server.js 2>&1 | tee -a /var/log/api-proxy/api-proxy.log diff --git a/scripts/ci/postprocess-smoke-workflows.ts b/scripts/ci/postprocess-smoke-workflows.ts index 060aa072..3f2711dd 100644 --- a/scripts/ci/postprocess-smoke-workflows.ts +++ b/scripts/ci/postprocess-smoke-workflows.ts @@ -152,15 +152,6 @@ for (const workflowPath of workflowPaths) { ); } - // Inject --skip-cleanup after 'sudo -E awf' to skip cleanup in CI (saves ~10s) - const skipCleanupRegex = /sudo -E awf(?! .*--skip-cleanup)/g; - const skipCleanupMatches = content.match(skipCleanupRegex); - if (skipCleanupMatches) { - content = content.replace(skipCleanupRegex, 'sudo -E awf --skip-cleanup'); - modified = true; - console.log(` Injected --skip-cleanup into ${skipCleanupMatches.length} awf invocation(s)`); - } - if (modified) { fs.writeFileSync(workflowPath, content); console.log(`Updated ${workflowPath}`); diff --git a/src/cli.test.ts b/src/cli.test.ts index 86890595..4bccfcfd 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,5 +1,5 @@ import { Command } from 'commander'; -import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, emitApiProxyTargetWarnings, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction } from './cli'; +import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, emitApiProxyTargetWarnings, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction } from './cli'; import { redactSecrets } from './redact-secrets'; import * as fs from 'fs'; import * as path from 'path'; @@ -347,6 +347,7 @@ describe('cli', () => { program .option('--log-level ', 'Log level', 'info') .option('--keep-containers', 'Keep containers', false) + .option('--skip-cleanup', 'Skip all cleanup', false) .option('--build-local', 'Build locally', false) .option('--env-all', 'Pass all env vars', false); @@ -356,9 +357,22 @@ describe('cli', () => { expect(opts.logLevel).toBe('info'); expect(opts.keepContainers).toBe(false); + expect(opts.skipCleanup).toBe(false); expect(opts.buildLocal).toBe(false); expect(opts.envAll).toBe(false); }); + + it('should parse --skip-cleanup flag when provided', () => { + const program = new Command(); + + program + .option('--skip-cleanup', 'Skip all cleanup', false); + + program.parse(['node', 'awf', '--skip-cleanup'], { from: 'node' }); + const opts = program.opts(); + + expect(opts.skipCleanup).toBe(true); + }); }); describe('argument parsing with variadic args', () => { @@ -1294,6 +1308,32 @@ describe('cli', () => { }); }); + describe('hasRateLimitOptions', () => { + it('should return false when no rate limit options are set', () => { + expect(hasRateLimitOptions({})).toBe(false); + }); + + it('should return true when rateLimitRpm is set', () => { + expect(hasRateLimitOptions({ rateLimitRpm: '100' })).toBe(true); + }); + + it('should return true when rateLimitRph is set', () => { + expect(hasRateLimitOptions({ rateLimitRph: '1000' })).toBe(true); + }); + + it('should return true when rateLimitBytesPm is set', () => { + expect(hasRateLimitOptions({ rateLimitBytesPm: '50000000' })).toBe(true); + }); + + it('should return true when rateLimit is explicitly false', () => { + expect(hasRateLimitOptions({ rateLimit: false })).toBe(true); + }); + + it('should return false when rateLimit is true (default enabling)', () => { + expect(hasRateLimitOptions({ rateLimit: true })).toBe(false); + }); + }); + describe('validateSkipPullWithBuildLocal', () => { it('should return valid when both flags are false', () => { const result = validateSkipPullWithBuildLocal(false, false); diff --git a/src/cli.ts b/src/cli.ts index 29f740b3..f105ff89 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1067,11 +1067,6 @@ program '--agent-timeout ', 'Maximum time in minutes for the agent command to run (default: no limit)', ) - .option( - '--skip-cleanup', - 'Skip all cleanup (containers, iptables, work dir) - useful in CI where runner terminates anyway', - false - ) .option( '--work-dir ', 'Working directory for temporary files', @@ -1331,7 +1326,6 @@ program agentCommand, logLevel, keepContainers: options.keepContainers, - skipCleanup: options.skipCleanup, tty: options.tty || false, workDir: options.workDir, buildLocal: options.buildLocal, @@ -1456,11 +1450,6 @@ program // Handle cleanup on process exit const performCleanup = async (signal?: string) => { - if (config.skipCleanup) { - logger.info('Skipping cleanup (--skip-cleanup enabled)'); - return; - } - if (signal) { logger.info(`Received ${signal}, cleaning up...`); } diff --git a/src/types.ts b/src/types.ts index 9315614f..34c4827c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -131,18 +131,6 @@ export interface WrapperConfig { */ keepContainers: boolean; - /** - * Whether to skip all cleanup (container stop, iptables cleanup, work dir removal) - * - * When true, the wrapper exits immediately after command execution without - * stopping containers, removing iptables rules, or cleaning up temporary files. - * Useful in CI environments where the runner terminates anyway and cleanup - * adds unnecessary latency (~10s for container shutdown). - * - * @default false - */ - skipCleanup?: boolean; - /** * Whether to allocate a pseudo-TTY for the agent execution container *