Write lane script with pinned merge

Task 5524 · Done · Coder · created · updated

Epic: Merge lane — CP-scheduled verification queue, plan-gated. Planner Task 5517; the full PRD is BA Task 5515 and the full architecture is Architect Task 5516. Read architecture §5, L2, L8, L11, L12 and §4.7 in full with `task_read`. Builds against C3a's committed wire fixture `apps/api/tests/fixtures/merge_lane_wire.json` (the `lane.batch` / `lane.event` contract). Principle (verbatim): "develop only moves to a tree the full suite passed." ## Requirement (work item 5, verbatim) "Lane: `tools/merge_lane.py`, a deterministic script (no model) dispatched to a verification slot through lease-drain, event-driven (a queued PR wakes it, like `task_wake`); at most k in flight. Fetch develop + queued PR heads (SHAs pinned at queue time), merge onto a temp branch, `./dc check full` once; green → `jaah-forge pr merge --sha <tested head>` in batch order; red → sequential bisect (merge the green first half, test the second half on the new base), post the failing tests on the culprit PR and deliver them to its runner's pane; rerun-once of failed tests on the same tree, pass-on-rerun = flake recorded + flake-fix task filed; after merging, fetch develop and verify its tree hash equals the certified one (mismatch → immediate full run on real develop + alert). Report results to the CP over the vocabulary socket; CP records the certificate (tree hash → green)." ## Approach (settled) `tools/merge_lane.py` is stdlib only. It shells out to `git`, `jaah-forge` and `jaah-call`, and runs in the slot's worktree. 1. Call `lane.batch`. If there are no rows, settle `outcome` and exit. 2. Fetch `origin/<integration_branch>` and record `base`. For each row, fetch the PR head (GitHub `pull/<n>/head`; per-forge refspec from `pr view`). - Forge head differs from the pinned SHA → `superseded`. - Merged or closed → `closed`. - `policy.check_mergeable` would refuse → `not_ready`. 3. Build `git checkout -B lane/<batch_id> <base>`, then `git merge --no-ff --no-edit <sha>` for each PR in queue order. - A conflict → `git merge --abort`, report `failed` with `kind: conflict` and the conflicting paths, and drop that PR (L12). 4. Run `gate_full_cmd` once. - Red → rerun the WHOLE gate once on the same tree (L8). Green on the rerun → a `flake` event per parsed id, and the batch is green. 5. Green path: - `git ls-remote origin <ib>` must still equal `base`. Otherwise report `requeue` and exit (D9). - For each PR in order: `jaah-forge pr merge <n> --sha <head>` → `merged`. - A mid-batch refusal → `requeue` for the unmerged rows, then step 6 on what did merge. 6. Verify: fetch, then require `origin/<ib>^{tree}` == the temp branch tree. - Equal → `certified`. - Unequal → `tree_mismatch`, then run the gate on real develop and report `certified` or `develop_red`. 7. Red path, sequential bisect: split into A and B. - Test A on base. Green → merge A (steps 5–6), and base = the new develop. Then test B on the new base. - Recurse to a single culprit PR → `failed` with its tests, plus `jaah-forge pr comment <n> --body-file` listing the tests and the excerpt. - NO `pr merge` for any tree that did not pass. 8. Settle `outcome` with a one-line summary such as "batch 3 PRs: 2 merged, 1 failed". The parser `parse_failures` is tolerant: - pytest `^FAILED (\S+)`; - vitest `^\s*(FAIL|×)\s+(.+)$`; - Playwright `^\s*✘\s+\d+\s+(.+)$`; - `GATE_RESULT ... failed=`; - otherwise the literal `gate`. The excerpt is the last 150 lines, redacted with the existing `transcript_redact` patterns. Forge pin (§4.7), in `apps/jaahd/forge_cli`: `pr merge N [--sha S]`. - If `view`'s `head_sha != S`, exit 2 ("head moved") BEFORE any write. - Otherwise send S as the pin on GitHub (`adapters/github.py:275`), GitLab (`gitlab.py:147`) and Azure (`azure.py:166`). - Bitbucket refuses `--sha` with exit 5. - `check_mergeable` is unchanged. Packaging (L2): add `tools/merge_lane.py` to the jaah package's tool distribution (`tools/portable_standard.py` / packaging) so jaahd's installed tools dir serves it. C3b's `ScriptAdapter` resolves it there. ## Acceptance (verbatim) - AC5.2 Batch run: 1. Fetch develop and the queued PRs' heads, **at the SHAs pinned in their rows**. A PR whose forge head no longer equals its pinned SHA is skipped and marked `superseded`. 2. Merge them, in queue order, onto a temp branch based on develop. 3. Run the project's `gate_full_cmd` **once**. - AC5.3 Green path (script half): - Before merging, the lane re-fetches develop. If develop moved from the tested base, it does **not** merge; it re-batches on the new base (D9). - Otherwise it runs `jaah-forge pr merge --sha <tested head>` for each PR in batch order. - It then fetches develop and asserts `git rev-parse develop^{tree}` equals the certified tree hash. - A mismatch triggers an immediate full run on real develop and an operator alert (D10). - AC5.4 Red path, with sequential bisect: - Split the batch. Test the first half on the base. If it is green, merge it, and then test the second half on the **new** base. Recurse until each red half is one PR. - That PR is the culprit. It is marked `failed`, and its failing test ids and their output excerpts are **posted on the PR** (via `jaah-forge`, by the lane node). - Non-culprit PRs in the batch still merge, each on a tested tree. - **No PR is ever merged on a tree that did not pass.** - AC5.5 Rerun-once (script half): a failing test is rerun once on the same tree. If it passes on the rerun, it is a **flake** (reported with test id and tree hash) and the batch counts as green. If the test fails twice, it is a real failure (AC5.4). - AC5.7 **Integration oracle (Done-means):** - Setup: a fixture repo + fake forge, two concurrent PRs: PR-A green, and PR-B breaking one test. - Expected: PR-A merges. PR-B is `failed` with the failing test listed on the PR and delivered. Develop's history never contains a commit whose tree includes PR-B's change. `develop^{tree}` after the run equals the certificate's tree hash. - Flake variant: a test that fails on its first run and passes on the rerun ends with both PRs merged and one flake row recorded. Credentials & reach: none. The fixture-integration tests use local git fixture repos, a fake `jaah-forge` and a fake `jaah-call` (verified; worktree-local). Out of scope: - Epic (verbatim): "GitHub merge queue / merge_group; any CP-side forge calls; changing the manual develop → master promotion; cgroup tuning of GHA runners; squash merges." - BA-derived (verbatim): "No bypass of forge protections: the lane does not merge a PR whose forge-required checks or reviews are unmet, and it uses no admin/bypass merge." - Do NOT: - use squash or rebase merges; - change `check_mergeable`; - add Bitbucket lane support (deferred); - add per-test reruns; - add CP routes or rows (C3a); - change dispatch or `ScriptAdapter` (C3b); - edit CI workflows. Done means: - AC5.7 — the integration test, using a fixture repo, a fake forge and a fake `jaah-call` that loads C3a's `merge_lane_wire.json`: - PR-A is merged; - PR-B gets a `failed` event with the broken test id and a `pr comment` on the fake forge; - `git log --format=%T` of develop never contains a tree with PR-B's change; - the final `develop^{tree}` equals the `certified` event's tree; - the flake variant ends with both merged and exactly one `flake` event. - AC5.3 — tests: - develop moved between test and merge gives `requeue` and no `pr merge` call; - forced tree inequality gives `tree_mismatch`, then a real-develop run, then `certified` or `develop_red`. - AC5.2 — tests: - a moved forge head gives `superseded` and is excluded; - a not-mergeable PR gives `not_ready`; - the gate is invoked exactly once on an all-green batch. - L12 — a conflicting PR gives `failed` with `kind: conflict`, and the rest of the batch proceeds. - AC5.4 — a 4-PR batch with one culprit: - the 3 innocents merge, each on a tested tree; - the fake forge records zero `pr merge` calls for any untested tree. - `parse_failures` unit fixtures for pytest, vitest, Playwright, `GATE_RESULT` and the fallback. - §4.7 — `jaah-forge` tests: - `--sha` mismatch exits 2 with no write; - a match sends the pin on GitHub, GitLab and Azure; - Bitbucket exits 5; - `pr merge` without `--sha` is unchanged. - L2 — the packaging test or manifest shows `merge_lane.py` in the distributed tools. Required checks: gate, review-lane

Outcome

Built tools/merge_lane.py (batch, bisect, rerun-once flakes, tree cert, bwrap-fenced gate) + jaah-forge pr merge --sha pin. Full gate green; PR #4829 merged; Jaah Deploy ok. Deferred: GitHub strict up-to-date protection blocks batch PR 2+.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)