diff --git a/.github/docker/Dockerfile.ci b/.github/docker/Dockerfile.ci index 97b5b6b55..aef646180 100644 --- a/.github/docker/Dockerfile.ci +++ b/.github/docker/Dockerfile.ci @@ -33,9 +33,11 @@ RUN printf 'Acquire::Retries "5";\nAcquire::http::Timeout "30";\nAcquire::https: # bash bins shell out to it (macOS ships python3; the base image doesn't). # file: skill-validation's no-compiled-binaries-in-git check runs `file --mime-type`. # poppler-utils: make-pdf's e2e gates hard-require pdftotext/pdffonts/pdfinfo in CI. +# zsh: the sourced-helper guard sources every skill helper under bash and zsh +# (macOS's default shell) and fails under CI=true when zsh is missing. RUN for i in 1 2 3; do \ apt-get update && apt-get install -y --no-install-recommends \ - git curl unzip xz-utils ca-certificates jq bc gpg python3 python3-venv file poppler-utils gcc libc6-dev && break || \ + git curl unzip xz-utils ca-certificates jq bc gpg python3 python3-venv file poppler-utils gcc libc6-dev zsh && break || \ (echo "apt retry $i/3 after failure"; sleep 10); \ done \ && rm -rf /var/lib/apt/lists/* diff --git a/.github/workflows/free-tests.yml b/.github/workflows/free-tests.yml index 33e04fae5..516978e13 100644 --- a/.github/workflows/free-tests.yml +++ b/.github/workflows/free-tests.yml @@ -102,7 +102,8 @@ jobs: # $TMPDIR sits under /var/folders and /var is a symlink to /private/var, so # code that compares a realpath with an unresolved path passes on Linux's # /tmp and fails here (#2982). TMPDIR is deliberately left at the runner's - # default, and the job fails if that layout is not symlinked. + # default, and the job fails if that layout is not symlinked. It also sources + # the Codex probe from zsh, macOS's default shell (#3024; no paid call). macos-named-regressions: runs-on: macos-latest timeout-minutes: 20 @@ -124,6 +125,13 @@ jobs: git config --global user.email "free-tests-ci@gstack.test" git config --global user.name "Free Tests CI" git config --global init.defaultBranch main + - name: Codex probe selects a model when sourced from zsh (macOS default shell, no paid call) + working-directory: ${{ runner.temp }} + run: | + set -euo pipefail + out="$(zsh -c 'source "$GITHUB_WORKSPACE/bin/gstack-codex-probe" && _gstack_codex_select_model exec' 2>&1)" + printf '%s\n' "$out" + printf '%s\n' "$out" | grep -E '^CODEX_MODEL: [A-Za-z0-9._:/-]+ \(exec; source: ' - name: Run the named macOS regressions on the default temp root run: | set -euo pipefail @@ -131,7 +139,7 @@ jobs: resolved="$(realpath "$tmp")" echo "temp root: $tmp -> $resolved" test "$resolved" != "${tmp%/}" - files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts) + files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts) for file in "${files[@]}"; do test -f "$file"; done bun test "${files[@]}" diff --git a/test/regression-issue3024-zsh-sourced-helpers.test.ts b/test/regression-issue3024-zsh-sourced-helpers.test.ts index 9494e9b9b..f144a710c 100644 --- a/test/regression-issue3024-zsh-sourced-helpers.test.ts +++ b/test/regression-issue3024-zsh-sourced-helpers.test.ts @@ -285,4 +285,23 @@ describe('#3024: sourced helpers work under bash and zsh', () => { fs.rmSync(empty, { recursive: true, force: true }); } }); + + test('CI keeps zsh: the free lane and the eval image install it, and the macOS lane sources the probe from zsh', () => { + const workflow = Bun.YAML.parse(fs.readFileSync(path.join(ROOT, '.github/workflows/free-tests.yml'), 'utf8')) as any; + const runs = (job: string) => (workflow.jobs[job].steps as any[]).map(step => String(step.run ?? '')).join('\n'); + expect(runs('free-suite')).toMatch(/apt-get install[^\n]*\bzsh\b/); + expect(fs.readFileSync(path.join(ROOT, '.github/docker/Dockerfile.ci'), 'utf8')).toMatch(/apt-get install -y --no-install-recommends \\\n[^\n]*\bzsh\b/); + const mac = workflow.jobs['macos-named-regressions']; + expect(mac['runs-on']).toMatch(/^macos-/); + const step = (mac.steps as any[]).find(s => String(s.run ?? '').includes('_gstack_codex_select_model exec')); + expect(step?.run).toContain(`zsh -c 'source "$GITHUB_WORKSPACE/bin/gstack-codex-probe" && _gstack_codex_select_model exec'`); + expect(runs('macos-named-regressions')).toContain('test/regression-issue3024-zsh-sourced-helpers.test.ts'); + if (skipShell('zsh')) return; + const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'zsh-helpers-ci-step-')); + try { + const r = run('bash', step.run, { GITHUB_WORKSPACE: ROOT, CODEX_HOME: codexHome }); + expect(r.code).toBe(0); + expect(r.stdout).toMatch(/CODEX_MODEL: \S+ \(exec; source: /); + } finally { fs.rmSync(codexHome, { recursive: true, force: true }); } + }); });