feat(ship-queue): land --pin merges a READY PR at its own green head under merge: direct (GSTA-258 L2 PR 1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-03 21:15:21 +00:00
1 parent f8085aa0e4
commit 60e5369daa
11 files changed
+578 -5

No files matched your search

+27
View File
@@ -271,6 +271,33 @@ the same product diff; any other rebase is a full /ship
([ship-policy.md](ship-policy.md#receipt-carry)). A controller never asks a lane to
rebase a reviewed branch.
## Direct land (`merge: direct`, gstack's default)
When the base policy sets `"merge": "direct"`, there are no trains, slot calls or stamp
commits. Each PR merges at its own green head, and versions are set at release
([ship-policy.md](ship-policy.md#land-direct)):
1. Lane: /ship, then `gstack-ship-queue ready --pr <n> --no-screen` at the pinned head,
then READY to the Captain with that pin. The PR carries a `changelog.d` fragment and
leaves VERSION, the `package.json` version and CHANGELOG.md alone.
2. Captain: `gstack-ship-queue land --pr <n> --pin <sha>`. It merges with
`--match-head-commit`, checks main's tree and writes a land note.
3. Release: the release gate stamps the version and folds the fragments.
What to do with each refusal (nothing was merged unless the code says so):
| Code | Who acts | Action |
|---|---|---|
| `QUEUE_LAND_STAMPED` | lane | run the printed `git checkout $(git merge-base origin/main HEAD) -- …`, keep the fragment, commit, /ship and `ready` again |
| `QUEUE_LAND_PIN_MOVED` | lane | /ship and `ready` at the new head, new READY |
| `QUEUE_RECEIPT_MISSING` / `QUEUE_RECEIPT_STALE` | lane | /ship at the pin |
| `QUEUE_NOT_READY` | lane, or Captain | no READY note: `ready --pr <n> --no-screen` at the pin; `merge: train`: use the train; closed or retargeted PR: as named |
| `QUEUE_LAND_CHECKS` | Captain, then lane | pending: `gstack-pr-wait <n> --until green` and rerun; red: back to the lane (rule 16 rerun for a flaky case); missing: dispatch the workflow on the branch |
| `QUEUE_LAND_NOT_CLEAN` | lane, or Captain | `CONFLICTING`: the lane merges main (rule 19), /ship, `ready`; `UNKNOWN`: rerun in a minute |
| `QUEUE_TRAIN_FROZEN` / `QUEUE_LAND_LOCKED` | Captain | finish the repair or the holder first |
| `QUEUE_TRAIN_UNTRUSTED` | Captain | a fork head; rewrite on a crew branch |
| `QUEUE_LAND_UNVERIFIED` | Captain | merged, but main isn't the pin on its parent: revert the stray change, report it |
## Merge trains
gstack's queue tool drives trains when the repository's `.gstack/ship-policy.json` carries
+37
View File
@@ -63,6 +63,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
| `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. |
| `docs` | `{ caps: [{ path, max_bytes }] }` or `null` | `null` | Documentation files with a byte cap. `/ship` Step 14.5 runs `gstack-ship-receipt docs-caps --base <base>` (the caps come from `origin/<base>`'s policy, never the branch's copy; `--policy <file>` is for a repository bootstrapping its policy) before the verification gate and prints one `DOCS_CAP: <path> <bytes>/<max> ok\|over` line per cap; any `over` (an absent file counts) exits 1 and blocks the ship until the file is trimmed or the cap raised on the base branch (`DOCS_AUDIT_BLOCKED`). At most 64 caps, each path contained. See [the ship receipt](#ship-receipt). Needs `min_gstack_version`. |
| `train` | `{ gate_workflow, free_workflow, full_gate_check, record_artifact, max_size, fill_hours, screen_selection, trial_reservation_usd, land? }`, `{ gates, land, max_size, fill_hours }` or `null` | `null` | Turns on [merge trains](#run-a-merge-train), read from the base revision only. Every field is required and an unknown field is `POLICY_INVALID`: `max_size` 1 to 6, `fill_hours` above 0 and at most 24, `screen_selection` `gstack-paid` \| `none`, `trial_reservation_usd` above 0 and at most 100. A block with `gates` ([any repository](#train-gates)) takes `max_size` 1 to 5 and needs `min_gstack_version` at least `TRAIN_GATES_KEY_VERSION`. `null` keeps the per-PR queue exactly as it is. Needs `min_gstack_version`. |
| `merge` | `train` \| `direct` | `train` | `direct` lands each READY PR at its own green head with [`land --pr <n> --pin <sha>`](#land-direct): no train and no stamp commit, and `train build` refuses. A PR then leaves VERSION, the `package.json` version and the CHANGELOG at its merge base, and the release sets them. `train` keeps merge trains. Read from the base revision only. Needs `min_gstack_version`. |
| `base_red` | `{ setup, select, timeout_sec }` or `null` | `null` | How `/ship` Step 5 proves a failure is the base branch's own before pausing on it; see [base_red](#base-red). Read from the base revision only. Needs `min_gstack_version`. |
| `changelog_fragments` | `{ dir, required_checks? }` or `null` | `null` | PRs carry unversioned [CHANGELOG fragments](#changelog-fragments) in `dir` (one flat, repo-relative directory, no `..`) and the merge step ([`land`](#land)) assigns the version. `required_checks` (up to 50 distinct names) are the checks [`train land`](#stamped-trains-under-changelog_fragments-train-land) requires present and green on a stamp commit. `null` keeps `[Unreleased]` sections and per-PR stamps. Needs `min_gstack_version`. |
@@ -1890,6 +1891,42 @@ command:
`--dry-run` merges and stamps in a scratch worktree and prints the version and
tree, taking no lock and pushing nothing.
<a id="land-direct"></a>
### `land --pin`: merge at the PR's own green head (`merge: direct`)
Under `"merge": "direct"` in the base policy, the Captain merges one READY PR
at a time with `gstack-ship-queue land --pr <n> --pin <sha>`, where `<sha>`
is the head the READY names. There's no train, no stamp commit, no base merge
and no extra CI round. GitHub's required checks plus `--match-head-commit`
stay the gate. Versions aren't set per PR: a PR carries only its
`changelog.d` fragment, VERSION stays where it is, and the release stamps the
version and folds the fragments. Land refuses, and merges nothing, unless all
of these hold:
| Check | Refusal |
|---|---|
| the base policy sets `merge: direct` | `QUEUE_NOT_READY` |
| the PR is open, on the base, from this repository | `QUEUE_NOT_READY`, `QUEUE_TRAIN_UNTRUSTED` |
| the PR head is the pin | [`QUEUE_LAND_PIN_MOVED`](troubleshooting.md#queue-land-pin-moved) |
| the PR body's /ship receipt is bound to the pin | `QUEUE_RECEIPT_MISSING`, `QUEUE_RECEIPT_STALE` |
| a READY note from `ready` exists at the pin | `QUEUE_NOT_READY` |
| no freeze is active and no land lock is held | `QUEUE_TRAIN_FROZEN`, `QUEUE_LAND_LOCKED` |
| VERSION, the `package.json` version and the CHANGELOG are unchanged since the merge base | [`QUEUE_LAND_STAMPED`](troubleshooting.md#queue-land-stamped) |
| every check at the pin is green with none pending, and each `changelog_fragments.required_checks` entry passed there | [`QUEUE_LAND_CHECKS`](troubleshooting.md#queue-land-checks) |
| GitHub reports the PR `MERGEABLE` and `CLEAN` | [`QUEUE_LAND_NOT_CLEAN`](troubleshooting.md#queue-land-not-clean) |
Release outputs (the agents digest) are checked by the Version Gate, which must
be green at the pin: it accepts an output only when it equals what the release
tool generates at the merge base's VERSION. If the base moved and GitHub still
reports the PR clean, land merges it as it is. Then it runs `gh pr merge
--squash --match-head-commit <pin>` (`--merge` under `history: merge-only`),
reads the merge commit, and checks that its tree is the pin merged onto the
merge commit's parent ([`QUEUE_LAND_UNVERIFIED`](troubleshooting.md#queue-land-unverified)
if not). Last, it writes a `land` note on `refs/notes/gstack-trains`. A rerun
after GitHub merged goes straight to the tree check. `--dry-run` runs every
check and merges nothing. Under `merge: direct`, `train build` refuses and
names this command.
<a id="migrating-to-fragments"></a>
### Migrating to fragments
+53
View File
@@ -5073,6 +5073,59 @@ the two allowed restamps, so `land` stopped instead of restamping again.
it), then rerun `gstack-ship-queue land --pr <n>`; the lock was released and
the rerun restamps from the pin.
<a id="queue-land-stamped"></a>
### `QUEUE_LAND_STAMPED`
**Meaning.** Under `merge: direct`, the PR changes VERSION, the
`package.json` version or the CHANGELOG. Versions and the CHANGELOG are set at
release, so a PR carries only its `changelog.d` fragment. Nothing was merged.
**Fix.** The lane runs the command the refusal prints, for example
`git checkout $(git merge-base origin/main HEAD) -- VERSION CHANGELOG.md`
(and sets the `package.json` version back by hand when it's named), keeps or
adds the fragment with the entry text, commits, then runs /ship and
`gstack-ship-queue ready --pr <n> --no-screen` at the new head.
<a id="queue-land-pin-moved"></a>
### `QUEUE_LAND_PIN_MOVED`
**Meaning.** The PR head isn't the pin `land --pin` was given, so the READY
doesn't describe what would merge. Nothing was merged.
**Fix.** The lane runs /ship and `ready --pr <n> --no-screen` at the new head
and sends a new READY; land that pin.
<a id="queue-land-checks"></a>
### `QUEUE_LAND_CHECKS`
**Meaning.** A check at the pin is red or pending, or a required check never
ran there. Nothing was merged.
**Fix.** Pending: `gstack-pr-wait <n> --until green`, then rerun land. Red:
the lane fixes it at a new head (a flaky case gets its one rerun under rule
16). Missing: dispatch that workflow on the branch, then rerun land.
<a id="queue-land-not-clean"></a>
### `QUEUE_LAND_NOT_CLEAN`
**Meaning.** GitHub doesn't report the PR `MERGEABLE` and `CLEAN`. Nothing
was merged.
**Fix.** `CONFLICTING`: the lane merges the base into its branch, resolves,
runs /ship and `ready` at the new head. `UNKNOWN`: GitHub is still
computing, so rerun land in a minute. `BLOCKED` or `BEHIND`: meet the branch
rule it names.
<a id="queue-land-unverified"></a>
### `QUEUE_LAND_UNVERIFIED`
**Meaning.** GitHub merged the PR, but the base branch's merge commit tree
isn't the pin merged onto that commit's parent, so something besides the
reviewed pin reached main. No land note was written.
**Fix.** Compare the merge commit with `git merge-tree --write-tree <parent>
<pin>`, revert what doesn't belong to the PR, and report it to the Captain.
<a id="queue-train-stamp-mismatch"></a>
### `QUEUE_TRAIN_STAMP_MISMATCH`