Skip to content

Move the three untested inline workflow step bodies into scripts/ci with self-tests - #26386

Merged
os-zhuang merged 6 commits into
mainfrom
claude/project-thread-y83svr
Oct 27, 2026
Merged

os-zhuang merged 6 commits into
mainfrom
claude/project-thread-y83svr

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Part of #26086

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:三个只在「没有 PR 能触发的事件」上跑的工作流步骤(定时全量红卡、nightly 红卡、看板备份上传 R2),把内联主体搬进 scripts/ci/,每个都配了离线自测,并接进 lint-ci-engine 道。
  • 为什么改:这三段主体原来没有任何测试能碰到,第一次执行就是线上真跑。卡上其余 17 个超 80 行的主体,大多已经有门禁在读它的原样字节;release.yml 的 3 步有信任边界,不能搬。逐个原因见下文 Acceptance notes。
  • 风险与回滚:行为不变。github-script 步骤保留原来的 token、retries: 和权限,只是改成 import 模块后薄调用;bash 主体逐字节不变。回滚就是 revert 这个 PR。
  • 先看:scripts/ci/scheduled-full-run-card.mjs(调用和回调接线)、scripts/ci/nightly-tiers-card.mjs、.github/workflows/lint.yml 新增的那一步。
  • 两分钟验证:node scripts/ci/scheduled-full-run-card.mjs --self-test && node scripts/ci/nightly-tiers-card.mjs --self-test && bash scripts/ci/board-snapshot-r2-upload.selftest.sh,三行 ✓,共 7 + 7 + 5 个用例。
  • 需要你做的:这是 Tier H(改 .github/workflows/**),需要 GOVERNED_APPROVERS(os-zhuang 或 hotlong)批准;PR 1 的范围(只搬没有测试台的部分,还是照卡全搬)在项目线程里等 Steve 裁决。

What changes

Three inline step bodies that no test reached move into scripts/ci/, each with an offline self-test:

Workflow step Lines moved New home Self-test
scheduled-full-run-card.yml · File or refresh the card (github-script) 121 scripts/ci/scheduled-full-run-card.mjs · fileCardStep 7 cases
test-nightly-tiers.yml · File or refresh the card (github-script) 158 scripts/ci/nightly-tiers-card.mjs · fileNightlyCard 7 cases
board-snapshot.yml · Upload the archive to R2 (run) 125 scripts/ci/board-snapshot-r2-upload.sh scripts/ci/board-snapshot-r2-upload.selftest.sh, 5 cases / 27 checks
  • github-script bodies keep their step. The step now does await import(GITHUB_WORKSPACE + '/scripts/ci/X.mjs') and calls the exported function with the same github / context / core. That keeps the token, the retries: / retry-exempt-status-codes: policy and the job's permissions: exactly as they were. It follows scheduled-full-run-card.yml's existing import of scripts/ci/scheduled-full-run.mjs. This is a deliberate deviation from the card's literal "the YAML step becomes node scripts/ci/X.mjs": a bare node step would need its own octokit, token plumbing and retry policy, which is a behaviour change. Both jobs already check out the repository.
  • The bash body moves byte for byte. scripts/ci/board-snapshot-r2-upload.sh is the inline block plus a header and the set -e that GitHub's default bash -e {0} shell supplied. cmp against the YAML-parsed block is identical. The step keeps its env: and runs bash scripts/ci/board-snapshot-r2-upload.sh.
  • Spelling changes inside the moved JS, and only these: require('fs') / require('path') became static imports. process.env became an env parameter that defaults to process.env. The two new Date() reads that stamp a body became an injectable now that defaults to new Date(). The import of scheduled-full-run.mjs became static.
  • Wiring: one new gate step in lint-ci-engine, Moved workflow step bodies self-test (with an id: and continue-on-error: true, so the lane verdict reads it), runs all three self-tests through the repo's run_self_test collector, so one red does not hide the others.
  • Adoption lists: board-snapshot.yml's header and scripts/pm/board-snapshot.mjs's docblock both said "copy FOUR files". The R2 step now needs a fifth, so both lists name it. That is the one prose edit, and it is the card's own carve-out: a literal the touched step makes false.
  • Found while moving: the root .gitignore ignores test-*.mjs, so the nightly module could not be named test-nightly-tiers-card.mjs. Its first push silently left it out, and it is now nightly-tiers-card.mjs.

Tests

Behaviour preservation was checked before each move, against the inline bodies at 6390fe52 (main when this branch was cut):

  • github-script bodies: a one-off harness ran the inline body extracted from the YAML (compiled the way actions/github-script compiles it) and then the module, under identical doubles, files and a frozen clock. It recorded every API call with its full parameters, bodies included, and every core call. The traces are identical: scheduled-full-run-card 7/7 scenarios, nightly-tiers-card 7/7. The committed self-tests pin the values taken from those traces. For the scheduled card the body is pinned as exact renderBody(...) arguments, so a wording change in scheduled-full-run.mjs does not red this file. For the nightly card it is pinned as the SHA-256 of the inline body's bytes.
  • bash body: the committed self-test passes against the inline block (extracted from the YAML at 6390fe52) and against the moved script. Negative control: with --delete dropped from the sync line, two checks go red.
  • Each self-test carries a battery roster with per-case floors, a pinned roster size and a verdict handshake (AGENTS.md, "Writing a --self-test").

Derived gate families: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gives 60 commands. Results are below. CI runs the rest.

60 derived, 60 run, 0 red. node scripts/pm/dispatch-gates.mjs --ran ran.list gives 60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED. The first round had 4 reds, all caused by this diff and fixed in b422bbdf:

  • pnpm check:bash32-floor: the R2 self-test's roster used declare -A. It now uses parallel indexed arrays.
  • node scripts/check-step-collectors.mjs (and its --self-test): the new lint step ran its three self-tests as a bare sequence. It now uses the run_self_test collector.
  • pnpm check:declared-population-live: the nightly self-test spelled its $RUNNER_TEMP fixture paths as literals, so they read as a declared population. It now builds them and carries a no-path-population marker.

Not in the derived list, run by hand: bash scripts/ci/board-snapshot-r2-upload.selftest.sh passes (5 cases, 27 checks), and the new lint step's run: block, extracted from the YAML and run under bash -e, prints three PASS lines.

Second-round results (exit code, command, seconds)
0  node scripts/check-aggregator-roster.mjs  0s
0  node scripts/check-aggregator-roster.mjs --self-test  2s
0  node scripts/check-ci-filter-parity.mjs  0s
0  node scripts/check-closing-keyword-parity.mjs  2s
0  node scripts/check-closing-keyword-parity.mjs --self-test  5s
0  node scripts/check-comment-mask-corpus.mjs  181s
0  node scripts/check-declaration-mirrors.mjs  1s
0  node scripts/check-declaration-mirrors.mjs --self-test  0s
0  node scripts/check-merged-branch-reaper-outcome.mjs  0s
0  node scripts/check-merged-branch-reaper-outcome.mjs --self-test  0s
0  node scripts/check-scripts-symbol-anchors.mjs  3s
0  node scripts/check-scripts-symbol-anchors.mjs --self-test  4s
0  node scripts/check-self-test-wired.mjs  2s
0  node scripts/check-self-test-wired.mjs --self-test  5s
0  node scripts/check-self-test-workflow-commands.mjs  116s
0  node scripts/check-self-test-workflow-commands.mjs --self-test  3s
0  node scripts/check-step-collectors.mjs  1s
0  node scripts/check-step-collectors.mjs --self-test  5s
0  node scripts/check-whole-set-label-write.mjs  2s
0  node scripts/check-whole-set-label-write.mjs --self-test  1s
0  node scripts/ci/nightly-tiers-card.mjs --self-test  0s
0  node scripts/ci/scheduled-full-run-card.mjs --self-test  0s
0  node scripts/ci/scheduled-full-run.mjs --self-test  0s
0  node scripts/docs-audit/check-drift-comment.mjs  1s
0  node scripts/nightly-tiers.mjs --self-test  0s
0  node scripts/pm/bare-root-worklist.mjs --self-test  30s
0  node scripts/pm/board-snapshot.mjs --self-test  1s
0  node scripts/pm/ci-failure.mjs --self-test  0s
0  pnpm check:agent-test-spelling  3s
0  pnpm check:bash32-floor  2s
0  pnpm check:cli-command-ids  17s
0  pnpm check:cross-package-test-inputs  33s
0  pnpm check:declared-population-live  36s
0  pnpm check:driver-memory-census  5s
0  pnpm check:entry-guard  38s
0  pnpm check:gitlink-declared  1s
0  pnpm check:node-version  1s
0  pnpm check:nul-bytes  4s
0  pnpm check:parse-guard  3s
0  pnpm check:pm-board-snapshot  2s
0  pnpm check:pm-dispatch-gates  396s
0  pnpm check:pm-expected-skips  6s
0  pnpm check:pm-governed-merges  9s
0  pnpm check:pm-post-stamped  1s
0  pnpm check:pm-record-recognisers  4s
0  pnpm check:pnpm-acquisition  1s
0  pnpm check:pnpm-filter-targets  3s
0  pnpm check:pnpm-flag-spellings  6s
0  pnpm check:ratchet-remedy-authority  4s
0  pnpm check:refd-timer-probe  23s
0  pnpm check:required-contexts  4s
0  pnpm check:select-gate-families  4s
0  pnpm check:shard-attestation  3s
0  pnpm check:stall-guard-budget  1s
0  pnpm check:stall-guard-headroom  5s
0  pnpm check:type-check-coverage  5s
0  pnpm check:type-check-debt  6s
0  pnpm check:watch-hint-literal  7s
0  pnpm check:workflow-status-functions  1s
0  pnpm check:workflow-step-name-quoting  1s

Acceptance notes

The card's premise holds for a minority of the volume. On 6390fe52: 20 inline bodies over 80 lines, 4,414 lines. Most of those lines are already exercised byte-for-byte by a gate that reads the YAML, so moving them buys no test reach and means rewriting the harness:

Body (lines) What already reads its bytes Status
merge-queue-triage · Post the triage comment (901) check-merge-queue-triage-outcome.mjs compiles it as github-script does, 28 mutations; its P0 also pins that the job holds no contents: grant, which a checkout for an import would need stays
docs-drift-check · Comment on PR (597) docs-audit/check-drift-comment.mjs runs it against fixture diffs stays
cross-repo-issue-closer (416) check-cross-repo-closer-outcome.mjs (executes), check-closing-keyword-parity.mjs (mutates its regex) stays
release.yml · Audit the release (277) release-verify-npm.mjs batteries 12/13 run the step text stays
merged-branch-reaper (174) check-merged-branch-reaper-outcome.mjs (executes) stays
shard-timings-refresh · Choose (82), Regenerate (136) ci/select-shard-timings-run.mjs runs both stays
shard-timings-refresh · Compose (206) text pin in ci/select-shard-timings-run.mjs stays
shard-timings-refresh · Push (102) executed for real on every pull_request run of the lane (the DRY_RUN leg) stays
pr-automation · Require a changeset (167) text pins in check-empty-changeset.mjs stays
platform-checklist-watchdog (145) check-platform-checklist-watchdog.mjs scans the script for board writes; moving it would blind that negative pin stays
duplicate-fix-guard (108) check-closing-keyword-parity.mjs mutates its regex stays
release.yml · Guard the approved release (99) trust boundary: the publish job checks out the VERSION COMMIT, so a moved guard would be read from the tree it is guarding stays
release.yml · Render (84), Validate (167) the post-version tree trust boundary: both run after changeset version has written the working tree, so a moved validator would be read from the tree it validates stays
ci.yml · Run this shard's tests (185) the step #26084 is changing; moves after it lands deferred
cut-rc.yml · Build the single version commit (164) the file is deleted by #26196 out
the three above nothing moved here

After this PR: 17 bodies over 80 lines, 4,010 lines (the census re-run on the branch). Whether the harnessed bodies should move anyway, which means rewriting about ten harnesses and granting contents: read to jobs that hold none today, is the open scope question for this card's PR 1. It is in front of the maintainer in the project thread. This PR is the part both answers need.

Verification

node scripts/ci/scheduled-full-run-card.mjs --self-test
node scripts/ci/nightly-tiers-card.mjs --self-test
bash scripts/ci/board-snapshot-r2-upload.selftest.sh

Tier H (.github/workflows/**): draft until a GOVERNED_APPROVERS approval (os-zhuang, hotlong).


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmQq9EYQdTQwmRutQTS

…ripts/ci/scheduled-full-run-card.mjs with a --self-test

The github-script step keeps its token, retries and permissions and now
imports the module. The self-test's expected values were captured by running
the inline body at 6390fe5 under the same doubles; the full call traces
(bodies included) match on all seven scenarios.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQq9EYQdTQwmRutQTS
…/ci/test-nightly-tiers-card.mjs with a --self-test

The github-script step keeps its token, retries and permissions and imports
the module. Expected bodies were captured from the inline step at 6390fe5
under the same doubles; full call traces match on all seven scenarios. The
moved bodies' self-tests run as one lint-ci-engine gate step.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQq9EYQdTQwmRutQTS
…s/ci/board-snapshot-r2-upload.sh with a self-test

The step runs the script under the same env. The body is byte-identical to
the inline block; the self-test (stub aws, throwaway archive with known
committer times) passes against the inline body at 6390fe5 and against the
moved script, and a dropped --delete turns it red.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQq9EYQdTQwmRutQTS
…which .gitignore does not match

The root .gitignore ignores test-*.mjs, so the module the step imports never
reached the branch under its first name.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQq9EYQdTQwmRutQTS
The R2 upload self-test used associative arrays, which bash 3.2 lacks;
its roster is now parallel indexed arrays. The lint step runs its three
self-tests through a run_self_test collector so one red does not hide
the others. The nightly card self-test builds its log-tail fixture names
instead of spelling them, and says it reads no tracked population.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQq9EYQdTQwmRutQTS
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review October 27, 2026 01:49
@os-zhuang
os-zhuang enabled auto-merge October 27, 2026 01:49
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · devx seat 2 · 2026-10-27T01:49Z

Seat domain:devx#2 (#20163) · session_01Q4MDXRbEJCNdPAP4SiXwp9 · reviewed head b422bbdf · card #26086, claim 6430730698 (wave 8, delta C, PR 1). PR carries Part of #26086. Tier H (check-governed-merges: governed, tier H, humanMerge); +1348/−410, under the 3000-line human-merge line. The approval, readying and auto-merge on this PR are a GOVERNED_APPROVERS member's own acts, not the seat's or the card thread's.

  • Scope, as the seat approved: only the three bodies no gate reads today move (404 lines): the scheduled-full-run red card, the nightly-tiers red card and the board-snapshot R2 upload. The other 17 bodies over 80 lines stay, each with its reason in the PR body; whether to move them waits on the maintainer's decision card. No ci.yml, no ui/, no cut-rc (that is ci(release): retire the rc channel — delete cut-rc.yml, state the two channels (latest, next), and have release-dist-tags remove the retired rc tag instead of moving it #26196).
  • The two github-script steps keep the step (token, retries:, permissions: unchanged) and become a two-line import() off $GITHUB_WORKSPACE plus one call with the same github / context / core. That import form already runs on main in scheduled-full-run-card.yml. Both jobs do a plain full actions/checkout, so the module and its ../invoked-as.mjs import are on disk; both modules import only node: builtins and repo-local files, matching the no-install jobs.
  • The R2 step keeps its id: r2, if: and the whole env: block, and runs bash scripts/ci/board-snapshot-r2-upload.sh from the same working directory; the script restates the default bash -e line, then the inline body. Secrets are read, never echoed.
  • Behaviour pinned: each module's --self-test expected values were captured by running the inline body from main (6390fe5) under the same doubles (call traces 7/7 and 7/7). The three self-tests are wired into the Lint · CI, release & engine gates lane as a collector step with an id and continue-on-error, the lane's existing pattern, so the lane verdict counts it.
  • Adopt lists: board-snapshot.yml and scripts/pm/board-snapshot.mjs now both say FIVE files and name the new script, so a sibling-repo copy does not install a step whose body is missing.

PR 2 (switch to pnpm run check:*, Fixes #26086 + #26091) opens after this merges.

@os-zhuang
os-zhuang added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 671df26 Oct 27, 2026
69 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-y83svr branch October 27, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants