mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
ship-queue verify: say what happens to a slot unreadable beside a merged one
The freeze reason, docs and CHANGELOG said the slot was kept out of unmerged 'until read', but nothing reads it again: the NEXT after the repair is built from unmerged alone. Say so (the operator seats it in a train by hand once gh answers) and pin the record shape with a test. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
5905c768f7
commit
351cc55262
5 files changed
+29
-8
No files matched your search
+1
-1
@@ -5,7 +5,7 @@
|
||||
**A merge the pre-merge verify accepted no longer freezes the queue, and a slot stamp touches only the stamp files.** Train 24 (2026-10-27) merged #3434 and #3453, then froze on #3453's post-merge check and paid a $31 backstop: the pre-merge verify judged the lane's CHANGELOG as main's plus the pin's own entry and said MERGE_CONDITION, the post-merge check compared main to T2 section by section, and T2's one `[Unreleased]` (both stacked entries) had become two released sections. In both slots `gstack-restamp` also listed `lib/ship-policy.ts` as a fifth stamp file, because the lane's uncommitted base merge made #3511's `DOCS_CAPS_KEY_VERSION` look like the branch's own tentative key. And `mergeChangelogs` refused the stacked child outright, so the lane's hand resolve dropped #3453's two wording fixes to the released 1.91.276.0 entry.
|
||||
|
||||
### Fixed
|
||||
- **`gstack-ship-queue verify --train`** (`lib/ship-queue-train-verify.ts`): one CHANGELOG rule before and after the merge. Post-merge, main confirms when its tree is T<i> modulo stamps, or differs from T<i> in the CHANGELOG alone while being its own first parent's CHANGELOG (the main the pre-merge verify saw) plus the PR's own entry, the rule `mergedIn` applied pre-merge; the `main:` line says "except CHANGELOG.md, main <sha12>'s CHANGELOG.md plus #<n>'s own entry". The own lines are the pin's authored CHANGELOG lines or the lines `mergeChangelogs` writes for that pin against main, so a tool-written correction bullet passes. A freeze lists a slot GitHub reports MERGED as merged (its reason says "merged on GitHub without a verified tree"), never under `unmerged`, so the rebuild after the repair does not seat it again; the record is built from the one GitHub read that decided to freeze, so a slot that read MERGED stays merged when GitHub stops answering a moment later, and a slot unreadable beside a merged one is kept out of `unmerged` and named in the reason ("unreadable on GitHub, kept out of unmerged until read"); and a first slot GitHub reports MERGED whose squash fails the post-merge check freezes the queue instead of answering "nothing of the train merged" while main already holds the unverified squash; when GitHub's answer for a slot cannot be read, the verify refuses (`QUEUE_PROOF_MISSING`, "whether #<n> merged on GitHub could not be read") and asks to be rerun, never "nothing merged". A main whose CHANGELOG equals its parent's is not "the parent's plus the PR's own entry" (`ownEntryAboveMain`: "no entry of the PR's own above it"), so a squash that lost the entry the gate carried is refused.
|
||||
- **`gstack-ship-queue verify --train`** (`lib/ship-queue-train-verify.ts`): one CHANGELOG rule before and after the merge. Post-merge, main confirms when its tree is T<i> modulo stamps, or differs from T<i> in the CHANGELOG alone while being its own first parent's CHANGELOG (the main the pre-merge verify saw) plus the PR's own entry, the rule `mergedIn` applied pre-merge; the `main:` line says "except CHANGELOG.md, main <sha12>'s CHANGELOG.md plus #<n>'s own entry". The own lines are the pin's authored CHANGELOG lines or the lines `mergeChangelogs` writes for that pin against main, so a tool-written correction bullet passes. A freeze lists a slot GitHub reports MERGED as merged (its reason says "merged on GitHub without a verified tree"), never under `unmerged`, so the rebuild after the repair does not seat it again; the record is built from the one GitHub read that decided to freeze, so a slot that read MERGED stays merged when GitHub stops answering a moment later, and a slot unreadable beside a merged one is left out of `unmerged` and named in the reason ("unreadable on GitHub, left out of unmerged; seat it in a train by hand once gh pr view answers"), so the `NEXT:` after the repair omits it; and a first slot GitHub reports MERGED whose squash fails the post-merge check freezes the queue instead of answering "nothing of the train merged" while main already holds the unverified squash; when GitHub's answer for a slot cannot be read, the verify refuses (`QUEUE_PROOF_MISSING`, "whether #<n> merged on GitHub could not be read") and asks to be rerun, never "nothing merged". A main whose CHANGELOG equals its parent's is not "the parent's plus the PR's own entry" (`ownEntryAboveMain`: "no entry of the PR's own above it"), so a squash that lost the entry the gate carried is refused.
|
||||
- **`gstack-restamp` stamp_paths** (`lib/restamp.ts`): a line the base added since the pin's fork point is the base's, whether the base merge is committed or still staged; `branchAddedLines` leaves it out and the stage detail says "N line(s) the base added since the fork point are the base's, not this branch's, and stay (<file>: N)". #3514's receipt bound (`stampOnlyLine`) refuses such a move after the fact only while the moved version is older than the base's at the build; a key at the base version passes it, so the restamp is the guard. A base diff git cannot run is a refusal ("cannot diff <since>..<base> for the stamp_paths stage"), never an empty "nothing added" set that would move the base's lines again.
|
||||
- **`mergeChangelogs`** (`lib/changelog-check.ts`): a stacked child whose parent squash-merged and was released since the merge base (`carryReleasedParent`) is a merge, not a hand resolve. The parent's lines leave the child's `[Unreleased]`; each line the child changed inside them becomes `- Correction to the <version> entry below: "<old>" should read "<new>".` (a dropped line "no longer applies", a line added between the parent's "Addition to the <version> entry below") under the child's own `### Gate integrity`, appended to one it already has. The released entry stays main's byte for byte. Without the merge base the shape is GSTA-67's dropped heading and stays null. `mergeChangelogsWithNotes` returns the carried corrections; the slot one-liner in docs/ship-policy.md prints them. `mergeStep` (train build) passes the merge base, so a child of an already-released parent seats; from slot 2 on, where the prefix already holds an earlier slot's `[Unreleased]` and the sections are unioned, the union takes the carried child, so the gated tree never repeats the released parent's text under the child's heading.
|
||||
|
||||
|
||||
+3
-1
@@ -624,7 +624,9 @@ freezes the same way, since main already holds the unverified squash. The
|
||||
record comes from the one GitHub read that decided to freeze, never a second
|
||||
one: a slot that read MERGED stays merged even when GitHub stops answering
|
||||
before the record is written, and a slot unreadable beside a merged one is
|
||||
kept out of `unmerged` and named in the reason until it can be read. When
|
||||
left out of `unmerged` and named in the reason; nothing reads it again, so
|
||||
the `NEXT:` after the repair omits it and the operator seats it in a train by
|
||||
hand once `gh pr view` answers. When
|
||||
whether a slot merged on GitHub cannot be read, verify refuses
|
||||
(`QUEUE_PROOF_MISSING`, "whether #<n> merged on GitHub could not be read")
|
||||
and asks to be rerun once `gh pr view` answers; an unreadable state is never
|
||||
|
||||
@@ -4352,9 +4352,11 @@ the repair's rebuild never seats it again), main's tree and the backstop run
|
||||
dispatched on main's SHA. The record is built from the one GitHub read that
|
||||
decided to freeze: a slot that read MERGED stays merged even when `gh pr view`
|
||||
stops answering a moment later, and a slot whose state could not be read when
|
||||
another slot had merged is kept out of `unmerged` and named in the reason
|
||||
(`#<n> unreadable on GitHub, kept out of unmerged until read`), so the
|
||||
rebuild after the repair never seats a PR that may already be in. The
|
||||
another slot had merged is left out of `unmerged` and named in the reason
|
||||
(`#<n> unreadable on GitHub, left out of unmerged; seat it in a train by hand
|
||||
once gh pr view answers`), so the rebuild after the repair never seats a PR
|
||||
that may already be in; nothing reads that slot again, so the `NEXT:` after
|
||||
the repair omits it and the operator seats it by hand. The
|
||||
freeze lives in local state and on `refs/notes/gstack-trains`. Ordinary
|
||||
trains and other trains' merges are refused until a repair lands.
|
||||
|
||||
|
||||
@@ -114,8 +114,9 @@ function published(env: TrainEnv, t: TrainRecord, i: number, main: string): { sa
|
||||
* anything remote, then the backstop gate is dispatched on main's sha.
|
||||
* `states` is the one GitHub read of every unverified slot, made by the
|
||||
* caller: a slot that read MERGED is merged, verified tree or not, and a slot
|
||||
* that could not be read is left out of `unmerged` and named in the reason,
|
||||
* so the rebuild after the repair never seats a PR that may already be in.
|
||||
* that could not be read is left out of `unmerged` and named in the reason
|
||||
* (nothing reads it again; the operator seats it by hand once gh answers), so
|
||||
* the rebuild after the repair never seats a PR that may already be in.
|
||||
*/
|
||||
async function freezeQueue(env: TrainEnv, t: TrainRecord, reason: string, mainSha: string, states: ReadonlyMap<string, string>): Promise<FreezeRecord> {
|
||||
const now = nowIso();
|
||||
@@ -127,7 +128,7 @@ async function freezeQueue(env: TrainEnv, t: TrainRecord, reason: string, mainSh
|
||||
schema: 1, kind: 'freeze', repo_key: env.repo.key, train: t.nonce, merged: [...t.merged.map(m => m.pr), ...landed],
|
||||
unmerged: unverified.filter(s => !landed.includes(s.pr) && !unreadable.includes(s.pr)).map(s => ({ pr: s.pr, head: s.head })),
|
||||
main: { sha: mainSha, tree: git(env.ctx.repoRoot, ['rev-parse', `${mainSha}^{tree}`]).out },
|
||||
reason: [reason, landed.length ? `${prs(landed)} merged on GitHub without a verified tree` : '', unreadable.length ? `${prs(unreadable)} unreadable on GitHub, kept out of unmerged until read` : ''].filter(Boolean).join('; '),
|
||||
reason: [reason, landed.length ? `${prs(landed)} merged on GitHub without a verified tree` : '', unreadable.length ? `${prs(unreadable)} unreadable on GitHub, left out of unmerged; seat it in a train by hand once gh pr view answers` : ''].filter(Boolean).join('; '),
|
||||
backstop: null, repair: null,
|
||||
created_at: now, cleared_at: null, cleared_by: null, updated_at: now,
|
||||
};
|
||||
|
||||
@@ -1069,6 +1069,22 @@ describe('verify, freeze, repair and clear', () => {
|
||||
expect(JSON.parse(fs.readFileSync(path.join(fx.state, 'ship-queue', 'freeze', `${repoKey(fx)}.json`), 'utf8'))).toMatchObject({ merged: ['1'], unmerged: [{ pr: '2', head: h2 }] });
|
||||
}, 300_000);
|
||||
|
||||
// Value: protects=a slot unreadable on GitHub beside a merged one is left out of unmerged and named in the reason, so the rebuild after the repair never seats a PR that may already be in; fails_when=freezeQueue files an unreadable slot under unmerged, or drops it without naming it; why_new=the test above keeps slot 2 readable; seam=the fixture's gh pr view vanishing for slot 2; no_claim=the backstop's outcome
|
||||
test("a slot unreadable on GitHub beside a merged one is left out of unmerged and named in the freeze reason", () => {
|
||||
const fx = make();
|
||||
const { nonce } = twoPrTrain(fx);
|
||||
expect(queue(fx, ['train', 'verdict', '--train', nonce]).status).toBe(EXIT.ok);
|
||||
const t = trainJson(fx, nonce);
|
||||
const coord = coordinatorClone(fx.repo);
|
||||
const smuggled = fx.repo.git('show', `${t.prefixes[0].commit}:CHANGELOG.md`).replace('# Changelog\n', '# Changelog\n\n## [1.2.4.0] - 2026-10-18\n\n- #1 change\n- added after the gate\n') + '\n';
|
||||
publishMain(fx, t.prefixes[1].commit, '1.2.4.0', { file: 'CHANGELOG.md', text: smuggled });
|
||||
fx.write(d => { (d.prs['1'] as any).state = 'MERGED'; d.vanish = { '2': 0 }; });
|
||||
const frozen = queue(fx, ['verify', '--pr', '1', '--train', nonce], {}, coord);
|
||||
expect(frozen.status).toBe(EXIT.fail);
|
||||
expect(line(frozen.out, 'freeze:')).toMatch(/; #1 merged on GitHub without a verified tree; #2 unreadable on GitHub, left out of unmerged; seat it in a train by hand once gh pr view answers; unmerged \(none\); repair none admitted$/);
|
||||
expect(JSON.parse(fs.readFileSync(path.join(fx.state, 'ship-queue', 'freeze', `${repoKey(fx)}.json`), 'utf8'))).toMatchObject({ merged: ['1'], unmerged: [] });
|
||||
}, 300_000);
|
||||
|
||||
// Value: protects=a post-merge CHANGELOG difference is excused only when main's parent is T<i-1> modulo stamps, so a foreign commit between the verified head and the squash freezes; fails_when=published() skips or inverts the parent check; why_new=the two tests above reach the CHANGELOG rule only with a parent that is T1 modulo stamps; seam=none; no_claim=a parent differing from T1 in the CHANGELOG alone
|
||||
test("a post-merge CHANGELOG difference is not excused when main's parent is not the verified prefix modulo stamps: the queue freezes naming the parent", () => {
|
||||
const fx = make();
|
||||
|
||||
Reference in new issue
Block a user