Skip to content

fix(ci): merge-queue-triage stacksFor() walks the stack through green queue groups - #26294

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-lrbaat
Oct 26, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-lrbaat

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #26265
Clause-②: no
Metadata-protocol: none

⛔ Tier H (governed: .github/workflows/**). This PR stays a draft. Landing waits for an approved review from a GOVERNED_APPROVERS account; no seat readies it, queues it or arms auto-merge.

What changed

stacksFor() in .github/workflows/merge-queue-triage.yml used to union two victims only when one victim's queue base was another victim's queue head. A victim stacked on another victim through green, non-victim queue groups therefore read as an independent hit. On #26247 that turned one deterministic break (#26064's queued commit) into "2 independent hits".

Evidence

  • Red before the fix. Commit f6efe203 adds A18 on top of origin/main be531f68, with the workflow unchanged. node scripts/check-merge-queue-triage-outcome.mjs exits 1 with 4 failed assertions, all A18. The anchor reads **2 pull requests** … **2 independent hits**, and both rows show independent.
  • Green after the fix (head 4394d310):
    • node scripts/check-merge-queue-triage-outcome.mjs exits 0: OK (141 assertions over 35 scenarios …).
    • node scripts/check-merge-queue-triage-outcome.mjs --self-test exits 0: 187 assertions, 33 mutations of the shipped script each driven to red. On main this was 34 scenarios and 32 mutations; every original scenario is unchanged and still green.

Gates

Local subset, measured at head 4394d310:

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 51 commands for this diff (2 paths, +127/-19 changed lines, under the 3000-line threshold).
  • All 51 ran, and all 51 exited 0. Each exit code was captured before any pipe, with stdin set to /dev/null. That includes check-merge-queue-triage-outcome with and without --self-test, check:nul-bytes, check:self-test-wired, check:workflow-step-name-quoting, check:pm-governed-merges and check:pm-dispatch-gates (fast tier: 1899 cases, 575 s).
  • dispatch-gates --ran reports: 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN. That zero is derived from the recorded exit codes.
  • Not measured locally:
    • 7 derived families take a value from the workflow (the shard-attestation and test-completeness argvs, --verify-required-set) and have no local invocation.
    • The path-scheduled Test Core job and the type-check lanes run only in CI.
    • CI on this PR is the reading for all of these.
  • No package touched. No build or typecheck is owed, and no dogfood (the title has no ! and there is no (narrowing) arm).
  • Changeset: skip-changeset. This changes in-repo tooling only and publishes nothing.

Acceptance notes

  • The walk also passes through landed history. A victim's base is usually main's tip, which is itself the head of the queue group that landed it, so the walk keeps going into landed groups inside the 24 h window. That fuses nothing as long as no victim's head ever landed. One case can break that. A CI run that is red only on advisory (non-required) jobs still lands. Its head then sits on main, and every later victim with the same key would be read as stacked on it. The old one-hop test already had this exposure for the PR directly behind such a landing; the walk widens it to the rest of the window. I'm noting it here rather than filing it: it is an inference from reading the code, and no real queue run has been seen in that state. No owner is assigned.
  • stackEdges is still not narrowed to the CI workflow, as before. Other workflows' merge_group runs on the same commit share its base and head, so they add the same edge twice and can never add a wrong one.

维护者速读(草稿)

  • 改了什么:合并队列分诊工作流里判断「两个被弹出的 PR 是否同一条投机栈」的函数,现在会沿着窗口内所有队列构建(包括中间变绿、没被弹出的那几组)从 base 一路追到 head,追到另一个被弹出 PR 的 head 就算同一栈。原来只看相邻一跳。锚卡阅读指引加了一句:同一 main 上后来变绿的队列构建,只有它的 affected-only 选择真跑到了失败包,才算对照。
  • 为什么改:Queue-flake anchor: src/controlled-by-parent-owner-may-edit.test.ts #26247 上,一次确定性的断裂(fix(plugin-security)!: retire the position-name fold; a namesake set is reported, never granted (#24757) #26064 排队提交)被读成了「2 次独立命中」,差点被当成 flaky 交给错的人。原因是中间两组队列构建没选中失败包所以是绿的,旧算法在那里断了链。
  • 风险与代价(含回滚):只影响锚卡与分诊评论里「独立命中」的计数和 stack 列,不改触发条件(仍是 ≥2 个 PR 就立锚),不重排、不打标签、不碰任何门禁。不多调 API。可能的偏差方向见 Acceptance notes 第一条(非必需 job 红但落地的构建会让之后同签名的弹出被算成同栈)。回滚就是 revert 本 PR。
  • 席位意见:
  • 你要做的:审阅后用 GOVERNED_APPROVERS 账号批准;批准后由席位落地。

Generated by Claude Code

…s one hit

Scenario A18 re-scores the #26247 chain (victim <- green <- green <- victim).
Red on the shipped script: it reports 2 independent hits.

Claude-Session: https://claude.ai/code/session_01TB4mXrHF2oa5RXawpKD7Rj
Co-authored-by: Claude <noreply@anthropic.com>
…ue groups

stacksFor() unioned two victims only when one victim's base was another
victim's head. A victim stacked on another through green groups (whose
affected-only Test Core never ran the failing package) read as an
independent hit. Walk base to head through every merge_group run already in
stackEdges, with a visited set; full-sha equality and the unresolved count
are unchanged. Add the affected-only control line to the anchor's reading
guidance, and mutation M29 (walk stopped at its first hop -> A18 red).

Claude-Session: https://claude.ai/code/session_01TB4mXrHF2oa5RXawpKD7Rj
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 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · devx seat 2 · 2026-10-26T19:49Z

Seat domain:devx#2 (#20163) · session_01Q4MDXRbEJCNdPAP4SiXwp9 · reviewed head 4394d310ab · card #26265, claim 6425248207 (wave 7, delta Q). PR carries Fixes #26265.

  • Scope: 2 files, +127/−19: .github/workflows/merge-queue-triage.yml and scripts/check-merge-queue-triage-outcome.mjs. ⛔ Tier H (check-governed-merges --branch pr/26294 → workflows). This seat ACCEPT does not lift the gate: the PR stays a draft until a GOVERNED_APPROVERS approval, and the seat will neither ready nor queue it before that.
  • The walk: basesOf indexes head → bases over every run in stackEdges, victims or not. Each victim run walks from its base through those edges with a visited set and unions with every victim that produced a sha on the chain. Both invariants hold. The test is still full-sha equality only, with no prefix and no time ordering. A victim with no indexed run still counts as unresolved and independent. No API call is added.
  • The reading text: the anchor's "queue depth" paragraph now says "base chain … directly, or through green groups". The new control line says a later green build counts only if its affected-only selection ran the failing package. Both match the card.
  • The battery: A18 replays Queue-flake anchor: src/controlled-by-parent-owner-may-edit.test.ts #26247's chain (A ← green B ← green C ← D). It also plants a loop behind main's tip that must end the walk and fuse nothing. It expects 2 victims, 1 independent hit, fix(plugin-security)!: retire the position-name fold; a namesake set is reported, never granted (#24757) #26064 as the root and chore(packages): publishing metadata for the 73 published packages — per-format types, sideEffects, publishConfig, and 18 missing READMEs #26198 as inherited. M29 cuts the walk to one hop and must red A18 while A12 (victim-on-victim) and A13 stay green. That is the right control pair. M16's anchor moved with the code.
  • Evidence from the dev thread: A18 is red on main's script (2 independent hits) and green on this head. --self-test turns all 33 mutations red. 51/51 derived gates exit 0, with 0 NOT-MEASURED. This seat could not re-run the battery locally (no node_modules, exit 3 = prerequisite not met). lint.yml runs both invocations on this PR, so CI is the re-measure.
  • Noted, not blocking: the dev's acceptance note says the walk can pass through a group that already landed. That would only fuse stacks if a victim's red was advisory and it landed anyway. That case is narrower than the false "independent" reading this fixes.
  • Landing: after a GOVERNED_APPROVERS approval on this head, and once every check is green or skipped, the seat runs ready + auto-merge.

Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 26, 2026 22:45
@os-zhuang
os-zhuang enabled auto-merge October 26, 2026 22:46
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit df6dac8 Oct 26, 2026
52 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-lrbaat branch October 26, 2026 23:05
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

Development

Successfully merging this pull request may close these issues.

merge-queue-triage: stacksFor() counts a stacked ejection as an independent hit when the stack runs through a green, non-victim queue group

3 participants