Repository navigation
ci(release): start the next snapshot preflight on a daily schedule, stopping at the maintainer approval (ADR-0125 amendment) - #30862
Conversation
…topping at the maintainer approval release.yml fires a second cron on its one `schedule:` (ADR-0125, amendment of 2026-11-06; ruling record 6677910954). The daily cron starts `snapshot-next-preflight` alone -- exactly the dispatch preflight -- and on a pass `snapshot-next` waits at `environment: release` as before. The two clocks are told apart by `github.event.schedule`, as fleet-patrols.yml tells its four apart: `version-pr` now names its 6-hourly cron (strictly subtractive), and the three snapshot jobs name the daily one. Every stable job stays skipped on either cron; `stale-prompts` is untouched. A new `snapshot-next-supersede` job (ruling line 3, "the next day's run supersedes it") cancels only an OLDER `schedule` run whose snapshot job is still waiting at the environment -- never a dispatch, never a job that has started, never a stable prompt, never the calling run; a read the API will not answer defers to tomorrow with a warning. `cancel-in-progress` stays false. snapshot-next.mjs gains the `supersede` mode, the clock readers (`declaredCrons`, `guardClock`), clock and supersede pins in `snapshotWiring`, and three self-test batteries (floor 13 -> 16). fleet-write's `jobsStartedBy` reads `github.event.schedule` from a new `schedule` argument, and its release.yml pins now evaluate each cron. The five landing details the maintainer is still confirming (the hour, the supersede, red run vs quiet skip, a card on a real failure, the docs scope) are each marked `[#30832 landing detail N of 5 -- UNCONFIRMED]` at the one place that changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
…heduled start of the preflight A dated amendment in the house shape (ruling record 6677910954 on #29466, drafted on #30832): it quotes the three 2026-11-05 sentences it replaces ("no `schedule` and no `push` can start it", the dispatch-only requirement, the never-from-schedule prohibition), states the four bound lines, says how the 2026-11-05 text and D1 to D6 read with it, what the lane must satisfy, what stays forbidden and what does not change. Dated pointer callouts sit at the D1, D4 and D5 callouts and at the three replaced sentences; D1 to D6 and the earlier amendments are not rewritten. A closing section records the draft's reading of the five landing details still being confirmed, in one place. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
…, published on approval The channels table row and the one sentence on release.yml's schedule, and nothing else (landing detail 5 of #30832, in its own commit so the scope can be changed in one place). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
…apshot-daily-schedule
…lane and its own next, and files one card The five landing details the maintainer confirmed on 2026-11-06 11:32Z (ruling 6677910954, #30832 round 2), on top of round 1's scheduled start: - Quiet skip (detail 3), schedule only. Five refusals are expected of a daily proposal and now conclude as a green run with nothing queued and no card -- a ::notice and a step-summary section only: the candidate already on `next` (same-commit), an RC cycle, a member not yet on npm (debut), no fully green commit on main, and a stable prompt waiting or a stable publish running. The switch is one schedule-only expression on the preflight job's refusing steps (`--quiet-skip`); a dispatch keeps today's loud refusal. The preflight job records `refusal` and `reason` outputs; `snapshot-next` and `snapshot-next-supersede` require an empty `refusal`, so a quiet skip queues nothing and cancels nothing. - The same-commit check. `record-commit` writes `gitHead` into every manifest after the version step (the TypeScript nightly way), the guard reads it back from every packed manifest (`--commit`), and the preflight reads the anchor's `next` version document on npm for the commit it records. Before the first snapshot nothing of that shape is on `next`, so the check passes through; a snapshot recording no commit is refused as the incident it is, not as an expected kind. - The stable lane (`stable-lane`), read with release-pending-publish.mjs's own readers (collectWaitingRuns, collectPublishRuns), through a client that refuses every non-GET; an unread answer is NOT MEASURED. - The card (detail 4): `snapshot-next-card`, under always(), files or refreshes ONE fixed-title card through the full-run card's own filer (snapshotCardStep) on an unexpected refusal or a publish failure after the approval; the body gives the reason, the run link, the candidate commit and the snapshot version reached, and says a partial publish is fixed by a fresh complete snapshot, never a republish, never `latest`. - snapshotWiring pins every piece of the above; fleet-write validate.mjs reads the fourth snapshot job. Self-tests: snapshot-next 178 cases across 21 batteries (five new), card modules, validate -- all green. - The hour and the supersede stand as built (details 1 and 2); the landing-detail markers in release.yml are removed and the header is re-pinned: the stable publish is never reachable from schedule, the preflight is the one exception, and no schedule can publish. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
…ing details The amendment now says what the maintainer confirmed on 2026-11-06 11:32Z (ruling 6677910954, the five-point list): the hour, the supersede as built, the five quiet skips on the schedule (nothing queued, no card, a green run; a dispatch refuses loudly), the one card for an unexpected refusal or a publish failure after the approval (a partial publish is fixed by a fresh complete snapshot, never a republish, never `latest`), and the text. "no `schedule` ... can start it" becomes "no `schedule` can publish"; the old proposal is replaced by the new; Prime Directive 15 is unchanged. The "Landing details being confirmed" section is removed, the "red run" requirement is replaced by the quiet skip, and the trial readings for the #29466 report are recorded: proposals, approvals, refusals by reason, merge-to-installable latency, and which app repositories pin `next`. docs/releases-maintenance.md: the snapshot row names the five quiet skips and the card (the row commit the list keeps). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
…apshot-daily-schedule
|
domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8 Contract reviewServed-tier: Read against: PR #30862 head ① Derived judgmentsThe safety line — holds.
The five confirmed details, in the tree.
The two readings the dev took (③ of the brief).
The new pieces (④ of the brief).
Pins and self-tests (⑤). Read, not run: Must-fix
Should-fix
② Semver levelNone. The diff touches ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: FAIL |
…efusal, the lane's tooling runs from the workflow's commit, and the dry run's early returns record a reason Round 3 of #30832, after the complete review of 5fe2149 (its must-fix and should-fixes 2 and 5): - M1: on a dispatch with no fully green commit the select step wrote `reason` alone and exited 1, so `snapshot-next-card` read the loud refusal as an UNEXPECTED one (a failed preflight with an empty `refusal`) and filed a false card. The dispatch branch now records `refusal=no-green-commit` like the schedule branch does, and `snapshotWiring` pins each branch on its own (the classification by event, the schedule branch's echo, the dispatch branch's echo), with a firing case for each. - S2: the preflight detached to the selected commit and ran every snapshot-next.mjs mode from THAT tree, whose copy predates this change and knows no `stable-lane`, `--quiet-skip`, `--commit` or `record-commit`; the snapshot job checked that commit out and did the same. Both jobs now take `scripts/` off `GITHUB_SHA` into the runner's temp dir before the tree moves (`git archive`), link the tree's node_modules beside it (the script imports `semver`), name the tree in the job env (`SNAPSHOT_NEXT_ROOT`, read by `laneRoot`), and call the copy alone; the build, the publish script and the registry read-back stay the selected tree's own. Pinned: the take before the detach / the install, the link, the env, no call off the tree; a self-test case loads the copy from outside the tree and reads the tree from the env, writing nothing into it. - S5: `dryRun()`'s early returns (a dirty tree, a missing changeset binary, and the version step failing) now record `reason=` the way the catch path does, so the card never says no reason was recorded. Self-test: 191 cases across 23 batteries (two new batteries, four new cases in the quiet-skip battery). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
…replaces, marks the trial readings as this record's, and states where the lane's checks run from Round 3 of #30832, the review's should-fixes 3 and 4 and the record of S2: - "The quiet skip replaces this record's earlier reading of a scheduled refusal as a red run" claimed to replace a reading the merged 2026-11-05 amendment never carried (it was dispatch-only). The sentence now names what it replaces: the lane's first draft on #30832 and the card's acceptance line. - "Trial readings for the #29466 report" is marked as this record's reading, not the ruling's, in the house phrase: no ruling record binds a metric and the five confirmed details say nothing of the report. - One requirement bullet states that the lane's checks run from the workflow's own commit while the selected commit's tree is built and published, and that the stable lane's "Known edges" reading is untouched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
…apshot-daily-schedule
…apshot-daily-schedule
|
domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8 Contract reviewServed-tier: Delta review of the round-1 FAIL record (comment 6685551244 on #30832). Read against: Separating the merges from the dev's commits. The range holds four commits: ① Derived judgmentsM1 (must-fix 1) — closed. release.yml's select step now writes S2 (should-fix 2) — closed; sound and complete.
S3 — closed. The ADR sentence now reads: the quiet skip replaces the red-run reading of the lane's first draft on #30832 and of that card's acceptance line; "the merged 2026-11-05 amendment was dispatch-only and carried no reading of a scheduled refusal, so nothing in the record above is replaced by it." Accurate to the record's history. S4 — closed. The heading is now "Trial readings for the #29466 report — this record's reading, not the ruling's", opening with a sentence disclaiming any binding metric — the house phrase the 2026-11-05 amendment uses. S5 — closed. The new ADR requirement bullet — inside scope. "The lane's checks run from the workflow's own commit" states the mechanism this PR's own tree needs so that landing detail 3 (quiet skip, five kinds) and detail 4 (card on an unexpected refusal only) hold on the first scheduled runs after merge; without it the lane contradicts both details for days (the round-1 should-fix 2). It restricts itself to "the snapshot lane's pre-queue checks alone", says explicitly that the stable lane's 2026-09-29 "Known edges" reading is untouched, and decides nothing about publish, approval, the stable lane, or the five details' content. It is the ADR recording how its consumer satisfies what was ruled — the same relation the amendment's other requirement bullets have to release.yml — not a new decision in the maintainer's domain. One note, stated not flagged: like the two readings in round 1 it is the lane's reading of the ruled list, so if the maintainer wants the five details to be the only text with the status "confirmed", the bullet could carry the "this record's reading" marker S4 now uses. Not a fix. Safety line — intact. The stable jobs ( Pins and self-tests (read, not run). Must-fix None. Should-fix None new. Round 1's five are closed as above. ② Semver levelNone. The delta touches ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
Requested by 田騏 · project thread
Fixes #30832
Ruling-ref: 6677910954
Clause-②: no
Metadata-protocol: none
维护者速读
next快照通道每天 01:17Z(北京 09:17)自动跑一遍与手动触发完全相同的预检;通过后快照任务停在release环境等你批准,不批准就不发布;昨天没人批准的定时提示被今天的替换。按你 11:32Z 「同意」的五点落地:定时跑到五种预期内的"没得发"(候选 commit 就是当前next已发的那个、处于 RC 周期、有包还没首发、main 没有全绿 commit、稳定发布的提示在等批准或正在发布)时静默跳过——绿色、不排队、不开卡,只留一条 notice 和摘要;手动触发仍是响亮的红,也不开卡。真正的异常(读不到、组不对、干跑失败)或批准后发布失败(含发了一半)只开/刷新一张固定标题的卡(复用scheduled-full-run-card的开卡器),卡里写明:半截发布只能用新时间戳重发一份完整快照,⛔ 不往半截版本里补发,⛔ 不碰latest。每份快照把源 commit 写进gitHead(TypeScript nightly 的做法),预检从 npm 读回来做"同一 commit"判断。第三轮按完整评审改了五处:① 手动触发遇到"main 没有全绿 commit"时,选 commit 的步骤原来只写reason不写refusal,开卡任务会把这次预期内的红误判为异常并开一张假卡——现已两条分支都写refusal=no-green-commit,自测对每条分支各钉一例;② 预检和快照任务原来在切到选中的旧 commit 之后,用那棵旧树里的snapshot-next.mjs跑新模式,合并后头几天必红并开假卡——现在两个 job 都先从本 workflow 的 commit(GITHUB_SHA)把scripts/取到$RUNNER_TEMP,所有snapshot-next.mjs调用都从那份副本跑,SNAPSHOT_NEXT_ROOT指向工作区;③④ ADR 两处措辞(见下);⑤ 干跑的三个提前返回现在都写reason,卡上不再出现"没有记录原因"。@objectstack/service-push首发之前每天都会红一次,而没人会看定时任务的失败邮件(GitHub 只发给 cron 作者);静默跳过加一张卡,让"每天没什么可发"安静、"真出问题"有人看到。第三轮的改动都是让树真正做到第二轮文字所说的:预期内的拒绝在手动触发下是红但不开卡;合并后的第一天不会因为旧脚本误开卡。schedule不能发布(方案 B 没选);稳定通道的每个 job 在这个 cron 上都不运行,stale-prompts原样不动,⛔ 第 15 条 Prime Directive 不变。静默跳过只在schedule事件生效,靠一处表达式开关;预检任务把refusal/reason写成输出,快照任务和替换任务都要求refusal为空,开卡任务要求预检失败且refusal为空,所以静默跳过不排队、不取消昨天的提示,预期内的手动拒绝不开卡。工具从 workflow 自己的 commit 取:构建、发布脚本和 npm 回读仍用选中 commit 那棵树自己的。自测钉住以上每一处(snapshotWiring的两条分支各一例、取工具的先后、软链、环境变量、不许从旧树调用,外加一例真的把副本放到树外加载并从环境变量读树),改错任何一处都过不了。开卡任务只有issues: write,不碰actions。回滚就是 revert 本 PR。.github/workflows/release.yml快照通道头注释"THE FIVE LANDING DETAILS";②snapshot-next-preflight的env/outputs、新步骤Take the lane's tooling from the workflow's own commit、Select the commit步骤两条分支的refusal=no-green-commit、新步骤stable-lane,以及snapshot-next/snapshot-next-supersedeif:里的refusal == '';③ 文件末尾新 jobsnapshot-next-card;④scripts/ci/snapshot-next.mjs头注释"Where the tooling runs from"与"The quiet skip"、proposalVerdict、laneRoot;⑤docs/adr/0125-…md末尾修订的"The lane's checks run from the workflow's own commit"/"The quiet skip"/"The card"/"Trial readings"各段。node scripts/ci/snapshot-next.mjs --self-test预期最后一行snapshot-next: self-test OK -- 191 case(s) across 23 batteries;node scripts/ci/scheduled-full-run-card.mjs --self-test预期13 cases over the two card steps' API plumbing;node scripts/pm/fleet-write/validate.mjs --self-test预期340 cases pass across 17 batteries。release.yml和docs/adr/**是受管面)。What this PR states (the ruled behaviour)
Ruling record 6677910954 on #29466 (option A on the maintainer's decision card, 2026-11-06 05:42Z) binds four lines; the maintainer confirmed the five landing details as one list on 2026-11-06 11:32Z (「同意」). Both are built and stated here; nothing below re-argues either.
The four bound lines.
schedulestarts the snapshot preflight once a day, exactly the dispatch preflight.release.ymlfires17 1 * * *beside the 6-hourly bookkeeping cron on oneschedule:, told apart bygithub.event.schedule;snapshot-next-preflightruns the same steps on either start. Two checks were added to the preflight on both starts by the landing details (the same-commit check and the stable lane, below).environment: releaseand publishes only on the maintainer's approval.snapshot-next'sif:issuccess() && needs.snapshot-next-preflight.outputs.refusal == '' && (<daily cron> || <snapshot dispatch>). Noschedulecan publish; no seat dispatches the lane or approves it (PD15).snapshot-next-supersedecancels only an olderschedule-started run whose snapshot job is stillwaitingatrelease— never a human's dispatch, never a job that has started, never a stable prompt — and only once today's preflight passed (a quiet skip leaves yesterday's prompt standing).stale-promptsandscripts/release-pending-publish.mjsare untouched.The five landing details (confirmed 2026-11-06 11:32Z).
Time —
17 1 * * *(09:17 Beijing), every day of the week, as built; the manual dispatch path is unchanged.Supersede — as built (above).
Quiet skip (schedule only). Five refusals are expected of a daily proposal and each now concludes as a GREEN run with nothing queued and no card — a
::notice::and a step-summary section only:nextwas published from. New:snapshot-next.mjs record-commit --commit "$COMMIT"writesgitHeadinto every manifest after the version step (the TypeScript nightly way),guard --commitreads it back from every packed manifest, andpreflight --commitreads the anchor'snextversion document on npm (readPublishedCommit) and refusessame-commitwhen it matches. Before the first snapshot nothing of snapshot shape is onnext, so the check passes through; anextsnapshot that records no commit was not cut by this lane and is refused as the incident it is (same-commit-unrecorded, NOT an expected kind);rc-cycle, read at the commit and at main's tip);debut— daily until@objectstack/service-pushdebuts);mainhas no fully green commit (no-green-commit): the select step classifies--snapshot-commit's exit 1 withcommit: nullby event, and BOTH branches recordrefusal=no-green-commit— the schedule branch exits 0 (the quiet skip), the dispatch branch exits 1 (the loud refusal) — so neither files the card; exit 3 (NOT MEASURED) stays red withreasonalone;stable-lane). New modesnapshot-next.mjs stable-lane --workflow release.yml, reading release.yml's runs with release-pending-publish.mjs's own readers (collectWaitingRuns— the waiting filter PLUS each version commit's push run — andcollectPublishRuns), judged byPUBLISH_JOB_NAMEandPUBLISH_SETTLED_JOB_STATUSES, through a client that refuses every non-GET; an unread answer is NOT MEASURED, never "clear".The switch is ONE expression on the preflight job's three refusing calls —
${{ github.event_name == 'schedule' && ' --quiet-skip' || '' }}(QUIET_SKIP_EXPRESSION) — and the select step's ownEVENT_NAMEtest.proposalVerdictis the one place the kinds, the flag and the exit code meet: an expected refusal with--quiet-skipexits 0 and recordsrefusal=<kind>+reason=; the same reading on a dispatch exits 1 with the same outputs (today's loud refusal, no card); an unexpected refusal, or an expected one beside anything unread, exits 1 withreasonalone; nothing refused but something unread exits 3. The select step's shell does the same forno-green-commiton both of its branches. The preflight job exposesrefusal/reasonoutputs (||chains overcommit,stable-lane,preflight,dry-run); its later steps andsnapshot-next/snapshot-next-supersederead them. ⛔ No--quiet-skipafter the approval: the post-approval preflight is loud.SNAPSHOT_EXPECTED_REFUSALSis spelled once, inscheduled-full-run.mjs, and both the preflight and the card read it. Every early return ofdry-run(a dirty tree, a missing changeset binary, the version step failing) recordsreason=as the catch path does.Notification. A pass needs nothing. New job
snapshot-next-card(needs: [snapshot-next-preflight, snapshot-next],always() && (<arms>) && ((preflight failed && refusal == '') || snapshot failed),contents: read+issues: write, noactions:) files or refreshes in place ONE fixed-title card —next snapshot lane: unexpected refusal or publish failure, markeros-next-snapshot-lane-card, labelsbug,domain:devx,priority:p1on create only — through the full-run card's own filer and de-dup (fileOrRefreshCard;snapshotCardStepinscheduled-full-run-card.mjs,snapshotCardIdentity/snapshotCardCase/renderSnapshotCardBodyinscheduled-full-run.mjs; no second filer). Two cases and no other: an unexpected refusal (the preflight failed with norefusalkind — NOT MEASURED, the group, a dry run that failed, a step that died) and a publish failure after the approval, a half-finished publish included (the card names the snapshot version the version step reached, or says nothing reached npm). The body gives the reason, the run link and the candidate commit, and says: ⛔ a partial publish is fixed by a fresh, complete snapshot with a new timestamp — never a republish into it, never a touch oflatest. A success files nothing; a quiet skip files nothing; an expected refusal on a dispatch files nothing (every expected kind recordsrefusalon either start, the select step's included). GitHub mails a scheduled run's failure to the cron's author alone, so nothing relies on that mail.Text. ADR-0125's
## Amendment (2026-11-06, #30832)states all of the above as ruled: the schedule can start the preflight and queue at approval, publishing stays the approval, the old proposal is replaced by the new, "noschedule… can start it" becomes "noschedulecan publish", the five skip rules, the card, ⛔ PD15 unchanged; the "Landing details being confirmed" section and every[#30832 landing detail N of 5]marker are removed; the trial readings for the [Decision] Release cadence: a week of merges is about 1,700 PRs, so each QA-gated release carries a very large delta and app repos wait days to install fixes #29466 report are recorded as this record's reading, not the ruling's (proposals, approvals, refusals by reason, merge-to-installable latency, which app repositories pinnext).release.yml's header keeps "the stable publish is never reachable fromschedule" and adds the preflight-exception sentence; fleet-writevalidate.mjs'sjobsStartedBypins read the fourth snapshot job; thedocs/releases-maintenance.mdrow commit is kept and the row names the five quiet skips and the card.Where the lane's tooling runs from. The preflight selects a commit older than
main's tip by construction and moves the tree to it; the snapshot job checks that commit out. That tree'sscripts/ci/snapshot-next.mjsneed not know the modes this PR adds, so until a commit at or after this PR's merge is the selected one, running the lane's checks off the selected tree would be a red run and a false card every day. Both jobs therefore takescripts/offGITHUB_SHAwithgit archiveinto${RUNNER_TEMP}/lane-toolingbefore the tree moves (the preflight before its detach, the snapshot job before its install), link the tree'snode_modulesbeside it (the script importssemver; a dangling link until the install, read by nothing before), setSNAPSHOT_NEXT_ROOT: ${{ github.workspace }}in the job env (read bylaneRoot, so the copy reads and versions the checkout, not its own directory), and run everysnapshot-next.mjscall asnode "${RUNNER_TEMP}/lane-tooling/scripts/ci/snapshot-next.mjs" <mode>. The build,scripts/release-snapshot-publish.sh,release-spec-changes.shandrelease-verify-npm.mjsstay the selected tree's own: they are what it ships. The supersede job readsmain's own checkout and takes nothing. The copy lives outside the tree, so the dry run's clean-tree check is untouched.Pins.
snapshotWiringrefuses: a preflight job withoutrefusal/reasonoutputs or whose chains miss a refusing step; a refusing call without the schedule-only expression, a bare--quiet-skip, or--quiet-skipafter the approval; a stable-lane read missing, doubled or placed after the approval; a select step that does not classify the no-green-commit record by event, or whose SCHEDULE branch, or whose DISPATCH branch, does not recordrefusal=no-green-commit(two findings, one per branch — the firing cases mutate one branch's echo and assert that only that branch's finding fires); a preflight (either job) without--commit; a snapshot job that never records the commit, records it after the guard, or guards without--commit; a snapshot job or supersede whoseif:does not require an emptyrefusal; the card job's whole shape (presence,always(), the two cases, needs, permissions, the step module and step, the five env keys, cron and dispatch arm); and, for the tooling, a job that takesscripts/offGITHUB_SHAother than exactly once, a take after the detach (preflight) or after the install (snapshot job), a missingnode_moduleslink, a missingSNAPSHOT_NEXT_ROOTin the job env, a call before the take, and anynode scripts/ci/snapshot-next.mjscall off the selected tree. fleet-writevalidate.mjsreads ten jobs; the snapshot cron starts the preflight alone withsnapshot-next,snapshot-next-cardandsnapshot-next-supersedebehind it; the relay's dispatch skips all four snapshot jobs.Round 3 (after the complete review of 5fe2149)
The review's must-fix and should-fixes 2 to 5, taken as the seat's round-3 list; the
docs/releases-maintenance.mdrow is kept.reasonalone and exited 1, so a maintainer dispatchingsnapshot_nextwhilemainhad no fully green commit got apriority:p1card for an expected refusal (a failed preflight with an emptyrefusalis the card's first case). The branch now recordsrefusal=no-green-committoo;snapshotWiringpins the classification by event and each branch's echo on its own, with a firing case per branch that asserts the OTHER branch's finding stays silent (not a string-presence check over the job).$RUNNER_TEMPbefore the detach, pinned. The tree forced no decision: the same shape serves both jobs, and the selected tree's build and publish are left its own.nextsnapshot preflight on a daily schedule, stopping at the maintainer approval (ADR-0125 amendment + release.yml) #30832 and the card's acceptance line — and says the merged 2026-11-05 amendment, dispatch-only, carried no reading of a scheduled refusal.dryRun(). The early returns (a dirty tree, a missingnode_modules/.bin/changeset, and the version step failing) recordreason=the way the catch path does; a self-test battery drives each on a scratch repository with the preflight stubbed and reads the output file back.Tests
node scripts/ci/snapshot-next.mjs --self-test: 191 cases across 23 batteries (round 2: 178 / 21). New in round 3: "dry-run: every early return records its reason" (3), "release.yml: the lane's tooling runs from the workflow's own commit" (6:laneRoot; the checked-in jobs' take, link, env and calls; a call off the selected tree in either job; a take moved after the detach and a job that never takes; a missing link and a missing env; and the copy itself loaded from a temp dir through a linkednode_modules, reading the tree fromSNAPSHOT_NEXT_ROOTand writing nothing into it), and four cases in "release.yml: the quiet skip and the card" (now 20: both branches record; the dispatch branch alone; the schedule branch alone; the classification removed). Round 2's batteries stand: "the same-commit check" (10), "the stable lane" (10), "the quiet skip" (14), "record-commit" (4).node scripts/ci/scheduled-full-run.mjs --self-test: OK, "the next snapshot card: identity, case and body" 13 cases (unexpected refusal and publish failure file; success, quiet skips and an expected dispatch refusal do not; the body carries the reason, run link, candidate commit, version reached, and the partial-publish rule).node scripts/ci/scheduled-full-run-card.mjs --self-test: 13 cases over the two card steps' API plumbing.node scripts/pm/fleet-write/validate.mjs --self-test: 340 cases across 17 batteries.node scripts/release-pending-publish.mjs --self-test: 146 cases across 35 batteries, untouched.release.ymlparses (ten jobs);check-workflow-status-functions,check-workflow-context-availability,check-workflow-step-name-quoting,check-self-test-workflow-commands: OK; eslint onscripts/ci/snapshot-next.mjs: clean.Acceptance notes
stable-laneandsupersederead through clients that refuse every write but the one cancel. The branch's writes are the push, this PR's body through the relay and theos-dev-reportcomments throughpost-stamped.mjs.open_questionsis empty: the tree forced no decision the ruled list did not make. Two readings the list left to the lane are stated, not decided: a snapshot job that fails after the approval but BEFORE the publish files the publish-failure card too (the card says nothing reached npm), and anextsnapshot recording no commit is an unexpected refusal rather than a pass-through. The review read both as inside the list.outputsbeing readable by analways()dependant (needs.snapshot-next-preflight.outputs.refusal,needs.snapshot-next.outputs.version) is documented by GitHub and not yet measured in this repository; the first dispatch refusal measures it, and the failure mode is one false card, never a publish.skip-changeset: the diff touches.github/workflows/**,scripts/**anddocs/**only.Verification
Derived gates on the final diff (after the third origin/main merge, e4c7785, merge base 616311c): 68 derived (the same 68 as round 2), 68 run, every one exit 0 at first run (no exit 3, no rebuild); --ran reconciled: 68 run, 0 NOT-MEASURED, 0 UNRUN. origin/main moved again (6ddf827) while the gates ran; not re-merged a third time, the landing merge resolves it
🤖 Generated with Claude Code