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

A pull opens on the origin forge in one action, and its check and review state come back here #8990

Merged opened this

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

Merged

A pull proposed from a fork of a mirrored repository now opens on the origin forge, and its check and reviews come back to our pull page. `tsc`, `scripts/gate.ts` and the related test files pass. I did not run the whole suite, since `scripts/fast.ts` named 1521 of 1996 files; I ran the origin, upstream, schema, OpenAPI and pull-API suites instead.

shipped production is serving it deploy 4915f35

opened this

A pull proposed from a fork of a mirrored repository now opens on the origin forge, and its check and reviews come back to our pull page. tsc, scripts/gate.ts and the related test files pass. I did not run the whole suite, since scripts/fast.ts named 1521 of 1996 files; I ran the origin, upstream, schema, OpenAPI and pull-API suites instead.

What was already there. "Propose upstream" on a mirror's fork already carried the branch to the origin and opened the pull there, through proposeUpstream, openPull and carryBranchThrough. I kept that path unchanged. What was missing was everything coming back.

What changed

  • Check state: origins/pull-check.ts re-reads one pull from the origin (the pull, its review verdicts, its check) and stores the check word in a new origin_pull_check table. The migration is pinned.
  • Providers: Provider gains an optional getPullCheck and watch.

- A forge origin answers with its own check word, which I added to GET …/pulls/:n. - A Gitea/Forgejo origin answers with the head's combined commit status. - A forge origin's reviews now import. ForgeProvider had been inheriting Gitea's review parsing, which does not fit the forge's review-states route.

  • Webhook: origins/hook.ts adds POST /api/v1/repos/:owner/:name/origin-hook. It is signed with x-forge-signature-256, using a secret derived from the forge key and the repository, so nothing is stored. A delivery only says "look again": the named pull is re-read from the origin. Every refusal is the same 404.
  • Registration: after a pull opens on a forge origin, openPull asks the origin once to deliver to that route. This is best effort, and it needs this forge's url to be https and a token with admin scope on the origin.
  • Poll: each sync sweep re-reads the newest 20 open pulls. I added this because a check finishing does not move a pull's updated time, so the incremental sync would never see it.
  • Pull page: it shows "Check on owner/name: passing / failing / running / has not run, as of …". It says "waiting for the new head" when the stored check is for an older head.
  • Docs and contract: docs/features/origin-pull-state.md and an OpenAPI row for the new route.

Test. test/origin-pull-state.test.ts runs two real forges and walks through:

  1. Propose from a fork; the pull opens on the origin with head alice/main, and the origin holds the registered hook.
  2. A wrongly signed delivery gets a 404; a correctly signed one brings the green check and a change-request review.
  3. The poll catches a check that went red with no webhook.
  4. With the origin stopped, the last word stands and the pull page still renders; after it returns, the next sync catches up.
  5. An unsigned delivery, or one for a repository with no origin, gets the same 404.

Not done

  • GitHub checks are not read. They use the check-runs API, which this does not cover, so a GitHub mirror shows no origin check.
  • The test cannot deliver a real webhook from the origin. Outbound hooks refuse loopback addresses, so it posts the signed delivery itself. It does confirm the hook was registered.
  • The reviewer in the test is the pull's own author on the origin, so it uses a change request; the origin refuses a self-approval.

Try it. Mirror a repository from another forge, fork it, commit to the fork, and press "Propose upstream" on the Following card. Then open the pull on our side, or run bun test test/origin-pull-state.test.ts.

🤖 Generated with Claude Code

Closes #8965

commented

A pull proposed from a fork of a mirrored repository now opens on the origin forge, and its check and reviews come back to our pull page. tsc, scripts/gate.ts and the related test files pass. I did not run the whole suite, since scripts/fast.ts named 1521 of 1996 files; I ran the origin, upstream, schema, OpenAPI and pull-API suites instead.

What was already there. "Propose upstream" on a mirror's fork already carried the branch to the origin and opened the pull there, through proposeUpstream, openPull and carryBranchThrough. I kept that path unchanged. What was missing was everything coming back.

What changed

  • Check state: origins/pull-check.ts re-reads one pull from the origin (the pull, its review verdicts, its check) and stores the check word in a new origin_pull_check table. The migration is pinned.
  • Providers: Provider gains an optional getPullCheck and watch.

- A forge origin answers with its own check word, which I added to GET …/pulls/:n. - A Gitea/Forgejo origin answers with the head's combined commit status. - A forge origin's reviews now import. ForgeProvider had been inheriting Gitea's review parsing, which does not fit the forge's review-states route.

  • Webhook: origins/hook.ts adds POST /api/v1/repos/:owner/:name/origin-hook. It is signed with x-forge-signature-256, using a secret derived from the forge key and the repository, so nothing is stored. A delivery only says "look again": the named pull is re-read from the origin. Every refusal is the same 404.
  • Registration: after a pull opens on a forge origin, openPull asks the origin once to deliver to that route. This is best effort, and it needs this forge's url to be https and a token with admin scope on the origin.
  • Poll: each sync sweep re-reads the newest 20 open pulls. I added this because a check finishing does not move a pull's updated time, so the incremental sync would never see it.
  • Pull page: it shows "Check on owner/name: passing / failing / running / has not run, as of …". It says "waiting for the new head" when the stored check is for an older head.
  • Docs and contract: docs/features/origin-pull-state.md and an OpenAPI row for the new route.

Test. test/origin-pull-state.test.ts runs two real forges and walks through:

  1. Propose from a fork; the pull opens on the origin with head alice/main, and the origin holds the registered hook.
  2. A wrongly signed delivery gets a 404; a correctly signed one brings the green check and a change-request review.
  3. The poll catches a check that went red with no webhook.
  4. With the origin stopped, the last word stands and the pull page still renders; after it returns, the next sync catches up.
  5. An unsigned delivery, or one for a repository with no origin, gets the same 404.

Not done

  • GitHub checks are not read. They use the check-runs API, which this does not cover, so a GitHub mirror shows no origin check.
  • The test cannot deliver a real webhook from the origin. Outbound hooks refuse loopback addresses, so it posts the signed delivery itself. It does confirm the hook was registered.
  • The reviewer in the test is the pull's own author on the origin, so it uses a change request; the origin refuses a self-approval.

Try it. Mirror a repository from another forge, fork it, commit to the fork, and press "Propose upstream" on the Following card. Then open the pull on our side, or run bun test test/origin-pull-state.test.ts.

🤖 Generated with Claude Code

commented

The check failed because no commit message had a "Try:" line, and I've fixed that. I added an empty commit that has one and pushed it. scripts/gate.ts now passes. I made no code changes, so I did not re-run the tests.

The line reads: on a mirror's fork press Propose upstream, then open the new pull at /<owner>/<repo>/pulls/<n> and read its "Check on …" line.

The rest of the pull is as described before: a pull proposed from a fork of a mirror opens on the origin, and its check and reviews come back on the origin's signed webhook or the sync poll.

commented

The two tests that failed the check weren't broken by this pull. They were flaky tests on main, which #8995 fixed after this branch was cut.

  • Snapshot-destination test ("a bad endpoint is refused in the page's words…"): it failed the same way on the base commit before my change, so my change didn't cause it.
  • Repository-pages test ("the repository, issues and pulls pages are lighter…"): it passed when I ran it alone here. I couldn't reproduce the failure.

I merged origin/main into the branch, which brings in the #8995 fix. After the merge, test/tui-backups.test.ts, test/repo-head-quiet.test.ts and test/origin-pull-state.test.ts pass (15 tests), and bun x tsc --noEmit is clean. I pushed the merge. I didn't run scripts/gate.ts or the full suite; the forge's check does that on the push.

What the pull does (#8965): a pull proposed to an origin forge now brings the origin's check and reviews back, on the origin's webhook or the poll. The pull page shows a "Check on …" line when the origin has reported. The new code is in src/origins/ (hook.ts, pull-check.ts), with a migration, an API route, docs/features/origin-pull-state.md and test/origin-pull-state.test.ts.

To try it on the preview: on a mirror's fork, press Propose upstream, open the new pull at /<owner>/<repo>/pulls/<n> and read its "Check on …" line.

Changes requested — before the latest push

Round 1, all changes in one list. The mechanism is sound: the signature is verified over the bytes actually read, refusals are a uniform 404, and the secret is derived rather than stored. Four things block it.

  1. Both doors. The pull page now shows the origin check line, but there is no terminal screen for it. Show the same "Check on <origin>: <word>, as of …" line on the pull screen in src/tui/, entered where AGENTS.md says, and test it. Link the new screen from docs/features/origin-pull-state.md.
  2. Design system. origin-check and data-check are new hand-rolled markup. Use the named components and tokens in docs/DESIGN.md, or document the new class in docs/design/areas/<area>.md. Run bun scripts/design-lint.ts.
  3. Amplification. A signed delivery with no pull number, and the hook is registered for every event (kinds: "*"), runs refreshOpenPulls: up to 20 pulls at 3–4 origin requests each, sequentially, per delivery. The sync sweep does the same every cycle for every mirror. Bound this: re-read only the named pull on a delivery and ignore deliveries that name none. In the sweep, only re-read pulls whose check is not settled, or cap the requests per sweep.
  4. Latency. openPull awaits watchPulls (two origin round-trips) before answering the user. Fire it off the request path, with a timeout.

Smaller points:

  • src/origins/pull-check.ts:6 refers to http/origin_hook.ts, which does not exist. It is origins/hook.ts.
  • The test title says the pull opens "under the author" but asserts only the branches. Assert the author on the origin, since box 1 of #8965 says "keeping the author".
  • The check field added to GET …/pulls/:n changes the API contract. Add it to the OpenAPI response schema.
  • The webhook delivery path is only exercised by a hand-posted request. State that in the docs.
commented

A pull proposed from a fork of a mirror now opens on the origin forge in one action, and the origin's check and reviews come back here. This round answers sentinel's four blocking points and the smaller ones. The gate, tsc, the design lint and the tests that cover the change pass. I did not run the full suite or the fast.ts selection (it named 1542 of 1998 files), so the forge's check does that.

What changed

  • Both doors. The terminal pull screen shows "Check on bob/app: passing, as of 2026-11-03 10:15 UTC" as one row in the merge box. The web page and the terminal build the first half of the line from one function, originCheckLead in origins/pull-check.ts. The new test is test/tui-pull-origin-check.test.ts. docs/features/origin-pull-state.md now mentions the terminal screen and that test, but it does not link to a screen file.
  • Design system. I dropped the origin-check class and the data-check attribute. The web line is now <p class="muted">…, as of ${time(…)}</p>, using the existing time() helper. No new class exists. The design lint is clean.
  • Delivery amplification. A signed delivery that names no pull is accepted and ignored. One that names a pull re-reads only that pull.
  • Sweep amplification. A sweep re-reads at most 20 pulls per mirror.

- Pulls with an unsettled check are re-read: never read, running, not yet run, or for an older head. - Settled pulls are re-read only if they haven't been read for an hour. - The least recently read go first.

  • Latency. openPull no longer waits for the hook registration. It runs it detached through watchPullsSoon, which gives up after 10 s and never rejects.
  • Smaller points.

- I corrected the path in the pull-check.ts comment to origins/hook.ts. - The test now fetches the commit on the origin's alice/main and asserts its author is alice. - I added check to the GET …/pulls/:n OpenAPI reply schema. - The docs now say the webhook path is tested only by a hand-posted, correctly signed request, with no real origin delivery yet.

Behaviour change to check

  • A check re-run on the same head stays unseen by the poll for up to an hour unless the webhook delivers it. That is the cost of bounding the sweep. I changed two tests to match: the poll and origin-down tests now backdate checked_at.

What I chose not to do

  • GitHub's checks are still not read.
  • The sweep has no hard cap on origin requests beyond the 20-pull bound.

How to try it

  • On a mirrored repository, use "Propose upstream" from a fork. The pull opens on the origin. After the origin reports, the pull page and the terminal pull screen show the check line.
  • bun test test/origin-pull-state.test.ts test/tui-pull-origin-check.test.ts

🤖 Generated with Claude Code

Approved

Round 2. All four blocking asks are met. (1) Both doors: the terminal pull screen shows the same "Check on …" line through the shared originCheckLead, with test/tui-pull-origin-check.test.ts. (2) Design: no new class; it is a muted p with time(). (3) Amplification: a delivery that names no pull is ignored, a named one re-reads one pull, and the sweep is bounded to 20 unsettled or hour-old pulls. (4) Latency: watchPullsSoon runs detached with a 10 s timeout and never rejects. The smaller points are also done: comment path, author assertion, OpenAPI reply, and the docs note on the hand-posted delivery. Non-blocking: the docs do not link a screen file, and a check re-run on the same head can stay unseen by the poll for up to an hour without the webhook.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs