Skip to content

Commit e4c2bc1

Browse files
committed
fix(release): bound artifact propagation checks
1 parent 26ab83b commit e4c2bc1

2 files changed

Lines changed: 114 additions & 32 deletions

File tree

scripts/assert-release-published.js

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -179,22 +179,36 @@ function sleep(ms) {
179179
});
180180
}
181181

182+
function nextRetryDelay(attempt, attempts, delayMs, options) {
183+
if (attempt >= attempts) return null;
184+
if (options.deadline === undefined) return delayMs;
185+
186+
const now = options.now || Date.now;
187+
const remainingMs = options.deadline - now();
188+
if (remainingMs <= 0) return null;
189+
return Math.min(delayMs, remainingMs);
190+
}
191+
182192
async function waitForNpmLatest(name, expectedVersion, options = {}) {
183193
const attempts =
184194
options.attempts || Number(process.env.RELEASE_ASSERT_ATTEMPTS || DEFAULT_ATTEMPTS);
185195
const delayMs =
186196
options.delayMs || Number(process.env.RELEASE_ASSERT_DELAY_MS || DEFAULT_DELAY_MS);
197+
const wait = options.sleep || sleep;
187198

188199
let latest = null;
189200
for (let attempt = 1; attempt <= attempts; attempt += 1) {
190201
latest = npmLatest(name);
191202
if (latest === expectedVersion) return latest;
192203

193-
if (attempt < attempts) {
204+
const retryDelay = nextRetryDelay(attempt, attempts, delayMs, options);
205+
if (retryDelay !== null) {
194206
console.log(
195207
`npm latest for ${name} is ${latest}; waiting for ${expectedVersion} (${attempt}/${attempts})`
196208
);
197-
await sleep(delayMs);
209+
await wait(retryDelay);
210+
} else {
211+
break;
198212
}
199213
}
200214

@@ -208,23 +222,28 @@ async function waitForPublishedArtifact(label, check, options = {}) {
208222
options.delayMs || Number(process.env.RELEASE_ASSERT_DELAY_MS || DEFAULT_DELAY_MS);
209223
const wait = options.sleep || sleep;
210224
let lastError = null;
225+
let attemptsMade = 0;
211226

212227
for (let attempt = 1; attempt <= attempts; attempt += 1) {
228+
attemptsMade = attempt;
213229
try {
214230
return await check();
215231
} catch (error) {
216232
lastError = error;
217-
if (attempt < attempts) {
233+
const retryDelay = nextRetryDelay(attempt, attempts, delayMs, options);
234+
if (retryDelay !== null) {
218235
console.log(
219236
`${label} is not ready: ${error.message}; retrying (${attempt}/${attempts})`
220237
);
221-
await wait(delayMs);
238+
await wait(retryDelay);
239+
} else {
240+
break;
222241
}
223242
}
224243
}
225244

226245
throw new Error(
227-
`${label} did not become ready after ${attempts} attempts: ${lastError?.message || 'unknown error'}`
246+
`${label} did not become ready after ${attemptsMade} attempts: ${lastError?.message || 'unknown error'}`
228247
);
229248
}
230249

@@ -245,40 +264,61 @@ async function main() {
245264

246265
console.log(`tags on HEAD: ${headTags.join(', ') || '(none)'}`);
247266
const expectedVersion = expectedTag.slice(1);
248-
const latest = await waitForNpmLatest(name, expectedVersion);
267+
const retryAttempts = Number(process.env.RELEASE_ASSERT_ATTEMPTS || DEFAULT_ATTEMPTS);
268+
const retryDelayMs = Number(process.env.RELEASE_ASSERT_DELAY_MS || DEFAULT_DELAY_MS);
269+
const retryOptions = {
270+
attempts: retryAttempts,
271+
delayMs: retryDelayMs,
272+
deadline: Date.now() + retryAttempts * retryDelayMs,
273+
};
274+
const latest = await waitForNpmLatest(name, expectedVersion, retryOptions);
249275

250276
console.log(`npm latest for ${name}: ${latest}`);
251277

252278
const expectedCommit = run('git', ['rev-parse', 'HEAD']);
253-
const metadata = await waitForPublishedArtifact('npm release metadata', () => {
254-
const result = npmReleaseMetadata(name, expectedVersion);
255-
if (result.version !== expectedVersion) {
256-
throw new Error(`npm metadata returned ${result.version}; expected ${expectedVersion}`);
257-
}
258-
if (result.gitHead !== expectedCommit) {
259-
throw new Error(`npm gitHead ${result.gitHead || '(missing)'} does not match HEAD`);
260-
}
261-
if (!result['dist.attestations']?.url) {
262-
throw new Error('npm attestation URL is missing');
263-
}
264-
return result;
265-
});
279+
const metadata = await waitForPublishedArtifact(
280+
'npm release metadata',
281+
() => {
282+
const result = npmReleaseMetadata(name, expectedVersion);
283+
if (result.version !== expectedVersion) {
284+
throw new Error(`npm metadata returned ${result.version}; expected ${expectedVersion}`);
285+
}
286+
if (result.gitHead !== expectedCommit) {
287+
throw new Error(`npm gitHead ${result.gitHead || '(missing)'} does not match HEAD`);
288+
}
289+
if (!result['dist.attestations']?.url) {
290+
throw new Error('npm attestation URL is missing');
291+
}
292+
return result;
293+
},
294+
retryOptions
295+
);
266296

267-
await waitForPublishedArtifact('npm provenance', async () => {
268-
const attestations = await httpsJson(metadata['dist.attestations'].url);
269-
verifyProvenance(provenanceStatement(attestations), expectedCommit);
270-
});
297+
await waitForPublishedArtifact(
298+
'npm provenance',
299+
async () => {
300+
const attestations = await httpsJson(metadata['dist.attestations'].url);
301+
verifyProvenance(provenanceStatement(attestations), expectedCommit);
302+
},
303+
retryOptions
304+
);
271305

272-
await waitForPublishedArtifact('GitHub Release', () => {
273-
const release = githubRelease(expectedTag);
274-
if (release.tagName !== expectedTag) {
275-
throw new Error(`GitHub Release tag ${release.tagName} does not match ${expectedTag}`);
276-
}
277-
verifyCuratedNotes(expectedTag, release);
278-
});
306+
await waitForPublishedArtifact(
307+
'GitHub Release',
308+
() => {
309+
const release = githubRelease(expectedTag);
310+
if (release.tagName !== expectedTag) {
311+
throw new Error(`GitHub Release tag ${release.tagName} does not match ${expectedTag}`);
312+
}
313+
verifyCuratedNotes(expectedTag, release);
314+
},
315+
retryOptions
316+
);
279317

280-
await waitForPublishedArtifact('installed CLI', () =>
281-
verifyInstalledCli(name, expectedVersion)
318+
await waitForPublishedArtifact(
319+
'installed CLI',
320+
() => verifyInstalledCli(name, expectedVersion),
321+
retryOptions
282322
);
283323

284324
console.log(`Release publication verified: ${name}@${latest}`);

tests/assert-release-published.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,46 @@ describe('release publication assertion', () => {
180180
);
181181
assert.strictEqual(checks, 2);
182182
});
183+
184+
it('shares one deadline across sequential publication checks', async () => {
185+
let now = 0;
186+
let checks = 0;
187+
const retryOptions = {
188+
attempts: 24,
189+
delayMs: 5,
190+
deadline: 10,
191+
now: () => now,
192+
sleep: (delay) => {
193+
now += delay;
194+
return Promise.resolve();
195+
},
196+
};
197+
198+
await assert.rejects(
199+
waitForPublishedArtifact(
200+
'npm provenance',
201+
() => {
202+
checks += 1;
203+
throw new Error('not ready');
204+
},
205+
retryOptions
206+
),
207+
/after 3 attempts/
208+
);
209+
assert.strictEqual(now, 10);
210+
211+
await assert.rejects(
212+
waitForPublishedArtifact(
213+
'GitHub Release',
214+
() => {
215+
checks += 1;
216+
throw new Error('not ready');
217+
},
218+
retryOptions
219+
),
220+
/after 1 attempts/
221+
);
222+
assert.strictEqual(checks, 4);
223+
assert.strictEqual(now, 10);
224+
});
183225
});

0 commit comments

Comments
 (0)