Public: anyone can read this; nobody can change it.
An account that can only read an origin repository proposes through its own fork there, and a protected repository is always proposed to that way #72
Check · Review · Land · Live — No review yet.
**Stacked on #71** (which is stacked on #70). This branch has those two commits first. Only the top commit is new. Land #70 and #71 first.
0 files · +0 −0
waiting checks and the preview wait for a writer to approve this head
Check on trevato-apps/forge: running, as of
Checks and the preview wait for a writer to approve this head
A guest
This pull comes from a guest: its preview runs without the app's variables or a forge token until a person who can write here approves it.
This pull cannot land as it is, whatever its review says, because it changes guarded paths: (the diff) (it could not be read).
Stacked on #71 (which is stacked on #70). This branch has those two commits first. Only the top commit is new. Land #70 and #71 first.
To carry a branch into an origin repository, the forge pushed the branch there. So the account needed push rights on that repository. An account that could only read it could not propose at all.
Now such an account proposes through its own fork at the origin (
src/origins/fork-route.ts). The fork is found or made. Found: in the picker's cache, by its two possible names, then byGET /repos/{o}/{r}/forks(bounded) for a fork the account owns under any name. Made:POST /forks, as<owner>-<name>when the name is taken. The branch is pushed into the fork, with a lease on where it stands there. The pull opens on the origin with head<account>:<branch>. The origin'spermissions.pushdecides the way when the pull opens, and a direct push that the origin refuses falls back to the fork.A repository that the operator protects (
FORGE_ORIGIN_DENY_PUSH, from #71) is always proposed to through a fork, whatever the credential can do. A branch pushed into a repository runs its workflows with that repository's Actions secrets; a pull from a fork runs them without the secrets (Forgejoservices/actions/secret.go). So push-through, a carried branch and a branch made on its mirror are refused there (origin_protected). The fork step of contribute uses the same fork (ensureFork), and a fork that reaches its origin through a fork says Propose via your fork. A push to the fork waits on the guard, as every other write does.The
owner:branchhead is Forgejo's own form (routers/api/v1/repo/pull.go,parseCompareInfoandGetForkedRepo). The note in the Forgejo provider that called it unverified is updated. This very pull is opened that way: headtdobbertin-espo:feat/propose-via-fork, from an account with read access only on this repository (this instance runs Forgejo 15.0.6).What is unchanged: an account with push rights on a repository that is not protected proposes as before, by a branch in the repository. Nothing new is stored, and there is no migration.
Tests
bun 1.3.13, a scrubbed environment (no agent variables):
test/propose-via-fork.test.ts: 10 pass, 0 fail, with a real forge serving git behind the fake API: a reader proposes through its fork with head<owner>:<branch>; the fork is reused, found under a name of its own, or made under a second name; an account that can push still pushes directly; a push the origin refuses falls back to the fork; a protected repository always takes the fork, even for an account that could push; a push into the fork waits on the guard.bun test --timeout 60000call: 544 pass, 0 fail across 41 files.bun scripts/gate.tsandtsc --noEmit: clean.the 41 files
propose-via-fork, origin-guard, origin-token-scopes, following, following-card, forks, fork-flow, fork-pull-approval, repo-fork-bar, crew-proposal, origins, origins-oauth, origins-operator, origins-reconnect, origins-security-auth, origins-security-provider, origins-security-push, origins-sync, origins-sync-settled, web-origins, web-origin-link, mcp-origins, mcp-origin-apps, push-through, trusted-hosts, trusted-hosts-ui, steps-lists, no-polling, refresh-meta, mirror-outbound, mirror-out, carried, migrate, migrate-after, tui-migrate, apps-feedback-mirror, and the five cross files reserved-names, studio, shards, check-list, hollow (each
test/<name>.test.ts).🤖 Generated with Claude Code