diff --git a/.github/scripts/bump-versions.mjs b/.github/scripts/bump-versions.mjs index fbeabe31ff0..c224ec5dc41 100644 --- a/.github/scripts/bump-versions.mjs +++ b/.github/scripts/bump-versions.mjs @@ -1,4 +1,5 @@ import semver from 'semver'; +import { parse } from 'yaml'; import { writeFile, readFile } from 'fs/promises'; import { resolve } from 'path'; import child_process from 'child_process'; @@ -7,14 +8,19 @@ import assert from 'assert'; const exec = promisify(child_process.exec); +/** + * @param {string | semver.SemVer} currentVersion + */ function generateExperimentalVersion(currentVersion) { const parsed = semver.parse(currentVersion); if (!parsed) throw new Error(`Invalid version: ${currentVersion}`); // Check if it's already an experimental version if (parsed.prerelease.length > 0 && parsed.prerelease[0] === 'exp') { + const minor = parsed.prerelease[1] || 0; + const minorInt = typeof minor === 'string' ? parseInt(minor) : minor; // Increment the experimental minor version - const expMinor = (parsed.prerelease[1] || 0) + 1; + const expMinor = minorInt + 1; return `${parsed.major}.${parsed.minor}.${parsed.patch}-exp.${expMinor}`; } @@ -23,7 +29,10 @@ function generateExperimentalVersion(currentVersion) { } const rootDir = process.cwd(); -const releaseType = process.env.RELEASE_TYPE; + +const releaseType = /** @type { import('semver').ReleaseType | "experimental" } */ ( + process.env.RELEASE_TYPE +); assert.match(releaseType, /^(patch|minor|major|experimental|premajor)$/, 'Invalid RELEASE_TYPE'); // TODO: if releaseType is `auto` determine release type based on the changelog @@ -39,8 +48,12 @@ const packages = JSON.parse( const packageMap = {}; for (let { name, path, version, private: isPrivate } of packages) { - if (isPrivate && path !== rootDir) continue; - if (path === rootDir) name = 'monorepo-root'; + if (isPrivate && path !== rootDir) { + continue; + } + if (path === rootDir) { + name = 'monorepo-root'; + } const isDirty = await exec(`git diff --quiet HEAD ${lastTag} -- ${path}`) .then(() => false) @@ -57,11 +70,94 @@ assert.ok( // Propagate isDirty transitively: if a package's dependency will be bumped, // that package also needs a bump (e.g. design-system → editor-ui → cli). +// Detect root-level changes that affect resolved dep versions without touching individual +// package.json files: pnpm.overrides (applies to all specifiers) +// and pnpm-workspace.yaml catalog entries (applies only to deps using a "catalog:…" specifier). + +const rootPkgJson = JSON.parse(await readFile(resolve(rootDir, 'package.json'), 'utf-8')); +const rootPkgJsonAtTag = await exec(`git show ${lastTag}:package.json`) + .then(({ stdout }) => JSON.parse(stdout)) + .catch(() => ({})); + +const getOverrides = (pkg) => ({ ...pkg.pnpm?.overrides, ...pkg.overrides }); + +const currentOverrides = getOverrides(rootPkgJson); +const previousOverrides = getOverrides(rootPkgJsonAtTag); + +const changedOverrides = new Set( + Object.keys({ ...currentOverrides, ...previousOverrides }).filter( + (k) => currentOverrides[k] !== previousOverrides[k], + ), +); + +const parseWorkspaceYaml = (content) => { + try { + return /** @type {Record} */ (parse(content) ?? {}); + } catch { + return {}; + } +}; +const workspaceYaml = parseWorkspaceYaml( + await readFile(resolve(rootDir, 'pnpm-workspace.yaml'), 'utf-8').catch(() => ''), +); +const workspaceYamlAtTag = parseWorkspaceYaml( + await exec(`git show ${lastTag}:pnpm-workspace.yaml`) + .then(({ stdout }) => stdout) + .catch(() => ''), +); +const getCatalogs = (ws) => { + const result = new Map(); + if (ws.catalog) { + result.set('default', /** @type {Record} */ (ws.catalog)); + } + + for (const [name, entries] of Object.entries(ws.catalogs ?? {})) { + result.set(name, entries); + } + + return result; +}; +// changedCatalogEntries: Map> +const currentCatalogs = getCatalogs(workspaceYaml); +const previousCatalogs = getCatalogs(workspaceYamlAtTag); +const changedCatalogEntries = new Map(); +for (const catalogName of new Set([...currentCatalogs.keys(), ...previousCatalogs.keys()])) { + const current = currentCatalogs.get(catalogName) ?? {}; + const previous = previousCatalogs.get(catalogName) ?? {}; + const changedDeps = new Set( + Object.keys({ ...current, ...previous }).filter((dep) => current[dep] !== previous[dep]), + ); + if (changedDeps.size > 0) { + changedCatalogEntries.set(catalogName, changedDeps); + } +} + +// Store full dep objects (with specifiers) so we can inspect "catalog:…" values below. const depsByPackage = {}; for (const packageName in packageMap) { const packageFile = resolve(packageMap[packageName].path, 'package.json'); const packageJson = JSON.parse(await readFile(packageFile, 'utf-8')); - depsByPackage[packageName] = Object.keys(packageJson.dependencies || {}); + depsByPackage[packageName] = /** @type {Record} */ ( + packageJson.dependencies ?? {} + ); +} + +// Mark packages dirty if any dep had a root-level override or catalog version change. +for (const [packageName, deps] of Object.entries(depsByPackage)) { + if (packageMap[packageName].isDirty) continue; + for (const [dep, specifier] of Object.entries(deps)) { + if (changedOverrides.has(dep)) { + packageMap[packageName].isDirty = true; + break; + } + if (typeof specifier === 'string' && specifier.startsWith('catalog:')) { + const catalogName = specifier === 'catalog:' ? 'default' : specifier.slice(8); + if (changedCatalogEntries.get(catalogName)?.has(dep)) { + packageMap[packageName].isDirty = true; + break; + } + } + } } let changed = true; @@ -69,7 +165,7 @@ while (changed) { changed = false; for (const packageName in packageMap) { if (packageMap[packageName].isDirty) continue; - if (depsByPackage[packageName].some((dep) => packageMap[dep]?.isDirty)) { + if (Object.keys(depsByPackage[packageName]).some((dep) => packageMap[dep]?.isDirty)) { packageMap[packageName].isDirty = true; changed = true; } diff --git a/.github/scripts/package.json b/.github/scripts/package.json index b3ef3607692..2851b783238 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -11,7 +11,8 @@ "glob": "13.0.6", "minimatch": "10.2.4", "semver": "7.7.4", - "tempfile": "6.0.1" + "tempfile": "6.0.1", + "yaml": "^2.8.3" }, "devDependencies": { "conventional-changelog-angular": "8.3.0" diff --git a/.github/scripts/pnpm-lock.yaml b/.github/scripts/pnpm-lock.yaml index ebb2db3ba38..fd84adfe419 100644 --- a/.github/scripts/pnpm-lock.yaml +++ b/.github/scripts/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: tempfile: specifier: 6.0.1 version: 6.0.1 + yaml: + specifier: ^2.8.3 + version: 2.8.3 devDependencies: conventional-changelog-angular: specifier: 8.3.0 @@ -292,6 +295,11 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + snapshots: '@actions/github@9.0.0': @@ -540,3 +548,5 @@ snapshots: walk-up-path@4.0.0: {} wordwrap@1.0.0: {} + + yaml@2.8.3: {}