Repository navigation
ci(scheduled-full-run): the release gate retries a transient 5xx or connection reset on a run listing, inside one budget per gate run - #24748
Merged
objectstack-fleet[bot] merged 4 commits intoOct 20, 2026
Conversation
…ub-read-retry.mjs The policy (BOARD_READ_RETRY, RETRYABLE_NETWORK_CODES, retryableNetworkCode, boardReadBudget, readBoard, readFailure, sleepMs) moves byte-for-byte out of check-issue-citations.mjs, which now imports it. No behaviour change: the citation gate's self-test answers 215 cases, 10 batteries, exit 0, as on the base. The module imports nothing, so a consumer run from a bare checkout with no install step can load it. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude <noreply@anthropic.com>
…onnection reset on a run listing, inside one budget per gate run readRuns reads each page through the shared retry policy (scripts/github-read-retry.mjs): a 5xx or a named connection failure is retried, 3 attempts per read at 500 / 1000 ms; a 4xx, an OK body with no workflow_runs array and every other error stay final on the first attempt. readFullBattery hands ONE budget (4 retries) to every workflow x event x page a gate run reads. A spent ladder or budget still refuses as `release gate NOT MEASURED`, exit 3, its text now naming the last answer and the attempts made. The read path is driven offline by runReleaseGate, and a new self-test battery pins the four cases. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude <noreply@anthropic.com>
…tions.mjs instead of lifting it Lifting the policy into its own module shifts check-issue-citations.mjs's line-anchored `local-env` census row in scripts/pm/dispatch-gates.self-test.mjs (204 -> 205), a file outside this card's surface. The citation gate keeps its policy byte-identical to the base, and scheduled-full-run.mjs imports BOARD_READ_RETRY, boardReadBudget, readBoard and readFailure from it. The added load closure (check-issue-citations, git-env, glob-match, js-comment-mask, symbol-anchors) imports node builtins only and runs nothing at load, so release.yml's install-free full-battery job still loads it. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude <noreply@anthropic.com>
…eYml line lost An incidental edit: the `const releaseYml = readFileSync(...)` line in the `release gate: release.yml wiring` battery had lost the space after `=`. It is byte-identical to the base again, and the self-test hunk is now a pure insertion. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
Seat reviewACCEPT ·
This seat readies the PR and queues it with auto-merge. Generated by Claude Code |
objectstack-fleet
Bot
deleted the
claude/issue-24741-release-gate-read-retry
branch
October 20, 2026 05:15
This was referenced Oct 20, 2026
This was referenced Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #24741
Clause-②: no
What this changes
One file:
scripts/ci/scheduled-full-run.mjs(HALF 3, the release gate's live read, and its--self-test).release.yml,lint.ymlandscheduled-full-run-card.ymlare unchanged, and no verdict changes.readRunsreads each page throughreadBoard, the citation gate's retry policy, imported. An HTTP 5xx or a named connection failure (ECONNRESET,UND_ERR_SOCKET) is retried: 3 attempts per read, with 500 / 1000 ms backoff. A 4xx, an OK body with noworkflow_runsarray, and every other error are final on the first attempt. It takes two new optional parameters,fetchImpl(default: the globalfetch) andbudget.readFullBattery(new export) reads every workflow x event x page of one gate run under ONEboardReadBudget(). That is 4 retries per gate run, never 4 perreadRunscall.runReleaseGate(new export) is the gate as{ exit, lines, verdict }, withreadHistory,fetchImplandsleepinjectable.--release-gateprints the same lines and exits the same codes as before: 0 pass, 1 refused, 3 NOT MEASURED.::error::release gate NOT MEASURED --and its suffix. Refusing: an unread full battery is not a green one., and the exit staysEXIT_PREREQUISITE_NOT_MET(3). Its text now names the last answer and the attempts,readFailure-style, for exampleGET https://api.github.com/repos/o/r/actions/workflows/lint.yml/runs?branch=main&event=schedule&status=completed&per_page=100&page=1 -> HTTP 503 after 3 attempts (every attempt one read is allowed, so not one blip).release gate: the read path retries a transient answer, inside one budget per run, with 16 cases and a pinned floor of 16. The battery-count floor moves from 6 to 7.The card's Wanted list, item by item
readRunsretries a transient failure, and only a transient failure. Done. Retried: HTTP 500 to 599,ECONNRESETandUND_ERR_SOCKET(the closed listRETRYABLE_NETWORK_CODES). Final at once, each one pinned: 401, 403, 404, 422 and 429; an OK body withoutworkflow_runs;ECONNREFUSED;ENOTFOUND; a body that is not JSON.check-issue-citationsreads one transient HTTP 500 from the board as PREREQUISITE NOT MET with no retry, so a GitHub blip turns the lint lane red and drops a PR from the merge queue #23361's policy, not a second copy of it. Done, by import.scheduled-full-run.mjsimportsBOARD_READ_RETRY,boardReadBudget,readBoardandreadFailurefromscripts/check-issue-citations.mjs, which is byte-identical to the base. Why import and not lift: see below.fetchandsleep. Done: four pins, and each is RED on the base (see below). No case touches the network or sleeps.release.yml, its environment or its dispatch. Held. The diff is one file underscripts/ci/. No workflow file is touched, and no release workflow was run or dispatched.The four pins, and the proof that each one can fail
7e7e605de35cad717ed4... page=1 answered HTTP 502. Refusing: ...-> HTTP 503 after 3 attempts, backoff 500,1000. Persistent resets nameECONNRESET. A persistent 500/502/503/504/reset/socket close on each of the 6 listings refuses.after 3 attempts-> HTTP 404 after 1 attempt (not retried. Also 401/403/422/429, noworkflow_runs, and ECONNREFUSED / ENOTFOUND / non-JSON.this run's budget of 4 retries is spent), 9 requests. Control: 4 blips are all measured.The CLI probe runs
node scripts/ci/scheduled-full-run.mjs --release-gate --release-commit 7e7e605de3…in each tree. It preloads a stubfetchand uses the real git history, so BASE's own code path is what goes red. On BASE, pin 3 holds its "at once" half (the base never retried anything) and goes red only on the attempt count. Two ablations on HEAD prove the behavioural halves of pins 3 and 4:retryableis widened to every status 400 and up incheck-issue-citations.mjs(viascripts/ablation-replace.mjs; anchor 1 -> 0, blob changed).--self-testthen exits 1 at pin 3:got exit 3 after 3 request(s), slept 500,1000. Restored: blob == HEAD, andgit diff HEADis empty.readFullBatterycreates a freshboardReadBudget()perreadRunscall.--self-testthen exits 1 at pin 4:got exit 0 after 12 requests and 6 backoff(s). Restored the same way.The dispatch's offline end-to-end probe is pin 1's row: one 502 on
lint.yml'sschedulelisting, then a 200. BASE refuses it as NOT MEASURED (exit 3); HEAD measures it and passes (exit 0).Why import, not lift
Both shapes leave one definition. I decided on the four axes:
check-issue-citationsreads one transient HTTP 500 from the board as PREREQUISITE NOT MET with no retry, so a GitHub blip turns the lint lane red and drops a PR from the merge queue #23361 for the citation gate. Neither adds a capability. Tie.check-issue-citations.mjsand four helpers (git-env,glob-match,js-comment-mask,symbol-anchors; about 5,600 lines) to that closure. Measured: every one of them imports onlynode:*, and none runs anything at load (0 child processes and 0 fs calls during the import). The gate already loadspm/check-half-states.mjs(about 35,000 lines), so the added coupling is small. Lift wins, narrowly.local-envcensus row inscripts/pm/dispatch-gates.self-test.mjsfrom:204to:205(pnpm check:pm-dispatch-gateswent 1 of 1913 cases red). That file is outside this card's surface, so the lift needs a fourth file. Import wins.Decision: import. I tried the lift first (
42643e8ca9) and reverted it in5cad717ed4, so the net diff is one file.The import loads in each job that loads this file:
release.ymlfull-battery:actions/checkout(fetch-depth: 0) andsetup-node22, with no install step. The closure is node builtins only. Proof: I loaded the module and ran--self-testfrom agit archiveofscripts/and.github/, with nonode_modulesanywhere above it. Exit 0.scheduled-full-run-card.ymlfile:actions/checkout, with no setup-node and no install.actions/github-scriptimport()s the file by absolute path, so the closure and the proof are the same.lint.ymllint-ci-engine(the step that runs--self-testand--check-concurrency): checkout (fetch-depth: 0), setup-node, thenpnpm install --frozen-lockfile. It loads there too.Gates
Run on HEAD
5cad717ed4, the final commit (the union ran after it):node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 32 commands (no paths given: it reads the change set off the merge base). All 32 exit 0, includingpnpm check:pm-dispatch-gates(1913 cases).--ranreconciles them:32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.node scripts/ci/scheduled-full-run.mjs --self-test: exit 0.--check-concurrency: exit 0.node scripts/check-issue-citations.mjs --self-test: exit 0 (215 cases, 10 batteries).pnpm exec eslint --no-inline-config scripts/ci/scheduled-full-run.mjs: exit 0.On BASE
7e7e605de3, the same three direct commands each exit 0, and the citation gate's self-test reports the same 215 cases.Acceptance notes
origin/mainmoved past the base (4 commits, to39e93b9ef9at the last fetch). None of them touches this file or its import closure, so I did not merge; CI tests the merge ref.lint.yml's job installs dependencies, so a non-builtin import added anywhere in the closure would fail first on a release run. This is not a defect today. No carrier: noted, not filed.scripts/belongs to the private root package (private: true), and no published package ships it. Following PR ci(scheduled-full-run): a refresh moves a stale pm:blocked back to pm:queue once every blocker has closed #24305's precedent,skip-changesetis declared when this PR is created.Generated by Claude Code