From 097b06300e82f0dbe093c3126fd2adcd641aa25e Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 1 Nov 2026 11:32:21 -0800 Subject: [PATCH] ci: pin the Playwright image by digest (#609) The v1.64.1-noble tag was re-pushed with Node 26, which fails the Node 24 guard in every browser job. Pin the last green image's digest so the browsers, fonts and Node only change through a reviewed bump. --- .github/workflows/test.yml | 2 +- .github/workflows/viewerjs-check.yml | 6 +++--- TESTING.md | 5 ++++- tests/vendor/browser-image.test.mjs | 6 ++++-- tests/vendor/ci-workflow.test.mjs | 4 ++-- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33a074b6..d276cd0d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,7 +80,7 @@ jobs: # pulled them from Ubuntu's apt mirror on every run, and three runs (#349, #378, #382) crawled at # ~26 kB/s for 11-12 minutes until the job hit its limit after every test had passed. container: - image: mcr.microsoft.com/playwright:v1.64.1-noble + image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea options: --user 1001 timeout-minutes: 24 steps: diff --git a/.github/workflows/viewerjs-check.yml b/.github/workflows/viewerjs-check.yml index 0b67b161..6aca9e43 100644 --- a/.github/workflows/viewerjs-check.yml +++ b/.github/workflows/viewerjs-check.yml @@ -53,7 +53,7 @@ jobs: # same pinned Playwright image as local runs (tag = @playwright/test in tools/vendor/package-lock.json; # tests/vendor/browser-image.test.mjs checks it). Browsers and fonts come from the image. container: - image: mcr.microsoft.com/playwright:v1.64.1-noble + image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea options: --user 1001 timeout-minutes: 20 # Public-repo ubuntu-latest runners have 4 vCPUs: three Playwright workers, each with its own test @@ -121,7 +121,7 @@ jobs: runs-on: ubuntu-latest # Same image as the shards: the raw files name sources by the container's workspace path. container: - image: mcr.microsoft.com/playwright:v1.64.1-noble + image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea options: --user 1001 timeout-minutes: 10 steps: @@ -192,7 +192,7 @@ jobs: # same pinned Playwright image as local runs (tag = @playwright/test in tools/vendor/package-lock.json; # tests/vendor/browser-image.test.mjs checks it). Browsers and fonts come from the image. container: - image: mcr.microsoft.com/playwright:v1.64.1-noble + image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea options: --user 1001 timeout-minutes: 20 # Public-repo ubuntu-latest runners have 4 vCPUs: three Playwright workers, each with its own test diff --git a/TESTING.md b/TESTING.md index 7f2bc2d1..79522c97 100644 --- a/TESTING.md +++ b/TESTING.md @@ -111,8 +111,11 @@ distributions, browser system dependencies may need manual installation. For a quick browser run, use `test:workflows -- --project chromium`. CI runs the browser jobs inside the pinned image -`mcr.microsoft.com/playwright:v<@playwright/test version>-noble` +`mcr.microsoft.com/playwright:v<@playwright/test version>-noble@sha256:` (`tests/vendor/browser-image.test.mjs` keeps the tag in step with the lockfile). +The digest is part of the pin, because Microsoft re-pushes tags: `v1.64.1-noble` +moved from Node 24 to Node 26 on 2026-11-01 (#609). To bump it, change the tag and +digest together and check the new image's Node against the Node 24 guard. Concept comparisons measure geometry, and geometry follows the fonts a browser falls back to: the concept names Inter, which neither the image nor a stock runner ships. On a plain `ubuntu-latest` runner the fallback is DejaVu Sans, diff --git a/tests/vendor/browser-image.test.mjs b/tests/vendor/browser-image.test.mjs index 1b969eb9..b5b6496e 100644 --- a/tests/vendor/browser-image.test.mjs +++ b/tests/vendor/browser-image.test.mjs @@ -8,6 +8,8 @@ import { readFileSync } from 'node:fs'; const workflow = readFileSync(new URL('../../.github/workflows/viewerjs-check.yml', import.meta.url), 'utf8'); const lock = JSON.parse(readFileSync(new URL('../../tools/vendor/package-lock.json', import.meta.url), 'utf8')); const version = lock.packages['node_modules/@playwright/test'].version; +// The tag alone is not a pin: v1.64.1-noble was re-pushed with Node 26 (#609). The digest fixes the image's contents. +const pinnedImage = `^ container:\\n image: mcr\\.microsoft\\.com/playwright:v${version.replaceAll('.', '\\.')}-noble@sha256:[0-9a-f]{64}$`; function job(name) { const match = workflow.match(new RegExp(`^ ${name}:\\n([\\s\\S]*?)(?=^ [a-z][a-z-]*:\\n|(?![\\s\\S]))`, 'm')); @@ -18,7 +20,7 @@ function job(name) { for (const name of ['frontend-coverage', 'browser-workflows']) { test(`${name} runs in the pinned Playwright image matching @playwright/test`, () => { const body = job(name); - assert.match(body, new RegExp(`^ container:\\n image: mcr\\.microsoft\\.com/playwright:v${version.replaceAll('.', '\\.')}-noble$`, 'm')); + assert.match(body, new RegExp(pinnedImage, 'm')); assert.doesNotMatch(body, /playwright install/, 'browsers and their fonts come from the image, not a runner install'); // Toolcache interpreters are the runner's own builds: their entry points name host paths that do not exist in // the container (setup-python failed with `spawn .../bin/pip ENOENT`). Use the image's Python and Node. @@ -39,7 +41,7 @@ test('real ComfyUI compatibility runs in the pinned Playwright image on the revi const match = testWorkflow.match(/^ {2}comfyui:\n([\s\S]*?)(?=^ {2}[a-z][a-z-]*:\n|(?![\s\S]))/m); assert.ok(match, 'comfyui job exists'); const body = match[1]; - assert.match(body, new RegExp(`^ container:\\n image: mcr\\.microsoft\\.com/playwright:v${version.replaceAll('.', '\\.')}-noble$`, 'm')); + assert.match(body, new RegExp(pinnedImage, 'm')); assert.doesNotMatch(body, /playwright install/, 'Chromium and its system libraries come from the image, not apt'); assert.doesNotMatch(body, /apt-get|actions\/setup-(python|node)@/); assert.match(body, /python3 -m venv --without-pip /, 'a user-space venv on the image Python'); diff --git a/tests/vendor/ci-workflow.test.mjs b/tests/vendor/ci-workflow.test.mjs index fc835910..3bd96145 100644 --- a/tests/vendor/ci-workflow.test.mjs +++ b/tests/vendor/ci-workflow.test.mjs @@ -10,7 +10,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; const workflow = readFileSync(new URL('../../.github/workflows/viewerjs-check.yml', import.meta.url), 'utf8'); -const PINNED_IMAGE = /^\s+image: mcr\.microsoft\.com\/playwright:v\d+\.\d+\.\d+-noble$/m; +const PINNED_IMAGE = /^\s+image: mcr\.microsoft\.com\/playwright:v\d+\.\d+\.\d+-noble@sha256:[0-9a-f]{64}$/m; function jobs(text) { const lines = text.split('\n'); @@ -237,7 +237,7 @@ test('every npm ci restores the npm download cache keyed on the vendor lockfile assert.match(jobSteps[cacheAt], /hashFiles\('tools\/vendor\/package-lock\.json'\)/, `${id} keys the cache on the lockfile`); assert.match(jobSteps[installAt], /npm ci --prefix tools\/vendor --ignore-scripts/, `${id} still installs without scripts`); // The key names the job's own pinned image, so an image bump starts a fresh cache. - const image = /^ {6}image: mcr\.microsoft\.com\/playwright:(v\d+\.\d+\.\d+)-noble$/m.exec(body)?.[1]; + const image = /^ {6}image: mcr\.microsoft\.com\/playwright:(v\d+\.\d+\.\d+)-noble@sha256:[0-9a-f]{64}$/m.exec(body)?.[1]; assert.ok(image, `${id} runs in the pinned Playwright image`); assert.match(jobSteps[cacheAt], new RegExp(`key: npm-\\$\\{\\{ runner\\.os \\}\\}-playwright-${image.replaceAll('.', '\\.')}-\\$\\{\\{ hashFiles`), `${id} keys the cache on its image ${image}`);