From e3d05d2a73d9dac41d9bdd4cfa5bc7bdff8ca95b Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:27:20 -0400 Subject: [PATCH] Read the image's Node.js version from its binary and key the browser package cache on Playwright The runtime stage is FROM scratch and carries no NODE_VERSION, so the scheduled scan now runs the image's node under QEMU where needed. The e2e apt cache matches only the same runner image and Playwright version, with no partial restore. --- .github/workflows/e2e.yml | 11 +++++++---- .github/workflows/scan-published.yml | 13 +++++++++++-- test/repo/release-workflow.test.js | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8296e912..95f2ee6c 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -47,16 +47,19 @@ jobs: # below fetches whatever that set is missing. restore-keys: playwright-${{ runner.os }}- - - name: Read the runner image + - name: Read the runner image and the Playwright version id: image - run: echo "version=$ImageOS-$ImageVersion" >> "$GITHUB_OUTPUT" + run: | + echo "version=$ImageOS-$ImageVersion" >> "$GITHUB_OUTPUT" + echo "playwright=$(node -p "require('./package-lock.json').packages['node_modules/playwright-core'].version")" >> "$GITHUB_OUTPUT" + # No restore-keys. A partial hit installs an older package set and skips + # --with-deps, so browsers that need a new package fail to launch. - name: Cache system packages for the browsers uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/apt-archives - key: apt-${{ steps.image.outputs.version }}-${{ hashFiles('package-lock.json') }} - restore-keys: apt-${{ steps.image.outputs.version }}- + key: apt-${{ steps.image.outputs.version }}-playwright-${{ steps.image.outputs.playwright }} - name: Install browsers run: | diff --git a/.github/workflows/scan-published.yml b/.github/workflows/scan-published.yml index 7f087451..652fd1b4 100644 --- a/.github/workflows/scan-published.yml +++ b/.github/workflows/scan-published.yml @@ -47,9 +47,18 @@ jobs: with: node-version: '24' + - name: Set up QEMU + if: ${{ !cancelled() && matrix.platform != 'linux/amd64' }} + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + with: + image: docker.io/tonistiigi/binfmt:qemu-v10.2.3@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 + + # Ask the binary. The image is built from scratch and carries no NODE_VERSION. - name: Check the image's Node.js for missed security releases if: ${{ !cancelled() }} run: | - version=$(docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \ - ghcr.io/sandobserver/stackyard:latest | sed -n 's/^NODE_VERSION=//p') + version=$(docker run --rm --platform "$PLATFORM" --entrypoint node \ + ghcr.io/sandobserver/stackyard:latest -p process.versions.node) node scripts/node-security.js "$version" + env: + PLATFORM: ${{ matrix.platform }} diff --git a/test/repo/release-workflow.test.js b/test/repo/release-workflow.test.js index b2761da6..2284ade6 100644 --- a/test/repo/release-workflow.test.js +++ b/test/repo/release-workflow.test.js @@ -206,6 +206,23 @@ test('the e2e workflow stops and restarts the built image after the specs', () = assert.match(run, /"\$before" != "\$after"/, 'the restart must leave the saved config as it was'); }); +test('the browser package cache matches only the same Playwright version', () => { + const e2e = yaml.load(fs.readFileSync(path.join(root, '.github/workflows/e2e.yml'), 'utf8')); + const cache = Object.values(e2e.jobs)[0].steps.find(s => s.name === 'Cache system packages for the browsers'); + assert.match(cache.with.key, /steps\.image\.outputs\.playwright/); + assert.ok(!cache.with['restore-keys'], 'a partial hit installs an older package set and skips --with-deps'); +}); + +test('the scheduled scan reads Node.js from the image binary', () => { + const scan = yaml.load(fs.readFileSync(path.join(root, '.github/workflows/scan-published.yml'), 'utf8')); + const step = scan.jobs.scan.steps.find(s => s.name === "Check the image's Node.js for missed security releases"); + assert.doesNotMatch(step.run, /NODE_VERSION/, 'the runtime stage is FROM scratch and has no NODE_VERSION'); + assert.match(step.run, /--platform "\$PLATFORM" --entrypoint node/); + assert.match(step.run, /-p process\.versions\.node/); + const qemu = scan.jobs.scan.steps.find(s => String(s.uses).startsWith('docker/setup-qemu-action@')); + assert.ok(qemu, 'the arm64 binary needs QEMU on an amd64 runner'); +}); + /* Every other gate reads a locally built copy. These pin the checks that read the published image, and that nothing announces a release before they pass. */