Skip to content

Commit 6bd2463

Browse files
ci: Account for pnpm-workspace changes in bump-versions.mjs (backport to 1.x) (#28506)
Co-authored-by: Matsu <huhta.matias@gmail.com>
1 parent 808bc6a commit 6bd2463

File tree

3 files changed

+114
-7
lines changed

3 files changed

+114
-7
lines changed

.github/scripts/bump-versions.mjs

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import semver from 'semver';
2+
import { parse } from 'yaml';
23
import { writeFile, readFile } from 'fs/promises';
34
import { resolve } from 'path';
45
import child_process from 'child_process';
@@ -7,14 +8,19 @@ import assert from 'assert';
78

89
const exec = promisify(child_process.exec);
910

11+
/**
12+
* @param {string | semver.SemVer} currentVersion
13+
*/
1014
function generateExperimentalVersion(currentVersion) {
1115
const parsed = semver.parse(currentVersion);
1216
if (!parsed) throw new Error(`Invalid version: ${currentVersion}`);
1317

1418
// Check if it's already an experimental version
1519
if (parsed.prerelease.length > 0 && parsed.prerelease[0] === 'exp') {
20+
const minor = parsed.prerelease[1] || 0;
21+
const minorInt = typeof minor === 'string' ? parseInt(minor) : minor;
1622
// Increment the experimental minor version
17-
const expMinor = (parsed.prerelease[1] || 0) + 1;
23+
const expMinor = minorInt + 1;
1824
return `${parsed.major}.${parsed.minor}.${parsed.patch}-exp.${expMinor}`;
1925
}
2026

@@ -23,7 +29,10 @@ function generateExperimentalVersion(currentVersion) {
2329
}
2430

2531
const rootDir = process.cwd();
26-
const releaseType = process.env.RELEASE_TYPE;
32+
33+
const releaseType = /** @type { import('semver').ReleaseType | "experimental" } */ (
34+
process.env.RELEASE_TYPE
35+
);
2736
assert.match(releaseType, /^(patch|minor|major|experimental|premajor)$/, 'Invalid RELEASE_TYPE');
2837

2938
// TODO: if releaseType is `auto` determine release type based on the changelog
@@ -39,8 +48,12 @@ const packages = JSON.parse(
3948

4049
const packageMap = {};
4150
for (let { name, path, version, private: isPrivate } of packages) {
42-
if (isPrivate && path !== rootDir) continue;
43-
if (path === rootDir) name = 'monorepo-root';
51+
if (isPrivate && path !== rootDir) {
52+
continue;
53+
}
54+
if (path === rootDir) {
55+
name = 'monorepo-root';
56+
}
4457

4558
const isDirty = await exec(`git diff --quiet HEAD ${lastTag} -- ${path}`)
4659
.then(() => false)
@@ -57,19 +70,102 @@ assert.ok(
5770
// Propagate isDirty transitively: if a package's dependency will be bumped,
5871
// that package also needs a bump (e.g. design-system → editor-ui → cli).
5972

73+
// Detect root-level changes that affect resolved dep versions without touching individual
74+
// package.json files: pnpm.overrides (applies to all specifiers)
75+
// and pnpm-workspace.yaml catalog entries (applies only to deps using a "catalog:…" specifier).
76+
77+
const rootPkgJson = JSON.parse(await readFile(resolve(rootDir, 'package.json'), 'utf-8'));
78+
const rootPkgJsonAtTag = await exec(`git show ${lastTag}:package.json`)
79+
.then(({ stdout }) => JSON.parse(stdout))
80+
.catch(() => ({}));
81+
82+
const getOverrides = (pkg) => ({ ...pkg.pnpm?.overrides, ...pkg.overrides });
83+
84+
const currentOverrides = getOverrides(rootPkgJson);
85+
const previousOverrides = getOverrides(rootPkgJsonAtTag);
86+
87+
const changedOverrides = new Set(
88+
Object.keys({ ...currentOverrides, ...previousOverrides }).filter(
89+
(k) => currentOverrides[k] !== previousOverrides[k],
90+
),
91+
);
92+
93+
const parseWorkspaceYaml = (content) => {
94+
try {
95+
return /** @type {Record<string, unknown>} */ (parse(content) ?? {});
96+
} catch {
97+
return {};
98+
}
99+
};
100+
const workspaceYaml = parseWorkspaceYaml(
101+
await readFile(resolve(rootDir, 'pnpm-workspace.yaml'), 'utf-8').catch(() => ''),
102+
);
103+
const workspaceYamlAtTag = parseWorkspaceYaml(
104+
await exec(`git show ${lastTag}:pnpm-workspace.yaml`)
105+
.then(({ stdout }) => stdout)
106+
.catch(() => ''),
107+
);
108+
const getCatalogs = (ws) => {
109+
const result = new Map();
110+
if (ws.catalog) {
111+
result.set('default', /** @type {Record<string,string>} */ (ws.catalog));
112+
}
113+
114+
for (const [name, entries] of Object.entries(ws.catalogs ?? {})) {
115+
result.set(name, entries);
116+
}
117+
118+
return result;
119+
};
120+
// changedCatalogEntries: Map<catalogName, Set<depName>>
121+
const currentCatalogs = getCatalogs(workspaceYaml);
122+
const previousCatalogs = getCatalogs(workspaceYamlAtTag);
123+
const changedCatalogEntries = new Map();
124+
for (const catalogName of new Set([...currentCatalogs.keys(), ...previousCatalogs.keys()])) {
125+
const current = currentCatalogs.get(catalogName) ?? {};
126+
const previous = previousCatalogs.get(catalogName) ?? {};
127+
const changedDeps = new Set(
128+
Object.keys({ ...current, ...previous }).filter((dep) => current[dep] !== previous[dep]),
129+
);
130+
if (changedDeps.size > 0) {
131+
changedCatalogEntries.set(catalogName, changedDeps);
132+
}
133+
}
134+
135+
// Store full dep objects (with specifiers) so we can inspect "catalog:…" values below.
60136
const depsByPackage = {};
61137
for (const packageName in packageMap) {
62138
const packageFile = resolve(packageMap[packageName].path, 'package.json');
63139
const packageJson = JSON.parse(await readFile(packageFile, 'utf-8'));
64-
depsByPackage[packageName] = Object.keys(packageJson.dependencies || {});
140+
depsByPackage[packageName] = /** @type {Record<string,string>} */ (
141+
packageJson.dependencies ?? {}
142+
);
143+
}
144+
145+
// Mark packages dirty if any dep had a root-level override or catalog version change.
146+
for (const [packageName, deps] of Object.entries(depsByPackage)) {
147+
if (packageMap[packageName].isDirty) continue;
148+
for (const [dep, specifier] of Object.entries(deps)) {
149+
if (changedOverrides.has(dep)) {
150+
packageMap[packageName].isDirty = true;
151+
break;
152+
}
153+
if (typeof specifier === 'string' && specifier.startsWith('catalog:')) {
154+
const catalogName = specifier === 'catalog:' ? 'default' : specifier.slice(8);
155+
if (changedCatalogEntries.get(catalogName)?.has(dep)) {
156+
packageMap[packageName].isDirty = true;
157+
break;
158+
}
159+
}
160+
}
65161
}
66162

67163
let changed = true;
68164
while (changed) {
69165
changed = false;
70166
for (const packageName in packageMap) {
71167
if (packageMap[packageName].isDirty) continue;
72-
if (depsByPackage[packageName].some((dep) => packageMap[dep]?.isDirty)) {
168+
if (Object.keys(depsByPackage[packageName]).some((dep) => packageMap[dep]?.isDirty)) {
73169
packageMap[packageName].isDirty = true;
74170
changed = true;
75171
}

.github/scripts/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"debug": "4.4.3",
1111
"glob": "13.0.6",
1212
"semver": "7.7.4",
13-
"tempfile": "6.0.1"
13+
"tempfile": "6.0.1",
14+
"yaml": "^2.8.3"
1415
},
1516
"devDependencies": {
1617
"conventional-changelog-angular": "8.3.0"

.github/scripts/pnpm-lock.yaml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)