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+$/); + }); +});