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

A pull opened on the Forgejo mirror reaches trunk as a real pull with its author, and the mirror stays a plain copy #9039

Merged opened this

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

Merged

A pull opened on the Forgejo mirror (`trevato-apps/forge`) now becomes a pull on this repository by itself. It carries its author's name and a link back, and it stays a guest pull unless the author is a trusted source. `test/mirror-pulls.test.ts` runs it against the fake Forgejo and passes; it has not been tried against the real mirror yet.

shipped production is serving it deploy 9b150b4

opened this

A pull opened on the Forgejo mirror (trevato-apps/forge) now becomes a pull on this repository by itself. It carries its author's name and a link back, and it stays a guest pull unless the author is a trusted source. test/mirror-pulls.test.ts runs it against the fake Forgejo and passes; it has not been tried against the real mirror yet.

What changed

  • New src/origins/mirror-pulls.ts: this runs for any repository that pushes to a read-only mirror. It reads the mirror's open pulls, fetches each head (refs/pull/<n>/head) into the branch mirror-pull/<n>, and opens it as a pull here under the account that connected the origin.

- The pull records the author (origin_author) and the mirror pull's URL (origin_html_url), and its description starts with From @author on <url>. - One comment on the mirror pull links to the pull here. - If the head moves on the mirror, the pull here follows while it is open.

  • Event and sweep: the mirror is asked once per boot for a webhook on its pull events (watch, newly added to the Forgejo provider). The existing /api/v1/repos/:owner/:name/origin-hook route now also accepts Forgejo's X-Forgejo-Signature / X-Gitea-Signature, and the signature is checked against the repository's derived secret. A sweep every five minutes picks up anything the webhook missed. It is started in src/main.ts next to the existing mirror push.
  • No double copies: the event and the sweep both look pulls up by the mirror pull's URL and run one at a time per repository. There is also a re-check after the fetch.
  • Guest vs trusted (src/origins/guest.ts): guestWaits now also checks foldsIn. A mirror pull from a trusted source on the mirror's host (recorded in pull_source) skips the guest wait for as long as the trust lasts. The guest notice on the pull page (src/web/pages/pull-guest.ts) uses the same check.
  • Nothing pushed to the mirror: the existing mirror push already sends only trunk's refs, and mirror-pull/* branches are never among them. The test checks that the only write the mirror sees is the comment.

What to look at

  • Should a pull from a trusted source skip the guest wait? That's the one real behaviour change, and it also applies to existing guest pulls whose source is recorded and trusted.
  • Registering the webhook needs admin rights on the mirror. If the connected account lacks them, only the sweep runs, so a new pull takes up to five minutes to show up.

Left out

  • There is no database-level unique index on the URL, so there's no migration; the per-repository queue and the re-check do that job.
  • A pull closed or merged on the mirror is not mirrored back here.
  • Comments and reviews on the mirror pull are not carried over.
  • At most 5 pages of open pulls are read per sweep.

Checks: bun scripts/gate.ts and bun x tsc --noEmit are clean. scripts/fast.ts named 1513 files (too broad), so I ran the covering tests: the new mirror-pulls test, guest-pull, origin-pull-state, mirror-out, fork-flow, propose-via-fork, trusted-fold, trusted-sources and the three forge-origin suites. All 113 pass. test/fake-origin.ts gained a Forgejo /hooks route for the webhook test. The issue asks for a plan kept with a keep_plan tool, but that tool wasn't available in this run, so no plan was posted.

Try it: bun test test/mirror-pulls.test.ts.

🤖 Generated with Claude Code

Closes #9011

commented

A pull opened on the Forgejo mirror (trevato-apps/forge) now becomes a pull on this repository by itself. It carries its author's name and a link back, and it stays a guest pull unless the author is a trusted source. test/mirror-pulls.test.ts runs it against the fake Forgejo and passes; it has not been tried against the real mirror yet.

What changed

  • New src/origins/mirror-pulls.ts: this runs for any repository that pushes to a read-only mirror. It reads the mirror's open pulls, fetches each head (refs/pull/<n>/head) into the branch mirror-pull/<n>, and opens it as a pull here under the account that connected the origin.

- The pull records the author (origin_author) and the mirror pull's URL (origin_html_url), and its description starts with From @author on <url>. - One comment on the mirror pull links to the pull here. - If the head moves on the mirror, the pull here follows while it is open.

  • Event and sweep: the mirror is asked once per boot for a webhook on its pull events (watch, newly added to the Forgejo provider). The existing /api/v1/repos/:owner/:name/origin-hook route now also accepts Forgejo's X-Forgejo-Signature / X-Gitea-Signature, and the signature is checked against the repository's derived secret. A sweep every five minutes picks up anything the webhook missed. It is started in src/main.ts next to the existing mirror push.
  • No double copies: the event and the sweep both look pulls up by the mirror pull's URL and run one at a time per repository. There is also a re-check after the fetch.
  • Guest vs trusted (src/origins/guest.ts): guestWaits now also checks foldsIn. A mirror pull from a trusted source on the mirror's host (recorded in pull_source) skips the guest wait for as long as the trust lasts. The guest notice on the pull page (src/web/pages/pull-guest.ts) uses the same check.
  • Nothing pushed to the mirror: the existing mirror push already sends only trunk's refs, and mirror-pull/* branches are never among them. The test checks that the only write the mirror sees is the comment.

What to look at

  • Should a pull from a trusted source skip the guest wait? That's the one real behaviour change, and it also applies to existing guest pulls whose source is recorded and trusted.
  • Registering the webhook needs admin rights on the mirror. If the connected account lacks them, only the sweep runs, so a new pull takes up to five minutes to show up.

Left out

  • There is no database-level unique index on the URL, so there's no migration; the per-repository queue and the re-check do that job.
  • A pull closed or merged on the mirror is not mirrored back here.
  • Comments and reviews on the mirror pull are not carried over.
  • At most 5 pages of open pulls are read per sweep.

Checks: bun scripts/gate.ts and bun x tsc --noEmit are clean. scripts/fast.ts named 1513 files (too broad), so I ran the covering tests: the new mirror-pulls test, guest-pull, origin-pull-state, mirror-out, fork-flow, propose-via-fork, trusted-fold, trusted-sources and the three forge-origin suites. All 113 pass. test/fake-origin.ts gained a Forgejo /hooks route for the webhook test. The issue asks for a plan kept with a keep_plan tool, but that tool wasn't available in this run, so no plan was posted.

Try it: bun test test/mirror-pulls.test.ts.

🤖 Generated with Claude Code

commented

A pull opened on the Forgejo mirror (trevato-apps/forge) now becomes a pull on this repository by itself. It carries its author's name and a link back, and it stays a guest pull unless the author is a trusted source. test/mirror-pulls.test.ts runs it against the fake Forgejo and passes; it has not been tried against the real mirror yet.

What changed

  • New src/origins/mirror-pulls.ts: this runs for any repository that pushes to a read-only mirror. It reads the mirror's open pulls, fetches each head (refs/pull/<n>/head) into the branch mirror-pull/<n>, and opens it as a pull here under the account that connected the origin.

- The pull records the author (origin_author) and the mirror pull's URL (origin_html_url), and its description starts with From @author on <url>. - One comment on the mirror pull links to the pull here. - If the head moves on the mirror, the pull here follows while it is open.

  • Event and sweep: the mirror is asked once per boot for a webhook on its pull events (watch, newly added to the Forgejo provider). The existing /api/v1/repos/:owner/:name/origin-hook route now also accepts Forgejo's X-Forgejo-Signature / X-Gitea-Signature, and the signature is checked against the repository's derived secret. A sweep every five minutes picks up anything the webhook missed. It is started in src/main.ts next to the existing mirror push.
  • No double copies: the event and the sweep both look pulls up by the mirror pull's URL and run one at a time per repository. There is also a re-check after the fetch.
  • Guest vs trusted (src/origins/guest.ts): guestWaits now also checks foldsIn. A mirror pull from a trusted source on the mirror's host (recorded in pull_source) skips the guest wait for as long as the trust lasts. The guest notice on the pull page (src/web/pages/pull-guest.ts) uses the same check.
  • Nothing pushed to the mirror: the existing mirror push already sends only trunk's refs, and mirror-pull/* branches are never among them. The test checks that the only write the mirror sees is the comment.
  • Gate fix: an empty follow-up commit carries the Try: line the gate requires for a pull that touches src/http/api.ts and src/web/pages/pull-guest.ts. The code is unchanged from the first push.

What to look at

  • Should a pull from a trusted source skip the guest wait? That's the one real behaviour change, and it also applies to existing guest pulls whose source is recorded and trusted.
  • Registering the webhook needs admin rights on the mirror. If the connected account lacks them, only the sweep runs, so a new pull takes up to five minutes to show up.

Left out

  • There is no database-level unique index on the URL, so there's no migration; the per-repository queue and the re-check do that job.
  • A pull closed or merged on the mirror is not mirrored back here.
  • Comments and reviews on the mirror pull are not carried over.
  • At most 5 pages of open pulls are read per sweep.

Checks: bun scripts/gate.ts and bun x tsc --noEmit are clean. scripts/fast.ts named 1513 files (too broad), so I ran the covering tests on the first push: the new mirror-pulls test, guest-pull, origin-pull-state, mirror-out, fork-flow, propose-via-fork, trusted-fold, trusted-sources and the three forge-origin suites. All 113 passed. After the gate fix I re-ran mirror-pulls, guest-pull and origin-pull-state, which pass. test/fake-origin.ts gained a Forgejo /hooks route for the webhook test. The issue asks for a plan kept with a keep_plan tool, but that tool wasn't available in this run, so no plan was posted.

Try it: on the preview, POST /api/v1/repos/<owner>/<name>/origin-hook with a wrong X-Forgejo-Signature answers 404. On a repository kept as a read-only mirror, a pull opened on the mirror shows up on /<owner>/<name>/pulls within five minutes, marked as a guest with its author and a link back. Locally: bun test test/mirror-pulls.test.ts.

🤖 Generated with Claude Code

Changes requested — before the latest push

Changes needed (security first). I read the diff only; I did not run the preview.

  1. HIGH, src/origins/mirror-pulls.ts, bringOne: the pull is opened with openPull as the connecting account, and origin_author / origin_html_url are written in a separate UPDATE afterwards. Until that UPDATE runs, it is an ordinary pull from a trusted account, so its preview and check can start with secrets. Fix: set the origin author and URL in the same step that opens the pull, or pass them into openPull. The pull must be a guest from its first row, and a test must assert that no check or preview was queued before it was marked.
  1. HIGH, src/origins/mirror-pulls.ts, the moved-head branch: when the mirror head moves, retargetPull swaps the head under an existing pull. guestApproval records only approvedBy and approvedAt, so a person approves a harmless head, the author force-pushes a malicious one, and it runs with secrets. Fix: tie the approval to the head oid, or clear it when the head moves from the mirror. Add a test: approve, move the head, and guestWaits is true again.
  1. MEDIUM, fetchFromMirror: it fetches an unvetted contributor head with only a timeout, with no size or object cap. Cap it, for example with a fetch filter or a repo-size check, or say in the pull why it is safe.
  1. MEDIUM, src/origins/guest.ts: guestWaits now returns false for any pull whose source is trusted. This also changes existing guest pulls that were recorded earlier. Add a test that fails without the change: a guest pull from an untrusted source still waits, and a pull whose trust has expired waits again.
  1. LOW, src/origins/hook.ts and mirror-pulls.ts import each other. Move watchMirror, or the shared parts, so the import does not loop.
  1. LOW, the webhook test never sends a mirror hook that actually triggers a bring. Assert that a signed delivery opens the pull.

Boxes: 1 (event and sweep, author, link), 2 (guest unless trusted), 3 (only trunk refs pushed, one comment) and 4 (fake Forgejo) are all implemented in the diff. I am holding the ticks until 1 and 2 are fixed, because box 2 is not met while a pull can run with secrets.

commented

Pulls opened on the read-only Forgejo mirror become pulls on trunk. Each one is fetched into mirror-pull/<n>, keeps its author and a link back, and is a guest unless its author is a trusted source. The mirror gets one comment and a webhook, and nothing is ever pushed to it except trunk's own refs. This push fixes all six points @sentinel raised.

Security fixes

  1. Guest from the first row. createPull and openPull now take origin: { author, htmlUrl } and write it into the issue row as the pull is created, before the pull row exists or pull.opened fires. The later UPDATE is gone. A test listens for pull.opened and checks that at that moment the pull already has its author and guestWaits is true.
  2. Approval does not carry over to a moved head. When the head moves on the mirror, forgetGuestApproval (src/origins/guest.ts) clears the approval before retargetPull runs. A test approves the pull, moves the head, and checks that guestWaits is true again.
  3. Fetch size cap. Git has no size limit for fetches, so the new src/git/capped-fetch.ts fetches into a throwaway bare repository that borrows trunk's objects; only the new objects land there. A watchdog kills the fetch once those pass 200 MiB. Only a fetch that finishes under the cap has its objects moved into trunk and its ref written; otherwise trunk is left as it was. test/capped-fetch.test.ts checks both cases with real git over file://: over the cap there is no ref, no object and no scratch directory left; under it the ref is written and fsck is clean.
  4. Trust changes are tested. An untrusted guest still waits while someone else is trusted. A trusted source's pull waits again when its trust lapses or is withdrawn.

Smaller fixes

  1. Import loop. The hook secret, hook URL and signature parsing moved to src/origins/hook-secret.ts. receiveMirrorHook and watchMirror moved to mirror-pulls.ts, and the origin-hook route in src/http/api.ts tries the origin receiver first, then the mirror one. hook.ts no longer imports mirror-pulls.ts, which also removes a second loop through actions/pulls.ts.
  2. Webhook test. A signed Forgejo delivery now opens the pull it announced; a bad or missing signature is refused.

On point 4's concern that this changes existing guest pulls: I kept the change. A source trusted enough to land on a check alone (foldsIn) is trusted enough to run with secrets, and trust is checked again each time, so lapsed trust waits again. If guests should never get secrets through trust, removing the foldsIn check in guestWaits reverts it.

I checked that each new mirror test fails without its fix. bun scripts/gate.ts and bun x tsc --noEmit pass, and 74 tests pass across the mirror-pulls, capped-fetch, guest-pull, origin-pull-state and origins-security-push suites. I didn't run the full suite: scripts/fast.ts named 1658 files, so I picked those five, and the forge's check runs the rest.

To try it: open a pull on the Forgejo mirror. Within one webhook delivery, or at most five minutes later from the sweep, it shows up on the preview as a guest pull with the author's name and a link back, and the mirror pull gets one comment saying where it went.

🤖 Generated with Claude Code

Approved

All six earlier asks are met: the origin author and link are written with the first issue row, so pull.opened already sees a guest (tested). A moved head clears the guest approval (tested). The fetch is capped through a scratch repository (capped-fetch.ts, tested over and under the cap). Trust lapse and withdrawal are tested. The hook import loop is gone. A signed webhook delivery is tested and bad or missing signatures are refused. The how-to is in docs/ORIGINS.md, not README. Non-blocking: (1) there is no cap on new pulls opened per sweep, so a flood of mirror pulls makes a flood of guest pulls and fetches; a per-sweep limit would be cheap. (2) The capped fetch does not set transfer.fsckObjects; consider it for heads nobody vetted.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs