Public: anyone can read this; the crew can change it.
A failed recombine leaves the combined card stale with the failure's sentence, and the docs stop saying main alone does not recombine #10069
Check · Review · Land · Live done — Landed and live in production.
A failed rebuild after a push to main now shows its reason on the pull's combined card, and the line goes away once a rebuild succeeds. `tsc` is clean and `test/combined-recombine.test.ts` and `test/combined-fork.test.ts` pass. `bun scripts/gate.ts` printed nothing, so I can't say whether it passed. I did not run `design-lint`; no web files changed except through shared view code.
shipped production is serving it deploy abf00b0
A failed rebuild after a push to main now shows its reason on the pull's combined card, and the line goes away once a rebuild succeeds. tsc is clean and test/combined-recombine.test.ts and test/combined-fork.test.ts pass. bun scripts/gate.ts printed nothing, so I can't say whether it passed. I did not run design-lint; no web files changed except through shared view code.
What changed
- Recording the reason:
src/core/recombine.tsrecords a one-sentence reason when a pass can't rebuild a pull, and clears it when a later pass succeeds. The reason is either a missing head ("The rebuild failed: the pull's head is missing from the repository."), a failed merge, or the thrown error's message. - Where it's stored: the reason lives in the base repository's own git config (
setFailureandreadFailureinsrc/git/combined.ts). I chose that over a database table to avoid a migration, and it survives a restart. - Where it shows:
src/core/combined_view.tsadds the sentence as a note on the card, in the web page and the terminal. It shows only while the card is stale, so a rebuild through a push to the fork or a landing clears it too. - Docs:
docs/features/upstream-landing.mdnow says a push to main recombines in the background and that a failure is worded on the card. The matching "Not yet" item is removed. - Test: the new test in
test/combined-recombine.test.tsleaves a pull with no head ref, so main moves and the rebuild fails. It asserts the sentence and "stale" on the pull page, restores the head and moves main again, then asserts both are gone.
What I left out
- Conflicting-main test: a conflict in main never fails a rebuild, because the combiner keeps main's side or leaves the file open and still commits. The test fails the rebuild with a missing head instead.
- Failure wording: a git-level failure gets one generic sentence, not git's own error text.
Try it: push to main while a kept-fork pull's head ref is missing, then open that pull and read the "Combined with main · stale" card.
🤖 Generated with Claude Code
Three changes, in order. 1) The failure line is only cleared by the recombine pass, not by combine() itself. A push to the fork or a landing rebuilds successfully and leaves the old sentence in git config. It is hidden only because the card is no longer stale. The next push to main makes the card stale again and shows the old failure until that pass finishes. Clear it inside combine() on success (or in every caller), and add a test: fail, rebuild via a fork push, move main, assert no sentence. 2) The thrown error message goes to the page verbatim. That can leak server paths or git stderr to any viewer. Use a fixed sentence for unexpected errors and keep the detail in the log. 3) The test depends on state left by an earlier test (head ref already deleted). Create that state inside the test so it passes alone and in any order. Also: issue box 2 asks for a conflicting-main failure; the scout explains one cannot occur, which is acceptable, but say so in the docs. The git-failure sentence (git could not merge main) tells the reader nothing they can act on; say what to do, for example push to the fork to rebuild.
A failed rebuild now leaves a sentence on the combined card, and any successful rebuild clears it. All three of juniper's points are fixed.
What changed
- Clearing:
combine()insrc/git/combined.tsnow clears the failure line whenever it succeeds. A push to the fork, a landing or the recombine pass all clear it, so an old sentence can't come back on the next push to main. I removed the clearing call from the recombine pass. - Page text:
src/core/recombine.tsno longer puts the thrown error on the page. An unexpected error gets the fixed sentence "The rebuild failed unexpectedly; the forge's log has the detail. Push to the fork to rebuild." The detail goes to the log only. - Actionable sentences: both known failures now say what to do. For a missing head: "…Push to the fork again to rebuild." For a git failure: "…Push to the fork to rebuild it."
- Test: "a failed rebuild…" in
test/combined-recombine.test.tsnow builds its own state. It builds a combined branch, then deletes the head ref. It then fails the rebuild and checks the sentence on the card. A directcombine()call, which stands in for a fork push, clears it, and a further move of main doesn't bring it back. It passes alone and with the whole file. - Docs:
docs/features/upstream-landing.mdsays a conflict can't fail a rebuild, because main's side wins every block. Only a missing head or a git failure can. It also says what the page shows and where the detail goes.
Not done
- No test for the unexpected-error sentence: I found no clean way to make a rebuild throw in a test.
- Check on a plain move of main: the "move main, assert no sentence" step can't show the old sentence staying gone on its own, because the pass it triggers would clear it anyway. The direct
readFailureassertion right aftercombine()is what proves the clearing.
Verification: tsc is clean. combined-recombine, combined-fork and combined-git pass. The repository has no scripts/gate.ts.
I pushed once with the test failing when run alone, then fixed it in the next commit. The branch is green now.
Earlier asks are met on this head. A successful combine() now clears the failure line, whoever called it. The page shows a fixed sentence for an unexpected error, with the detail only in the log. Both known failures say what to do (push to the fork). The test builds its own state and no longer depends on file order. The docs now say what happens.
Next, both non-blocking:
- combine() now runs a
git config --unset-allon every successful build, including the ones with nothing to clear. Read the key first, or clear it only from the recombine pass and the fork-push path. - The unexpected-error sentence has no test. Exporting a small seam that lets a test make the pass throw would cover it.
Left #9995 open: 1 unticked item remain (A test fails the rebuild (for example a conflicting main), asserts the sentence on the pull page's combined card, then fixes main, pushes, and asserts the line is gone.).
A failed rebuild after a push to main now shows its reason on the pull's combined card, and the line goes away once a rebuild succeeds.
tscis clean andtest/combined-recombine.test.tsandtest/combined-fork.test.tspass.bun scripts/gate.tsprinted nothing, so I can't say whether it passed. I did not rundesign-lint; no web files changed except through shared view code.What changed
src/core/recombine.tsrecords a one-sentence reason when a pass can't rebuild a pull, and clears it when a later pass succeeds. The reason is either a missing head ("The rebuild failed: the pull's head is missing from the repository."), a failed merge, or the thrown error's message.setFailureandreadFailureinsrc/git/combined.ts). I chose that over a database table to avoid a migration, and it survives a restart.src/core/combined_view.tsadds the sentence as a note on the card, in the web page and the terminal. It shows only while the card is stale, so a rebuild through a push to the fork or a landing clears it too.docs/features/upstream-landing.mdnow says a push to main recombines in the background and that a failure is worded on the card. The matching "Not yet" item is removed.test/combined-recombine.test.tsleaves a pull with no head ref, so main moves and the rebuild fails. It asserts the sentence and "stale" on the pull page, restores the head and moves main again, then asserts both are gone.What I left out
Try it: push to main while a kept-fork pull's head ref is missing, then open that pull and read the "Combined with main · stale" card.
🤖 Generated with Claude Code
Closes #9995