@@ -2,10 +2,29 @@ import fs from 'node:fs';
22import path from 'node:path' ;
33
44const ROOT = process . cwd ( ) ;
5+ const META_DIR = process . env . UPGRADE_DEPS_META_DIR ;
6+
7+ const isFullSha = ( s ) => / ^ [ 0 - 9 a - f ] { 40 } $ / . test ( s ) ;
8+
9+ /** @type {Map<string, { old: string | null, new: string, tag?: string }> } */
10+ const changes = new Map ( ) ;
11+
12+ function recordChange ( name , oldValue , newValue , tag ) {
13+ const entry = { old : oldValue ?? null , new : newValue } ;
14+ if ( tag ) {
15+ entry . tag = tag ;
16+ }
17+ changes . set ( name , entry ) ;
18+ if ( oldValue !== newValue ) {
19+ console . log ( ` ${ name } : ${ oldValue ?? '(unset)' } -> ${ newValue } ` ) ;
20+ } else {
21+ console . log ( ` ${ name } : ${ newValue } (unchanged)` ) ;
22+ }
23+ }
524
625// ============ GitHub API ============
7- async function getLatestTagCommit ( owner , repo ) {
8- const res = await fetch ( `https://api.github.com/repos/${ owner } /${ repo } /tags` , {
26+ async function getLatestTag ( owner , repo ) {
27+ const res = await fetch ( `https://api.github.com/repos/${ owner } /${ repo } /tags?per_page=1 ` , {
928 headers : {
1029 Authorization : `token ${ process . env . GITHUB_TOKEN } ` ,
1130 Accept : 'application/vnd.github.v3+json' ,
@@ -18,11 +37,11 @@ async function getLatestTagCommit(owner, repo) {
1837 if ( ! Array . isArray ( tags ) || ! tags . length ) {
1938 throw new Error ( `No tags found for ${ owner } /${ repo } ` ) ;
2039 }
21- if ( ! tags [ 0 ] ?. commit ?. sha ) {
22- throw new Error ( `Invalid tag structure for ${ owner } /${ repo } : missing commit SHA` ) ;
40+ if ( ! tags [ 0 ] ?. commit ?. sha || ! tags [ 0 ] ?. name ) {
41+ throw new Error ( `Invalid tag structure for ${ owner } /${ repo } : missing SHA or name ` ) ;
2342 }
24- console . log ( `${ repo } -> ${ tags [ 0 ] . name } ` ) ;
25- return tags [ 0 ] . commit . sha ;
43+ console . log ( `${ repo } -> ${ tags [ 0 ] . name } ( ${ tags [ 0 ] . commit . sha . slice ( 0 , 7 ) } ) ` ) ;
44+ return { sha : tags [ 0 ] . commit . sha , tag : tags [ 0 ] . name } ;
2645}
2746
2847// ============ npm Registry ============
@@ -45,11 +64,16 @@ async function updateUpstreamVersions() {
4564 const filePath = path . join ( ROOT , 'packages/tools/.upstream-versions.json' ) ;
4665 const data = JSON . parse ( fs . readFileSync ( filePath , 'utf8' ) ) ;
4766
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' ) ;
67+ const oldRolldownHash = data . rolldown . hash ;
68+ const oldViteHash = data [ 'vite' ] . hash ;
69+ const [ rolldown , vite ] = await Promise . all ( [
70+ getLatestTag ( 'rolldown' , 'rolldown' ) ,
71+ getLatestTag ( 'vitejs' , 'vite' ) ,
72+ ] ) ;
73+ data . rolldown . hash = rolldown . sha ;
74+ data [ 'vite' ] . hash = vite . sha ;
75+ recordChange ( 'rolldown' , oldRolldownHash , rolldown . sha , rolldown . tag ) ;
76+ recordChange ( 'vite' , oldViteHash , vite . sha , vite . tag ) ;
5377
5478 fs . writeFileSync ( filePath , JSON . stringify ( data , null , 2 ) + '\n' ) ;
5579 console . log ( 'Updated .upstream-versions.json' ) ;
@@ -60,38 +84,66 @@ async function updatePnpmWorkspace(versions) {
6084 const filePath = path . join ( ROOT , 'pnpm-workspace.yaml' ) ;
6185 let content = fs . readFileSync ( filePath , 'utf8' ) ;
6286
63- // Update vitest-dev override (handle pre-release versions like -beta.1, -rc.0)
64- content = content . replace (
65- / v i t e s t - d e v : n p m : v i t e s t @ \^ [ \d . ] + ( - [ \w . ] + ) ? / ,
66- `vitest-dev: npm:vitest@^${ versions . vitest } ` ,
67- ) ;
68-
69- // Update tsdown in catalog (handle pre-release versions)
70- content = content . replace ( / t s d o w n : \^ [ \d . ] + ( - [ \w . ] + ) ? / , `tsdown: ^${ versions . tsdown } ` ) ;
71-
72- // Update @oxc -node/cli in catalog
73- content = content . replace (
74- / ' @ o x c - n o d e \/ c l i ' : \^ [ \d . ] + ( - [ \w . ] + ) ? / ,
75- `'@oxc-node/cli': ^${ versions . oxcNodeCli } ` ,
76- ) ;
77-
78- // Update @oxc -node/core in catalog
79- content = content . replace (
80- / ' @ o x c - n o d e \/ c o r e ' : \^ [ \d . ] + ( - [ \w . ] + ) ? / ,
81- `'@oxc-node/core': ^${ versions . oxcNodeCore } ` ,
82- ) ;
83-
84- // Update oxfmt in catalog
85- content = content . replace ( / o x f m t : = [ \d . ] + ( - [ \w . ] + ) ? / , `oxfmt: =${ versions . oxfmt } ` ) ;
86-
87- // Update oxlint in catalog (but not oxlint-tsgolint)
88- content = content . replace ( / o x l i n t : = [ \d . ] + ( - [ \w . ] + ) ? \n / , `oxlint: =${ versions . oxlint } \n` ) ;
87+ // oxlint's trailing \n in the pattern disambiguates from oxlint-tsgolint.
88+ const entries = [
89+ {
90+ name : 'vitest' ,
91+ pattern : / v i t e s t - d e v : n p m : v i t e s t @ \^ ( [ \d . ] + (?: - [ \w . ] + ) ? ) / ,
92+ replacement : `vitest-dev: npm:vitest@^${ versions . vitest } ` ,
93+ newVersion : versions . vitest ,
94+ } ,
95+ {
96+ name : 'tsdown' ,
97+ pattern : / t s d o w n : \^ ( [ \d . ] + (?: - [ \w . ] + ) ? ) / ,
98+ replacement : `tsdown: ^${ versions . tsdown } ` ,
99+ newVersion : versions . tsdown ,
100+ } ,
101+ {
102+ name : '@oxc-node/cli' ,
103+ pattern : / ' @ o x c - n o d e \/ c l i ' : \^ ( [ \d . ] + (?: - [ \w . ] + ) ? ) / ,
104+ replacement : `'@oxc-node/cli': ^${ versions . oxcNodeCli } ` ,
105+ newVersion : versions . oxcNodeCli ,
106+ } ,
107+ {
108+ name : '@oxc-node/core' ,
109+ pattern : / ' @ o x c - n o d e \/ c o r e ' : \^ ( [ \d . ] + (?: - [ \w . ] + ) ? ) / ,
110+ replacement : `'@oxc-node/core': ^${ versions . oxcNodeCore } ` ,
111+ newVersion : versions . oxcNodeCore ,
112+ } ,
113+ {
114+ name : 'oxfmt' ,
115+ pattern : / o x f m t : = ( [ \d . ] + (?: - [ \w . ] + ) ? ) / ,
116+ replacement : `oxfmt: =${ versions . oxfmt } ` ,
117+ newVersion : versions . oxfmt ,
118+ } ,
119+ {
120+ name : 'oxlint' ,
121+ pattern : / o x l i n t : = ( [ \d . ] + (?: - [ \w . ] + ) ? ) \n / ,
122+ replacement : `oxlint: =${ versions . oxlint } \n` ,
123+ newVersion : versions . oxlint ,
124+ } ,
125+ {
126+ name : 'oxlint-tsgolint' ,
127+ pattern : / o x l i n t - t s g o l i n t : = ( [ \d . ] + (?: - [ \w . ] + ) ? ) / ,
128+ replacement : `oxlint-tsgolint: =${ versions . oxlintTsgolint } ` ,
129+ newVersion : versions . oxlintTsgolint ,
130+ } ,
131+ ] ;
89132
90- // Update oxlint-tsgolint in catalog
91- content = content . replace (
92- / o x l i n t - t s g o l i n t : = [ \d . ] + ( - [ \w . ] + ) ? / ,
93- `oxlint-tsgolint: =${ versions . oxlintTsgolint } ` ,
94- ) ;
133+ for ( const { name, pattern, replacement, newVersion } of entries ) {
134+ let oldVersion ;
135+ content = content . replace ( pattern , ( _match , captured ) => {
136+ oldVersion = captured ;
137+ return replacement ;
138+ } ) ;
139+ if ( oldVersion === undefined ) {
140+ throw new Error (
141+ `Failed to match ${ name } in pnpm-workspace.yaml — the pattern ${ pattern } is stale, ` +
142+ `please update it in .github/scripts/upgrade-deps.mjs` ,
143+ ) ;
144+ }
145+ recordChange ( name , oldVersion , newVersion ) ;
146+ }
95147
96148 fs . writeFileSync ( filePath , content ) ;
97149 console . log ( 'Updated pnpm-workspace.yaml' ) ;
@@ -128,15 +180,93 @@ async function updateCorePackage(devtoolsVersion) {
128180 const filePath = path . join ( ROOT , 'packages/core/package.json' ) ;
129181 const pkg = JSON . parse ( fs . readFileSync ( filePath , 'utf8' ) ) ;
130182
131- // Update @vitejs /devtools in devDependencies
132- if ( pkg . devDependencies ?. [ '@vitejs/devtools' ] ) {
133- pkg . devDependencies [ '@vitejs/devtools' ] = `^ ${ devtoolsVersion } ` ;
183+ const currentDevtools = pkg . devDependencies ?. [ ' @vitejs/devtools' ] ;
184+ if ( ! currentDevtools ) {
185+ return ;
134186 }
187+ pkg . devDependencies [ '@vitejs/devtools' ] = `^${ devtoolsVersion } ` ;
188+ recordChange ( '@vitejs/devtools' , currentDevtools . replace ( / ^ [ \^ ~ ] / , '' ) , devtoolsVersion ) ;
135189
136190 fs . writeFileSync ( filePath , JSON . stringify ( pkg , null , 2 ) + '\n' ) ;
137191 console . log ( 'Updated packages/core/package.json' ) ;
138192}
139193
194+ // ============ Write metadata files for PR description ============
195+ function writeMetaFiles ( ) {
196+ if ( ! META_DIR ) {
197+ return ;
198+ }
199+
200+ fs . mkdirSync ( META_DIR , { recursive : true } ) ;
201+
202+ const versionsObj = Object . fromEntries ( changes ) ;
203+ fs . writeFileSync (
204+ path . join ( META_DIR , 'versions.json' ) ,
205+ JSON . stringify ( versionsObj , null , 2 ) + '\n' ,
206+ ) ;
207+
208+ const changed = [ ...changes . entries ( ) ] . filter ( ( [ , v ] ) => v . old !== v . new ) ;
209+ const unchanged = [ ...changes . entries ( ) ] . filter ( ( [ , v ] ) => v . old === v . new ) ;
210+
211+ const formatVersion = ( v ) => {
212+ if ( v . tag ) {
213+ return `${ v . tag } (${ v . new . slice ( 0 , 7 ) } )` ;
214+ }
215+ if ( isFullSha ( v . new ) ) {
216+ return v . new . slice ( 0 , 7 ) ;
217+ }
218+ return v . new ;
219+ } ;
220+ const formatOld = ( v ) => {
221+ if ( ! v . old ) {
222+ return '(unset)' ;
223+ }
224+ if ( isFullSha ( v . old ) ) {
225+ return v . old . slice ( 0 , 7 ) ;
226+ }
227+ return v . old ;
228+ } ;
229+
230+ const commitLines = [ 'feat(deps): upgrade upstream dependencies' , '' ] ;
231+ if ( changed . length ) {
232+ for ( const [ name , v ] of changed ) {
233+ commitLines . push ( `- ${ name } : ${ formatOld ( v ) } -> ${ formatVersion ( v ) } ` ) ;
234+ }
235+ } else {
236+ commitLines . push ( '- no version changes detected' ) ;
237+ }
238+ commitLines . push ( '' ) ;
239+ fs . writeFileSync ( path . join ( META_DIR , 'commit-message.txt' ) , commitLines . join ( '\n' ) ) ;
240+
241+ const bodyLines = [ '## Summary' , '' ] ;
242+ if ( changed . length ) {
243+ bodyLines . push ( 'Automated daily upgrade of upstream dependencies.' ) ;
244+ } else {
245+ bodyLines . push ( 'Automated daily upgrade run — no upstream version changes detected.' ) ;
246+ }
247+ bodyLines . push ( '' , '## Dependency updates' , '' ) ;
248+ if ( changed . length ) {
249+ bodyLines . push ( '| Package | From | To |' ) ;
250+ bodyLines . push ( '| --- | --- | --- |' ) ;
251+ for ( const [ name , v ] of changed ) {
252+ bodyLines . push ( `| \`${ name } \` | \`${ formatOld ( v ) } \` | \`${ formatVersion ( v ) } \` |` ) ;
253+ }
254+ } else {
255+ bodyLines . push ( '_No version changes._' ) ;
256+ }
257+ if ( unchanged . length ) {
258+ bodyLines . push ( '' , '<details><summary>Unchanged dependencies</summary>' , '' ) ;
259+ for ( const [ name , v ] of unchanged ) {
260+ bodyLines . push ( `- \`${ name } \`: \`${ formatVersion ( v ) } \`` ) ;
261+ }
262+ bodyLines . push ( '' , '</details>' ) ;
263+ }
264+ bodyLines . push ( '' , '## Code changes' , '' , '_No additional code changes recorded._' , '' ) ;
265+ fs . writeFileSync ( path . join ( META_DIR , 'pr-body.md' ) , bodyLines . join ( '\n' ) ) ;
266+
267+ console . log ( `Wrote metadata files to ${ META_DIR } ` ) ;
268+ }
269+
140270console . log ( 'Fetching latest versions…' ) ;
141271
142272const [
@@ -181,4 +311,6 @@ await updatePnpmWorkspace({
181311await updateTestPackage ( vitestVersion ) ;
182312await updateCorePackage ( devtoolsVersion ) ;
183313
314+ writeMetaFiles ( ) ;
315+
184316console . log ( 'Done!' ) ;
0 commit comments