From 003342f604a78e88fd958b3ef2e7d4506d41190c Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 18 Jul 2026 19:55:07 -0400 Subject: [PATCH] hotfix(docker): select Alpine-compatible CLIProxy asset --- .github/workflows/docker-release.yml | 12 ++- docker/Dockerfile.integrated | 2 + src/cliproxy/binary/platform-detector.ts | 12 ++- .../__tests__/backend-selection.test.js | 76 +++++++++++++++++++ .../docker-release-workflow-context.test.ts | 9 +++ 5 files changed, 108 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index a4c95d3a..1f7ba8c2 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -365,6 +365,14 @@ jobs: -p 18317:8317 \ "${{ steps.image.outputs.ref }}" + print_service_logs() { + docker logs "${CONTAINER_NAME}" --tail 100 >&2 || true + docker exec "${CONTAINER_NAME}" \ + sh -c 'tail -n 100 /var/log/ccs/cliproxy.log' >&2 || true + docker exec "${CONTAINER_NAME}" \ + sh -c 'tail -n 100 /var/log/ccs/ccs-dashboard.log' >&2 || true + } + echo "[i] Waiting for container healthcheck (up to 60s)..." HEALTHY=0 for i in $(seq 1 12); do @@ -376,7 +384,7 @@ jobs: fi if [[ "${STATUS}" == "unhealthy" ]]; then echo "[X] Container marked unhealthy" - docker logs "${CONTAINER_NAME}" --tail 50 >&2 + print_service_logs exit 1 fi echo " [${i}/12] status=${STATUS}, waiting 5s..." @@ -384,7 +392,7 @@ jobs: done if [[ "${HEALTHY}" -ne 1 ]]; then echo "[X] Container did not become healthy within 60s (last status: ${STATUS})" >&2 - docker logs "${CONTAINER_NAME}" --tail 100 >&2 + print_service_logs docker inspect "${CONTAINER_NAME}" --format='{{json .State}}' >&2 || true exit 1 fi diff --git a/docker/Dockerfile.integrated b/docker/Dockerfile.integrated index 3ef0a74f..0b080ccb 100644 --- a/docker/Dockerfile.integrated +++ b/docker/Dockerfile.integrated @@ -7,6 +7,8 @@ FROM eceasy/cli-proxy-api:latest@sha256:4fa722b9ff83adfbe6e350d433b7dcb754756aa6 ARG CCS_NPM_VERSION=latest +ENV CCS_CLIPROXY_NO_PLUGIN_ASSET=1 + # CCS integrated image: CCS CLI + CLIProxy + supervisord. # Design choice (issue #1251 final scope): single-image strategy. The originally- # proposed `:full` variant bundling claude-code/gemini-cli/grok-cli/opencode was diff --git a/src/cliproxy/binary/platform-detector.ts b/src/cliproxy/binary/platform-detector.ts index 14c3a07b..fdb78d83 100644 --- a/src/cliproxy/binary/platform-detector.ts +++ b/src/cliproxy/binary/platform-detector.ts @@ -166,7 +166,17 @@ export function detectPlatform( const ver = version || config.fallbackVersion; const extension: ArchiveExtension = os === 'windows' ? 'zip' : 'tar.gz'; const assetArch = getReleaseArchForBackend(backend, ver, arch, releaseArch); - const assetVariant = backend === 'plus' && usesPlusNoPluginAsset(ver, os) ? '_no-plugin' : ''; + // The integrated Alpine image opts into the portable static upstream asset. + // Normal Linux installs keep plugin-capable archives for backward compatibility. + const usesOriginalNoPluginAsset = + process.env.CCS_CLIPROXY_NO_PLUGIN_ASSET === '1' && + backend === 'original' && + os === 'linux' && + isAtLeastVersion(ver, '7.1.52'); + const assetVariant = + usesOriginalNoPluginAsset || (backend === 'plus' && usesPlusNoPluginAsset(ver, os)) + ? '_no-plugin' + : ''; const binaryName = `${config.binaryPrefix}_${ver}_${os}_${assetArch}${assetVariant}.${extension}`; return { diff --git a/src/cliproxy/config/__tests__/backend-selection.test.js b/src/cliproxy/config/__tests__/backend-selection.test.js index 3de155cb..e48d1091 100644 --- a/src/cliproxy/config/__tests__/backend-selection.test.js +++ b/src/cliproxy/config/__tests__/backend-selection.test.js @@ -24,6 +24,26 @@ describe('Backend Selection', () => { } } + function withEnvironmentVariable(name, value, callback) { + const originalValue = process.env[name]; + + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } + + try { + callback(); + } finally { + if (originalValue === undefined) { + delete process.env[name]; + } else { + process.env[name] = originalValue; + } + } + } + describe('BACKEND_CONFIG', () => { it('has correct configuration for original backend', () => { const config = platformDetector.BACKEND_CONFIG.original; @@ -73,6 +93,62 @@ describe('Backend Selection', () => { assert(!info.binaryName.includes('CLIProxyAPIPlus')); }); + it('uses static no-plugin assets when the integrated Linux image opts in', () => { + withEnvironmentVariable('CCS_CLIPROXY_NO_PLUGIN_ASSET', '1', () => { + withMockedProcessPlatform('linux', 'x64', () => { + assert.strictEqual( + platformDetector.detectPlatform('7.2.88', 'original').binaryName, + 'CLIProxyAPI_7.2.88_linux_amd64_no-plugin.tar.gz' + ); + assert.strictEqual( + platformDetector.getDownloadUrl('7.2.88', 'original'), + 'https://github.com/router-for-me/CLIProxyAPI/releases/download/v7.2.88/CLIProxyAPI_7.2.88_linux_amd64_no-plugin.tar.gz' + ); + }); + + withMockedProcessPlatform('linux', 'arm64', () => { + assert.strictEqual( + platformDetector.detectPlatform('7.2.88', 'original').binaryName, + 'CLIProxyAPI_7.2.88_linux_aarch64_no-plugin.tar.gz' + ); + }); + }); + }); + + it('keeps plugin-capable original archives for normal Linux installs', () => { + withMockedProcessPlatform('linux', 'arm64', () => { + assert.strictEqual( + platformDetector.detectPlatform('7.2.88', 'original').binaryName, + 'CLIProxyAPI_7.2.88_linux_aarch64.tar.gz' + ); + }); + }); + + it('keeps original archive names compatible before Linux no-plugin assets existed', () => { + withEnvironmentVariable('CCS_CLIPROXY_NO_PLUGIN_ASSET', '1', () => { + withMockedProcessPlatform('linux', 'x64', () => { + assert.strictEqual( + platformDetector.detectPlatform('7.1.51', 'original').binaryName, + 'CLIProxyAPI_7.1.51_linux_amd64.tar.gz' + ); + }); + }); + + withMockedProcessPlatform('darwin', 'arm64', () => { + assert.strictEqual( + platformDetector.detectPlatform('7.2.88', 'original').binaryName, + 'CLIProxyAPI_7.2.88_darwin_aarch64.tar.gz' + ); + }); + + withMockedProcessPlatform('win32', 'x64', () => { + assert.strictEqual( + platformDetector.detectPlatform('7.2.88', 'original').binaryName, + 'CLIProxyAPI_7.2.88_windows_amd64.zip' + ); + }); + }); + it('keeps old plus non-Windows archive names unsuffixed', () => { withMockedProcessPlatform('darwin', 'arm64', () => { assert.strictEqual( diff --git a/tests/unit/docker/docker-release-workflow-context.test.ts b/tests/unit/docker/docker-release-workflow-context.test.ts index 8a5ba0c3..bd65d81f 100644 --- a/tests/unit/docker/docker-release-workflow-context.test.ts +++ b/tests/unit/docker/docker-release-workflow-context.test.ts @@ -43,6 +43,15 @@ describe('docker release workflow context', () => { expect(dockerfile).toContain('HEALTHCHECK'); expect(dockerfile).toContain('127.0.0.1:3000'); expect(dockerfile).toContain('127.0.0.1:8317'); + expect(dockerfile).toContain('ENV CCS_CLIPROXY_NO_PLUGIN_ASSET=1'); + }); + + test('prints service logs when the integrated image fails its healthcheck', () => { + const workflow = readFileSync(join(repoRoot, '.github/workflows/docker-release.yml'), 'utf8'); + + expect(workflow).toContain('docker logs "${CONTAINER_NAME}"'); + expect(workflow).toContain('/var/log/ccs/cliproxy.log'); + expect(workflow).toContain('/var/log/ccs/ccs-dashboard.log'); }); test('lets network-contract smoke tests avoid fixed host port collisions', () => {