Repository navigation
Commit 7fa67da
fix(pm): fleet-write lands a pull mutation only when its answer shows the requested state (#20974)
Fixes #20826
Clause-②: no
The relay judged a pull-request GraphQL row landed on HTTP 200 with
`data` and no `errors`, whatever the returned pull showed.
`enablePullRequestAutoMerge` answering `autoMergeRequest: null` on a
pull it had neither armed nor queued printed `✓ … auto-merge off`, and
the run concluded `success`. This PR has the op table declare what
landed means for each pull mutation. The executor now reports a FAILED
action (exit 5, later actions not attempted), naming what the answer
showed, whenever the state is missing. That is how a REST 4xx is already
reported.
## What was measured before choosing the predicate
All readings are from the relay's own run logs in this repository's
Actions (`fleet-write.yml`). They were read through the job-log API.
1. **The card's "GraphQL `errors` are not surfaced" is false as
stated.** At the relay head those runs executed (`79114850f`),
`requestLanded()` already fails any answer that carries a non-empty
`errors` array. Runs 36694856488 and 36695124034 each printed `✓ action
1 automerge_enable: POST /graphql enablePullRequestAutoMerge -> HTTP 200
· #2491 · auto-merge off`. So both answers were 200 with
`data.enablePullRequestAutoMerge.pullRequest.autoMergeRequest: null` and
no `errors` entry. The raw bodies are not in the logs; only the
formatted row is printed. The defect is the honesty half alone.
2. **The shape is shared.** `pr_ready`, `pr_draft`, `automerge_enable`
and `automerge_disable` are the four ops whose descriptor carries a
`pull`. All four went through the same 200-plus-data-means-landed test.
`transfer` already judged its answer against the target, and the REST
ops are unchanged.
3. **On a merge-queue base, arming a green pull answers with no
`autoMergeRequest`, and that landing worked.** Two relay landings on
this repository show it:
- Run 36786665458: `pr_ready` then `automerge_enable` on objectstack PR
20952 printed `auto-merge off`. The PR timeline has
`added_to_merge_queue` by the fleet bot at 22:38:08Z, inside that
mutation's request window. There is no `auto_merge_enabled` event, and
the pull merged.
- Run 36780440589 (PR 20942) shows the same shape.
So the naive predicate "`autoMergeRequest` non-null or fail" would have
failed working landings. Relay landings that armed before the checks
settled show the other shape: run 36784403107 (PR 20948) printed
`auto-merge MERGE` for a `SQUASH` request, followed by
`auto_merge_enabled` and a later `added_to_merge_queue`. GitHub's own
input documentation says "when merging with a merge queue any input
value for merge method is ignored", so the merge method is never judged.
## The change
- `ops.mjs` has a new `PR_LANDED_STATE` entry per pull mutation, carried
on its descriptor as `landed`:
- `pr_ready` lands on `isDraft: false`, and `pr_draft` on `isDraft:
true`.
- `automerge_enable` lands on an `autoMergeRequest`, or on
`isInMergeQueue: true`.
- `automerge_disable` lands on `autoMergeRequest: null`. It deliberately
does not also require the pull to be out of the queue: disabling does
not dequeue, and failing there would stop the stroke's later `pr_draft`,
the second half of the disarm AGENTS.md prescribes. Its row prints `in
the merge queue` when the answer says so.
- Both auto-merge mutations now also select `isInMergeQueue` and
`isMergeQueueEnabled` (`PullRequest`, both `Boolean!`).
- The first is the queue half of the arm's landing test.
- The second says in the row whether the base has a queue, which the
cloud diagnosis below needs.
- `gh pr merge` reads both fields to take its merge-queue path, and it
runs under a workflow token that holds only `contents` and
`pull-requests`. That is the evidence that the relay's token (the same
two, plus `issues` and `metadata`) can read them.
- `execute.mjs`: `requestLanded()` judges every pull mutation against
its `landed` entry. A pull descriptor with no entry, or an answer with
no `pullRequest`, is never a landing. The new `pullStateText()` prints
every selected field in the row, on success and on failure.
- The card's interim suggestion was a header note in `ops.mjs` saying
cloud arms need the maintainer's click. That note is not needed: an arm
that does not land now fails loudly, and its row names what came back.
The reported shape now reads:
```text
FAILED: GraphQL: HTTP 200 with no errors, but the answer shows #2491 · auto-merge off · not in the merge queue · its base has no merge queue — the op asked for auto-merge armed (an autoMergeRequest) or the pull in the merge queue (isInMergeQueue true)
```
## Verification (at `f620d1ce4`)
- `node scripts/pm/fleet-write/execute.mjs --self-test`: exit 0, `71
cases pass across 11 batteries`. Before the change it was 59 across 10.
The new battery pins 12 cases under a raised floor of 11 batteries:
- the reported shape fails, names what came back, and stops the stroke;
- an armed arm on a base with no queue lands;
- the enqueued-at-once merge-queue shape lands;
- a `MERGE`-for-`SQUASH` arm lands;
- the mirror rows for `automerge_disable`, `pr_ready` and `pr_draft`;
- an answer with no `pullRequest`, and a descriptor with no entry, are
never landings;
- the table pin: every pull mutation carries its entry, and its query
selects every field that entry reads.
- Reverse verification, all run from the committed state through
`scripts/ablation-replace.mjs`. Each mutation was proven on disk, and
each restore was proven by blob equal to HEAD with `git diff HEAD`
empty:
- dropping the landed check in `requestLanded()` turned 5 of 71 red (the
reported shape, the stop, the disable, and both flips);
- dropping the queue half of the arm predicate turned 1 of 71 red (the
merge-queue case);
- dropping `isInMergeQueue` from the arm's selection turned 1 of 71 red
(the table pin).
- Named and derived gates, all exit 0: `pnpm
check:pm-fleet-write-execute`, `check:pm-fleet-write-validate`,
`check:pm-fleet-write-dispatch`, `node
scripts/check-self-test-wired.mjs`, `node
scripts/check-scripts-symbol-anchors.mjs`, `pnpm check:nul-bytes`, plus
every family `node scripts/pm/dispatch-gates.mjs --commands` derives for
this diff (listed in the report on the card).
## Acceptance notes
- NOT MEASURED, family relay-live-answer. What the enqueued-at-once
answer carries in `isInMergeQueue`, and whether the fleet token reads
that field, cannot be measured before merge. The relay is the only
GraphQL route with that token, and GraphQL is closed to agent sessions.
The evidence for `true`: the `added_to_merge_queue` event lands inside
the mutation's own request window, with the mutation's own actor, on
both measured landings, and the payload is resolved after the mutation.
The first live objectstack landing after merge measures it: its row
reads `in the merge queue`. If it ever reads `not in the merge queue` on
a pull the timeline shows queued, the relay reports FAILED on a queued
pull. That is loud, not silent, and the timeline stays the authoritative
reading, so do not re-arm on the row alone.
- The cause on cloud stays open, and nothing about it is encoded here.
It is carried in the card report's open question, together with the
reads that settle it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 05be352 commit 7fa67da
2 files changed
Lines changed: 134 additions & 17 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | | - | |
45 | | - | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
46 | 49 | | |
47 | 50 | | |
48 | 51 | | |
| |||
85 | 88 | | |
86 | 89 | | |
87 | 90 | | |
88 | | - | |
| 91 | + | |
89 | 92 | | |
90 | 93 | | |
91 | 94 | | |
| |||
131 | 134 | | |
132 | 135 | | |
133 | 136 | | |
134 | | - | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
135 | 143 | | |
136 | 144 | | |
137 | 145 | | |
| |||
144 | 152 | | |
145 | 153 | | |
146 | 154 | | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
147 | 162 | | |
148 | 163 | | |
149 | 164 | | |
150 | 165 | | |
151 | 166 | | |
152 | 167 | | |
153 | 168 | | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
154 | 182 | | |
155 | 183 | | |
156 | 184 | | |
157 | 185 | | |
158 | 186 | | |
159 | 187 | | |
160 | 188 | | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
| 189 | + | |
165 | 190 | | |
166 | 191 | | |
167 | 192 | | |
| |||
362 | 387 | | |
363 | 388 | | |
364 | 389 | | |
| 390 | + | |
365 | 391 | | |
366 | 392 | | |
367 | 393 | | |
368 | 394 | | |
369 | 395 | | |
370 | 396 | | |
371 | | - | |
| 397 | + | |
372 | 398 | | |
373 | 399 | | |
374 | 400 | | |
| |||
530 | 556 | | |
531 | 557 | | |
532 | 558 | | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
533 | 602 | | |
534 | 603 | | |
535 | 604 | | |
| |||
664 | 733 | | |
665 | 734 | | |
666 | 735 | | |
667 | | - | |
| 736 | + | |
668 | 737 | | |
669 | 738 | | |
670 | 739 | | |
| |||
0 commit comments