Skip to content

ci: lint.yml's scheduled run skips the battery when a green push run already covers the sha (3 of 3) - #27085

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-26071-lint-schedule-skip
Oct 28, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-26071-lint-schedule-skip

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #26071

Clause-②: no
Metadata-protocol: none
Ruling-ref: 6416984681

维护者速读

  • 改了什么:lint.yml 每 4 小时的定时运行新增第一个 job schedule-gate:同一 commit 上已有跑完且全绿的 push 运行时,其余 14 个 job(12 条 lane 和 2 个汇总)全部跳过;PR、合并队列、push、手动触发的行为不变。
  • 为什么改:这是裁决 A 的第 (i) 步((ii)(iii) 已在 ci: fold the four daily patrols into one fleet-patrols.yml workflow (1 of 3) #26512、ci: fold the four PR-body guards into one pr-body-guards.yml job (2 of 3) #26570 落地)。定时运行测的是 main 的 head,而这个 commit 的 push 运行已经跑过同一套门禁。实测最近 13 次定时运行:push 运行最后都是绿的,但定时运行启动时已经跑完的只有 4 次。所以按裁决的写法,大约 4/13 会跳过,其余照常全量跑。
  • 风险与回滚:门禁 job 一律偏向「照跑」:API 报错、403、读不懂返回、push 还在跑,都按「不跳过」处理;门禁 job 本身崩溃,各 lane 也照常运行。新增的风险面是 check-required-contexts.mjs 的一条接线断言,现在多接受一种精确拼写。回滚:revert 本 PR。
  • 先看:.github/workflows/lint.yml 里的 schedule-gate job 和两个汇总 job(lint、typecheck)的 if:;scripts/check-required-contexts.mjs 的 pinLanesCarryNoArrangeableIf。
  • 两分钟验证:pnpm check:required-contexts && pnpm check:workflow-status-functions && pnpm check:pm-expected-skips,应看到三组 ✓。合并后在 Actions 里看下一次 Lint & Type Check 定时运行:要么只有 Schedule gate 一个 job 在跑、其余显示 skipped,要么(push 还没跑完时)全量运行。
  • 需要你做的:这是 Tier H(改 .github/workflows/**)。请审阅并 Approve,之后由席位落地。

What changes

Step (i) of ruling 6416984681 (letter A), the last of the card's three serial PRs. Steps (ii) and (iii) landed as #26512 (3fcafcd3) and #26570 (9bfd6a62).

  • New first job schedule-gate (Schedule gate · skip when a green push run covers the commit). Job-level if: github.event_name == 'schedule', permissions: actions: read only, no checkout, timeout-minutes: 10 (the file's job-wall floor). It runs one step, gh api repos/REPO/actions/workflows/lint.yml/runs?event=push&branch=main&head_sha=SHA&status=completed&per_page=10, and keeps only runs with event == push, status == completed, conclusion == success and head_sha == github.sha. It sets skip=true only on a positive match. Every other outcome sets skip=false: no run, a red run, a run on another sha, a failed read (403 included), or a body that does not parse. The step has no set -e, so a read failure cannot fail the job. If the job crashes anyway, its output reads empty and the lanes still run.
  • The 12 lanes each gain the same two lines, needs: [schedule-gate] and if: ${{ !cancelled() && needs.schedule-gate.outputs.skip != 'true' }}. On any event other than schedule the gate job is skipped and its output is the empty string. !cancelled() runs a job past a skipped or failed need, so each lane runs as before. This is ci.yml's existing !cancelled() && needs.filter.outputs.* idiom. No lane body changed: a yaml-parsed comparison against HEAD finds every job identical apart from needs/if.
  • The two required aggregates (Lint & Repo Gates, TypeScript Type Check) keep always(). Each gains one schedule-only conjunct: ${{ always() && !(github.event_name == 'schedule' && contains(needs.*.result, 'skipped')) }}. On any other event that conjunct is true, so always() decides exactly as before, and a skipped required context can never appear on a PR or a queue build. Their needs: and rosters are unchanged, so check:aggregator-roster and their run-time roster verdicts are unaffected.
  • on: comment block now describes the skip, how it fails closed, and why ci.yml's schedule is not gated (there push tests the affected set only). The schedule: cron, the other triggers and the concurrency: key are byte-identical.
  • scripts/check-required-contexts.mjs: the wiring assertion "the lane hosting the required-context pin must carry NO job-level if:" went red, because the pin lane (lint-ci-engine) now carries the skip. It now admits exactly one shape and still refuses every other if:. That shape is the lane's byte-exact skip if:, plus needs: on schedule-gate, plus a schedule-gate job whose if: is byte-exactly github.event_name == 'schedule'. There are six new self-test cases in both directions, and the battery floor rises from 13 to 23, its measured count (17 before, plus the 6). REQUIRED_CONTEXTS is untouched.

Job-by-job, before → after. Before this PR every job ran on every event.

job push pull_request merge_group workflow_dispatch schedule, green push run already completed on the sha schedule, none (or still running, red, unreadable)
schedule-gate (new) skipped skipped skipped skipped runs, skip=true runs, skip=false
12 lanes (Lint · *, Type Check · *) runs → runs runs → runs runs → runs runs → runs runs → skipped runs → runs
Lint & Repo Gates (required) runs → runs runs → runs runs → runs runs → runs runs → skipped runs → runs
TypeScript Type Check (required) runs → runs runs → runs runs → runs runs → runs runs → skipped runs → runs

Tests

All local readings are on head 8fd56eab (the final commit, after merging origin/main 5cba8fc2).

Derived gate union: 61 of 61 exit 0

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 2 paths vs merge base 5cba8fc2f, 186 changed lines) derived 61 commands. All 61 ran with exit codes captured before any pipe, all 0. Among them: check:required-contexts, check:workflow-status-functions (58 job-level if:, 29 read needs.*.outputs.*, all naming a status function), check:pm-expected-skips (206 self-test cases), check:aggregator-roster, check:ci-filter-parity, check:shard-attestation, check:select-gate-families, node scripts/ci/scheduled-full-run.mjs --self-test, check:nul-bytes, and check:pm-dispatch-gates (fast tier, 1935 cases). node scripts/pm/dispatch-gates.mjs --ran FILE --repo objectstack-ai/objectstack: 61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED (a DERIVED zero — all 61 recorded an exit code and none of them is 3).

The first derivation, on the lint.yml-only commit, named 42 families. 41 were green and check:required-contexts was red with wiring: the lane hosting the required-context pin must carry NO job-level if:, which led to the gate change above.

Reverse verification on the live file (committed first, trap-restored)

Each mutation was made through node scripts/ablation-replace.mjs (anchor hit 1 → 0, blob changed), run against node scripts/check-required-contexts.mjs --self-test, and restored with git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap.

  • Leg A: the schedule-gate job's if: widened to github.event_name != 'merge_group' (the gate could now say skip on a PR): exit 1, wiring: the lane hosting the required-context pin must carry NO job-level if: other than the scheduled-run skip.
  • Leg B: the pin lane's if: respelled needs.schedule-gate.outputs.skip == 'false': exit 1, same finding.
  • Leg C: the battery floor raised past its count: registered 23 case(s), below its pinned floor of 999. That shows the six new cases register, and the floor is now 23.
  • Restore proof: git hash-object of both files equals the HEAD blob, git diff HEAD and git status --porcelain are empty, and the self-test exits 0 again.
The gate step's behaviour, offline and live
  • Offline: the step's run: body was extracted from the parsed YAML and driven with a fake gh that applies the real --jq filter through jq. Results: green push run on the sha → skip=true (it picks the success run past a cancelled one on the same sha); red → false; success on another sha → false; empty listing → false; HTTP 403 → false (the runs API did not answer (… HTTP 403)); non-JSON body → false. The step exits 0 in all six cases.
  • Live: the step's exact query, read against the 3 newest scheduled runs' shas, returns total_count=1 with one green push run each; an all-zero sha returns total_count 0.
  • YAML: yaml parse with no errors; on keys, on.schedule and concurrency are identical to HEAD. actionlint: NOT MEASURED, because it is not on PATH in this container.
  • ui/scripts/__tests__/merge-queue-reporting.test.ts, the one test file naming the edited gate script (in prose), ran under the verify lock: Test Files 1 passed (1), Tests 4 passed (4), VERDICT command-exit 0.
  • No package source is touched, so no package build, test or typecheck is owed. test:repo, pnpm lint and whole-tree scans are CI's. No dogfood run: there is no ! in the title and no (narrowing) arm.

Acceptance notes

  • Premise P1, corrected by measurement. The card measured that each of the 12 newest scheduled runs repeated a commit whose push run ended success, and that still holds for all 13 newest. But the ruled mechanism skips only on a completed green push run, checked when the scheduled run starts, and that holds for 4 of 13: f708cf22, 6390fe52, f143cd88, 05f7758f. In the other 9, the push run of the same commit finished 8 to 18 minutes after the scheduled run started (each push run's last job completed_at vs the schedule's run_started_at, 2026-10-26T06:25Z → 2026-10-28T04:14Z). A scheduled run starts late past its cron slot and main merges many times a day, so the head it tests is often a commit whose push run is still in flight. Those 9 run the full battery exactly as today; nothing is lost, only less is saved. Waiting for an in-progress push run would have skipped all 13, but that is a runner-spend shape the ruling did not name, so it is raised to the seat rather than built here.
  • What a skipped run reports. The gate job succeeds and every other job is skipped, so the run's conclusion is expected to be success. The first post-merge scheduled run is the measurement. scheduled-full-run.mjs --release-gate reads the newest schedule/push run on the nearest commit. On a skipped commit that is either this run or the green push run of the same sha, so both answer success, as the battery did. scheduled-full-run-card.yml files only on failure/timed_out and is unaffected.
  • Live-state reads. Gates in lint-ci-engine that read GitHub state (issues: read, pull-requests: read) are not re-read at schedule time on a skipped run. The next merge's push run re-reads them.
  • Scope. The edit to scripts/check-required-contexts.mjs sits outside the claim's literal file surface ("a gate that reads that leg by name") but inside the dispatch's instruction to re-point what asserts on the changed shape. It touches only the wiring predicate and its self-test; REQUIRED_CONTEXTS is unchanged. No other gate read the shape: the 61-family union is green.
  • The file's ⭐ WHERE A GATE LIVES block still says every job runs on every main run. The scheduled skip is the one exception, stated in the schedule-gate header. The block itself was left unedited, per the scope fence.
  • Region-disjoint from open draft ci: prune the build-core, typecheck and pack-smoke Turbo seeds before saving them #27067 (typecheck-workspace hunks near the turbo summarize lines): a local git merge-tree --write-tree of this head with b782ee2d exits 0 and writes a tree. lint.yml is not routed to the custom merge driver.

Verification

  • Tier H (.github/workflows/**): the PR stays draft until an authorized approval. The seat lands it; nothing here readies, arms or enqueues it.
  • CI on this PR runs the whole lint.yml (no paths filter). On pull_request the new Schedule gate check-run reads skipped, which check-expected-skips admits as non-pr-event schedule; derived. Every lane and both aggregates run as before.

Generated by Claude Code

…already covers the sha

A new first job, schedule-gate, runs on `schedule` only and asks the runs
API for a completed `push` run of lint.yml on github.sha that concluded
success; every lane needs it under `!cancelled()` and skips on skip=true,
and the two aggregates skip with their lanes on a scheduled run only.
Fails closed toward running the battery.

Claude-Session: https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg
Co-authored-by: Claude <noreply@anthropic.com>
… the pin lane

The lane hosting the required-context pin may now carry exactly one
job-level if: -- lint.yml's scheduled-run skip, admitted only as the whole
shape (byte-exact lane if, needs on schedule-gate, a gate job gated to
schedule alone). Any other if: is still refused; six self-test cases pin
both directions.

Claude-Session: https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg
Co-authored-by: Claude <noreply@anthropic.com>
…nt-schedule-skip

Claude-Session: https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 — 席位终稿 · PR #27085 · head 8fd56eab · domain:skills#2 · 2026-10-28T04:58Z

  • 改了什么:lint.yml 每 4 小时的定时运行新增第一个 job schedule-gate:同一 commit 上已有跑完且全绿的 push 运行时,其余 14 个 job(12 条 lane + 2 个汇总)全部 skipped;push / PR / 合并队列 / 手动触发的行为逐字节不变(on: 块非注释行与 main 相同,席位核过)。另一个文件 scripts/check-required-contexts.mjs:接线断言精确放行这一种跳过形状(6 条新自测,REQUIRED_CONTEXTS 常量未动)。
  • 为什么改:裁决 A 第 (i) 步(6416984681),前两步 ci: fold the four daily patrols into one fleet-patrols.yml workflow (1 of 3) #26512、ci: fold the four PR-body guards into one pr-body-guards.yml job (2 of 3) #26570 已落地。前提修正:席位认领写「12/12 次定时运行都重复了已跑完的绿 push run」——dev 实测 13 次里 push run 最终都绿,但定时运行启动时已跑完的只有 4 次;席位的读数比的是结论、没比完成时刻,是席位之误,在此公开更正。按裁决原文(「completed green push run」),本 PR 约跳过 4/13,其余照常全量跑——省得比预想少,不多花一分钟。
  • 风险与回滚:门禁 job 一律偏向「照跑」:API 报错、403、读不懂、push 还在跑、job 自己崩溃,全部不跳过。新增风险面只有 required-contexts 的一条接线断言多接受一种精确拼写(dev 做了三腿反向验证,放宽拼法即红)。回滚 = revert 本 PR。
  • 先看:lint.yml 的 schedule-gate job(L219 起)与两个汇总 job 的 if:(always() && !(schedule && skipped));scripts/check-required-contexts.mjs 的 pinLanesCarryNoArrangeableIf。
  • 两分钟验证:pnpm check:required-contexts && pnpm check:workflow-status-functions && pnpm check:pm-expected-skips 三组 ✓;合并后看下一次 Lint & Type Check 定时运行:要么只有 Schedule gate 在跑、其余 skipped,要么(push 未跑完)全量。
  • 需要你做的:Tier H(.github/workflows/**)——请 Approve(中继已请审 hotlong、os-zhuang);批准后席位落地。另一件可选:dev 问「push run 还在跑时门禁要不要等它(最多 ~25 分钟)再决定」,等了能跳 13/13,但多占一个小 runner、改变裁决记下的花费形态;席位推荐不等、维持裁决原文,你若想要「等」,是一张新决策,一句话我立卡。
  • 席位意见:ACCEPT(Tier H 终局四件套之①)。

席位核对 — ACCEPT · domain:skills#2 · session_01QSeKYmhU5KEj1nukYhp1Tg · 2026-10-28T04:58Z

  1. 形态:draft → main,首行 Fixes #26071(本卡三棒的最后一棒,合并应关),Clause-②: no、Metadata-protocol: none、Ruling-ref: 6416984681 行首三行在;两个文件 +177/−9;assignee os-justin;skip-changeset 正确(workflow 与仓根脚本不发布);请审 hotlong、os-zhuang 在(中继 run)。
  2. 前提实测:席位在取回的 head 上核——on: 块非注释行与 merge-base 5cba8fc2 相同;12 条 lane 各带同一对 needs: [schedule-gate] + if: !cancelled() && skip != 'true';两汇总 if 加 schedule 专属合取;REQUIRED_CONTEXTS 赋值零改动;与 ci: prune the build-core, typecheck and pack-smoke Turbo seeds before saving them #27067 的 lint.yml 区域不相交(dev merge-tree exit 0,席位读 ci: prune the build-core, typecheck and pack-smoke Turbo seeds before saving them #27067 hunks 在 typecheck job 区)。P1 前提修正如上,记入本记录。
  3. 门禁 check-runs(head 8fd56eab,本笔读数 2026-10-28T04:58Z):50 success · 14 skipped · 0 in progress;dev 本地并集 61/61 exit 0 钉同 head,其中 check:required-contexts 在首版 lint.yml-only commit 上 exit 1 → 修断言后 0。等人批期间席位继续读;批准后落地前再核两 required job 结论。
  4. 接受的偏差:① scripts/check-required-contexts.mjs 在认领字面文件面之外,但在派发令假设 5(「只改断言你所改形状的门禁,自测证读」)之内,且不改它就无法让 pin lane 跳过——接受并记录;② ⭐ WHERE A GATE LIVES 注释块未改(范围围栏),schedule-gate 头注写了例外;③ actionlint NOT MEASURED(不在 PATH)。
  5. 范围外去向:none filed;Acceptance notes 载「skipped 的定时运行预期结论 success,首次合并后定时运行即测量」(carrier none)。Skills-docs: 不欠(kind none)。dev 的 open_question(等 in-flight push run)→ 席位呈维护者(速读「需要你做的」),⛔ 不代裁。
  6. 落地条款:Tier H ⇒ 本 PR 悬着等 GOVERNED_APPROVERS 批准;席位 ⛔ 不 ready / 不 arm / 不入队;批准在案后席位跑 check-governed-merges.mjs --pr 27085、核 mergeable_state 与全绿,再经中继 pr_ready + automerge_enable。卡保留 pm:dispatched(已裁决定卡 ⛔ 不重挂 needs-user-decision;等人批清单由轮报④与请审承载)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

3 participants