Skip to content

Commit 84a2302

Browse files
colinhackspullfrog[bot]
authored andcommitted
bootstrap: retry a release npm lists but does not serve yet (#1451)
* bootstrap: retry a release npm lists but does not serve yet, instead of failing the run * bootstrap: give the last prefetch retry margin past the cached 404; fix the patch skill's run count
1 parent a3352e7 commit 84a2302

1 file changed

Lines changed: 43 additions & 0 deletions

File tree

‎runCli.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
200242
function 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

Comments
 (0)