Skip to content

Commit 18f0349

Browse files
committed
feat(workflow): generate accurate upgrade-deps PR descriptions via Claude
Previously the `Upgrade Upstream Dependencies` workflow produced a generic template commit message and PR body (see #1401) that didn't reflect what actually changed. This wires up a second claude-code-action pass whose only job is to write a diff-accurate commit message and PR body. - `.github/scripts/upgrade-deps.mjs` now captures old -> new for every dependency it touches (including rolldown/vite tag + short SHA) and writes `versions.json`, `commit-message.txt`, and `pr-body.md` to `$UPGRADE_DEPS_META_DIR` (outside the repo, so not committed). - `.github/workflows/upgrade-deps.yml`: - Exports `UPGRADE_DEPS_META_DIR` via `$GITHUB_ENV` in a setup step. - Adds an "Enhance PR description with Claude" step that reads the baseline files + `git diff` and overwrites them with a concrete summary, a dependency table, and a real code-changes section. - Adds a "Read generated PR content" step that exposes the files as multi-line step outputs. - `peter-evans/create-pull-request` now consumes those outputs instead of a static template body. - Tightens the `check-upgrade-dependencies` final check to require BOTH `just build` AND `pnpm bootstrap-cli:ci && pnpm test` to pass (with manual snap-test diff inspection). - Bumps both `claude-code-action` pins to v1.0.99 (Claude Code 2.1.112). If the enhancement step fails, `continue-on-error` keeps the workflow going and the baseline content produced by the Node script is shipped instead of a generic message.
1 parent 87aa125 commit 18f0349

File tree

2 files changed

+268
-56
lines changed

2 files changed

+268
-56
lines changed

.github/scripts/upgrade-deps.mjs

Lines changed: 154 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,24 @@ import fs from 'node:fs';
22
import path from 'node:path';
33

44
const ROOT = process.cwd();
5+
const META_DIR = process.env.UPGRADE_DEPS_META_DIR;
6+
7+
/** @type {Map<string, { old: string | null, new: string, tag?: string }>} */
8+
const changes = new Map();
9+
10+
function recordChange(name, oldValue, newValue, tag) {
11+
const entry = { old: oldValue ?? null, new: newValue };
12+
if (tag) entry.tag = tag;
13+
changes.set(name, entry);
14+
if (oldValue !== newValue) {
15+
console.log(` ${name}: ${oldValue ?? '(unset)'} -> ${newValue}`);
16+
} else {
17+
console.log(` ${name}: ${newValue} (unchanged)`);
18+
}
19+
}
520

621
// ============ GitHub API ============
7-
async function getLatestTagCommit(owner, repo) {
22+
async function getLatestTag(owner, repo) {
823
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/tags`, {
924
headers: {
1025
Authorization: `token ${process.env.GITHUB_TOKEN}`,
@@ -18,11 +33,11 @@ async function getLatestTagCommit(owner, repo) {
1833
if (!Array.isArray(tags) || !tags.length) {
1934
throw new Error(`No tags found for ${owner}/${repo}`);
2035
}
21-
if (!tags[0]?.commit?.sha) {
22-
throw new Error(`Invalid tag structure for ${owner}/${repo}: missing commit SHA`);
36+
if (!tags[0]?.commit?.sha || !tags[0]?.name) {
37+
throw new Error(`Invalid tag structure for ${owner}/${repo}: missing SHA or name`);
2338
}
24-
console.log(`${repo} -> ${tags[0].name}`);
25-
return tags[0].commit.sha;
39+
console.log(`${repo} -> ${tags[0].name} (${tags[0].commit.sha.slice(0, 7)})`);
40+
return { sha: tags[0].commit.sha, tag: tags[0].name };
2641
}
2742

2843
// ============ npm Registry ============
@@ -45,11 +60,16 @@ async function updateUpstreamVersions() {
4560
const filePath = path.join(ROOT, 'packages/tools/.upstream-versions.json');
4661
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
4762

48-
// rolldown -> rolldown/rolldown
49-
data.rolldown.hash = await getLatestTagCommit('rolldown', 'rolldown');
50-
51-
// vite -> vitejs/vite
52-
data['vite'].hash = await getLatestTagCommit('vitejs', 'vite');
63+
const oldRolldownHash = data.rolldown.hash;
64+
const oldViteHash = data['vite'].hash;
65+
const [rolldown, vite] = await Promise.all([
66+
getLatestTag('rolldown', 'rolldown'),
67+
getLatestTag('vitejs', 'vite'),
68+
]);
69+
data.rolldown.hash = rolldown.sha;
70+
data['vite'].hash = vite.sha;
71+
recordChange('rolldown', oldRolldownHash, rolldown.sha, rolldown.tag);
72+
recordChange('vite', oldViteHash, vite.sha, vite.tag);
5373

5474
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
5575
console.log('Updated .upstream-versions.json');
@@ -60,38 +80,59 @@ async function updatePnpmWorkspace(versions) {
6080
const filePath = path.join(ROOT, 'pnpm-workspace.yaml');
6181
let content = fs.readFileSync(filePath, 'utf8');
6282

63-
// Update vitest-dev override (handle pre-release versions like -beta.1, -rc.0)
64-
content = content.replace(
65-
/vitest-dev: npm:vitest@\^[\d.]+(-[\w.]+)?/,
66-
`vitest-dev: npm:vitest@^${versions.vitest}`,
67-
);
68-
69-
// Update tsdown in catalog (handle pre-release versions)
70-
content = content.replace(/tsdown: \^[\d.]+(-[\w.]+)?/, `tsdown: ^${versions.tsdown}`);
71-
72-
// Update @oxc-node/cli in catalog
73-
content = content.replace(
74-
/'@oxc-node\/cli': \^[\d.]+(-[\w.]+)?/,
75-
`'@oxc-node/cli': ^${versions.oxcNodeCli}`,
76-
);
77-
78-
// Update @oxc-node/core in catalog
79-
content = content.replace(
80-
/'@oxc-node\/core': \^[\d.]+(-[\w.]+)?/,
81-
`'@oxc-node/core': ^${versions.oxcNodeCore}`,
82-
);
83-
84-
// Update oxfmt in catalog
85-
content = content.replace(/oxfmt: =[\d.]+(-[\w.]+)?/, `oxfmt: =${versions.oxfmt}`);
86-
87-
// Update oxlint in catalog (but not oxlint-tsgolint)
88-
content = content.replace(/oxlint: =[\d.]+(-[\w.]+)?\n/, `oxlint: =${versions.oxlint}\n`);
83+
// The capture regex returns the current version in $1; the replacement string
84+
// substitutes the new version into the same anchor text.
85+
// oxlint's trailing \n disambiguates from oxlint-tsgolint.
86+
const entries = [
87+
{
88+
name: 'vitest',
89+
pattern: /vitest-dev: npm:vitest@\^([\d.]+(?:-[\w.]+)?)/,
90+
replacement: `vitest-dev: npm:vitest@^${versions.vitest}`,
91+
newVersion: versions.vitest,
92+
},
93+
{
94+
name: 'tsdown',
95+
pattern: /tsdown: \^([\d.]+(?:-[\w.]+)?)/,
96+
replacement: `tsdown: ^${versions.tsdown}`,
97+
newVersion: versions.tsdown,
98+
},
99+
{
100+
name: '@oxc-node/cli',
101+
pattern: /'@oxc-node\/cli': \^([\d.]+(?:-[\w.]+)?)/,
102+
replacement: `'@oxc-node/cli': ^${versions.oxcNodeCli}`,
103+
newVersion: versions.oxcNodeCli,
104+
},
105+
{
106+
name: '@oxc-node/core',
107+
pattern: /'@oxc-node\/core': \^([\d.]+(?:-[\w.]+)?)/,
108+
replacement: `'@oxc-node/core': ^${versions.oxcNodeCore}`,
109+
newVersion: versions.oxcNodeCore,
110+
},
111+
{
112+
name: 'oxfmt',
113+
pattern: /oxfmt: =([\d.]+(?:-[\w.]+)?)/,
114+
replacement: `oxfmt: =${versions.oxfmt}`,
115+
newVersion: versions.oxfmt,
116+
},
117+
{
118+
name: 'oxlint',
119+
pattern: /oxlint: =([\d.]+(?:-[\w.]+)?)\n/,
120+
replacement: `oxlint: =${versions.oxlint}\n`,
121+
newVersion: versions.oxlint,
122+
},
123+
{
124+
name: 'oxlint-tsgolint',
125+
pattern: /oxlint-tsgolint: =([\d.]+(?:-[\w.]+)?)/,
126+
replacement: `oxlint-tsgolint: =${versions.oxlintTsgolint}`,
127+
newVersion: versions.oxlintTsgolint,
128+
},
129+
];
89130

90-
// Update oxlint-tsgolint in catalog
91-
content = content.replace(
92-
/oxlint-tsgolint: =[\d.]+(-[\w.]+)?/,
93-
`oxlint-tsgolint: =${versions.oxlintTsgolint}`,
94-
);
131+
for (const { name, pattern, replacement, newVersion } of entries) {
132+
const oldVersion = content.match(pattern)?.[1];
133+
content = content.replace(pattern, replacement);
134+
recordChange(name, oldVersion, newVersion);
135+
}
95136

96137
fs.writeFileSync(filePath, content);
97138
console.log('Updated pnpm-workspace.yaml');
@@ -128,15 +169,83 @@ async function updateCorePackage(devtoolsVersion) {
128169
const filePath = path.join(ROOT, 'packages/core/package.json');
129170
const pkg = JSON.parse(fs.readFileSync(filePath, 'utf8'));
130171

131-
// Update @vitejs/devtools in devDependencies
132-
if (pkg.devDependencies?.['@vitejs/devtools']) {
172+
const currentDevtools = pkg.devDependencies?.['@vitejs/devtools'];
173+
if (currentDevtools) {
133174
pkg.devDependencies['@vitejs/devtools'] = `^${devtoolsVersion}`;
175+
recordChange('@vitejs/devtools', currentDevtools.replace(/^[\^~]/, ''), devtoolsVersion);
134176
}
135177

136178
fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n');
137179
console.log('Updated packages/core/package.json');
138180
}
139181

182+
// ============ Write metadata files for PR description ============
183+
function writeMetaFiles() {
184+
if (!META_DIR) return;
185+
186+
fs.mkdirSync(META_DIR, { recursive: true });
187+
188+
const versionsObj = Object.fromEntries(changes);
189+
fs.writeFileSync(
190+
path.join(META_DIR, 'versions.json'),
191+
JSON.stringify(versionsObj, null, 2) + '\n',
192+
);
193+
194+
const changed = [...changes.entries()].filter(([, v]) => v.old !== v.new);
195+
const unchanged = [...changes.entries()].filter(([, v]) => v.old === v.new);
196+
197+
const isFullSha = (s) => /^[0-9a-f]{40}$/.test(s);
198+
const formatVersion = (v) => {
199+
if (v.tag) return `${v.tag} (${v.new.slice(0, 7)})`;
200+
if (isFullSha(v.new)) return v.new.slice(0, 7);
201+
return v.new;
202+
};
203+
const formatOld = (v) => {
204+
if (!v.old) return '(unset)';
205+
if (isFullSha(v.old)) return v.old.slice(0, 7);
206+
return v.old;
207+
};
208+
209+
const commitLines = ['feat(deps): upgrade upstream dependencies', ''];
210+
if (changed.length) {
211+
for (const [name, v] of changed) {
212+
commitLines.push(`- ${name}: ${formatOld(v)} -> ${formatVersion(v)}`);
213+
}
214+
} else {
215+
commitLines.push('- no version changes detected');
216+
}
217+
commitLines.push('');
218+
fs.writeFileSync(path.join(META_DIR, 'commit-message.txt'), commitLines.join('\n'));
219+
220+
const bodyLines = ['## Summary', ''];
221+
if (changed.length) {
222+
bodyLines.push('Automated daily upgrade of upstream dependencies.');
223+
} else {
224+
bodyLines.push('Automated daily upgrade run — no upstream version changes detected.');
225+
}
226+
bodyLines.push('', '## Dependency updates', '');
227+
if (changed.length) {
228+
bodyLines.push('| Package | From | To |');
229+
bodyLines.push('| --- | --- | --- |');
230+
for (const [name, v] of changed) {
231+
bodyLines.push(`| \`${name}\` | \`${formatOld(v)}\` | \`${formatVersion(v)}\` |`);
232+
}
233+
} else {
234+
bodyLines.push('_No version changes._');
235+
}
236+
if (unchanged.length) {
237+
bodyLines.push('', '<details><summary>Unchanged dependencies</summary>', '');
238+
for (const [name, v] of unchanged) {
239+
bodyLines.push(`- \`${name}\`: \`${formatVersion(v)}\``);
240+
}
241+
bodyLines.push('', '</details>');
242+
}
243+
bodyLines.push('', '## Code changes', '', '_No additional code changes recorded._', '');
244+
fs.writeFileSync(path.join(META_DIR, 'pr-body.md'), bodyLines.join('\n'));
245+
246+
console.log(`Wrote metadata files to ${META_DIR}`);
247+
}
248+
140249
console.log('Fetching latest versions…');
141250

142251
const [
@@ -181,4 +290,6 @@ await updatePnpmWorkspace({
181290
await updateTestPackage(vitestVersion);
182291
await updateCorePackage(devtoolsVersion);
183292

293+
writeMetaFiles();
294+
184295
console.log('Done!');

0 commit comments

Comments
 (0)