mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
screen #3613: merge 9e1593ded9 onto main 319ef500ca
This commit is contained in:
commit
fd11b56eef
49 files changed
+3074
-88
No files matched your search
@@ -257,6 +257,13 @@ loop, per train:
|
||||
`gh pr merge --match-head-commit`; `verify --train` again.
|
||||
4. `train prune` when every slot has merged.
|
||||
|
||||
Under `changelog_fragments` ([ship-policy.md](ship-policy.md#train-land)) lanes READY with a
|
||||
fragment (`changelog.d/<slug>.md`) instead of an `[Unreleased]` section, there is no slot
|
||||
call and no lane stamp, and step 3 is one command per slot: `train land --train <nonce> --pr
|
||||
<n>` commits the stamped prefix S_i on [pin, main], pushes it, waits for checks, merges and
|
||||
confirms main is S_i exactly. Outside a train, `gstack-ship-queue land --pr <n>` is the same
|
||||
step for one PR ([ship-policy.md](ship-policy.md#land)).
|
||||
|
||||
A red train: `train bisect` on the red cases over every prefix. An attributed PR leaves
|
||||
the train and goes back to its lane; red on the base branch itself is fixed forward
|
||||
through an urgent one-PR train (or `train build --repair` while the queue is frozen, which
|
||||
|
||||
+5
-2
@@ -450,7 +450,8 @@ receipt for H when the two trees differ only as a stamp does:
|
||||
stamps, each hunk in them must be the exact old → new rewrite under
|
||||
`stamp_patterns` (literal `{version}` templates) or the built-in forms
|
||||
(`(was X)`, `since: X`, `vX`), and the version file, `package.json`'s
|
||||
`version`, a lockfile's root version keys and a CHANGELOG re-heading have
|
||||
`version`, a lockfile's root version keys and a CHANGELOG re-heading (under
|
||||
`changelog_fragments`, also the fold of the fragments the head deletes) have
|
||||
their own built-in rules. There is no second list and no looser rule here:
|
||||
the file verdicts `clear` reaches on a pair are the verdicts
|
||||
`gstack-tree-receipt --gated G --current H` reaches, and its gate rule holds
|
||||
@@ -469,7 +470,9 @@ receipt for H when the two trees differ only as a stamp does:
|
||||
the policy label (`origin/<base>@<sha>:.gstack/ship-policy.json`) and every
|
||||
differing path with the rule that classed it (`version-file`,
|
||||
`version-manifest`, `manifest-version`, `lockfile-root-version`,
|
||||
`changelog-heading`, `stamp_files`, `stamp_paths`, `release_outputs`). `clear` prints the same
|
||||
`changelog-heading`, `changelog-fragments` for the CHANGELOG under the fold,
|
||||
`changelog_fragments` for a folded fragment's deletion, `stamp_files`,
|
||||
`stamp_paths`, `release_outputs`). `clear` prints the same
|
||||
as `PREGATE_RECEIPT_SUBSTITUTED: …`, `verify` repeats it on its ok line, and
|
||||
the ship receipt carries the receipt string. The receipt's own tests and
|
||||
failed checks apply as before (its exit status is checked only when
|
||||
|
||||
@@ -129,8 +129,9 @@ A lane (one crew thread, one branch) does this, in order. Every command is
|
||||
harness-generic; only the thread and the PR tool are Capy.
|
||||
|
||||
1. **Branch from the default branch, build, test with focused checks.** Write
|
||||
the CHANGELOG entry under `## [Unreleased]`; leave VERSION at main's value.
|
||||
Never pre-pick a version.
|
||||
the CHANGELOG entry under `## [Unreleased]` (under `changelog_fragments`, a
|
||||
fragment in the policy's fragment directory instead); leave VERSION at
|
||||
main's value. Never pre-pick a version.
|
||||
2. **Push the final head.** Everything below binds to the pushed head; a later
|
||||
push invalidates the receipt, the pregate record and the review binding.
|
||||
3. **Open the PR** with the host's PR tool (Capy: `tools.pr.create`, never
|
||||
@@ -182,6 +183,11 @@ reference:
|
||||
<sha>`, or `--merge` where the repository keeps merge commits), and
|
||||
verifies again. A stamp commit that holds anything beyond the stamp files
|
||||
fails `verify --train`, freezes the queue and costs a backstop gate.
|
||||
Under `changelog_fragments` there is no slot call and no lane stamp: the
|
||||
controller runs `gstack-ship-queue train land --train <nonce> --pr 41` per
|
||||
slot, which pushes the stamped prefix the gate covered, waits for checks,
|
||||
merges and confirms main
|
||||
([ship-policy.md#train-land](ship-policy.md#train-land)).
|
||||
4. `train prune` when every slot has merged.
|
||||
|
||||
**Who merges.** One controller, never the lane. Both crews that ran this
|
||||
@@ -317,7 +323,7 @@ closed) holds the longer history behind these numbers; from here on
|
||||
| Running a workflow | the SKILL.md files, `GSTACK_SESSION_KIND=unattended`, `GSTACK_RESULT:` lines | the `GSTACK_LAUNCH` assignments, `GSTACK_CAPY=1` and the `capy-asks` block, no enforced hooks (safety skills are advisory) |
|
||||
| Approvals | `gstack-approvals`, `gstack-owner-brief render --captain`, the `## Approvals` table | the Captain relay (`needs_you`), the Captain drive `approvals/` mirror |
|
||||
| Pull requests | `/ship`, the ship receipt, `gstack-pr-shots` | `tools.pr.create` (callbacks), `tools.pr.uploadAsset` (before/after images) |
|
||||
| Merging | `gstack-ship-queue` (ready, train build, verdict, verify), `gstack-restamp`, `gh pr merge --match-head-commit` | the controller is a Captain thread; slot calls are thread messages |
|
||||
| Merging | `gstack-ship-queue` (ready, train build, verdict, verify; train land and land under `changelog_fragments`), `gstack-restamp`, `gh pr merge --match-head-commit` | the controller is a Captain thread; slot calls are thread messages |
|
||||
| Flaky pass | the rerun rule, `flake-records-a*` and `flake-ledger-*` artifacts, `test:health` | the daily automation and the ledger in the Captain's Drive |
|
||||
| Release | `release-cut`, `weekly-release.yml`, the `stable` channel | nothing |
|
||||
| Scorecard | `gstack-factory-scorecard`, `/retro` Step 7.5 | nothing (the CSV can be kept in the Captain's Drive) |
|
||||
|
||||
+223
-6
@@ -61,6 +61,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 }` 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. `null` keeps the per-PR queue exactly as it is. 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`. |
|
||||
|
||||
Containment, checked by `validate` and again at write time: paths and globs are
|
||||
repo-relative with no `..` segment, under 256 characters, at most 64 per list,
|
||||
@@ -148,7 +149,8 @@ ship receipt into the coordinator protocol, so nobody proves a stamp diff by
|
||||
hand. Lanes own their PRs and never merge; the coordinator owns the order and
|
||||
every merge. Each step ends in a line the other side acts on, then
|
||||
`GSTACK_RESULT: skill=ship-queue status=complete|refused run=<path>`. Nothing
|
||||
here pushes.
|
||||
here pushes, except `ready` under `changelog_fragments` (its READY receipt goes
|
||||
to `refs/notes/gstack-trains`) and [`land`](#land).
|
||||
|
||||
**1. Lane: report ready (unstamped).** After the gate and pregate pass on the
|
||||
pushed head:
|
||||
@@ -169,8 +171,12 @@ ci: green at <head12> (passed=31 failed=0 pending=0 skipped=2)
|
||||
PREGATE: pass tree=<tree12> head=<head12> checks=6
|
||||
gate: command="<gate_command>" full_e2e=no gate-ahead=none
|
||||
SHIP_RECEIPT: pr=42 head=<head12> …
|
||||
READY_RECEIPT: <state root>/ship-queue/ready/<repo key>/42-<head>.json (notes pushed to origin refs/notes/gstack-trains)
|
||||
```
|
||||
|
||||
Under `changelog_fragments`, a `READY_RECEIPT:` line follows, written only when
|
||||
every check above passed; `land` refuses a pin without one.
|
||||
|
||||
CI's Version Gate (`scripts/compare-pr-version.ts`) accepts that head as
|
||||
unstamped when `VERSION` and `package.json`'s version match its merge base, the
|
||||
CHANGELOG change is confined to `[Unreleased]`, and any changed release output
|
||||
@@ -359,6 +365,65 @@ are mirrored to origin as git notes on `refs/notes/gstack-trains` before any
|
||||
remote effect, so a lost machine loses nothing: `train status` on a clean
|
||||
state root rebuilds them. `GSTACK_SHIP_QUEUE_NOW` pins the clock for tests.
|
||||
|
||||
<a id="train-land"></a>
|
||||
### Stamped trains under `changelog_fragments`: `train land`
|
||||
|
||||
When the base policy also declares [`changelog_fragments`](#changelog-fragments),
|
||||
`train build` builds every prefix twice. T_i is the plain chain above. S_i is
|
||||
what slot i publishes: S_0 is the base and S_i = stamp(merge(S_(i-1), pin_i)),
|
||||
stamped by `gstack-restamp` with S_(i-1) as its base, so slot i folds only its
|
||||
own fragments (a stacked pin's parent fragments are already folded) and the
|
||||
versions run in slot order, each with its slot's effective bump. Before
|
||||
anything is pushed, each S_i must be merge(S_(i-1), pin_i) modulo stamps,
|
||||
keep every release entry of S_(i-1) byte for byte, and equal T_i outside
|
||||
release paths (version files, CHANGELOG, fragments, `release_outputs`,
|
||||
`stamp_paths`, `stamp_files`); otherwise `QUEUE_TRAIN_STAMP_MISMATCH`. Pass
|
||||
`--allow-repo-commands` when the policy has a `restamp_command` or
|
||||
`release_tool`. The record keeps both chains (`prefixes` and `stamped`), the
|
||||
train head and branch are S_k, and the one full gate runs on S_k, the exact
|
||||
tree the last merge publishes.
|
||||
|
||||
Slots merge with `train land --train <nonce> --pr <n>`, never a slot call:
|
||||
|
||||
1. The record must be the gated head's own chain. Walking first parents from
|
||||
S_k gives T0 and S_1..S_k with the recorded commits and trees, and each
|
||||
S_i's second parent is slot i's pin. A record rebuilt from notes that
|
||||
differs anywhere refuses (`QUEUE_PROOF_MISSING`), and every later step uses
|
||||
only that verified chain. main's tree must equal S_(i-1)'s exactly (slot 1:
|
||||
the train base), and the PR's head its pin, with its /ship receipt. main
|
||||
equal to an earlier prefix refuses and names the slot to land first; main
|
||||
equal to a later prefix confirms the slot (a rerun). A base that cannot be
|
||||
fetched refuses (`QUEUE_PROOF_MISSING`), a fork head refuses
|
||||
(`QUEUE_TRAIN_UNTRUSTED`), and a PR whose base is no longer the train's
|
||||
base refuses (`QUEUE_NOT_READY`), here and again before the merge.
|
||||
2. It commits S_i's tree with parents [pin, main], adds the
|
||||
`evals-no-debounce` label (a tool-made head gets no later push, so the
|
||||
10-minute quiet window protects nothing), and pushes under
|
||||
`--force-with-lease=<branch>:<pin>`; a foreign push is never overwritten
|
||||
(`QUEUE_TRAIN_MOVED`). The stamp commit is deterministic and journaled
|
||||
(`land-<n>.json` beside the train record) before the push, so a rerun
|
||||
adopts its own stamp head instead of reading it as a move. A push that
|
||||
fails while the branch still reads the pin is `QUEUE_NOT_READY`; rerun.
|
||||
3. It waits for the PR's checks on that commit (`--no-wait` prints the
|
||||
command). Required checks run live at the slot until GSTA-156's exact-tree
|
||||
reuse is on main. Every name in `changelog_fragments.required_checks` must
|
||||
be present and green on the stamp commit (a check run of that name, all of
|
||||
its `<name> (…)` matrix runs, or a status context); a missing one holds the
|
||||
merge (`QUEUE_NOT_READY`) and the fix names it. Without the list, any green
|
||||
set counts, so set it to the checks branch protection does not enforce.
|
||||
4. It refuses if main moved during the checks, merges with
|
||||
`--match-head-commit`, and confirms main's tree is S_i exactly. Any other
|
||||
main is the freeze path above (`QUEUE_TRAIN_TREE_MISMATCH`, backstop).
|
||||
A slot counts as merged only when GitHub reports its PR MERGED. Main at
|
||||
S_i with the PR still open refuses (`QUEUE_PROOF_MISSING`), and the
|
||||
train's merged count, its freeze decision and the freeze record read
|
||||
GitHub's state for every slot, never the record's `merged` list alone.
|
||||
|
||||
`verify --train` refuses a stamped train and names `train land`. The backstop
|
||||
covers main only when its exact tree is some S_i of a green record (plain
|
||||
T_i records still cover modulo stamps). A dropped slot changes every later
|
||||
version, so the train is rebuilt, as today.
|
||||
|
||||
<a id="ship-before-ready"></a>
|
||||
**0. Lane: /ship before READY.** Every READY follows a /ship run on the branch
|
||||
at the head being READY'd, including a re-READY after a push (Garry,
|
||||
@@ -493,9 +558,10 @@ records does not stand; see [the ship receipt](#ship-receipt)),
|
||||
`QUEUE_TRAIN_CONFLICT: train <id> stopped at #<pr> (conflicts: <files>) after
|
||||
merging #<a>,#<b>; nothing dispatched, no branch pushed; fix: …` line names
|
||||
the build without that PR) and `QUEUE_TRAIN_FREE_RED` (free tests red,
|
||||
missing, cancelled or on another SHA). In a merge, version files take the
|
||||
prefix's side and the CHANGELOG merges by section, since the stamp at merge
|
||||
rewrites both; a PR whose CHANGELOG drops or renames a released heading (or
|
||||
missing, cancelled or on another SHA). In a merge, the version file takes the
|
||||
prefix's side, a manifest or npm lockfile conflict merges three-way with the
|
||||
prefix's version (so a pin's dependency edit survives), and the CHANGELOG
|
||||
merges by section, since the stamp at merge rewrites them; a PR whose CHANGELOG drops or renames a released heading (or
|
||||
carries a released entry's text in a section of its own) is a CHANGELOG
|
||||
conflict, never merged with the damage, except the stacked child whose parent
|
||||
was released since their merge base, which `mergeChangelogs` carries (see *A
|
||||
@@ -530,7 +596,8 @@ gstack-ship-queue train rebuild --train <nonce>
|
||||
```
|
||||
|
||||
The bisect dispatches the red cases (`cases`) on every prefix T0..Tk
|
||||
(`prefix_shas`), one trial each, after checking every prefix is an ancestor
|
||||
(`prefix_shas`; for a [stamped train](#train-land), T0 and S_1..S_k, the
|
||||
chain its gate ran), one trial each, after checking every prefix is an ancestor
|
||||
of `train/<nonce>` (`QUEUE_TRAIN_PREFIX_INVALID`). It reserves every
|
||||
case×prefix trial (unknown costs at `trial_reservation_usd`) and launches only
|
||||
the prefixes that fit the cap (`QUEUE_TRAIN_BISECT_CAPPED`; completed prefixes
|
||||
@@ -779,6 +846,7 @@ which carry the workflow consequences the skill prose would otherwise spell out:
|
||||
| `ship: preregistration: …` | `gstack-evidence ancestor --from-policy --base <base>` must print only `ANCESTOR: ok` before push |
|
||||
| `ship: release_tool: …` | the command runs only inside `gstack-restamp` (staging worktree, outputs confined to `release_outputs`) |
|
||||
| `ship: review_scope …` | Step 9.4 item 0 runs `gstack-review-scope classify` on each pass's findings; see [review_scope](#review-scope) |
|
||||
| `ship: changelog_fragments <dir>: …` | Step 12 writes no version; Step 13 writes the entry as `<dir>/<branch-slug>.md` (entry text only, `bump:` front matter only when the change warrants more than the policy bump), never an `[Unreleased]` section, linted with `gstack-prose-lint <file> --gate`; `gstack-ship-queue land` assigns the version; see [changelog fragments](#changelog-fragments) |
|
||||
|
||||
The PR body's first section is the ship receipt (`gstack-ship-receipt write … --body`),
|
||||
written by the pr-body section of `/ship` after the body is composed.
|
||||
@@ -1158,6 +1226,154 @@ Read from the base branch and run only with `--allow-repo-commands`, like
|
||||
of its own choosing cannot run here; give it a mode that takes the version and
|
||||
writes only stamps.
|
||||
|
||||
The command also gets `GSTACK_RESTAMP_BASE_SHA`, the release predecessor: the
|
||||
base branch commit the version is allocated after (origin/<base> for a per-PR
|
||||
restamp and for `land`, S_(i-1) for a [stamped train](#train-land)'s S_i). A repo counter that must be predecessor + 1 (a
|
||||
SAVE_VERSION, a rules number) reads its old value from that commit
|
||||
(`git show "$GSTACK_RESTAMP_BASE_SHA:<file>"`), never from the branch, so two PRs
|
||||
cut from one base never both claim the same number. Regenerated goldens go in
|
||||
`release_outputs`.
|
||||
|
||||
<a id="changelog-fragments"></a>
|
||||
## `changelog_fragments`: entries without versions, versioned at merge
|
||||
|
||||
With `changelog_fragments: { "dir": "changelog.d" }` on the base branch, a PR
|
||||
never edits VERSION, the manifest `version` key or a released CHANGELOG
|
||||
section. It adds one or more fragments, and the merge step assigns the
|
||||
version, so two PRs cut from one base cannot collide and no lane redoes a
|
||||
stamp after another PR merges.
|
||||
|
||||
A fragment is `<dir>/<slug>.md`, one regular file directly in `dir`
|
||||
(`README.md` there is skipped). Its body is the entry text, exactly what the
|
||||
CHANGELOG section will hold, with no `## [x]` heading. It may open with front
|
||||
matter that raises the bump:
|
||||
|
||||
```markdown
|
||||
---
|
||||
bump: minor
|
||||
---
|
||||
**What changed, in one sentence.**
|
||||
|
||||
### Fixed
|
||||
- ...
|
||||
```
|
||||
|
||||
The effective bump of a stamp is the highest of the policy bump and every
|
||||
folded fragment's `bump:` (`micro` < `patch` < `minor` < `major`). Fragments
|
||||
are read as Git blobs at the commit being stamped (`git ls-tree` and
|
||||
`git cat-file`, never the working tree), so a symlink, a gitlink, a nested
|
||||
path, a non-`.md` file, an empty fragment, one over 64 KiB, one carrying a
|
||||
release heading, or one whose name already landed on the base with other text
|
||||
is `CHANGELOG_FRAGMENT_INVALID`, and nothing is allocated. Only fragments the
|
||||
branch adds against the base count.
|
||||
|
||||
**/ship.** `show-effective` prints a `ship: changelog_fragments` line, so /ship
|
||||
writes no version in Step 12 and writes Step 13's entry as
|
||||
`<dir>/<branch-slug>.md`, linted with `gstack-prose-lint <file> --gate`, never an
|
||||
`[Unreleased]` section.
|
||||
|
||||
**The fold.** `gstack-restamp` (and so `land`) concatenates the fragment
|
||||
bodies in path order above any legacy `[Unreleased]` section, re-heads both as
|
||||
one `## [<version>] - <date>` section at the top, and deletes the fragment
|
||||
files, in one stamp. The tree receipt, and so `gstack-pregate clear`'s
|
||||
stamp-only substitution, calls a deleted fragment a stamp (rule
|
||||
`changelog_fragments`) only when the new CHANGELOG section is exactly the
|
||||
deleted fragments' bodies in path order above any legacy `[Unreleased]` text
|
||||
(rule `changelog-fragments`); an edited fold, or a fragment deleted without
|
||||
its text, is a change.
|
||||
|
||||
**Version Gate.** The workflow runs on every PR (no path filter), so a PR
|
||||
that touches no release file still has to prove its entry. Under the key, an
|
||||
unstamped head must keep VERSION, the manifest `version` key and every
|
||||
released CHANGELOG section at its merge base's values, and add at least one
|
||||
valid fragment (a legacy `[Unreleased]`-only entry passes during migration;
|
||||
a `[bot]` author, such as Dependabot, needs none). Other manifest edits, a
|
||||
dependency or a script, pass. A head made by the merge step, a commit whose
|
||||
parents are [pin, base], must carry the base's version plus the effective
|
||||
bump of the fragments its pin adds; a stacked pin's parent fragment whose
|
||||
text the base already released is skipped, not counted again. Under a train policy `gstack-ship-queue
|
||||
ready` applies the same rule itself; without one, `ready` requires the head's
|
||||
checks green, Version Gate included.
|
||||
|
||||
<a id="land"></a>
|
||||
## `gstack-ship-queue land`: the per-PR merge step
|
||||
|
||||
`land --pr <n>` merges one READY PR whose head is unstamped. It replaces the
|
||||
lane's stamp round trip. Nobody stamps by hand; the tool makes and pushes the
|
||||
stamp commit. Steps, each journaled in
|
||||
`.gstack/tmp/ship-queue/land/<n>.json` and resumable by rerunning the same
|
||||
command:
|
||||
|
||||
1. **Refuse** a fork head, a closed PR, a PR whose base is another branch, a
|
||||
base policy with `changelog_fragments` off (`QUEUE_NOT_READY`), a pin
|
||||
without a READY receipt (`gstack-ship-queue ready --pr <n>` records one
|
||||
on `refs/notes/gstack-trains` once its pregate, full_e2e and receipt checks
|
||||
pass; land checks nothing weaker), a /ship receipt that is missing or
|
||||
bound to another head (`QUEUE_RECEIPT_MISSING`, `QUEUE_RECEIPT_STALE`), and
|
||||
required checks that are not green at the pin.
|
||||
2. **Lock.** Take the repository land lock, `refs/notes/gstack-land` (its own
|
||||
ref, never the trains notes ref), by exact lease. It records the PR, holder,
|
||||
phase and journal. A held lock is `QUEUE_LAND_LOCKED` naming the holder;
|
||||
`--break-lock` takes over a crashed holder's lock and is refused while that
|
||||
holder entered a push or a merge less than 10 minutes ago, and for a drift
|
||||
lock. A refusal before the push releases the lock.
|
||||
3. **Merge and stamp.** In a scratch worktree, merge the base into the pin.
|
||||
Conflicts in release fields only (the version file and `release_outputs`
|
||||
take the base's side, the CHANGELOG merges by section, a manifest or npm
|
||||
lockfile is re-merged with every side's release version set to the base's,
|
||||
so a clean dependency edit survives) are resolved; anything else is
|
||||
`QUEUE_LAND_CONFLICT`, nothing pushed, back to the lane. A legacy pin that
|
||||
bumped VERSION itself has its release fields put back to its merge base
|
||||
first and its entry re-headed `[Unreleased]` (a manifest version source
|
||||
keeps its other edits; a symlinked release file refuses). A PR with no entry gets a
|
||||
fragment written from its title. Then `gstack-restamp --next` folds and
|
||||
allocates (`restamp_command` and `release_tool` need
|
||||
`--allow-repo-commands`, as everywhere).
|
||||
4. **Push.** `commit-tree` the stamped tree with parents [pin, base] and push
|
||||
it to the PR branch under `--force-with-lease=<branch>:<pin>`. The journal
|
||||
keeps the reviewed pin apart from the last pushed head, so a rerun leases
|
||||
on what it pushed and never on a head someone else pushed
|
||||
(`QUEUE_TRAIN_MOVED`, never overwritten).
|
||||
5. **Wait** for the PR's checks on the stamp commit, polling every 30 seconds
|
||||
with the same check read the pin passed (`--timeout <min>`, default 60). `--no-wait`
|
||||
stops after the push and prints `gstack-pr-wait <n> --until green &&
|
||||
gstack-ship-queue land --pr <n>` to run instead.
|
||||
CI runs for real on the stamp commit in this mode.
|
||||
6. **Merge.** Re-read the base first: if it moved, restamp from the new base
|
||||
and push again under the last-pushed lease, at most twice
|
||||
(`QUEUE_LAND_BASE_BUSY` on a third move, which releases the lock). Then
|
||||
`gh pr merge --squash --match-head-commit <stamp>` (`--merge` under
|
||||
`history: merge-only`) and confirm the base's
|
||||
tree is the stamped tree on the base land stamped. The stamp date is taken
|
||||
once and reused by every restamp. A rerun after GitHub merged the PR
|
||||
(an error reply, a crash) goes straight to that tree check.
|
||||
7. **Drift.** GitHub's merge API cannot bind the base, so a merge made outside
|
||||
`land` can slip in between the check and the squash. The post-merge check
|
||||
then fails with `QUEUE_LAND_DRIFT` and keeps the lock.
|
||||
`land --pr <n> --repair` opens a PR from the base whose only change is a
|
||||
fragment for what the outside merge brought (its unfolded fragments, moved,
|
||||
or one entry naming its commits) and records it on the lock; /ship and land
|
||||
that PR, from any checkout, and its land takes over the drift lock and
|
||||
releases it. A rerun of `--repair` finds the PR it already opened.
|
||||
|
||||
`--dry-run` merges and stamps in a scratch worktree and prints the version and
|
||||
tree, taking no lock and pushing nothing.
|
||||
|
||||
<a id="migrating-to-fragments"></a>
|
||||
### Migrating to fragments
|
||||
|
||||
Before turning the key on, run `gstack-ship-queue land --census` (read-only).
|
||||
It prints one line per open PR, saying whether land would fold its fragments, re-head a legacy
|
||||
`[Unreleased]`, rewrite a tentative per-PR VERSION bump, resolve a
|
||||
release-field-only conflict, write an entry from the title, or refuse on a
|
||||
product conflict. Then set `changelog_fragments` and `min_gstack_version` in
|
||||
one policy PR; a claim-at-open repository switches `queue_mode` to
|
||||
`stamp-at-merge` in the same PR. Open PRs need no change: `land` handles each
|
||||
census case. Rollback is removing the key. With it off, `gstack-restamp`
|
||||
neither folds nor deletes fragment files, so first land every open PR that
|
||||
carries a fragment (`land --census` lists them) or move each fragment's text
|
||||
under `[Unreleased]` in its PR; otherwise that entry never reaches CHANGELOG.md.
|
||||
|
||||
<a id="tree-receipt"></a>
|
||||
## The tree receipt
|
||||
|
||||
@@ -1181,7 +1397,8 @@ Hunks are classified per file type: the version file (old → new only), the
|
||||
manifest (`version` key only), npm and bun lockfiles (root `version` keys
|
||||
only, where `gstack-version-bump` writes; a dependency entry is `changed`),
|
||||
the CHANGELOG (a heading move or re-heading, compared as an order-independent
|
||||
multiset of entries), `stamp_paths` and `release_outputs` (every
|
||||
multiset of entries; under `changelog_fragments`, also exactly the deleted
|
||||
fragments folded under the new heading, and those deletions are stamps), `stamp_paths` and `release_outputs` (every
|
||||
removed/added pair must be an exact `(was X)` / `since: X` / `vX` rewrite (a quote around X in the first two forms included) with
|
||||
X the gated version, or a newer version of the repo's width that the
|
||||
stamp_paths stage moves (a tentative `*_KEY_VERSION` written as the next
|
||||
|
||||
+98
-7
@@ -4242,12 +4242,18 @@ HEAD is not the PR's pushed head (read through `gh pr view`), CI at that
|
||||
exact head is not green (failed, pending or unreadable), or the pregate result
|
||||
is missing, failed or recorded on another tree (`PREGATE_STALE`). Under
|
||||
`--dispatch-ci` it is also a dispatched workflow run that concluded red (a red
|
||||
run clears no pregate lane) or a `gh workflow run` that failed.
|
||||
run clears no pregate lane) or a `gh workflow run` that failed. `train land`
|
||||
uses it while a slot cannot merge yet: checks on the stamp commit are not
|
||||
green, a name in `changelog_fragments.required_checks` is missing or not green
|
||||
there, the PR's base is no longer the train's base, the stamp push failed while
|
||||
the branch still read the pin, `gh pr merge` refused, or the merge is not yet
|
||||
visible on main.
|
||||
|
||||
**Fix.** Commit and push, wait for CI on that head, rerun
|
||||
`gstack-pregate run --allow-repo-commands` when the tree changed, then rerun
|
||||
`gstack-ship-queue ready --pr <n>`. The block it prints before refusing shows
|
||||
which line is not yet true.
|
||||
which line is not yet true. For `train land`, do what `NEXT:` says, then rerun
|
||||
the same `train land` command; it adopts its own journaled stamp commit.
|
||||
|
||||
<a id="queue-proof-missing"></a>
|
||||
### `QUEUE_PROOF_MISSING`
|
||||
@@ -4263,8 +4269,13 @@ train still building, running or merging, `train watch` on an unknown record,
|
||||
unreadable run), `verify --pr <n> --train <nonce>` when main's tree mismatches
|
||||
and whether a slot merged on GitHub cannot be read (so whether the queue must
|
||||
freeze is unknown; it never answers "nothing merged" from an unreadable
|
||||
state), and `ready --screen` when the scratch merge branch cannot be
|
||||
pushed. Nothing was written unless the `QUEUE_STAMP_REFUSED:` line says
|
||||
state), `train land` when the base or the train branch cannot be fetched or
|
||||
the record's T0 and S_1..S_k are not the gated head's first-parent chain
|
||||
(each S_i's second parent slot i's pin), `train land` when main's tree is the
|
||||
slot's S_i but GitHub does not report the PR as MERGED (a record whose slot
|
||||
numbers or merged entries disagree with GitHub),
|
||||
and `ready --screen` when the
|
||||
scratch merge branch cannot be pushed. Nothing was written unless the `QUEUE_STAMP_REFUSED:` line says
|
||||
`committed=yes`.
|
||||
|
||||
**Fix.** Supply the named input (fetch the gated head, pass the current base
|
||||
@@ -4303,7 +4314,9 @@ predecessor PR moved after the gate.
|
||||
**Meaning.** The ship policy sets `full_e2e: true`, and no passing full-gate
|
||||
result is bound to the tree being stamped or verified: no recorded gate-ahead
|
||||
attempt for that gated head, no `--gate-exit 0 --gate-log <file>` from a run
|
||||
on it, or the recorded run exited non-zero.
|
||||
on it, or the recorded run exited non-zero. The train commands use it too: a
|
||||
PR that is not in the train, a train without a green gate, `verify --train` on
|
||||
a stamped train and `train land` on a plain one (each names the other).
|
||||
|
||||
**Fix.** Run the gate the policy's `gate_command` names in the gate-ahead
|
||||
worktree and bind it with `gstack-ship-queue gate-ahead --record`, or pass the
|
||||
@@ -4341,18 +4354,92 @@ a `train` block with `min_gstack_version` on the base branch first
|
||||
**Meaning.** A pinned PR's head repository is not the base repository. The
|
||||
train branch and its dispatches run with repository secrets, so fork work
|
||||
never rides a train. `ready --dispatch-ci` refuses a fork head for the same
|
||||
reason and dispatches nothing.
|
||||
reason and dispatches nothing, and `train land` refuses one and pushes nothing.
|
||||
|
||||
**Fix.** Run the printed `train build --prs <list without it>`; the fork PR
|
||||
stays on the trusted-branch path.
|
||||
|
||||
<a id="changelog-fragment-invalid"></a>
|
||||
### `CHANGELOG_FRAGMENT_INVALID`
|
||||
|
||||
**Meaning.** A CHANGELOG fragment under the policy's `changelog_fragments`
|
||||
directory is not one regular `.md` file directly in that directory with entry
|
||||
text, optional `bump:` front matter and no release heading, or it is over
|
||||
64 KiB, or a fragment of that name already landed with other text. Nothing was
|
||||
allocated or written. See [changelog fragments](ship-policy.md#changelog-fragments).
|
||||
|
||||
**Fix.** Rewrite the named fragment as a flat `<dir>/<slug>.md` holding only
|
||||
the entry text (and optional `bump: minor|major` front matter); rename it if
|
||||
the name already landed. Then rerun.
|
||||
|
||||
<a id="queue-land-locked"></a>
|
||||
### `QUEUE_LAND_LOCKED`
|
||||
|
||||
**Meaning.** Another `gstack-ship-queue land` holds the repository land lock
|
||||
(`refs/notes/gstack-land`), or took it from under this run, so this one would
|
||||
race it for the next version. The line names the holder, its PR and phase.
|
||||
|
||||
**Fix.** Wait for the holder to finish. If it crashed, rerun with
|
||||
`--break-lock`; that is refused while the holder entered a push or a merge
|
||||
less than 10 minutes ago. A lock in phase `drift` is released by landing its
|
||||
repair PR ([QUEUE_LAND_DRIFT](#queue-land-drift)).
|
||||
|
||||
<a id="queue-land-conflict"></a>
|
||||
### `QUEUE_LAND_CONFLICT`
|
||||
|
||||
**Meaning.** The PR conflicts with the base branch in a product file (or a
|
||||
release field that cannot be resolved alone, such as a real dependency
|
||||
conflict in a manifest), so the merge step cannot stamp it. Nothing was
|
||||
pushed.
|
||||
|
||||
**Fix.** The lane merges the base into the PR, resolves the named files, runs
|
||||
/ship and reports READY again.
|
||||
|
||||
<a id="queue-land-drift"></a>
|
||||
### `QUEUE_LAND_DRIFT`
|
||||
|
||||
**Meaning.** After the squash merge, the base branch's tree is not the
|
||||
stamped tree on the base `land` stamped: a merge made outside `land` slipped
|
||||
in between the last check and the merge. The lock stays held, in phase
|
||||
`drift`, so no other land allocates on a release record that is now wrong.
|
||||
|
||||
**Fix.** Run `gstack-ship-queue land --pr <n> --repair`. It opens a PR whose
|
||||
only change is a fragment for what the outside merge brought. Run /ship on it
|
||||
and land it; that land takes over the lock and releases it. Then find what
|
||||
merged outside `land`.
|
||||
|
||||
<a id="queue-land-base-busy"></a>
|
||||
### `QUEUE_LAND_BASE_BUSY`
|
||||
|
||||
**Meaning.** The base branch moved during the stamp commit's CI on each of
|
||||
the two allowed restamps, so `land` stopped instead of restamping again.
|
||||
|
||||
**Fix.** Find what is merging outside `land` (every merge should go through
|
||||
it), then rerun `gstack-ship-queue land --pr <n>`; the lock was released and
|
||||
the rerun restamps from the pin.
|
||||
|
||||
<a id="queue-train-stamp-mismatch"></a>
|
||||
### `QUEUE_TRAIN_STAMP_MISMATCH`
|
||||
|
||||
**Meaning.** While building a stamped train (`changelog_fragments`), a prefix
|
||||
S_i could not be stamped, or it is not the merge of S_(i-1) and its pin
|
||||
modulo stamps, or it rewrote an earlier release entry, or it differs from the
|
||||
plain prefix T_i outside release paths. Nothing was pushed or dispatched. See
|
||||
[stamped trains](ship-policy.md#train-land).
|
||||
|
||||
**Fix.** Read the named slot and files. A stamp hook (`restamp_command`,
|
||||
`release_tool`) that writes product files is fixed in the hook; an invalid
|
||||
fragment goes back to its lane. Otherwise rebuild the train without that slot
|
||||
and report it.
|
||||
|
||||
<a id="queue-train-moved"></a>
|
||||
### `QUEUE_TRAIN_MOVED`
|
||||
|
||||
**Meaning.** A PR's head is no longer the SHA the train pinned (at build, or
|
||||
past its pin beyond stamps at verify), so the train would not test what
|
||||
merges. At verify after earlier slots merged, the queue also freezes
|
||||
(`QUEUE_TRAIN_FROZEN`).
|
||||
(`QUEUE_TRAIN_FROZEN`). At `train land` it is a PR head or branch that moved
|
||||
off the pin (or off land's own stamp commit); land never overwrites it.
|
||||
|
||||
**Fix.** Ask the lane for a fresh READY on the new head, then build the train
|
||||
with the new pin (`train build --prs <n>@<new sha>,…`).
|
||||
@@ -4505,6 +4592,10 @@ tested. After a merge, a main that differs from the prefix after the PR in
|
||||
the CHANGELOG alone, while being its own first parent's CHANGELOG plus the
|
||||
PR's own entry (the rule the pre-merge verify applied to the head), is not a
|
||||
mismatch ([one CHANGELOG rule, before and after](ship-policy.md#run-a-merge-train)).
|
||||
For a stamped train, `train land` compares exact trees: main must be S_(i-1)
|
||||
before the merge, must not move during the slot's checks, and must be S_i
|
||||
after it ([stamped trains](ship-policy.md#train-land)); main equal to an
|
||||
earlier prefix refuses without freezing and names the slot to land first.
|
||||
|
||||
**Fix.** Stop merging. If a PR of the train merged (verified, or reported
|
||||
MERGED by GitHub without a verified tree, slot 1's own squash included), the
|
||||
|
||||
Reference in new issue
Block a user