diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9283fbf0..7448c0e13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,8 +49,9 @@ jobs: # font stack, which a bare runner does not provide. # # This tag must match the `playwright` devDependency, which ships the - # matching browser build. Renovate groups the two so they always move - # together; `scripts/visual.sh` reads the version straight from package.json. + # matching browser build. `scripts/visual.sh` derives the image from it for + # local runs, and `src/main/ciDependencyCompatibility.test.ts` asserts the + # two stay in sync so the version cannot drift unnoticed. container: mcr.microsoft.com/playwright:v1.64.0-noble@sha256:06a9939e57531807f8d5fd76ce44b53165ffb7d7501d87ab10e285c20b1e971f steps: diff --git a/renovate.json b/renovate.json index 7e1d62942..55fb04f13 100644 --- a/renovate.json +++ b/renovate.json @@ -12,13 +12,6 @@ "matchPackageNames": ["got"], "allowedVersions": "<12" }, - { - "description": "Group Playwright with the browser container image used by visual regression tests, and wait for both updates to pass the release-age check.", - "matchPackageNames": ["playwright", "mcr.microsoft.com/playwright"], - "groupName": "playwright", - "minimumReleaseAge": "3 days", - "internalChecksFilter": "strict" - }, { "description": "Give core runtime dependency updates a distinct scope so release-please can include them in the changelog.", "matchPackageNames": [ diff --git a/src/main/ciDependencyCompatibility.test.ts b/src/main/ciDependencyCompatibility.test.ts new file mode 100644 index 000000000..94f6c6bd0 --- /dev/null +++ b/src/main/ciDependencyCompatibility.test.ts @@ -0,0 +1,79 @@ +import { readFileSync } from 'node:fs'; + +/** + * Guards version couplings that CI depends on but that no single manifest + * expresses. Renovate (or a manual edit) can move one half of a pair and leave + * the other behind, which otherwise only surfaces as a confusing failure deep in + * the test or visual-regression run. Asserting the couplings here — against the + * versions actually installed — turns that into a fast, explicit failure. + * + * The vitest couplings are exact peer dependencies: `@vitest/browser-playwright` + * and `@vitest/coverage-v8` peer on the same `vitest` version that `vite-plus` + * bundles, and `vite` must resolve to the `@voidzero-dev/vite-plus-core` build + * that `vite-plus` ships. + */ +const repoRoot = new URL('../../', import.meta.url); + +function readJson(path: string): T { + return JSON.parse(readFileSync(new URL(path, repoRoot), 'utf8')) as T; +} + +type PackageJson = { + name?: string; + version: string; + dependencies?: Record; +}; + +const workflow = readFileSync(new URL('.github/workflows/test.yml', repoRoot), 'utf8'); + +const playwright = readJson('node_modules/playwright/package.json'); +const vitest = readJson('node_modules/vitest/package.json'); +const browserPlaywright = readJson( + 'node_modules/@vitest/browser-playwright/package.json', +); +const coverageV8 = readJson('node_modules/@vitest/coverage-v8/package.json'); +const vitePlus = readJson('node_modules/vite-plus/package.json'); +const vite = readJson('node_modules/vite/package.json'); + +const PINNED_PLAYWRIGHT_IMAGE = + /container:\s+mcr\.microsoft\.com\/playwright:v([^@\s]+)-noble@(sha256:[0-9a-f]{64})/; + +describe('CI dependency compatibility', () => { + describe('playwright', () => { + it('pins the visual-regression image to the installed playwright version', () => { + const pinned = workflow.match(PINNED_PLAYWRIGHT_IMAGE); + + expect( + pinned, + 'Expected a digest-pinned mcr.microsoft.com/playwright container in the visual-regression job of .github/workflows/test.yml.', + ).not.toBeNull(); + + expect( + pinned?.[1], + [ + `The visual-regression container must run mcr.microsoft.com/playwright:v${playwright.version}-noble to match the installed playwright (driven by the package.json devDependency).`, + 'Update the tag and refresh the pinned digest with:', + ` docker buildx imagetools inspect mcr.microsoft.com/playwright:v${playwright.version}-noble --format '{{.Manifest.Digest}}'`, + ].join('\n'), + ).toBe(playwright.version); + }); + }); + + describe('vitest family', () => { + it('installs the browser provider and coverage reporter at the vitest version', () => { + expect(browserPlaywright.version).toBe(vitest.version); + expect(coverageV8.version).toBe(vitest.version); + }); + + it('installs vitest at the version bundled by vite-plus', () => { + expect(vitest.version).toBe(vitePlus.dependencies?.vitest); + }); + }); + + describe('vite family', () => { + it('resolves vite to the vite-plus-core build that vite-plus ships', () => { + expect(vite.name).toBe('@voidzero-dev/vite-plus-core'); + expect(vitePlus.dependencies?.vite).toBe(`npm:${vite.name}@${vite.version}`); + }); + }); +});