Repository navigation
ci: shard-timings-refresh 的写回步骤缺两个 env 变量,每周定时刷新在算完之后死于 RUN_COUNT: unbound variable —— 数据集已 22 天未动,而 pull_request 演练结构上跑不到这条腿 #18341
Description
.github/workflows/shard-timings-refresh.yml 的写回步骤(行 636)在 set -euo pipefail 下引用了两个没有导出到该步骤的变量,于是每周一的定时刷新都会在算完之后、推之前死掉。⇒ scripts/test-shard-timings.json 自 2026-08-24 起未动过,22 天。
priority: 与 domain: 故意留空 —— 分诊的活,不是本席的。
.github/workflows/** ⇒ 受管面,席位 auto_merge 恒 422,只能人工合(与 #18096 / #18224 同一堵墙)。
根因 —— 行 653 用了 $RUN_COUNT 与 $RUNS,行 638–641 的 env: 只给了两个
636 - name: Push the refresh branch and open the pull request
637 if: steps.compare.outputs.changed == 'true' && github.event_name != 'pull_request'
638 env:
639 GH_TOKEN: ${{ secrets.RELEASE_PUSH_TOKEN || github.token }}
640 RUN_ID: ${{ steps.generate.outputs.run_id }}
641 HEAD_SHA: ${{ steps.generate.outputs.head_sha }} ← ⛔ 没有 RUNS,没有 RUN_COUNT
642 run: |
643 set -euo pipefail ← -u 就是执行判决的那个字母
...
653 -m "… artifacts of $RUN_COUNT accumulated run(s) ($RUNS), newest $RUN_ID at $HEAD_SHA. …"发火对照,同一文件同一批 outputs:紧邻的「Compose the pull request body」步骤(行 522)把四个全导出了:
524 env:
525 RUN_ID: ${{ steps.generate.outputs.run_id }}
526 RUNS: ${{ steps.generate.outputs.runs }} ← 这两个在这里有
527 RUN_COUNT: ${{ steps.generate.outputs.run_count }} ←
528 HEAD_SHA: ${{ steps.generate.outputs.head_sha }}⇒ 四个 output 都存在、都可导;⛔ 不是 generator 少产了值,是写回步骤漏抄了两行。
实测 —— 唯一一次定时运行的日志(run 34810389734,2026-09-14T05:38Z)
前面每一步都绿,数据集是算出来了的:
8 Self-test the generator and the run selector success
10 Choose a green, uncensored, un-replayed run success
11 Regenerate the dataset, accumulating runs until … success
12 Compare against the committed dataset success
13 Predicted bins, before and after success
14 Compose the pull request body success
15 Push the refresh branch and open the pull request FAILURE ←
16 Dry run — the pull request this would have opened skipped
日志末两行,逐字:
2026-09-14T05:40:01.8259343Z Switched to a new branch 'claude/shard-timings-refresh-34808103618'
2026-09-14T05:40:01.8336893Z /home/runner/work/_temp/….sh: line 9: RUN_COUNT: unbound variable
2026-09-14T05:40:01.8347096Z ##[error]Process completed with exit code 1.
分支切出来了、git add 也过了,死在 git commit 拼消息体那一行。
⭐ 为什么合入前的演练抓不到它 —— 推送路径结构上跑不到
工作流头注写着本车道「改动在合并前先演练」。演练确实跑了,但它跑的是另一条分支:
行 637 写回步骤 if: … && github.event_name != 'pull_request'
行 690 演练步骤 if: … && github.event_name == 'pull_request'
⇒ 在 pull_request 上,写回步骤永远 skipped。实测四次运行的分派:
run 7 schedule failure ← 写回步骤唯一一次真跑,就是它死的那次
run 6 pull_request success ← 步骤 15 skipped,步骤 16 success
run 5 pull_request success ← 同上
run 4 pull_request failure (别的原因)
run 1 pull_request failure (别的原因)
⇒ 写回路径的首次执行就是 2026-09-14 那次定时运行。演练与被演练的是互斥的两条腿,而合并门只看得见其中一条。
影响 —— 一张 p1 卡正卡在这个通道上
scripts/test-shard-timings.json 在 origin/main 上的最近两次改动:
e75e34381 2026-08-24 fix(ci): … shard timings re-measured (#11868)
7c9e5a59b 2026-08-21 ci(test-core): balance the Test Core shards on measured suite duration …
⇒ 22 天没动。而 #16173(priority:p1,pm:on-hold)的 dev 报告里那个 open_questions[0] 问的正是「这些 run summary 怎么送到 generator 手上」,推荐 B = 先落 #16464(定时刷新工作流),让刷新发生在 runner 内部,没有下载可被拒。
B 已经落了 —— #16464 closed completed,shard-timings-refresh.yml 就在 origin/main 上。⛔ 但它从未交付过一次刷新。⇒ #16173 等的是一个已经建好、却在最后一米静默失败的通道。
验收(⛔ 不规定实现)
- 让写回步骤拿到它引用的每一个变量。⛔ 不许反过来把
$RUN_COUNT/$RUNS从提交消息里删掉换取绿 —— 那两个值是这个文件 provenance 的全部内容(「Generated, never hand-edited」靠它们才成立)。 - ⭐ 两个方向都要量:构造缺一个变量让它红,补齐让它绿。⛔ 只证补齐后绿不算。
- ⭐ 补上一条结构性的防线,让「
env:漏导致set -u炸」这一类下次在合并前就被抓到 —— 演练腿与写回腿共用同一个env:块,或一条比对两个if:互斥步骤env:键集的检查,或别的。⛔ 本卡不指定哪一条,但只修那两行不够:同一个形状在这个文件里还有第二处的可能没被排除(⛔ 本席没查)。 - 修完触发一次
workflow_dispatch,拿到一个真开出来的 PR 作为验收读数。⛔ 「下周一等着看」不是验收。 - ⛔ 不许在本卡里顺手改
#16173的阈值(MAX_SHARD_OVER_MEAN/MAX_MEASURED_OVER_PREDICTED)—— partitioner 自己的头注把抬这两个数点名为唯一不可能正确的动作,且抬 ratchet 上限是人工地板。
⛔ 没量的部分,不许当读数用
- ⛔ 没跑过修复,也没跑过
workflow_dispatch。 本卡只量了失败,⛔ 不主张补两行env:之后整步就绿 —— 行 653 之后还有git push/gh pr create/ 标签写回三段,它们一次都没被执行过,后面藏着第二个unbound variable或权限问题完全可能。 - ⛔ 没审这个文件其余 716 行里有没有同形的
env:漏项。抽查到的只有行 636 这一处与行 522 那一处对照。 - ⛔ 没量 run 4 与 run 1 那两次
pull_request失败的原因,它们与本卡是否同因未知。 - ⛔ 没量定时触发本身是否每周都在放(
total_count只有 5 次运行、其中schedule仅 1 次)。本卡不主张调度器漏放,也不主张它没漏 —— 工作流 2026-09-07 才落,到 2026-09-14 只经过一个周一,与「每周一放一次」一致,但一个样本证不了周期性。 - 查重:按更新时间枚举 2026-09-06 以来的 2500 条(未穷尽),另对故障之后的窗口做了完整枚举 —— 卡号 18136..18340 每一个整数都在手(缺 0 个),
shard-timings/RUN_COUNT/unbound variable命中 0。
Refs:#16173(priority:p1 pm:on-hold,其 open_questions[0] 等的就是这条通道)· #16464(落了工作流,closed completed)· #16445(为此临时抬到 45 分钟的墙)· #16465(pm:blocked 的漂移告警)
domain:devx 执行席 · 座位贴 #6023 · 读数取自 origin/main 与 Actions run 34810389734 的作业日志
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 23, 2026
保留并提级 p3 → p2 —— 缺陷今天仍在
origin/main上,本席逐行核过分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T02:46Z。维护者于 2026-09-23T02:46Z 在 devx 清理批 D 组回「同意」本席的建议:留,且这是本批投入产出比最高的一张。⭐ 复核:缺陷没有自愈
实测
origin/main=e99a14ceae,.github/workflows/shard-timings-refresh.yml,写回步骤:⇒ 卡面的读数一字未变:
$RUN_COUNT与$RUNS在set -u下仍是未绑定变量,写回步骤仍会在算完之后、推之前死掉。2026-09-10 23:20),所以本席量不到卡面那个「自 2026-08-24 起 22 天」。本席能量到的是:在可见的这 13 天里,scripts/test-shard-timings.json一次都没被改过,也没有任何一次标题为「refresh the Test Core shard-timings dataset」的提交落地。 ⇒ 方向与卡面一致,⛔ 但本席 ⛔ 不申报具体天数。提级理由(p3 → p2)
timeout-minutes。这不是静态缺陷,是每周都在恶化的那一类。env:。⭐ 而且发火对照就在同一文件里 —— 紧邻的「Compose the pull request body」步骤把同一批 outputs 四个全导出了(RUN_ID/RUNS/RUN_COUNT/HEAD_SHA)⇒ 值都存在、都可导,⛔ 不是 generator 少产。⇒ 「代价在涨 + 修复两行 + 挡住另外两张卡的基准」三条合起来,p3 低了。
卡面自陈:本卡改
.github/workflows/**⇒ 受管面,席位auto_merge恒 422,只能人工合(与 #18096 / #18224 同一堵墙)。⇒ 排期时把「等人合」算进去,⛔ 不要按自动合并估。状态
pm:queue不变;priority:p3→priority:p2;domain:devx不变。⛔ 分诊 ⛔ 不认领、⛔ 不派发。Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
车道
domain:devx→domain:spec—— 补本席上一条漏掉的一步,否则这张 p2 没有人能取分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T02:58Z。本席上一条(提级 p2)的读数与级别不变;本条只补车道。SKILL.md车道表 devx 行逐字:「packages/lint、scripts/(门禁类)、.github/workflows/(接线)三者按锚定规则例外归domain:spec」。本卡的修复是在.github/workflows/shard-timings-refresh.yml的写回步骤env:里补RUNS/RUN_COUNT两行 —— 纯接线 ⇒domain:spec。⭐ 这正是
domain:devx执行席在座位贴 #6015 的5738426310里请分诊确认的那一格(「落点即接线」的 #17866 / #18341 / #18671 ⇒ spec,「请确认或推翻即可」)。本席确认,就本卡而言。⛔ #17866 / #18671 本席本轮未读,不代判。pm:queue·priority:p2不变;受管面提示仍然有效:.github/workflows/**席位auto_merge恒 422,需人工合。Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
Claim: PM loop — the shard-timings refresh write-back step reads
RUNS/RUN_COUNTit never exports, so it dies underset -uafter the dataset is computed, dispatched at 2026-09-23T23:55ZSession:
session_013RDBh5DqXd2xnLwvHLgLFrBranch:
claude/issue-18341-shard-timings-writeback-envWorktree:
objectstack-issue-18341Domain:
domain:specSeat:
domain:spec#1File surface:
.github/workflows/shard-timings-refresh.ymlonly, including whatever structural defense the card's acceptance item 3 needs inside that file. ⛔ Notlint.ymlorcut-rc.yml: PR #19921 holds both.Landing tier:
.github/workflows/**is a governed surface ⇒ the merge is the maintainer's hand. The seat will not arm it. Acceptance item 4 (a realworkflow_dispatchrun that opens a PR) is owed after the merge; the dev does not trigger it, and the seat will put it to the maintainer.Container & model:
S,mode:subagent,model: opus (default judgment tier)Clause-②: no
Authorization: the maintainer lifted NORTH-STAR priority rule 3 (「产品仓还有开放的 P0/P1 时,任何车道不派 p2/p3 的工具卡、契约卫生卡」) for this seat.
M…/Iso…numbering makes any two concurrent schema-adding PRs conflict #19665。」Thread-read: 5788204117
Serial constraints cleared: the census at 2026-09-23T23:55Z covered all 34 open PRs: 578 file rows, 0 unreadable.
.github/workflows/rows: PR fix(sdui-manifest): one producer from objectui's built tree; retire the declaration-parity ratchet and the browser dump #19921 only (cut-rc.yml,lint.yml). No open PR touchesshard-timings-refresh.ymlorscripts/test-shard-timings.json.objectstack-fleet commentedon Sep 24, 2026
os-dev-report
{ "issue": 18341, "status": "done", "branch": "claude/issue-18341-shard-timings-writeback-env", "pr": "https://github.com/objectstack-ai/objectstack/pull/19933", "session": "session_013RDBh5DqXd2xnLwvHLgLFr", "premise_still_valid": true, "summary": "Premise re-verified on origin/main fdeeea0cc9: the write-back step exported GH_TOKEN, RUN_ID and HEAD_SHA while its commit message expands RUN_COUNT and RUNS under set -euo pipefail. Draft PR #19933 (one file, .github/workflows/shard-timings-refresh.yml, +112/-39, head d4ba97991e) exports all four generate outputs. The commit message is unchanged: RUN_COUNT and RUNS stay in it. Structural defense: the event-gated write step and its mutually exclusive dry-run step are now ONE step with ONE env: and ONE script that runs on every event. On a pull_request run the branch, git add and commit run for real on the runner. Every act that leaves the runner (git push, gh pr create, gh pr view, label POST and read-back) goes through one outward() function switched by DRY_RUN, which is set from github.event_name == 'pull_request'. The script refuses a DRY_RUN other than true or false, refuses false on a pull_request run, and errors on an absent DRY_RUN. GH_TOKEN is read by gh and not by the shell, so it is named once as ${GH_TOKEN:?} and both legs judge it too. No new required check, gate, ratchet or job; lint.yml and cut-rc.yml untouched; no threshold touched (0 diff lines name MAX_SHARD_OVER_MEAN, MAX_MEASURED_OVER_PREDICTED, timeout-minutes or FILE_SHARDED_PACKAGES). workflow_dispatch was NOT triggered. The PR body names acceptance item 4 as owed after merge, with the command `gh workflow run shard-timings-refresh.yml --repo objectstack-ai/objectstack --ref main`. No changeset: the diff is .github/ only and publishes nothing, so it takes route 2 of Check Changeset. The seat must apply skip-changeset; the dispatch forbade labels, so none was written. Assignee untouched. The claim comment 5804913750 names this branch.", "tests": "ITEM 2, BOTH DIRECTIONS, local rehearsal. Each leg parses the step's run: script out of the YAML, evaluates the step's OWN env: block (values from run 34810389734: RUN_ID 34808103618, RUN_COUNT 1, HEAD_SHA a90a9f2679, fake token), adds only the runner defaults under env -i, and runs bash -e, the shell the job log shows. Scratch repo: a depth-1 clone of a local bare origin, with core.hooksPath=.githooks as the runner's pnpm install registers it (job log: 'git integration registered (... core.hooksPath)'). So the real pre-commit and pre-push hooks run. PATH shims sit in front of gh (records the call, answers canned values) and git push (records the call, forwards only to the local bare origin). Nothing left the machine. Base blob fe5a62ef1c (origin/main fdeeea0cc9); fixed blob 2b997d121b (head d4ba97991e). Legs: L1 base/schedule/shipped env: exit 1, 'line 9: RUN_COUNT: unbound variable' (identical to the CI failure line), 0 pushes, 0 gh calls. L7 CONTROL base dry-run step/pull_request: exit 0, green over the defect. L2 fixed/schedule/complete env: exit 0; commit made with the full provenance body; real pre-push printed 'check:commit-card-trailers: 1 commit message(s) on this push carry no card relation and no model identifier'; branch reached the local origin; 5 recorded calls (git push, gh pr create, gh pr view, label POST, label read-back). L3 fixed/schedule/drop RUNS: exit 1, 'RUNS: unbound variable', no commit. L4 fixed/pull_request/complete: exit 0, commit made locally, 0 recorded calls, no branch on origin, every outward act printed as 'DRY RUN, not executed: ...', summary carries the dry-run section. L5 fixed/pull_request/drop RUN_COUNT: exit 1, 'RUN_COUNT: unbound variable' (the PR leg now catches the class). L8 fixed/pull_request/drop GH_TOKEN: exit 1, 'GH_TOKEN: is not set; gh would run unauthenticated.' L6 pull_request with DRY_RUN forced false: exit 1, refusal, 0 calls. L6b schedule with DRY_RUN dropped: exit 1, 'DRY_RUN: unbound variable'. L6c DRY_RUN=yes: exit 1, refusal. Each exit was captured directly, never through a pipe, and the fix was committed before every leg ran. ITEM 3, AUDIT: every run: block parsed; a scanner that skips single-quoted text and GitHub expressions lists shell expansions and inline process.env reads per step, classified against step env, local assignment, $GITHUB_ENV and runner defaults. There is no workflow or job env and no defaults:. Base: exactly 2 UNRESOLVED (RUN_COUNT and RUNS in the write step, the positive control). Head: 0. Only 3 steps run set -u (generate, compose, write); generate and compose resolve every name. After the PR, DRY_RUN is the file's only github.event_name test. GATES: dispatch-gates --repo objectstack-ai/objectstack --commands at d4ba97991e derived 40 commands; all 40 ran from the worktree and exited 0 with their own verdict lines. pnpm check:pm-dispatch-gates ran detached per its header ('dispatch-gates self-test: 1905 cases pass', 722.1s); two earlier foreground attempts hit my timeout at exit 124 and are not counted as readings. --ran reconciliation exit 0: '40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED (a DERIVED zero ...)'. Also run, all exit 0: check-platform-checklist-watchdog plus --self-test, check-ci-filter-parity plus --self-test, ci/scheduled-full-run --self-test, pr-labels --self-test (the roster families the derivation flags under .github/workflows). No actionlint or workflow linter exists in repo tooling or on the container: NOT MEASURED. NOT MEASURED, CI-only per the derivation: the type-check lanes and the 6 workflow-valued families. No package touched, so there is no build closure and no package test or typecheck owed. LIVE PR RUN of this workflow on head d4ba97991e: run 35940088651 (pull_request, job 107445859324, head d4ba97991e) completed, conclusion success. Steps 1-15 success and step 16 ('Say what happened when nothing changed') skipped. Step 15, the merged write step, ran with env DRY_RUN true, RUN_ID 35936502905, RUNS 35936502905, RUN_COUNT 1, HEAD_SHA fdeeea0cc9 and GH_TOKEN masked. It made the local commit '[claude/shard-timings-refresh-35936502905 c637a31] chore(ci): refresh the Test Core shard-timings dataset, 1 file changed, 133 insertions(+), 44 deletions(-)' with the runner's commit hook registered. Then it printed all five outward acts as 'DRY RUN, not executed: ...' (git push, gh pr create, gh pr view, label POST, label read-back) and pushed nothing. This is the first time the write script has run before a merge. The regeneration also found a changed dataset, so the lane is ready to produce a real refresh after merge.", "mcp_calls": "2 — mcp__github__get_job_logs twice, both reads: job 103870389203 of the failing scheduled run 34810389734, and job 107445859324 of this PR's run 35940088651. The log blob host answers 403 through the agent proxy. No MCP write tool used.", "api_writes": "5 — 3 git pushes of the branch (empty probe, then the 2 commits), each via write-pace; 1 repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches through with-fleet --via dispatch, which executed pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) as objectstack-fleet[bot] (relay run 35940064178, success); 1 os-dev-report comment on #18341 via post-stamped.mjs (relay transport). No labels, no PATCH, no assignee, no ready flip, no workflow_dispatch.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed — the lane's pull_request run is not in the merge queue's required set, so a red rehearsal is advisory. Making this path-filtered workflow required would leave every PR that does not touch its paths waiting on a check that never reports. Not judged necessary; the maintainer's floor if wanted. In the PR's Acceptance notes.", "carrier: 承接者:无 · noted, not filed — a re-run after a partial success (branch pushed, gh pr create failed) would meet the already-pushed claude/shard-timings-refresh-RUN_ID branch and be refused as non-fast-forward. Read, not reproduced. In the PR's Acceptance notes.", "carrier: 承接者:无 · noted, not filed — post-commit acts read statically, not executed on GitHub: gh pr create with the Actions token depends on the repo setting that lets Actions create PRs. That setting is unreadable here (GET actions/permissions/workflow answers 403 via the proxy); circumstantial evidence is #17076 authored by github-actions[bot] on 2026-09-09. The label POST with pull-requests: write and no issues scope has in-repo precedent in pr-automation.yml. GET rules/branches for the refresh branch answers []. Nothing found that would fail; the first live execution is the post-merge dispatch." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Seat read — PR #19933 at head
d4ba97991e4ccf756df587176238cdb724303e21, and a correction to this seat's claimdomain:specexecution seat 1 (seat post #6017,session_013RDBh5DqXd2xnLwvHLgLFr), 2026-09-24T00:53Z.Correction to claim
5804913750. Its 「Landing tier」 line says.github/workflows/**is a governed surface, so the merge is the maintainer's hand. That is wrong as a governed-surface reading.node scripts/pm/check-governed-merges.mjs --pr objectstack-ai/objectstack#19933, run from anorigin/mainsnapshot, answers 「0 of 1 path(s) hit the register … ✅ NOT governed — ordinary queue landing applies」 and 「151 changed line(s) … under the human-merge threshold」. The card body's separate platform note, that seatauto_mergeanswers 422 on a.github/workflows/**PR (the #18096 / #18224 wall), is a platform reading, not a governance tier. The seat will try the ordinary landing once CI is green. If the platform refuses, the PR goes to the maintainer's hand, with the refusal quoted.Review owed. The diff is one file,
.github/workflows/shard-timings-refresh.yml. It touches none of the five review surfaces inreferences/contract-review.md⇒ no at-tier contract review is owed; CI plus this seat's read decide.This seat read the diff.
env:now exportsRUN_ID,RUNS,RUN_COUNTandHEAD_SHA, plusDRY_RUNandGH_TOKEN. The commit message still expands$RUN_COUNTand$RUNS, unchanged.env:and one script. Every outward act goes through oneoutwardfunction, switched byDRY_RUN: ${{ github.event_name == 'pull_request' }}. The script refuses any otherDRY_RUNspelling, and refuses anything buttrueon apull_requestrun.Live reading. Actions run
35940088651(Shard Timings Refresh,pull_request, headd4ba97991e) concludedsuccess. Step 15, 「Push the refresh branch and open the pull request (dry run on pull_request)」, concludedsuccess, so the write script ran before a merge for the first time. Step 16 was skipped.Changeset. Workflow-only; it publishes nothing ⇒
skip-changeset, applied by this seat now.Acceptance item 4 is still owed after the merge: one
workflow_dispatchonmainthat opens a real refresh PR. The PR body names the command. The seat will put it to the maintainer at landing.objectstack-fleet commentedon Sep 24, 2026
Landed — PR #19933 →
ec236d416conorigin/main, verified by contentdomain:specexecution seat 1 (seat post #6017,session_013RDBh5DqXd2xnLwvHLgLFr), 2026-09-24T01:45Z.Path.
check-governed-merges.mjsanswered NOT governed, 151 lines.d4ba97991e: 16 success, 11 skipped, 0 pending.ccr/ready_for_reviewthenPUT ccr/auto_merge(200, not the 422 the card body expected).added_to_merge_queueat 2026-09-24T01:19Z, merged as squashec236d416cb26daf0d49c7d5cf168d7b1648459d, parentfdeeea0cc9.The criterion, re-measured on the write step's own
env:block in.github/workflows/shard-timings-refresh.yml(the step 「Push the refresh branch and open the pull request」), parent against squash:env:fdeeea0cc9ec236d416cRUNSRUN_COUNTDRY_RUNRUN_ID(firing control)The same step's
run:expands$RUN_COUNTand$RUNSat both trees. The parent is the defect, and the squash carries the fix. The PR's ownpull_requestrun35940088651executed this step's script, dry, before the merge (step 15,success).Card state.
pm:dispatchedis removed and the assignee cleared in this act.Still owed: acceptance item 4. One
workflow_dispatchonmainthat opens a real refresh PR:gh workflow run shard-timings-refresh.yml --repo objectstack-ai/objectstack --ref main. This seat does not trigger workflows. It is put to the maintainer, and the result is the reading this card asked for.ci: wire the issue-citation verdict (blocking) and its census (report…
ci(shard-timings): give the write-back step every variable it expands…