diff --git a/.github/actions/version-matrix/src/matrices/node/playwright.ts b/.github/actions/version-matrix/src/matrices/node/playwright.ts index 8e3a97e8..17486b3b 100644 --- a/.github/actions/version-matrix/src/matrices/node/playwright.ts +++ b/.github/actions/version-matrix/src/matrices/node/playwright.ts @@ -4,6 +4,7 @@ import { needsToRunMatrixGeneration, updateCacheState, } from '../../shared/cache.ts'; +import { resolveCamoufoxPlaywrightVersions } from '../../shared/camoufox.ts'; import { emptyMatrix, latestNodeVersion, @@ -30,12 +31,24 @@ let latestApifyVersion = apifyVersions.at(-1)!; let latestCrawleeVersion = crawleeVersions.at(-1)!; let latestCamoufoxVersion = camoufoxVersions.at(-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(); 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 (camoufox-js@${latestCamoufoxVersion} supports ${camoufoxPlaywrightRange ?? 'nothing we could resolve'})`, + 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..56588236 100644 --- a/.github/actions/version-matrix/src/matrices/python/playwright.ts +++ b/.github/actions/version-matrix/src/matrices/python/playwright.ts @@ -5,6 +5,7 @@ import { needsToRunMatrixGeneration, updateCacheState, } from '../../shared/cache.ts'; +import { resolveCamoufoxPlaywrightVersions } from '../../shared/camoufox.ts'; import { emptyMatrix, latestPythonVersion, @@ -38,11 +39,23 @@ const latestFivePlaywrightVersions = playwrightVersions.slice(shouldUseLastFive const latestPlaywrightVersion = latestFivePlaywrightVersions.at(-1)!; const latestCamoufoxVersion = camoufoxVersions.at(-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(); console.error('Last five versions:', latestFivePlaywrightVersions); console.error('Latest playwright version:', latestPlaywrightVersion); console.error('Latest camoufox version:', latestCamoufoxVersion); +console.error( + `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); console.error('Certificates updated at:', certificatesUpdatedAt || '(not available)'); @@ -51,6 +64,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 +106,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/camoufox.test.ts b/.github/actions/version-matrix/src/shared/camoufox.test.ts new file mode 100644 index 00000000..41f57246 --- /dev/null +++ b/.github/actions/version-matrix/src/shared/camoufox.test.ts @@ -0,0 +1,56 @@ +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'; + +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, 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. + assert.notEqual(range, undefined); + assert.deepEqual( + versions, + playwrightVersions.filter((version) => satisfies(version, range!)), + ); + }); + + 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.12.0'); + + 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/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..8baf4b51 --- /dev/null +++ b/.github/actions/version-matrix/src/shared/pypi.test.ts @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +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}`); + } +} 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)))