diff --git a/.llms/skills/release-copilot/SKILL.md b/.llms/skills/release-copilot/SKILL.md index 1388a1d..b2704f1 100644 --- a/.llms/skills/release-copilot/SKILL.md +++ b/.llms/skills/release-copilot/SKILL.md @@ -255,7 +255,7 @@ Seven steps, the last five through `scripts/hermes-release.mjs`: 6. `hermes-push`: pushes that commit only, after checking it changes exactly the three pin files and sits on the branch's current tip. 7. `hermes-next-bump`: opens the PR moving the Hermes branch to the next version. - Skippable, since the pin does not depend on it. + Required: the release is not finished until the branch names the next version. `hermesReadyToCut` matters because `RN Build Static Hermes` has **no version input** and reads `npm/hermes-compiler/package.json` verbatim. `hermesPublishable` checks the @@ -366,8 +366,8 @@ captain to **retype the version or branch** rather than accept `y`. Declining normally **stops the run**, because later steps assume earlier ones happened. Side steps nothing later depends on are **skippable** and say so with a -`skip:` line: the nightly, the template branch, the board, the support-table PR, a -board Target Release and the Hermes next bump. Declining one skips it, the run carries +`skip:` line: the nightly, the template branch, the board, the support-table PR and a +board Target Release. Declining one skips it, the run carries on and lists it at the end as still to do. On the 0.89 cut the broken nightly could only be accepted (and fail) or declined (and stop), so it blocked the publish. diff --git a/.llms/skills/release-copilot/__tests__/copilot.test.mjs b/.llms/skills/release-copilot/__tests__/copilot.test.mjs index 90c4eb0..ddc9b8c 100644 --- a/.llms/skills/release-copilot/__tests__/copilot.test.mjs +++ b/.llms/skills/release-copilot/__tests__/copilot.test.mjs @@ -3284,7 +3284,7 @@ test('hermes phase: wait, verify, pin, push and next bump all go through hermes- assert.equal(push.confirmToken, '0.89-stable'); const bump = one('hermes-next-bump'); assert.equal(tail(bump), 'next-bump 260318099.0.0-stable 260318099.0.5 260318099.0.6'); - assert.equal(bump.skippable, true, 'the next bump does not block the pin'); + assert.equal(bump.skippable, false, 'the next bump is part of finishing the release, so declining it stops the run'); for (const a of [one('hermes-monitor'), one('hermes-verify-tag'), one('hermes-pin'), push, bump]) { assert.match(a.args[0], /hermes-release\.mjs$/); } @@ -3888,3 +3888,12 @@ test('template check: only a full pass on both platforms is recorded', async () assert.equal(templateResult(v).ok, true); rmSync(join(TEMPLATE_RESULTS, `${v}.json`)); }); + +test('hermes wait: only a dispatch from the last two hours counts', async () => { + const {recentDispatch} = await import('../scripts/hermes-release.mjs'); + const now = Date.parse('2026-11-07T20:14:00Z'); + const list = [{databaseId: 42409982858, createdAt: '2026-11-07T20:13:06Z'}, {databaseId: 41524889130, createdAt: '2026-11-02T21:03:54Z'}]; + assert.equal(recentDispatch(list, {now}).databaseId, 42409982858); + assert.equal(recentDispatch([list[1]], {now}), null, 'a run from days ago is not this cut'); + assert.equal(recentDispatch([], {now}), null); +}); diff --git a/.llms/skills/release-copilot/scripts/hermes-release.mjs b/.llms/skills/release-copilot/scripts/hermes-release.mjs index b4acfa3..e2f2282 100644 --- a/.llms/skills/release-copilot/scripts/hermes-release.mjs +++ b/.llms/skills/release-copilot/scripts/hermes-release.mjs @@ -82,13 +82,29 @@ export function judgeRun(run) { return {ok: true, done: true, detail: `run ${run.databaseId} tagged and published: ${run.url}`}; } -export async function waitForRun(version, {branch, sinceMinutes = 120, log = console.log} = {}) { - const list = JSON.parse( - await must('gh', ['run', 'list', '--repo', HERMES_REPO, '--workflow', WORKFLOW, '--branch', branch, '--event', 'workflow_dispatch', '--limit', '5', '--json', 'databaseId,createdAt,url,status']), - ); - const recent = list.find(r => Date.now() - Date.parse(r.createdAt) < sinceMinutes * 60000); +/** The newest dispatch on the branch from the last `sinceMinutes`, or null. */ +export function recentDispatch(list, {sinceMinutes = 120, now = Date.now()} = {}) { + return (list ?? []).find(r => now - Date.parse(r.createdAt) < sinceMinutes * 60000) ?? null; +} + +export async function waitForRun(version, {branch, sinceMinutes = 120, appearMinutes = 5, log = console.log} = {}) { + // A run is not listed until a few seconds after its dispatch returns. On + // 260318099.0.7 the wait asked straight after the cut, found nothing and + // stopped the phase while the run was building. + let recent = null; + const until = Date.now() + appearMinutes * 60000; + for (;;) { + const list = JSON.parse( + await must('gh', ['run', 'list', '--repo', HERMES_REPO, '--workflow', WORKFLOW, '--branch', branch, '--event', 'workflow_dispatch', '--limit', '5', '--json', 'databaseId,createdAt,url,status']), + ); + recent = recentDispatch(list, {sinceMinutes}); + if (recent || Date.now() >= until) { + break; + } + await sleep(10000); + } if (!recent) { - throw new Error(`no ${WORKFLOW} dispatch on ${branch} in the last ${sinceMinutes} minutes. Did the cut step run?`); + throw new Error(`no ${WORKFLOW} dispatch on ${branch} in the last ${sinceMinutes} minutes, after waiting ${appearMinutes} for one to appear. Did the cut step run?`); } log(`Watching ${recent.url} for ${version}`); for (;;) { diff --git a/.llms/skills/release-copilot/scripts/phases.mjs b/.llms/skills/release-copilot/scripts/phases.mjs index 15e29e3..7f2792f 100644 --- a/.llms/skills/release-copilot/scripts/phases.mjs +++ b/.llms/skills/release-copilot/scripts/phases.mjs @@ -952,12 +952,11 @@ export const PHASES = { 'without it merged, the next cut re-cuts the version just released', ], reversible: 'yes, close the PR and delete the branch', - skippable: true, }), ]; }, note: - 'This is the tail of THIS release, not the head of the next one: the file must always name the next version to publish. IMPORT the PR once it is open, the import is what adds rn-oss and Hermes as reviewers. See reference/hermes.md.', + 'This is the tail of THIS release, not the head of the next one: the file must always name the next version to publish. Open it as soon as the release is out, or it gets forgotten: the .0.3 release never did and the branch named a published version for weeks. Declining stops the run so it is not skipped quietly. IMPORT the PR once it is open, the import is what adds rn-oss and Hermes as reviewers. See reference/hermes.md.', }, ], },