From c8722b243de765a11181dafa3279c5ceda1b53e7 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 29 Aug 2026 04:41:04 +0000 Subject: [PATCH] test(ci): bind the three-way image-tag hashFiles() expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit evals.yml, evals-periodic.yml, and ci-image.yml each compute the CI image tag from hashFiles('.github/docker/Dockerfile.ci', 'bun.lock', 'patches/**') — synced by comment only (TODOS.md 'CI three-way image-tag drift'). If one input list drifts, that workflow computes a different tag for the same content: eval lanes silently rebuild the image every run, or ci-image prebuilds a tag nobody looks up. The test extracts each tag-computation site and fails on any mismatch. Co-Authored-By: Claude Fable 5 --- test/ci-image-tag-binding.test.ts | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/ci-image-tag-binding.test.ts diff --git a/test/ci-image-tag-binding.test.ts b/test/ci-image-tag-binding.test.ts new file mode 100644 index 000000000..b4238215d --- /dev/null +++ b/test/ci-image-tag-binding.test.ts @@ -0,0 +1,36 @@ +/** + * The CI image tag is a content hash computed independently in three + * workflows — evals.yml, evals-periodic.yml, ci-image.yml — and they were + * synced by comment only (filed in TODOS.md as the "three-way image-tag + * drift" gap). If one file's hashFiles() input list drifts, that workflow + * computes a DIFFERENT tag for the same content: the eval lanes stop finding + * the prebuilt image and silently rebuild it on every run (minutes per run, + * no red check), or ci-image prebuilds a tag nobody looks up. + */ +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 FILES = ['evals.yml', 'evals-periodic.yml', 'ci-image.yml']; + +function hashFilesCalls(name: string): string[] { + const source = fs.readFileSync( + path.join(ROOT, '.github', 'workflows', name), 'utf-8'); + // Only tag-computation sites: hashFiles() inside a `tag=` output line. + return [...source.matchAll(/tag=[^\n]*?(hashFiles\([^)]*\))/g)].map((m) => m[1]); +} + +describe('ci image tag binding', () => { + test('all three workflows compute the tag from the identical hashFiles() input list', () => { + const perFile = FILES.map((f) => ({ file: f, calls: hashFilesCalls(f) })); + for (const { file, calls } of perFile) { + // Each workflow computes the tag exactly once; zero means the scan + // regex rotted (must fail loudly, not vacuously pass). + expect(calls, `${file}: expected exactly one tag hashFiles() site`).toHaveLength(1); + } + const expressions = [...new Set(perFile.map((p) => p.calls[0]))]; + const detail = perFile.map((p) => `${p.file} → ${p.calls[0]}`).join('\n'); + expect(expressions, `image-tag hashFiles() drift:\n${detail}`).toHaveLength(1); + }); +});