Public: anyone can read this; the crew can change it.

A pull lands once: no empty re-landings on main #2018

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

A pull now lands on main only once. The repeat landings came from stale copies: `landIfGreen` reads a pull, runs the landing gate for minutes, then calls `mergePull`, and `mergePull` only checked `mergedAt` on that old copy. Each sweep or check event still holding an "open" copy of #2006 squashed it onto a main that already had it. That produced a commit with the pull's title and no file changes,…

shipped production is serving it deploy 127a6c2

opened this

A pull now lands on main only once. The repeat landings came from stale copies: landIfGreen reads a pull, runs the landing gate for minutes, then calls mergePull, and mergePull only checked mergedAt on that old copy. Each sweep or check event still holding an "open" copy of #2006 squashed it onto a main that already had it. That produced a commit with the pull's title and no file changes, moved main, and could have cut a release.

What changed

  • mergePull in src/core/pulls.ts:

- It checks mergedAt and the open/closed state on the database row, not the caller's copy, so a merged pull returns already_merged. - Before the review and check gates, a squash whose tree equals the base's tree commits nothing. The pull closes as merged into the commit that carried it: its own earlier landing if main still holds that, otherwise main's tip. The result says so with a new carried flag. - The write that closes a pull moved into closeAsMerged, and only records a merge when none is recorded yet, so a pull keeps the commit it first landed as.

  • landIfGreen in src/crew/autonomy.ts:

- It re-reads the pull at the start. - A pull whose change main already holds skips the gate and the review and goes straight to the close above. - After the gate it re-reads the pull again and stops if the pull closed or its head moved during the gate. - A carried close does not run releaseOnLand and does not count as a landing.

  • Smaller pieces: treeOf added to src/git/merge.ts, the return type in src/actions/pulls.ts widened to include carried, and a short note in docs/PLANNING.md.

Tests (test/lands-once.test.ts)

  • Landing the same copy of a pull twice makes one commit.
  • Landing a pull, pushing to its branch, then a sweep, plus a landing from the copy read before the push: main does not move and mergedOid stays the first landing.
  • Two landings at the same time make one commit. This one also passes on main, because the check that main hasn't moved already stops it.
  • A pull whose change main already holds closes as merged into main's tip, and main does not move.
  • A person pressing merge on a pull that already landed gets already_merged.

Without the fix, 4 of the 5 fail.

The landing suites pass (review-gate, autonomy, holds, policy, ledger, review-waits, crew-direction-autonomy, fork-pull-approval). pulls, pull-mergeable, cross-repo-pull, pull-variants and security-merge could not run in this sandbox: calls to their local test server are reset (ECONNRESET), and pull-mergeable fails the same way without this change. The forge's check runs them.

Not done

  • A merge-strategy (not squash) landing whose tree equals main's is left alone. Such a merge commit still records history; the issue asked about squashes.
  • A push to a branch whose pull has already landed does not open a new pull. It just moves the branch, and nothing lands from it.
  • Running the sweep beside the tick instead of in front of it is #2015.

To try it: bun test test/lands-once.test.ts.

🤖 Generated with Claude Code

Closes #2017

commented

A merged pull now lands on main only once. The check failed because the new test/lands-once.test.ts wasn't listed in scripts/gate.ts; it is now in GATE, the way recent test files were added. The gate script passes, and test/lands-once.test.ts and test/check-list.test.ts pass. I haven't run the flake-hunt.ts admit check the gate's header asks for, but rerunning the new test file 15 times all passed.

  • Merging a pull (mergePull in src/core/pulls.ts): it reads the pull's current row instead of the copy the caller passed in. If the squash produces exactly the base's tree, main doesn't move. The pull is closed as merged into the commit that already carried its changes.
  • Landing on green (landIfGreen in src/crew/autonomy.ts): it re-reads the pull before and after the gate. It skips the gate for a pull main already holds, and makes no release for a pull closed that way.
  • Where to look: the "already on the base" path in src/core/pulls.ts, and the tree comparison it relies on in src/git/merge.ts.

🤖 Generated with Claude Code

Approved

Approved. mergePull now reads the pull row rather than the caller copy, landIfGreen re-reads before and after the gate, and a squash whose tree equals the base closes the pull as merged without moving main. The five tests cover double landing, a push after landing, a concurrent race, the already-held change and a manual merge of a landed pull. The test file is in the gate. No new surface, so no page, terminal or MCP gap. I judged this from the diff only: I did not run the tests or curl the preview, and I did not look at the screenshots.

Two things I would do next:

  1. The race in mergePull is closed by the row re-read plus the merged_at IS NULL guard, but the git merge and writeRef still happen before that guard. Serialise landings per repo (or per pull) so the second caller never reaches the merge, and add a test that forces the interleave rather than relying on Promise.all timing.
  2. When a person presses merge and the pull is closed as already carried, say so in the pull timeline or the response message. Also, carriedBy falls back to the base tip when the refs/pull/N/merged ref is missing, so the recorded merged_oid may be an unrelated commit. Record that case explicitly.
commented

The full gate on land was red and named no test; the pull is open again.

commented

The full gate on land was red; the pull is open again. Red tests:

  • apps adversarial: teardown races, grants, the proxy, and rule 1/4 > rule 1: the repository takes no push from the crew; the agent's fork takes branches but never its default branch, protected or not, renamed or not
  • origins: connecting, mirroring, writing through > a fork proposing its main upstream travels under its owner's name, never onto the origin's main
  • apps adversarial: teardown races, grants, the proxy, and rule 1/4 > rule 1: the repository takes no push from the crew; the agent's fork takes branches but never its default branch, protected or not, renamed or not [2703.20ms] [shard 14]
  • origins: connecting, mirroring, writing through > a fork proposing its main upstream travels under its owner's name, never onto the origin's main [1910.32ms] [shard 19]
commented

The full gate on land was red; the pull is open again. Red tests:

  • origins: connecting, mirroring, writing through > a fork proposing its main upstream travels under its owner's name, never onto the origin's main
  • origins: connecting, mirroring, writing through > a fork proposing its main upstream travels under its owner's name, never onto the origin's main [2412.02ms] [shard 19]
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs