@@ -34,9 +34,11 @@ function planNightly(run) {
3434 if ( scheduled && latest && run . tags . get ( latest ) === run . sha ) {
3535 return skip ( `${ latest } already ships ${ run . sha } .` ) ;
3636 }
37- if ( scheduled && latest ?. includes ( `-nightly.${ run . today } .` ) ) {
38- return skip ( `${ latest } shipped today.` ) ;
39- }
37+ // The npm dist-tag is the durable record: a nightly can publish and then fail to tag.
38+ const shippedToday = [ latest , run . publishedNightly ] . find ( ( shipped ) =>
39+ shipped ?. includes ( `-nightly.${ run . today } .` ) ,
40+ ) ;
41+ if ( scheduled && shippedToday ) return skip ( `${ shippedToday } shipped today.` ) ;
4042 if ( run . ciConclusion !== 'success' ) {
4143 const reason = `CI on ${ run . sha } is ${ run . ciConclusion } ; a nightly ships only a commit whose CI passed.` ;
4244 if ( scheduled ) return skip ( reason ) ;
@@ -66,6 +68,28 @@ function planStable(run, tag) {
6668 return { mode : 'stable' , version, distTag : 'latest' , commit : run . sha , previousTag } ;
6769}
6870
71+ const VERSION_FILE = / ^ ( p a c k a g e s \/ [ ^ / ] + \/ ) ? p a c k a g e \. j s o n $ | ^ s e r v e r \. j s o n $ / ;
72+
73+ /**
74+ * Whether a single-parent commit only rewrites `"version"` fields, as the release workflow's
75+ * `-dev` bump does. Its push runs no workflows, so the parent's CI stands for it.
76+ */
77+ export function isVersionOnlyCommit ( commit ) {
78+ return (
79+ commit . parents . length === 1 &&
80+ commit . files . length > 0 &&
81+ commit . files . every (
82+ ( file ) =>
83+ VERSION_FILE . test ( file . filename ) &&
84+ typeof file . patch === 'string' &&
85+ file . patch
86+ . split ( '\n' )
87+ . filter ( ( line ) => / ^ [ + - ] / . test ( line ) )
88+ . every ( ( line ) => / ^ [ + - ] \s * " v e r s i o n " : " [ ^ " ] * " , ? $ / . test ( line ) ) ,
89+ )
90+ ) ;
91+ }
92+
6993function skip ( reason ) {
7094 return { mode : 'skip' , reason } ;
7195}
@@ -93,22 +117,34 @@ function remoteTags() {
93117 return tags ;
94118}
95119
96- async function ciConclusion ( env ) {
120+ async function ciConclusion ( env , sha ) {
97121 const { workflow_runs : runs } = await github (
98122 env ,
99- `actions/workflows/ci.yml/runs?head_sha=${ env . GITHUB_SHA } &event=push&per_page=1` ,
123+ `actions/workflows/ci.yml/runs?head_sha=${ sha } &event=push&per_page=1` ,
100124 ) ;
101- if ( runs . length === 0 ) return 'missing' ;
102- return runs [ 0 ] . status === 'completed' ? runs [ 0 ] . conclusion : runs [ 0 ] . status ;
125+ if ( runs . length > 0 ) return runs [ 0 ] . status === 'completed' ? runs [ 0 ] . conclusion : runs [ 0 ] . status ;
126+ const commit = await github ( env , `commits/${ sha } ` ) ;
127+ return isVersionOnlyCommit ( commit ) ? ciConclusion ( env , commit . parents [ 0 ] . sha ) : 'missing' ;
128+ }
129+
130+ /** A tag pushed right after a merge waits, within the Plan job's budget, for that merge's CI. */
131+ async function settledCiConclusion ( env , sha ) {
132+ for ( let attempt = 0 ; ; attempt += 1 ) {
133+ const conclusion = await ciConclusion ( env , sha ) ;
134+ if ( ! [ 'queued' , 'in_progress' , 'waiting' , 'pending' ] . includes ( conclusion ) || attempt >= 40 ) {
135+ return conclusion ;
136+ }
137+ await new Promise ( ( resolve ) => setTimeout ( resolve , 30_000 ) ) ;
138+ }
103139}
104140
105141async function isOnMain ( env ) {
106142 const { status } = await github ( env , `compare/main...${ env . GITHUB_SHA } ` ) ;
107143 return status === 'identical' || status === 'behind' ;
108144}
109145
110- async function latestStable ( ) {
111- const response = await fetch ( ' https://registry.npmjs.org/agent-device/latest' ) ;
146+ async function publishedVersion ( distTag ) {
147+ const response = await fetch ( ` https://registry.npmjs.org/agent-device/${ distTag } ` ) ;
112148 if ( response . status === 404 ) return null ;
113149 if ( ! response . ok ) throw new Error ( `npm registry lookup failed: ${ response . status } ` ) ;
114150 return ( await response . json ( ) ) . version ;
@@ -138,8 +174,13 @@ async function readRun(env) {
138174 today : new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) . replaceAll ( '-' , '' ) ,
139175 runNumber : env . GITHUB_RUN_NUMBER ,
140176 tags : event === 'pull_request' ? new Map ( ) : remoteTags ( ) ,
141- latestStable : onBranch || onTag ? await latestStable ( ) : null ,
142- ciConclusion : onBranch || onTag ? await ciConclusion ( env ) : undefined ,
177+ latestStable : onBranch || onTag ? await publishedVersion ( 'latest' ) : null ,
178+ publishedNightly : onBranch ? await publishedVersion ( 'nightly' ) : null ,
179+ ciConclusion : onTag
180+ ? await settledCiConclusion ( env , env . GITHUB_SHA )
181+ : onBranch
182+ ? await ciConclusion ( env , env . GITHUB_SHA )
183+ : undefined ,
143184 onMain : onTag ? await isOnMain ( env ) : undefined ,
144185 } ;
145186}
0 commit comments