From e8bdc9076c91c4496bb4f274ed23c2bc7abc20e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Thu, 30 Jul 2026 13:29:15 +0200 Subject: [PATCH 1/6] fix(camoufox): only build camoufox images with Playwright <1.61.0 Camoufox ships its own Firefox build whose Juggler protocol is not compatible with Playwright 1.61.0 and newer: - the Python `camoufox` package declares `playwright<1.61`, so the image build fails outright with `ResolutionImpossible` during `pip install` - `camoufox-js` declares no such constraint, but the image breaks as soon as a page is opened: `Protocol error (Browser.setDefaultViewport): Found property ".viewport.isMobile" - false which is not described in this scheme` Both were verified against Playwright 1.61.0/1.62.0; 1.60.0 is the newest version that still works. The camoufox images now get their own list of Playwright versions, taken from all releases matching the supported range instead of the last five overall - otherwise they would disappear from the matrix entirely as soon as the last five releases are all >=1.61.0. They also get their own "latest" version, so they keep receiving the moving tags (`:latest`, `:3.14`, `:24`, ...). The other playwright images are unaffected; their matrix entries are unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/matrices/node/playwright.ts | 29 +++++++++++++-- .../src/matrices/python/playwright.ts | 36 +++++++++++++++---- .../version-matrix/src/shared/cache.ts | 2 ++ .../version-matrix/src/shared/constants.ts | 14 ++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/.github/actions/version-matrix/src/matrices/node/playwright.ts b/.github/actions/version-matrix/src/matrices/node/playwright.ts index 8e3a97e8..ebbdd4df 100644 --- a/.github/actions/version-matrix/src/matrices/node/playwright.ts +++ b/.github/actions/version-matrix/src/matrices/node/playwright.ts @@ -1,3 +1,4 @@ +import { satisfies } from 'semver'; import { type CacheValues, getCertificatesUpdatedAt, @@ -5,6 +6,7 @@ import { updateCacheState, } from '../../shared/cache.ts'; import { + camoufoxPlaywrightVersionRange, emptyMatrix, latestNodeVersion, setParametersForTriggeringUpdateWorkflowOnActorTemplates, @@ -30,12 +32,23 @@ let latestApifyVersion = apifyVersions.at(-1)!; let latestCrawleeVersion = crawleeVersions.at(-1)!; let latestCamoufoxVersion = camoufoxVersions.at(-1)!; +// Camoufox is incompatible with the newest Playwright releases, so its images are built with the newest Playwright +// versions Camoufox still supports, picked from all releases instead of just the last five. +const camoufoxPlaywrightVersions = playwrightVersions + .filter((version) => satisfies(version, camoufoxPlaywrightVersionRange)) + .slice(shouldUseLastFive ? -5 : -1); +const latestCamoufoxPlaywrightVersion = camoufoxPlaywrightVersions.at(-1); + const certificatesUpdatedAt = await getCertificatesUpdatedAt(); console.error('Latest five versions', latestFivePlaywrightVersions); console.error('Latest apify version', latestApifyVersion); console.error('Latest crawlee version', latestCrawleeVersion); console.error('Latest camoufox version', latestCamoufoxVersion); +console.error( + `Playwright versions for camoufox (${camoufoxPlaywrightVersionRange})`, + camoufoxPlaywrightVersions.length ? camoufoxPlaywrightVersions : '(none, camoufox images will be skipped)', +); console.error('Node runtime versions', nodeRuntimeVersions); console.error('Certificates updated at', certificatesUpdatedAt || '(not available)'); @@ -56,6 +69,7 @@ const cacheParams: CacheValues = { APIFY_VERSION: [latestApifyVersion], CRAWLEE_VERSION: [latestCrawleeVersion], CAMOUFOX_VERSION: [latestCamoufoxVersion], + CAMOUFOX_PLAYWRIGHT_VERSION: camoufoxPlaywrightVersions, CERTIFICATES_UPDATED_AT: certificatesUpdatedAt ? [certificatesUpdatedAt] : [], }; @@ -95,8 +109,17 @@ const matrix = { }; for (const nodeVersion of supportedNodeVersions) { - for (const playwrightVersion of latestFivePlaywrightVersions) { - for (const imageName of imageNames) { + for (const imageName of imageNames) { + const isCamoufoxImage = imageName === 'node-playwright-camoufox'; + + // The camoufox image only supports a subset of the Playwright versions, so it gets its own version list, along + // with its own "latest" version - otherwise it would never receive the moving tags (e.g. `:24`, `:latest`). + const imagePlaywrightVersions = isCamoufoxImage ? camoufoxPlaywrightVersions : latestFivePlaywrightVersions; + const latestImagePlaywrightVersion = isCamoufoxImage + ? latestCamoufoxPlaywrightVersion + : latestPlaywrightVersion; + + for (const playwrightVersion of imagePlaywrightVersions) { matrix.include.push({ 'image-name': imageName, 'node-version': nodeVersion, @@ -104,7 +127,7 @@ for (const nodeVersion of supportedNodeVersions) { 'apify-version': `^${latestApifyVersion}`, 'crawlee-version': `^${latestCrawleeVersion}`, 'camoufox-version': `^${latestCamoufoxVersion}`, - 'is-latest': playwrightVersion === latestPlaywrightVersion ? 'true' : 'false', + 'is-latest': playwrightVersion === latestImagePlaywrightVersion ? 'true' : 'false', 'latest-node-version': latestNodeVersion, 'supports-arm64': arm64UnsupportedImages.has(imageName) ? 'false' : 'true', }); diff --git a/.github/actions/version-matrix/src/matrices/python/playwright.ts b/.github/actions/version-matrix/src/matrices/python/playwright.ts index e906c6c7..069b470d 100644 --- a/.github/actions/version-matrix/src/matrices/python/playwright.ts +++ b/.github/actions/version-matrix/src/matrices/python/playwright.ts @@ -6,6 +6,7 @@ import { updateCacheState, } from '../../shared/cache.ts'; import { + camoufoxPlaywrightVersionRange, emptyMatrix, latestPythonVersion, setParametersForTriggeringUpdateWorkflowOnActorTemplates, @@ -38,11 +39,22 @@ const latestFivePlaywrightVersions = playwrightVersions.slice(shouldUseLastFive const latestPlaywrightVersion = latestFivePlaywrightVersions.at(-1)!; const latestCamoufoxVersion = camoufoxVersions.at(-1)!; +// Camoufox is incompatible with the newest Playwright releases, so its images are built with the newest Playwright +// versions Camoufox still supports, picked from all releases instead of just the last five. +const camoufoxPlaywrightVersions = playwrightVersions + .filter((version) => satisfies(version, camoufoxPlaywrightVersionRange)) + .slice(shouldUseLastFive ? -5 : -1); +const latestCamoufoxPlaywrightVersion = camoufoxPlaywrightVersions.at(-1); + const certificatesUpdatedAt = await getCertificatesUpdatedAt(); console.error('Last five versions:', latestFivePlaywrightVersions); console.error('Latest playwright version:', latestPlaywrightVersion); console.error('Latest camoufox version:', latestCamoufoxVersion); +console.error( + `Playwright versions for camoufox (${camoufoxPlaywrightVersionRange}):`, + camoufoxPlaywrightVersions.length ? camoufoxPlaywrightVersions : '(none, camoufox images will be skipped)', +); console.error('Python runtime versions:', pythonRuntimeVersions); console.error('Certificates updated at:', certificatesUpdatedAt || '(not available)'); @@ -51,6 +63,7 @@ const cacheParams: CacheValues = { PYTHON_RUNTIME_VERSION: pythonRuntimeVersions, PLAYWRIGHT_VERSION: latestFivePlaywrightVersions, CAMOUFOX_VERSION: [latestCamoufoxVersion], + CAMOUFOX_PLAYWRIGHT_VERSION: camoufoxPlaywrightVersions, CERTIFICATES_UPDATED_AT: certificatesUpdatedAt ? [certificatesUpdatedAt] : [], }; @@ -92,20 +105,29 @@ for (const pythonVersion of supportedPythonVersions) { return satisfies(`${pythonVersion}.0`, constraint); })?.[1]; - for (const playwrightVersion of latestFivePlaywrightVersions) { - if (maybePlaywrightVersionConstraint) { - if (!satisfies(playwrightVersion, maybePlaywrightVersionConstraint)) { - continue; + for (const imageName of imageNames) { + const isCamoufoxImage = imageName === 'python-playwright-camoufox'; + + // The camoufox image only supports a subset of the Playwright versions, so it gets its own version list, along + // with its own "latest" version - otherwise it would never receive the moving tags (e.g. `:3.14`, `:latest`). + const imagePlaywrightVersions = isCamoufoxImage ? camoufoxPlaywrightVersions : latestFivePlaywrightVersions; + const latestImagePlaywrightVersion = isCamoufoxImage + ? latestCamoufoxPlaywrightVersion + : latestPlaywrightVersion; + + for (const playwrightVersion of imagePlaywrightVersions) { + if (maybePlaywrightVersionConstraint) { + if (!satisfies(playwrightVersion, maybePlaywrightVersionConstraint)) { + continue; + } } - } - for (const imageName of imageNames) { matrix.include.push({ 'image-name': imageName, 'python-version': pythonVersion, 'playwright-version': playwrightVersion, 'camoufox-version': latestCamoufoxVersion, - 'is-latest': playwrightVersion === latestPlaywrightVersion ? 'true' : 'false', + 'is-latest': playwrightVersion === latestImagePlaywrightVersion ? 'true' : 'false', 'latest-python-version': latestPythonVersion, 'supports-arm64': arm64UnsupportedImages.has(imageName) ? 'false' : 'true', }); diff --git a/.github/actions/version-matrix/src/shared/cache.ts b/.github/actions/version-matrix/src/shared/cache.ts index 9f996106..b03c9f29 100644 --- a/.github/actions/version-matrix/src/shared/cache.ts +++ b/.github/actions/version-matrix/src/shared/cache.ts @@ -8,6 +8,7 @@ export const SELENIUM_VERSION_MARKER = 'SELENIUM_VERSION'; export const PLAYWRIGHT_VERSION_MARKER = 'PLAYWRIGHT_VERSION'; export const CRAWLEE_VERSION_MARKER = 'CRAWLEE_VERSION'; export const CAMOUFOX_VERSION_MARKER = 'CAMOUFOX_VERSION'; +export const CAMOUFOX_PLAYWRIGHT_VERSION_MARKER = 'CAMOUFOX_PLAYWRIGHT_VERSION'; export const NODE_RUNTIME_VERSION_MARKER = 'NODE_RUNTIME_VERSION'; export const PYTHON_RUNTIME_VERSION_MARKER = 'PYTHON_RUNTIME_VERSION'; export const CERTIFICATES_UPDATED_AT_MARKER = 'CERTIFICATES_UPDATED_AT'; @@ -39,6 +40,7 @@ export type CacheValues = Partial< | typeof PLAYWRIGHT_VERSION_MARKER | typeof CRAWLEE_VERSION_MARKER | typeof CAMOUFOX_VERSION_MARKER + | typeof CAMOUFOX_PLAYWRIGHT_VERSION_MARKER | typeof CERTIFICATES_UPDATED_AT_MARKER, string[] > diff --git a/.github/actions/version-matrix/src/shared/constants.ts b/.github/actions/version-matrix/src/shared/constants.ts index dbddcde2..02838592 100644 --- a/.github/actions/version-matrix/src/shared/constants.ts +++ b/.github/actions/version-matrix/src/shared/constants.ts @@ -8,6 +8,20 @@ export const shouldUseLastFive = process.env.SHOULD_USE_LAST_FIVE === 'true'; export const emptyMatrix = JSON.stringify({ include: [] }); +/** + * Camoufox ships its own Firefox build, and its Juggler protocol is not compatible with Playwright 1.61.0 and newer. + * + * - the Python `camoufox` package declares `playwright<1.61`, so the image build fails outright with + * `ResolutionImpossible` during `pip install` + * - the `camoufox-js` package declares no such constraint, but the image breaks at runtime, as soon as a page is + * opened: `Protocol error (Browser.setDefaultViewport): Found property ".viewport.isMobile" ... which is not + * described in this scheme` + * + * The Camoufox images are therefore built only with the Playwright versions matching this range. Bump it once Camoufox + * catches up with the newer Playwright releases. + */ +export const camoufoxPlaywrightVersionRange = '<1.61.0'; + /** * The version of Python to be considered as the "default" version for the built image tags. */ From bdbb6501ce913fb2875ca43c550e5d4dfbf248a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Thu, 30 Jul 2026 16:40:39 +0200 Subject: [PATCH 2/6] refactor(camoufox): read the supported Playwright range from camoufox Replaces the hardcoded `<1.61.0` range with one read from the camoufox package metadata at matrix generation time, so this repo does not have to track what camoufox supports: - `camoufox-js` declares it as a `playwright-core` peer dependency, which is a semver range already. `playwright` and `playwright-core` are released in lockstep under the same version numbers, so it applies as-is to the `playwright` version installed in the image. - `camoufox` on PyPI declares it as a `playwright` requirement, which is a PEP 440 specifier set and needs converting (`<1.61` -> `<1.61`, `~=1.5.2` -> `~1.5.2`, `==1.5.*` -> `1.5.x`, ...). If the range cannot be resolved - camoufox declares no Playwright dependency, the specifier set has no semver equivalent (`!=`), or the registry request fails - the camoufox image is left out of the matrix rather than built against an unverified Playwright version. The other images are unaffected in either case. Note that camoufox-js currently declares `playwright-core: "*"`, so the node camoufox image stays unconstrained until that is pinned. The python one resolves to `<1.61` today. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/matrices/node/playwright.ts | 16 +-- .../src/matrices/python/playwright.ts | 15 +-- .../src/shared/camoufox.test.ts | 51 ++++++++++ .../version-matrix/src/shared/camoufox.ts | 98 +++++++++++++++++++ .../version-matrix/src/shared/constants.ts | 14 --- .../actions/version-matrix/src/shared/npm.ts | 22 +++++ .../version-matrix/src/shared/pypi.test.ts | 75 ++++++++++++++ .../actions/version-matrix/src/shared/pypi.ts | 85 +++++++++++++++- 8 files changed, 346 insertions(+), 30 deletions(-) create mode 100644 .github/actions/version-matrix/src/shared/camoufox.test.ts create mode 100644 .github/actions/version-matrix/src/shared/camoufox.ts create mode 100644 .github/actions/version-matrix/src/shared/pypi.test.ts diff --git a/.github/actions/version-matrix/src/matrices/node/playwright.ts b/.github/actions/version-matrix/src/matrices/node/playwright.ts index ebbdd4df..17486b3b 100644 --- a/.github/actions/version-matrix/src/matrices/node/playwright.ts +++ b/.github/actions/version-matrix/src/matrices/node/playwright.ts @@ -1,12 +1,11 @@ -import { satisfies } from 'semver'; import { type CacheValues, getCertificatesUpdatedAt, needsToRunMatrixGeneration, updateCacheState, } from '../../shared/cache.ts'; +import { resolveCamoufoxPlaywrightVersions } from '../../shared/camoufox.ts'; import { - camoufoxPlaywrightVersionRange, emptyMatrix, latestNodeVersion, setParametersForTriggeringUpdateWorkflowOnActorTemplates, @@ -32,11 +31,12 @@ let latestApifyVersion = apifyVersions.at(-1)!; let latestCrawleeVersion = crawleeVersions.at(-1)!; let latestCamoufoxVersion = camoufoxVersions.at(-1)!; -// Camoufox is incompatible with the newest Playwright releases, so its images are built with the newest Playwright -// versions Camoufox still supports, picked from all releases instead of just the last five. -const camoufoxPlaywrightVersions = playwrightVersions - .filter((version) => satisfies(version, camoufoxPlaywrightVersionRange)) - .slice(shouldUseLastFive ? -5 : -1); +// Camoufox does not support every Playwright release, so its images are built with the newest Playwright versions it +// declares support for, picked from all releases instead of just the last five - otherwise the camoufox image would +// drop out of the matrix entirely once the last five releases are all unsupported. +const { range: camoufoxPlaywrightRange, versions: supportedCamoufoxPlaywrightVersions } = + await resolveCamoufoxPlaywrightVersions('node', latestCamoufoxVersion, playwrightVersions); +const camoufoxPlaywrightVersions = supportedCamoufoxPlaywrightVersions.slice(shouldUseLastFive ? -5 : -1); const latestCamoufoxPlaywrightVersion = camoufoxPlaywrightVersions.at(-1); const certificatesUpdatedAt = await getCertificatesUpdatedAt(); @@ -46,7 +46,7 @@ console.error('Latest apify version', latestApifyVersion); console.error('Latest crawlee version', latestCrawleeVersion); console.error('Latest camoufox version', latestCamoufoxVersion); console.error( - `Playwright versions for camoufox (${camoufoxPlaywrightVersionRange})`, + `Playwright versions for camoufox (camoufox-js@${latestCamoufoxVersion} supports ${camoufoxPlaywrightRange ?? 'nothing we could resolve'})`, camoufoxPlaywrightVersions.length ? camoufoxPlaywrightVersions : '(none, camoufox images will be skipped)', ); console.error('Node runtime versions', nodeRuntimeVersions); diff --git a/.github/actions/version-matrix/src/matrices/python/playwright.ts b/.github/actions/version-matrix/src/matrices/python/playwright.ts index 069b470d..56588236 100644 --- a/.github/actions/version-matrix/src/matrices/python/playwright.ts +++ b/.github/actions/version-matrix/src/matrices/python/playwright.ts @@ -5,8 +5,8 @@ import { needsToRunMatrixGeneration, updateCacheState, } from '../../shared/cache.ts'; +import { resolveCamoufoxPlaywrightVersions } from '../../shared/camoufox.ts'; import { - camoufoxPlaywrightVersionRange, emptyMatrix, latestPythonVersion, setParametersForTriggeringUpdateWorkflowOnActorTemplates, @@ -39,11 +39,12 @@ const latestFivePlaywrightVersions = playwrightVersions.slice(shouldUseLastFive const latestPlaywrightVersion = latestFivePlaywrightVersions.at(-1)!; const latestCamoufoxVersion = camoufoxVersions.at(-1)!; -// Camoufox is incompatible with the newest Playwright releases, so its images are built with the newest Playwright -// versions Camoufox still supports, picked from all releases instead of just the last five. -const camoufoxPlaywrightVersions = playwrightVersions - .filter((version) => satisfies(version, camoufoxPlaywrightVersionRange)) - .slice(shouldUseLastFive ? -5 : -1); +// Camoufox does not support every Playwright release, so its images are built with the newest Playwright versions it +// declares support for, picked from all releases instead of just the last five - otherwise the camoufox image would +// drop out of the matrix entirely once the last five releases are all unsupported. +const { range: camoufoxPlaywrightRange, versions: supportedCamoufoxPlaywrightVersions } = + await resolveCamoufoxPlaywrightVersions('python', latestCamoufoxVersion, playwrightVersions); +const camoufoxPlaywrightVersions = supportedCamoufoxPlaywrightVersions.slice(shouldUseLastFive ? -5 : -1); const latestCamoufoxPlaywrightVersion = camoufoxPlaywrightVersions.at(-1); const certificatesUpdatedAt = await getCertificatesUpdatedAt(); @@ -52,7 +53,7 @@ console.error('Last five versions:', latestFivePlaywrightVersions); console.error('Latest playwright version:', latestPlaywrightVersion); console.error('Latest camoufox version:', latestCamoufoxVersion); console.error( - `Playwright versions for camoufox (${camoufoxPlaywrightVersionRange}):`, + `Playwright versions for camoufox (camoufox==${latestCamoufoxVersion} supports ${camoufoxPlaywrightRange ?? 'nothing we could resolve'}):`, camoufoxPlaywrightVersions.length ? camoufoxPlaywrightVersions : '(none, camoufox images will be skipped)', ); console.error('Python runtime versions:', pythonRuntimeVersions); diff --git a/.github/actions/version-matrix/src/shared/camoufox.test.ts b/.github/actions/version-matrix/src/shared/camoufox.test.ts new file mode 100644 index 00000000..7549419e --- /dev/null +++ b/.github/actions/version-matrix/src/shared/camoufox.test.ts @@ -0,0 +1,51 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { resolveCamoufoxPlaywrightVersions } from './camoufox.ts'; +import { fetchPackageManifest } from './npm.ts'; + +const playwrightVersions = ['1.56.0', '1.59.0', '1.60.0', '1.61.0', '1.62.0']; + +describe('resolveCamoufoxPlaywrightVersions', () => { + it('reads the supported range from the camoufox package on PyPI', async () => { + // camoufox 0.5.4 declares `playwright<1.61` + const { range, versions } = await resolveCamoufoxPlaywrightVersions('python', '0.5.4', playwrightVersions); + + assert.equal(range, '<1.61'); + assert.deepEqual(versions, ['1.56.0', '1.59.0', '1.60.0']); + }); + + it('reads the supported range from the camoufox-js package on npm', async () => { + const { range } = await resolveCamoufoxPlaywrightVersions('node', '0.11.5', playwrightVersions); + + // camoufox-js declares the range as a `playwright-core` peer dependency. It is unbounded (`*`) as of 0.11.5, + // so this only asserts that we read it - the range itself is expected to change. + assert.equal(typeof range, 'string'); + }); + + it('returns no versions when the range cannot be resolved', async () => { + const { range, versions } = await resolveCamoufoxPlaywrightVersions( + 'python', + '0.0.0-does-not-exist', + playwrightVersions, + ); + + assert.equal(range, undefined); + assert.deepEqual(versions, []); + }); +}); + +describe('fetchPackageManifest', () => { + it('reads the declared dependency ranges of a published version', async () => { + const manifest = await fetchPackageManifest('playwright', '1.60.0'); + + assert.equal(manifest.version, '1.60.0'); + assert.equal(manifest.dependencies?.['playwright-core'], '1.60.0'); + }); + + it('reads the declared peer dependency ranges of a published version', async () => { + const manifest = await fetchPackageManifest('camoufox-js', '0.11.5'); + + assert.equal(typeof manifest.peerDependencies?.['playwright-core'], 'string'); + }); +}); diff --git a/.github/actions/version-matrix/src/shared/camoufox.ts b/.github/actions/version-matrix/src/shared/camoufox.ts new file mode 100644 index 00000000..410c2703 --- /dev/null +++ b/.github/actions/version-matrix/src/shared/camoufox.ts @@ -0,0 +1,98 @@ +import { satisfies } from 'semver'; +import { fetchPackageManifest } from './npm.ts'; +import { fetchPackageVersion, findRequirementSpecifierSet, pythonSpecifierSetToSemverRange } from './pypi.ts'; + +/** + * Camoufox ships its own Firefox build, and its Juggler protocol only speaks to a limited range of Playwright + * versions - for example, camoufox 0.5.4 does not work with Playwright 1.61.0 and newer. + * + * That range is not pinned here. It is read from the camoufox package metadata instead, so that the images follow + * whatever camoufox currently supports: + * + * - `camoufox-js` declares it as a `playwright-core` peer dependency + * - `camoufox` (PyPI) declares it as a `playwright` requirement + * + * Note that the images install camoufox with a caret / compatible-release range, so the version actually installed + * may be newer than the one we read the metadata from. It is the newest version at the time the matrix is generated, + * which is as close as we can get without resolving the range twice. + */ +const camoufoxPackages = { + node: { + packageName: 'camoufox-js', + // `playwright` and `playwright-core` are released in lockstep under the same version numbers, so the range + // declared for `playwright-core` applies as-is to the `playwright` version installed in the image + dependencyName: 'playwright-core', + }, + python: { + packageName: 'camoufox', + dependencyName: 'playwright', + }, +} as const; + +async function fetchNodeCamoufoxPlaywrightRange(camoufoxVersion: string) { + const { dependencyName } = camoufoxPackages.node; + const manifest = await fetchPackageManifest(camoufoxPackages.node.packageName, camoufoxVersion); + + return manifest.peerDependencies?.[dependencyName] ?? manifest.dependencies?.[dependencyName]; +} + +async function fetchPythonCamoufoxPlaywrightRange(camoufoxVersion: string) { + const { dependencyName } = camoufoxPackages.python; + const { info } = await fetchPackageVersion(camoufoxPackages.python.packageName, camoufoxVersion); + + const specifierSet = findRequirementSpecifierSet(info.requires_dist ?? [], dependencyName); + + if (specifierSet === undefined) { + return undefined; + } + + return pythonSpecifierSetToSemverRange(specifierSet); +} + +/** + * Resolves which of the given Playwright versions can be used to build the camoufox image, based on the range the + * camoufox package declares for its Playwright dependency. + * + * If the range cannot be determined - the dependency is not declared, or it cannot be expressed as a semver range - + * this returns no versions at all, which leaves the camoufox image out of the matrix. Publishing an image that is + * known to be broken is worse than not publishing one, and the other images are unaffected either way. + */ +export async function resolveCamoufoxPlaywrightVersions( + runtime: 'node' | 'python', + camoufoxVersion: string, + playwrightVersions: string[], +) { + const { packageName } = camoufoxPackages[runtime]; + + let range: string | undefined; + + try { + range = + runtime === 'node' + ? await fetchNodeCamoufoxPlaywrightRange(camoufoxVersion) + : await fetchPythonCamoufoxPlaywrightRange(camoufoxVersion); + } catch (error) { + console.error( + `Failed to read the supported Playwright range from ${packageName}@${camoufoxVersion}, the camoufox image will be skipped`, + error, + ); + + return { range: undefined, versions: [] }; + } + + if (range === undefined) { + console.error( + `${packageName}@${camoufoxVersion} does not declare a Playwright dependency, the camoufox image will be skipped`, + ); + + return { range: undefined, versions: [] }; + } + + if (range === '*' || range === '') { + console.warn( + `${packageName}@${camoufoxVersion} declares no bounds on its Playwright dependency, the camoufox image will be built with the same versions as the other images`, + ); + } + + return { range, versions: playwrightVersions.filter((version) => satisfies(version, range)) }; +} diff --git a/.github/actions/version-matrix/src/shared/constants.ts b/.github/actions/version-matrix/src/shared/constants.ts index 02838592..dbddcde2 100644 --- a/.github/actions/version-matrix/src/shared/constants.ts +++ b/.github/actions/version-matrix/src/shared/constants.ts @@ -8,20 +8,6 @@ export const shouldUseLastFive = process.env.SHOULD_USE_LAST_FIVE === 'true'; export const emptyMatrix = JSON.stringify({ include: [] }); -/** - * Camoufox ships its own Firefox build, and its Juggler protocol is not compatible with Playwright 1.61.0 and newer. - * - * - the Python `camoufox` package declares `playwright<1.61`, so the image build fails outright with - * `ResolutionImpossible` during `pip install` - * - the `camoufox-js` package declares no such constraint, but the image breaks at runtime, as soon as a page is - * opened: `Protocol error (Browser.setDefaultViewport): Found property ".viewport.isMobile" ... which is not - * described in this scheme` - * - * The Camoufox images are therefore built only with the Playwright versions matching this range. Bump it once Camoufox - * catches up with the newer Playwright releases. - */ -export const camoufoxPlaywrightVersionRange = '<1.61.0'; - /** * The version of Python to be considered as the "default" version for the built image tags. */ diff --git a/.github/actions/version-matrix/src/shared/npm.ts b/.github/actions/version-matrix/src/shared/npm.ts index bbb510da..2bd5b4a0 100644 --- a/.github/actions/version-matrix/src/shared/npm.ts +++ b/.github/actions/version-matrix/src/shared/npm.ts @@ -1,11 +1,14 @@ import { compare } from 'semver'; const npmPackageInfoRoute = (pkg: string) => `https://registry.npmjs.org/${pkg}`; +const npmPackageVersionInfoRoute = (pkg: string, version: string) => `https://registry.npmjs.org/${pkg}/${version}`; interface PackageVersionInfo { name: string; version: string; engines?: Record; + dependencies?: Record; + peerDependencies?: Record; } interface PackageInfo { @@ -32,3 +35,22 @@ export async function fetchPackageVersions(packageName: string) { return versions.sort((a, b) => compare(a, b)); } + +/** + * Fetches the manifest (the `package.json`) of a single published version of a package. + */ +export async function fetchPackageManifest(packageName: string, version: string) { + const url = npmPackageVersionInfoRoute(packageName, version); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch package info for ${packageName}, version ${version}`, { + cause: await response.text(), + }); + } + + const json: PackageVersionInfo = await response.json(); + + return json; +} diff --git a/.github/actions/version-matrix/src/shared/pypi.test.ts b/.github/actions/version-matrix/src/shared/pypi.test.ts new file mode 100644 index 00000000..5e3c2fd3 --- /dev/null +++ b/.github/actions/version-matrix/src/shared/pypi.test.ts @@ -0,0 +1,75 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { satisfies } from 'semver'; + +import { findRequirementSpecifierSet, pythonSpecifierSetToSemverRange } from './pypi.ts'; + +describe('findRequirementSpecifierSet', () => { + // Trimmed down `requires_dist` of camoufox 0.5.4 + const requiresDist = [ + 'browserforge<2.0.0,>=1.2.4', + 'playwright<1.61', + 'PySide6; extra == "gui"', + 'geoip2; extra == "geoip"', + 'typing_extensions', + ]; + + it('returns the specifier set of a requirement', () => { + assert.equal(findRequirementSpecifierSet(requiresDist, 'playwright'), '<1.61'); + assert.equal(findRequirementSpecifierSet(requiresDist, 'browserforge'), '<2.0.0,>=1.2.4'); + }); + + it('returns an empty string for a requirement without version bounds', () => { + assert.equal(findRequirementSpecifierSet(requiresDist, 'typing_extensions'), ''); + }); + + it('returns undefined for a requirement that is not declared', () => { + assert.equal(findRequirementSpecifierSet(requiresDist, 'pytest'), undefined); + }); + + it('ignores requirements that only apply to an extra', () => { + assert.equal(findRequirementSpecifierSet(requiresDist, 'geoip2'), undefined); + }); + + it('matches the package name case-insensitively and ignores extras', () => { + assert.equal(findRequirementSpecifierSet(['Playwright[foo]>=1.50'], 'playwright'), '>=1.50'); + }); +}); + +describe('pythonSpecifierSetToSemverRange', () => { + it('converts comparison specifiers as-is', () => { + assert.equal(pythonSpecifierSetToSemverRange('<1.61'), '<1.61'); + assert.equal(pythonSpecifierSetToSemverRange('>=1.50'), '>=1.50'); + }); + + it('ANDs a comma-separated specifier set together', () => { + assert.equal(pythonSpecifierSetToSemverRange('<2.0.0,>=1.2.4'), '<2.0.0 >=1.2.4'); + }); + + it('converts equality specifiers', () => { + assert.equal(pythonSpecifierSetToSemverRange('==1.5.0'), '=1.5.0'); + assert.equal(pythonSpecifierSetToSemverRange('==1.5.*'), '1.5.x'); + }); + + it('converts compatible-release specifiers', () => { + assert.equal(pythonSpecifierSetToSemverRange('~=1.5.2'), '~1.5.2'); + assert.equal(pythonSpecifierSetToSemverRange('~=1.5'), '^1.5'); + }); + + it('treats an empty specifier set as unbounded', () => { + assert.equal(pythonSpecifierSetToSemverRange(''), '*'); + }); + + it('throws for specifiers that have no semver equivalent', () => { + assert.throws(() => pythonSpecifierSetToSemverRange('!=1.4.0'), /Cannot convert/); + assert.throws(() => pythonSpecifierSetToSemverRange('<= not a version'), /Cannot convert/); + }); + + it('produces a range that excludes the Playwright versions camoufox does not support', () => { + const range = pythonSpecifierSetToSemverRange('<1.61'); + + assert.equal(satisfies('1.60.0', range), true); + assert.equal(satisfies('1.61.0', range), false); + assert.equal(satisfies('1.62.0', range), false); + }); +}); diff --git a/.github/actions/version-matrix/src/shared/pypi.ts b/.github/actions/version-matrix/src/shared/pypi.ts index 3fd2c7ab..af9e4382 100644 --- a/.github/actions/version-matrix/src/shared/pypi.ts +++ b/.github/actions/version-matrix/src/shared/pypi.ts @@ -70,4 +70,87 @@ export async function fetchPackageVersion(packageName: string, version: string) return json; } -export const pythonRequirementRegex = /(?[a-zA-Z0-9\-]+)(?[<>=]+)(?[0-9\.]+)/; +/** + * Matches a single entry of a `requires_dist` list, e.g. `playwright<1.61` or `geoip2; extra == "geoip"`. + */ +const pythonRequirementRegex = + /^(?[A-Za-z0-9._-]+)\s*(?:\[(?[^\]]*)\])?\s*(?[^;]*?)\s*(?:;(?.*))?$/; + +/** + * Matches a single PEP 440 version specifier, e.g. `<1.61` or `~=1.5.2`. + */ +const pythonSpecifierRegex = /^(?===|==|~=|!=|<=|>=|<|>)\s*(?[0-9][0-9.]*(?:\.\*)?)$/; + +/** + * Finds the requirement for a given package in a `requires_dist` list, and returns its PEP 440 specifier set + * (e.g. `<1.61`). Requirements that only apply to an extra are ignored, as is the rest of the environment marker - + * we only care about the version bounds. + * + * Returns `undefined` when the package is not required at all, and an empty string when it is required without any + * version bounds. + */ +export function findRequirementSpecifierSet(requiresDist: string[], packageName: string) { + for (const requirement of requiresDist) { + const groups = pythonRequirementRegex.exec(requirement.trim())?.groups; + + if (!groups || groups.name.toLowerCase() !== packageName.toLowerCase()) { + continue; + } + + if (groups.marker?.includes('extra ==')) { + continue; + } + + return groups.specifierSet; + } + + return undefined; +} + +/** + * Converts a PEP 440 specifier set (e.g. `>=1.50,<1.61`) into an equivalent semver range (`>=1.50 <1.61`). + * + * Throws for specifiers that have no semver equivalent (such as `!=`), so that the caller can decide what to do + * instead of silently widening the range. + */ +export function pythonSpecifierSetToSemverRange(specifierSet: string) { + const specifiers = specifierSet + .split(',') + .map((specifier) => specifier.trim()) + .filter(Boolean); + + if (specifiers.length === 0) { + return '*'; + } + + // Space-separated semver comparators are ANDed together, same as the comma-separated PEP 440 specifiers + return specifiers.map(pythonSpecifierToSemverComparator).join(' '); +} + +function pythonSpecifierToSemverComparator(specifier: string) { + const groups = pythonSpecifierRegex.exec(specifier)?.groups; + + if (!groups) { + throw new Error(`Cannot convert Python version specifier to a semver range: ${specifier}`); + } + + const { operator, version } = groups; + + switch (operator) { + case '<': + case '<=': + case '>': + case '>=': + return `${operator}${version}`; + case '==': + case '===': + // `==1.5.*` -> `1.5.x` + return version.endsWith('.*') ? version.replace(/\.\*$/, '.x') : `=${version}`; + case '~=': + // `~=1.5.2` means `>=1.5.2, ==1.5.*`, while `~=1.5` means `>=1.5, ==1.*` + return version.split('.').length >= 3 ? `~${version}` : `^${version}`; + default: + // Notably `!=`, which cannot be expressed as a semver comparator + throw new Error(`Cannot convert Python version specifier to a semver range: ${specifier}`); + } +} From c6b0ead2697e88633d8be927af0be3e081b01bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Thu, 30 Jul 2026 16:43:32 +0200 Subject: [PATCH 3/6] test(camoufox): do not assert on the range camoufox-js currently declares The `playwright-core` peer dependency range is camoufox's to change. Assert that we read it and apply it, not what it happens to be right now. Co-Authored-By: Claude Opus 5 (1M context) --- .../version-matrix/src/shared/camoufox.test.ts | 17 +++++++++++------ .../version-matrix/src/shared/pypi.test.ts | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/actions/version-matrix/src/shared/camoufox.test.ts b/.github/actions/version-matrix/src/shared/camoufox.test.ts index 7549419e..708b1d84 100644 --- a/.github/actions/version-matrix/src/shared/camoufox.test.ts +++ b/.github/actions/version-matrix/src/shared/camoufox.test.ts @@ -1,5 +1,6 @@ -import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { satisfies } from 'semver'; import { resolveCamoufoxPlaywrightVersions } from './camoufox.ts'; import { fetchPackageManifest } from './npm.ts'; @@ -16,11 +17,15 @@ describe('resolveCamoufoxPlaywrightVersions', () => { }); it('reads the supported range from the camoufox-js package on npm', async () => { - const { range } = await resolveCamoufoxPlaywrightVersions('node', '0.11.5', playwrightVersions); - - // camoufox-js declares the range as a `playwright-core` peer dependency. It is unbounded (`*`) as of 0.11.5, - // so this only asserts that we read it - the range itself is expected to change. - assert.equal(typeof range, 'string'); + const { range, versions } = await resolveCamoufoxPlaywrightVersions('node', '0.11.5', playwrightVersions); + + // camoufox-js declares the range as a `playwright-core` peer dependency. What it declares is up to camoufox and + // is expected to change, so this asserts that we read a range and apply it - not what the range happens to be. + assert.notEqual(range, undefined); + assert.deepEqual( + versions, + playwrightVersions.filter((version) => satisfies(version, range!)), + ); }); it('returns no versions when the range cannot be resolved', async () => { diff --git a/.github/actions/version-matrix/src/shared/pypi.test.ts b/.github/actions/version-matrix/src/shared/pypi.test.ts index 5e3c2fd3..8baf4b51 100644 --- a/.github/actions/version-matrix/src/shared/pypi.test.ts +++ b/.github/actions/version-matrix/src/shared/pypi.test.ts @@ -1,5 +1,5 @@ -import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; import { satisfies } from 'semver'; import { findRequirementSpecifierSet, pythonSpecifierSetToSemverRange } from './pypi.ts'; From 523d46659e29b3f6de07728d40bf3d7d1cb0f3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Thu, 30 Jul 2026 17:04:35 +0200 Subject: [PATCH 4/6] fix(camoufox): skip the camoufox image when the supported range says nothing A `*` dependency range is the default a package gets when it does not declare anything, not a statement that every Playwright version works. Filtering by it kept the node camoufox image on the newest Playwright, which is exactly the combination that is broken - the CI test for node-playwright-camoufox at 1.62.0 fails with `browserType.launchPersistentContext: Protocol error (Browser.setDefaultViewport)`. Treat it like any other unresolvable range and leave the image out of the matrix instead. This means no node camoufox images are built until camoufox-js declares a bound on its `playwright-core` peer dependency, at which point they resume with no change needed here. The python image is unaffected - camoufox declares `playwright<1.61` and resolves to 1.56.0-1.60.0. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/shared/camoufox.test.ts | 17 ++++++++++--- .../version-matrix/src/shared/camoufox.ts | 24 ++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/.github/actions/version-matrix/src/shared/camoufox.test.ts b/.github/actions/version-matrix/src/shared/camoufox.test.ts index 708b1d84..94b5e69e 100644 --- a/.github/actions/version-matrix/src/shared/camoufox.test.ts +++ b/.github/actions/version-matrix/src/shared/camoufox.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { satisfies } from 'semver'; -import { resolveCamoufoxPlaywrightVersions } from './camoufox.ts'; +import { isUnboundedRange, resolveCamoufoxPlaywrightVersions } from './camoufox.ts'; import { fetchPackageManifest } from './npm.ts'; const playwrightVersions = ['1.56.0', '1.59.0', '1.60.0', '1.61.0', '1.62.0']; @@ -20,14 +20,25 @@ describe('resolveCamoufoxPlaywrightVersions', () => { const { range, versions } = await resolveCamoufoxPlaywrightVersions('node', '0.11.5', playwrightVersions); // camoufox-js declares the range as a `playwright-core` peer dependency. What it declares is up to camoufox and - // is expected to change, so this asserts that we read a range and apply it - not what the range happens to be. + // is expected to change, so this asserts how we treat the range we read - not what the range happens to be. assert.notEqual(range, undefined); assert.deepEqual( versions, - playwrightVersions.filter((version) => satisfies(version, range!)), + isUnboundedRange(range!) ? [] : playwrightVersions.filter((version) => satisfies(version, range!)), ); }); + it('returns no versions when the declared range says nothing', async () => { + // A `*` peer dependency is the npm default, not a statement that every Playwright version works + assert.equal(isUnboundedRange('*'), true); + assert.equal(isUnboundedRange(''), true); + assert.equal(isUnboundedRange('x'), true); + + assert.equal(isUnboundedRange('<1.61'), false); + assert.equal(isUnboundedRange('^1.53.1'), false); + assert.equal(isUnboundedRange('>=1.50 <1.61'), false); + }); + it('returns no versions when the range cannot be resolved', async () => { const { range, versions } = await resolveCamoufoxPlaywrightVersions( 'python', diff --git a/.github/actions/version-matrix/src/shared/camoufox.ts b/.github/actions/version-matrix/src/shared/camoufox.ts index 410c2703..453947da 100644 --- a/.github/actions/version-matrix/src/shared/camoufox.ts +++ b/.github/actions/version-matrix/src/shared/camoufox.ts @@ -49,13 +49,23 @@ async function fetchPythonCamoufoxPlaywrightRange(camoufoxVersion: string) { return pythonSpecifierSetToSemverRange(specifierSet); } +/** + * A range that matches everything (`*`, or no range at all) is not a statement of support - it is the default you get + * when a package does not say anything. We cannot tell which Playwright versions work from it, so it is treated the + * same as not being able to read the range at all. + */ +export function isUnboundedRange(range: string) { + return ['', '*', 'x', 'X'].includes(range.trim()); +} + /** * Resolves which of the given Playwright versions can be used to build the camoufox image, based on the range the * camoufox package declares for its Playwright dependency. * - * If the range cannot be determined - the dependency is not declared, or it cannot be expressed as a semver range - - * this returns no versions at all, which leaves the camoufox image out of the matrix. Publishing an image that is - * known to be broken is worse than not publishing one, and the other images are unaffected either way. + * If the range cannot be determined - the dependency is not declared, it says nothing (`*`), it cannot be expressed as + * a semver range, or the registry request fails - this returns no versions at all, which leaves the camoufox image out + * of the matrix. Publishing an image we have no reason to believe works is worse than not publishing one, and the + * other images are unaffected either way. */ export async function resolveCamoufoxPlaywrightVersions( runtime: 'node' | 'python', @@ -88,10 +98,12 @@ export async function resolveCamoufoxPlaywrightVersions( return { range: undefined, versions: [] }; } - if (range === '*' || range === '') { - console.warn( - `${packageName}@${camoufoxVersion} declares no bounds on its Playwright dependency, the camoufox image will be built with the same versions as the other images`, + if (isUnboundedRange(range)) { + console.error( + `${packageName}@${camoufoxVersion} declares its Playwright dependency as "${range}", which says nothing about what it supports - the camoufox image will be skipped`, ); + + return { range, versions: [] }; } return { range, versions: playwrightVersions.filter((version) => satisfies(version, range)) }; From c06e1c5abf7c5e7076d45379126502be8630d561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Fri, 31 Jul 2026 11:03:24 +0200 Subject: [PATCH 5/6] Revert "fix(camoufox): skip the camoufox image when the supported range says nothing" This reverts commit 523d46659e29b3f6de07728d40bf3d7d1cb0f3d4. --- .../src/shared/camoufox.test.ts | 17 +++---------- .../version-matrix/src/shared/camoufox.ts | 24 +++++-------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/.github/actions/version-matrix/src/shared/camoufox.test.ts b/.github/actions/version-matrix/src/shared/camoufox.test.ts index 94b5e69e..708b1d84 100644 --- a/.github/actions/version-matrix/src/shared/camoufox.test.ts +++ b/.github/actions/version-matrix/src/shared/camoufox.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { satisfies } from 'semver'; -import { isUnboundedRange, resolveCamoufoxPlaywrightVersions } from './camoufox.ts'; +import { resolveCamoufoxPlaywrightVersions } from './camoufox.ts'; import { fetchPackageManifest } from './npm.ts'; const playwrightVersions = ['1.56.0', '1.59.0', '1.60.0', '1.61.0', '1.62.0']; @@ -20,25 +20,14 @@ describe('resolveCamoufoxPlaywrightVersions', () => { const { range, versions } = await resolveCamoufoxPlaywrightVersions('node', '0.11.5', playwrightVersions); // camoufox-js declares the range as a `playwright-core` peer dependency. What it declares is up to camoufox and - // is expected to change, so this asserts how we treat the range we read - not what the range happens to be. + // is expected to change, so this asserts that we read a range and apply it - not what the range happens to be. assert.notEqual(range, undefined); assert.deepEqual( versions, - isUnboundedRange(range!) ? [] : playwrightVersions.filter((version) => satisfies(version, range!)), + playwrightVersions.filter((version) => satisfies(version, range!)), ); }); - it('returns no versions when the declared range says nothing', async () => { - // A `*` peer dependency is the npm default, not a statement that every Playwright version works - assert.equal(isUnboundedRange('*'), true); - assert.equal(isUnboundedRange(''), true); - assert.equal(isUnboundedRange('x'), true); - - assert.equal(isUnboundedRange('<1.61'), false); - assert.equal(isUnboundedRange('^1.53.1'), false); - assert.equal(isUnboundedRange('>=1.50 <1.61'), false); - }); - it('returns no versions when the range cannot be resolved', async () => { const { range, versions } = await resolveCamoufoxPlaywrightVersions( 'python', diff --git a/.github/actions/version-matrix/src/shared/camoufox.ts b/.github/actions/version-matrix/src/shared/camoufox.ts index 453947da..410c2703 100644 --- a/.github/actions/version-matrix/src/shared/camoufox.ts +++ b/.github/actions/version-matrix/src/shared/camoufox.ts @@ -49,23 +49,13 @@ async function fetchPythonCamoufoxPlaywrightRange(camoufoxVersion: string) { return pythonSpecifierSetToSemverRange(specifierSet); } -/** - * A range that matches everything (`*`, or no range at all) is not a statement of support - it is the default you get - * when a package does not say anything. We cannot tell which Playwright versions work from it, so it is treated the - * same as not being able to read the range at all. - */ -export function isUnboundedRange(range: string) { - return ['', '*', 'x', 'X'].includes(range.trim()); -} - /** * Resolves which of the given Playwright versions can be used to build the camoufox image, based on the range the * camoufox package declares for its Playwright dependency. * - * If the range cannot be determined - the dependency is not declared, it says nothing (`*`), it cannot be expressed as - * a semver range, or the registry request fails - this returns no versions at all, which leaves the camoufox image out - * of the matrix. Publishing an image we have no reason to believe works is worse than not publishing one, and the - * other images are unaffected either way. + * If the range cannot be determined - the dependency is not declared, or it cannot be expressed as a semver range - + * this returns no versions at all, which leaves the camoufox image out of the matrix. Publishing an image that is + * known to be broken is worse than not publishing one, and the other images are unaffected either way. */ export async function resolveCamoufoxPlaywrightVersions( runtime: 'node' | 'python', @@ -98,12 +88,10 @@ export async function resolveCamoufoxPlaywrightVersions( return { range: undefined, versions: [] }; } - if (isUnboundedRange(range)) { - console.error( - `${packageName}@${camoufoxVersion} declares its Playwright dependency as "${range}", which says nothing about what it supports - the camoufox image will be skipped`, + if (range === '*' || range === '') { + console.warn( + `${packageName}@${camoufoxVersion} declares no bounds on its Playwright dependency, the camoufox image will be built with the same versions as the other images`, ); - - return { range, versions: [] }; } return { range, versions: playwrightVersions.filter((version) => satisfies(version, range)) }; From 33c52fa26dd34254e63b96765619f25da329f515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Fri, 31 Jul 2026 14:18:16 +0200 Subject: [PATCH 6/6] chore(camoufox): bump the local camoufox-js version to 0.12.0 0.12.0 is the first version that declares the Playwright range it supports (`playwright-core: <1.61.0`), which is what the matrix generators read. The generators pick up the latest version from npm on their own; this only updates the version used by the local `make` targets and by the tests. Co-Authored-By: Claude Opus 5 (1M context) --- .github/actions/version-matrix/src/shared/camoufox.test.ts | 4 ++-- Makefile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/version-matrix/src/shared/camoufox.test.ts b/.github/actions/version-matrix/src/shared/camoufox.test.ts index 708b1d84..41f57246 100644 --- a/.github/actions/version-matrix/src/shared/camoufox.test.ts +++ b/.github/actions/version-matrix/src/shared/camoufox.test.ts @@ -17,7 +17,7 @@ describe('resolveCamoufoxPlaywrightVersions', () => { }); it('reads the supported range from the camoufox-js package on npm', async () => { - const { range, versions } = await resolveCamoufoxPlaywrightVersions('node', '0.11.5', playwrightVersions); + const { range, versions } = await resolveCamoufoxPlaywrightVersions('node', '0.12.0', playwrightVersions); // camoufox-js declares the range as a `playwright-core` peer dependency. What it declares is up to camoufox and // is expected to change, so this asserts that we read a range and apply it - not what the range happens to be. @@ -49,7 +49,7 @@ describe('fetchPackageManifest', () => { }); it('reads the declared peer dependency ranges of a published version', async () => { - const manifest = await fetchPackageManifest('camoufox-js', '0.11.5'); + const manifest = await fetchPackageManifest('camoufox-js', '0.12.0'); assert.equal(typeof manifest.peerDependencies?.['playwright-core'], 'string'); }); diff --git a/Makefile b/Makefile index 66e195e5..d69ea442 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ NODE_VERSION ?= 22 # Tag must have format: v1.42.0- PLAYWRIGHT_VERSION ?= v1.57.0- -CAMOUFOX_VERSION ?= 0.8.5 +CAMOUFOX_VERSION ?= 0.12.0 # Tag must have format: 22.6.2 PUPPETEER_VERSION ?= 24.34.0 PKG_JSON_PW_VERSION = $(subst v,,$(subst -,,$(PLAYWRIGHT_VERSION)))