@@ -197,13 +197,56 @@ function requireExecutable(params: {
197197 return resolved ;
198198}
199199
200+ // npm lists a new version before its CDN serves it, and the CDN then caches that
201+ // 404 for five minutes; every run in that window died on 0.1.93. the 330s total
202+ // outlasts a 404 this run's own first attempt cached. see wiki/action-bootstrap.md.
203+ const PREFETCH_RETRY_SECONDS = [ 30 , 60 , 90 , 150 ] ;
204+
205+ function isNotYetServed ( error : unknown ) : boolean {
206+ if ( ! ( error instanceof Error ) || ! ( "stderr" in error ) ) return false ;
207+ const stderr = String ( error . stderr ) ;
208+ return ( stderr . includes ( "E404" ) && stderr . includes ( ".tgz" ) ) || stderr . includes ( "ETARGET" ) ;
209+ }
210+
211+ /**
212+ * Download the package into npm's cache, retrying only a release npm lists but
213+ * does not serve yet, so the real run installs from the cache. Any other failure
214+ * is left for the real run to reproduce with its output visible.
215+ */
216+ function prefetchPackage ( params : {
217+ context : RuntimeContext ;
218+ npxPath : string ;
219+ packageSpec : string ;
220+ } ) : void {
221+ for ( let attempt = 0 ; ; attempt ++ ) {
222+ try {
223+ execFileSync (
224+ params . npxPath ,
225+ [ "--yes" , "--package" , params . packageSpec , "--" , "node" , "-e" , "0" ] ,
226+ {
227+ cwd : mkdtempSync ( join ( tmpdir ( ) , "pullfrog-bootstrap-" ) ) ,
228+ stdio : [ "ignore" , "ignore" , "pipe" ] ,
229+ env : params . context . env ,
230+ }
231+ ) ;
232+ return ;
233+ } catch ( error ) {
234+ const delay = PREFETCH_RETRY_SECONDS [ attempt ] ;
235+ if ( delay === undefined || ! isNotYetServed ( error ) ) return ;
236+ console . warn ( `» npm does not serve ${ params . packageSpec } yet; retrying in ${ delay } s` ) ;
237+ Atomics . wait ( new Int32Array ( new SharedArrayBuffer ( 4 ) ) , 0 , 0 , delay * 1000 ) ;
238+ }
239+ }
240+ }
241+
200242function runPackageCli ( context : RuntimeContext , packageSpec : string , cliArgs : string [ ] ) : void {
201243 // `verify` here and not in `requireExecutable`: this is the one resolution
202244 // whose failure is unattributable, because a broken launcher dies before any
203245 // of our logging exists. see #1084.
204246 const npxPath = resolveExecutable ( { command : "npx" , env : context . env , verify : true } ) ;
205247 if ( npxPath ) {
206248 console . log ( `» running ${ packageSpec } via ${ npxPath } ` ) ;
249+ prefetchPackage ( { context, npxPath, packageSpec } ) ;
207250 runCommand ( { context, command : npxPath , args : [ "--yes" , packageSpec , ...cliArgs ] } ) ;
208251 return ;
209252 }
0 commit comments