Repository navigation
Commit c5bf56b
fix(release): the stale-prompts sweep retries a transient 5xx, and defers with a warning instead of reading as main red (#28272)
domain:devx#2 · os-justin · session_01QSeKYmTYfaPbLQD4BJfomW
Fixes #28165
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:release.yml 的 stale-prompts
清扫(`scripts/release-pending-publish.mjs sweep`)读 GitHub API 遇到 5xx
或连接被重置时,按共享策略重试(每次读 3 次、间隔 500/1000 ms、整次运行最多 4 次重试);重试用尽仍失败时写一条 warning
和 job summary 小节后以 0 退出,不再把 main 的 Release 染红。
- 为什么改:2026-10-30 一天内 main 的 Release 因同一步骤遇到平台 502 红了 5 次,读起来像 main
红(p0),掩盖真正的发版失败;清扫每次 push 都会重跑,一次没读到只是推迟,不丢任何东西。
- 风险与回滚:改动限于 `scripts/` 下三个文件(未发布,无 changeset):清扫脚本本身;新的叶子模块
`scripts/board-read.mjs`(从 `check-issue-citations.mjs` 原样搬出的重试策略,零
import);`check-issue-citations.mjs` 改为
re-export,原有导入方不变。4xx、其它错误、取消请求失败仍然让 job 失败;in-flight 发布守卫的读取语义不变。回滚即
revert 本 PR。
- 先看:`scripts/release-pending-publish.mjs` 的 `retryingReads` /
`expectOk`(重试与分类)和 `sweep` 里 catch `TransientReadFailure` 的分支(唯一以 0
退出的地方);`scripts/board-read.mjs`(确认是原样搬移、零 import)。
- 两分钟验证:`node scripts/release-pending-publish.mjs --self-test`,预期末行 `146
cases across 35 batteries pass`。
- 需要你做的:无,席位落地。
## What changes
`scripts/release-pending-publish.mjs`, plus the shared retry policy
moved into a leaf module (`scripts/board-read.mjs`) and re-exported from
`scripts/check-issue-citations.mjs`. No workflow file is touched, so
this PR is not Tier H.
- **Retry, through the shared policy, not a copy.** The sweep's GETs now
go through `retryingReads`, which wraps the existing HTTP layer in
`readBoard` under `BOARD_READ_RETRY`. That is the same policy and reader
the release gate's full-battery read
(`scripts/ci/scheduled-full-run.mjs` `readRuns`) uses since the
publish-gate twin of this card: 3 attempts per read, 500 / 1000 ms
backoff, at most 4 retries for the whole run (at most 3 s of sleep a
sweep, against a 10-minute job timeout). Retried: an HTTP 5xx, or a
named connection failure (`ECONNRESET`, `UND_ERR_SOCKET`; the policy's
closed list). Final at once: any 4xx, and any other thrown error.
- **The outcome after the retries: a warning and exit 0.** A read still
answering a transient failure comes back flagged, `expectOk` throws it
as a `TransientReadFailure`, and `sweep` catches only that class. It
prints one `::warning::` line on stderr naming the read, the last answer
and the attempts, appends a `NOT READ this run` section to
`$GITHUB_STEP_SUMMARY`, and returns 0. Nothing was judged, so nothing
was cancelled, and the next push to main re-runs the sweep from scratch.
This is the only exit-0 failure path.
- **What still fails the job:** a 4xx, a body that does not add up, any
other thrown error, and a cancel `POST` that does not answer 202/409.
Writes are not retried: they pass through `retryingReads` unchanged.
- **The publish guard keeps its semantics.** The `in-flight` mode
(`publishInFlight`) still reads through the bare HTTP layer. An
unreadable answer is `in-flight` / `unreadable` after one request, which
a new case pins.
- `sweep` now takes injectable `env`, `http`, `versionCommits`,
`npmStateOf`, `now`, `sleep`, `log` and `warn`, and returns the exit
code (`main` exits with it). The live defaults are unchanged.
- **The policy now lives in a leaf module.** `BOARD_READ_RETRY`,
`RETRYABLE_NETWORK_CODES`, `retryableNetworkCode`, `boardReadBudget`,
`readBoard` and `readFailure` moved unchanged from
`check-issue-citations.mjs` into `scripts/board-read.mjs`, which imports
nothing. The only addition is `export` on `sleepMs`, which the citation
gate's defaults read. `check-issue-citations.mjs` re-exports every name,
so its importers (`scheduled-full-run.mjs`) are unchanged. Its self-test
still drives the policy. The import sits beside the re-export, so the
census-pinned marker on line 204 does not move.
`release-pending-publish.mjs` imports the leaf, so its `./x.mjs` closure
is now `board-read.mjs` and `invoked-as.mjs` only. Importing the
citation gate would have pulled in `pm/check-half-states.mjs` and the
rest of its closure, which `release-verify-npm.mjs --self-test` cannot
copy into its fixture repo. That was this PR's first CI red.
## H2: why the sweep keeps listing completed run 40078805767
**By design, so this PR leaves it as it is.** `collectWaitingRuns` finds
each version commit's push run by `head_sha`, walking the commits newest
first, and stops at the first run older than `WAITING_READ_BACK_DAYS`
(30). It then checks that each run it found carries that version's
publish job:
```js
const verify = carried && id !== String(currentRunId) && (r.status === 'waiting' || r.status === 'completed');
```
(`scripts/release-pending-publish.mjs` L1228). This came in with
`eead9dcf40`, the commit that added the targeted read. Its message says:
"It checks that each run it finds carries that version's publish job".
The self-test pins it: the `quiet` case reads completed runs 301 and
302, and the `wrong` case flags completed run 501, which has no publish
job.
Read-only GETs show:
- 40078805767 is the push run of the 18.1.0 version commit `e649c2d9`
("chore: version packages (#25606)"), created 2026-10-24, completed /
success.
- 38841989023, the run named at 13:42Z, is the push run of the previous
version commit `e5136ff1` ("chore: version packages (next) (#23481)"),
created 2026-10-16.
Both are inside the 30-day window. So every push re-reads the jobs
listing of every version-commit push run from the last 30 days.
40078805767 is simply the newest of them, and it is read on every sweep
until about 2026-11-23. The 502 lands on whichever of those jobs
listings GitHub fails to answer. The run itself is not special: a public
read of its jobs listing answered 200 with 6 jobs in 1.4 s. With this
PR, a 502 on any of those reads is retried, and if it persists the sweep
is deferred instead of turning red.
## Tests
The self-test has a new battery: `the sweep -> a transient read is
retried under one budget, still failing it defers with a warning and
exit 0, and a 4xx or a failed cancel stays red`. It has a floor of 10,
and the battery floor went from 34 to 35. Its cases drive the whole
`sweep` offline with a stubbed `http` and a real summary file in a temp
dir:
- control: every read answers, so the stale prompt is cancelled, with
exit 0, no backoff and no warning;
- **502 then 200** on a carried completed run's jobs listing (the
incident's read): retried once after 500 ms, then swept (cancel sent,
exit 0);
- **502 on every attempt**: 3 attempts and 500 + 1000 ms, nothing
cancelled, exit 0;
- the same case says so: one `::warning::` line naming the read, `HTTP
502 after 3 attempts`, and a `NOT READ` summary section;
- **ECONNRESET then 200**: retried, then swept;
- an unnamed network error (`ENOTFOUND`): rethrown on the first attempt,
so the job goes red;
- **404**: fatal at once, with one request, no backoff, nothing
cancelled, and the error is not a deferral;
- a flapping API: one budget for the whole sweep, 4 retries, then the
fifth transient answer defers;
- a cancel answering 502: sent once, not retried, exit 1 as before;
- the publish guard (`in-flight`) on a 502: still `unreadable`, so
`in-flight`, after one request.
The existing pin `a probe answering non-200 -> the read throws (a red
job)` still passes unchanged on the bare layer.
<details><summary>self-test at 88c63cd</summary>
```
✓ release-pending-publish self-test: 146 cases across 35 batteries pass.
os-verify-lock: VERDICT command-exit 0
```
(The baseline at 361ea24 was 136 cases across 34 batteries.)
</details>
<details><summary>Ablations, both via scripts/ablation-replace.mjs,
restored byte-identical</summary>
1. Retry removed (`if (method !== 'GET') return http(method, path);`
replaced by an unconditional pass-through): the mutation landed (anchor
1 to 0, blob `186cbaab511d` to `fb6db796de53`), and the self-test went
red with 5 failures. Among them were "a 502 then a 200 ... swept", "a
502 on every attempt ...", "a connection reset ... swept" and "a
flapping API ...". Restored: blob equals HEAD `186cbaab511d`, and `git
diff HEAD` is empty.
2. Deferral removed (the `TransientReadFailure` catch rethrows every
error): the mutation landed, and the self-test went red with 3 failures
("a 502 on every attempt ...", "and it says so ...", "a flapping API
..."). Restored byte-identical to HEAD.
</details>
## Acceptance notes
- `.github/workflows/release.yml`'s `stale-prompts` comment says "a
failed read here must be loud (a red job on main)". After this PR, a
read that keeps failing transiently is a `::warning::` annotation plus a
summary section, not a red job. Every other failure is still red. The
comment is in a Tier H file, so this PR does not edit it. carrier: none.
- The policy's network list stays closed (`ECONNRESET`,
`UND_ERR_SOCKET`). Other connection failures, such as `ENOTFOUND` and
`ETIMEDOUT`, stay final for the sweep exactly as they do for the release
gate. Widening the list is a change to the shared policy, not to this
script.
- No live `sweep` was run. All proof comes from the stubbed harness.
- `scripts/ci/scheduled-full-run.mjs` still imports the policy through
`check-issue-citations.mjs`, so the citation gate's PM closure still
loads in the `full-battery` job. Switching it to `./board-read.mjs` is a
one-line follow-up, left out of this PR on purpose. carrier: none.
## Verification
I ran the local gates at `88c63cd785`, the final commit. The derived
list came from `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 34 commands, and all 34 exited 0. They
include `node scripts/release-pending-publish.mjs --self-test`, `node
scripts/check-issue-citations.mjs`, `pnpm check:issue-citations` and
`pnpm check:pm-dispatch-gates` (fast tier, 1937 cases). The
`dispatch-gates --ran` reconciliation answered `34 derived famil(ies)
accounted for — 34 run, 0 NOT-MEASURED (a DERIVED zero — all 34 recorded
an exit code and none of them is 3)`. Outside the derived set, these
also exited 0: `node scripts/release-verify-npm.mjs --self-test` (169
cases, 16 batteries), `node scripts/check-issue-citations.mjs
--self-test`, `node scripts/ci/scheduled-full-run.mjs --self-test`, and
`eslint --no-inline-config` on the three changed files. No changeset:
`scripts/` publishes nothing, hence `skip-changeset`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QSeKYmTYfaPbLQD4BJfomW)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b358e79 commit c5bf56b
3 files changed
Lines changed: 475 additions & 156 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
694 | 694 | | |
695 | 695 | | |
696 | 696 | | |
697 | | - | |
698 | | - | |
699 | | - | |
700 | | - | |
701 | | - | |
702 | | - | |
703 | | - | |
704 | | - | |
705 | | - | |
706 | | - | |
707 | | - | |
708 | | - | |
709 | | - | |
710 | | - | |
711 | | - | |
712 | | - | |
713 | | - | |
714 | | - | |
715 | | - | |
716 | | - | |
717 | | - | |
718 | | - | |
719 | | - | |
720 | | - | |
721 | | - | |
722 | | - | |
723 | | - | |
724 | | - | |
725 | | - | |
726 | | - | |
727 | | - | |
728 | | - | |
729 | | - | |
730 | | - | |
731 | | - | |
732 | | - | |
733 | | - | |
734 | | - | |
735 | | - | |
736 | | - | |
737 | | - | |
738 | | - | |
739 | | - | |
740 | | - | |
741 | | - | |
742 | | - | |
743 | | - | |
744 | | - | |
745 | | - | |
746 | | - | |
747 | | - | |
748 | | - | |
749 | | - | |
750 | | - | |
751 | | - | |
752 | | - | |
753 | | - | |
754 | | - | |
755 | | - | |
756 | | - | |
757 | | - | |
758 | | - | |
759 | | - | |
760 | | - | |
761 | | - | |
762 | | - | |
763 | | - | |
764 | | - | |
765 | | - | |
766 | | - | |
767 | | - | |
768 | | - | |
769 | | - | |
770 | | - | |
771 | | - | |
772 | | - | |
773 | | - | |
774 | | - | |
775 | | - | |
776 | | - | |
777 | | - | |
778 | | - | |
779 | | - | |
780 | | - | |
781 | | - | |
782 | | - | |
783 | | - | |
784 | | - | |
785 | | - | |
786 | | - | |
787 | | - | |
788 | | - | |
789 | | - | |
790 | | - | |
791 | | - | |
792 | | - | |
793 | | - | |
794 | | - | |
795 | | - | |
796 | | - | |
797 | | - | |
798 | | - | |
799 | | - | |
800 | | - | |
801 | | - | |
802 | | - | |
803 | | - | |
804 | | - | |
805 | | - | |
806 | | - | |
807 | | - | |
808 | | - | |
809 | | - | |
810 | | - | |
811 | | - | |
812 | | - | |
813 | | - | |
814 | | - | |
815 | | - | |
816 | | - | |
817 | | - | |
818 | | - | |
819 | | - | |
820 | | - | |
821 | | - | |
822 | | - | |
823 | | - | |
824 | | - | |
825 | | - | |
826 | | - | |
827 | | - | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
828 | 707 | | |
829 | 708 | | |
830 | 709 | | |
| |||
0 commit comments