From ace904d40a47c4436e8a3ac12fa1221c1c94f29a Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 29 Aug 2026 04:40:41 +0000 Subject: [PATCH] fix(ci): one bun version everywhere + drift tripwire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lanes disagreed four ways: 1.3.13 (free-tests, windows, Dockerfile.ci), latest (quality-gate, make-pdf-gate), unpinned (skill-docs, version-gate — setup-bun installs latest), 1.3.10 (.gitlab-ci.yml). Different Bun versions change the runner output shapes the strict classifiers regex-match, spawn semantics, and shell parsing — a lane on a different Bun tests a different product; Dockerfile.ci's own comment records this class biting once already (silent 1.3.13/1.3.14 drift). All surfaces pinned to 1.3.13; test/bun-version-drift.test.ts scans every workflow setup-bun stanza + Dockerfile.ci + .gitlab-ci.yml and fails on any mismatch or unpinned stanza. skill-docs also gains --frozen-lockfile (was bare bun install). Co-Authored-By: Claude Fable 5 --- .github/workflows/make-pdf-gate.yml | 2 +- .github/workflows/quality-gate.yml | 2 +- .github/workflows/skill-docs.yml | 4 +- .github/workflows/version-gate.yml | 2 + .gitlab-ci.yml | 2 +- test/bun-version-drift.test.ts | 76 +++++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 test/bun-version-drift.test.ts diff --git a/.github/workflows/make-pdf-gate.yml b/.github/workflows/make-pdf-gate.yml index ddbd184f1..eca26beab 100644 --- a/.github/workflows/make-pdf-gate.yml +++ b/.github/workflows/make-pdf-gate.yml @@ -52,7 +52,7 @@ jobs: - uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.3.13 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index 5760d1a6b..531a23cbb 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -36,7 +36,7 @@ jobs: fetch-depth: 0 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: latest + bun-version: 1.3.13 - name: Install frozen dependencies run: bun install --frozen-lockfile --ignore-scripts diff --git a/.github/workflows/skill-docs.yml b/.github/workflows/skill-docs.yml index 173f0467f..721a5afdb 100644 --- a/.github/workflows/skill-docs.yml +++ b/.github/workflows/skill-docs.yml @@ -26,7 +26,9 @@ jobs: steps: - uses: actions/checkout@v7 - uses: oven-sh/setup-bun@v2 - - run: bun install + with: + bun-version: 1.3.13 + - run: bun install --frozen-lockfile # One generation pass for ALL 10 hosts. gen-skill-docs --host all # hard-fails on any per-host generation error (scripts/gen-skill-docs.ts # aggregates failures and exits non-zero), so every host is gated on diff --git a/.github/workflows/version-gate.yml b/.github/workflows/version-gate.yml index 00a2e25ec..96f9185bd 100644 --- a/.github/workflows/version-gate.yml +++ b/.github/workflows/version-gate.yml @@ -27,6 +27,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.13 - name: Read versions id: versions diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7e5e1fa31..583d5d78d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ stages: - check variables: - BUN_VERSION: "1.3.10" + BUN_VERSION: "1.3.13" .setup-bun: &setup-bun - apt-get update -qq && apt-get install -qq -y curl jq git diff --git a/test/bun-version-drift.test.ts b/test/bun-version-drift.test.ts new file mode 100644 index 000000000..ce3dd4dd5 --- /dev/null +++ b/test/bun-version-drift.test.ts @@ -0,0 +1,76 @@ +/** + * One Bun version across every CI surface. + * + * The drift class this pins: Dockerfile.ci's comment records that the old + * `| BUN_VERSION=x.y.z bash` form silently installed latest on every image + * rebuild (observed 1.3.13/1.3.14 drift vs the 1.3.10 devs ran locally), + * and before 2026-08-29 the lanes disagreed four ways (1.3.13 / latest / + * unpinned / 1.3.10). Different Bun versions change test-runner OUTPUT + * SHAPES the strict classifiers regex-match, spawn semantics, and shell + * parsing — a lane on a different Bun is testing a different product. + * + * Bumping Bun: change every surface in one commit; this test names each one. + */ +import { describe, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const ROOT = path.resolve(__dirname, '..'); +const WORKFLOWS_DIR = path.join(ROOT, '.github', 'workflows'); + +interface Pin { + surface: string; + version: string; +} + +function collectPins(): Pin[] { + const pins: Pin[] = []; + + for (const name of fs.readdirSync(WORKFLOWS_DIR).sort()) { + if (!/\.ya?ml$/.test(name)) continue; + const source = fs.readFileSync(path.join(WORKFLOWS_DIR, name), 'utf-8'); + const lines = source.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (!/uses:\s*oven-sh\/setup-bun@/.test(lines[i])) continue; + // A pinned stanza is `with:` + `bun-version: ` within the next few + // lines; an unpinned setup-bun is itself drift (installs latest). + const window = lines.slice(i + 1, i + 4).join('\n'); + const m = window.match(/bun-version:\s*["']?([\w.]+)["']?/); + pins.push({ + surface: `${name}:${i + 1}`, + version: m ? m[1] : '', + }); + } + } + + const dockerfile = fs.readFileSync( + path.join(ROOT, '.github', 'docker', 'Dockerfile.ci'), 'utf-8'); + const dockerPin = dockerfile.match(/bash -s ["']?bun-v([\w.]+)["']?/); + pins.push({ + surface: 'Dockerfile.ci', + version: dockerPin ? dockerPin[1] : '', + }); + + const gitlab = fs.readFileSync(path.join(ROOT, '.gitlab-ci.yml'), 'utf-8'); + const gitlabPin = gitlab.match(/BUN_VERSION:\s*["']?([\w.]+)["']?/); + pins.push({ + surface: '.gitlab-ci.yml', + version: gitlabPin ? gitlabPin[1] : '', + }); + + return pins; +} + +describe('bun version pins', () => { + test('every CI surface pins the same bun version', () => { + const pins = collectPins(); + // Sanity: the scan found the known surfaces (a regex rot that finds + // nothing must fail loudly, not vacuously pass). + expect(pins.length).toBeGreaterThanOrEqual(6); + + const versions = [...new Set(pins.map((p) => p.version))]; + const detail = pins.map((p) => `${p.surface} → ${p.version}`).join('\n'); + expect(versions, `bun version drift across CI surfaces:\n${detail}`).toHaveLength(1); + expect(versions[0]).toMatch(/^\d+\.\d+\.\d+$/); + }); +});