Feature #240: pull-only mirror repos #250

Merged
crueber merged 2 commits from feat/issue-240 into main 2026-09-09 20:06:36 +00:00
Owner

Implements Forgejo issue #240 (plan revision R1 normative): mirror repos with scheduled upstream syncs.

What: meta/mirror.json sidecar (Create-once-then-CAS'd, frozen-list amendment in 14_extensibility.md) + mirror-sync task kind + bucket lease (leases/mirror-<owner>-<name>.pb) + follow-shaped 1m loop (registry enumeration, no LIST) + followOnce-shaped converge (ff-only, rewind refuse+narrate, force escape) + funnel push refusal (discovery 403, in-pipeline ng, SSH pre-advertisement; sync bypasses via direct Publish) + import-vs-sync exclusion (Begin 409 / claim-skip) + failure counter + capped backoff + computed next-fire + create-from-URL flow (public upstream, memory-only first-sync token) + mirror badge/next-sync UI + Settings Mirror tab + Sync-now + schedule presets + EVIDENCE E15.

Decisions appended: (a) public-only v1, (b) sidecar+frozen list, (c) funnel placement, (f) ff-only (11_mirror.md + 14 + go docs 02/04/05/06/07/10/12). No new Go modules, no new npm deps. walhub mirror CLI stub stays a stub (out of scope, documented).

Tests: internal/mirror 97.0% (-race), server 95.6%, api 95.4%, repoimport 95.9%, store 95.1%; node --test 471 green; make vet + contract green; full short suite green; e2e green; push-budget test wires the shipped guard (cold 10/warm 9 ops, other collab families zero). Live-server proof: create -> first sync -> scheduled loop fire -> push refused (403/ng/real git client) -> summary/badge data (E15). In-browser render recorded OPEN: shared Chrome daemon network-guard blocks all private/loopback targets, private daemon forbidden.

Closes #240 (do not merge from here).

Implements Forgejo issue #240 (plan revision R1 normative): mirror repos with scheduled upstream syncs. **What:** `meta/mirror.json` sidecar (Create-once-then-CAS'd, frozen-list amendment in 14_extensibility.md) + `mirror-sync` task kind + bucket lease (`leases/mirror-<owner>-<name>.pb`) + follow-shaped 1m loop (registry enumeration, no LIST) + followOnce-shaped converge (ff-only, rewind refuse+narrate, force escape) + funnel push refusal (discovery 403, in-pipeline ng, SSH pre-advertisement; sync bypasses via direct Publish) + import-vs-sync exclusion (Begin 409 / claim-skip) + failure counter + capped backoff + computed next-fire + create-from-URL flow (public upstream, memory-only first-sync token) + mirror badge/next-sync UI + Settings Mirror tab + Sync-now + schedule presets + EVIDENCE E15. **Decisions appended:** (a) public-only v1, (b) sidecar+frozen list, (c) funnel placement, (f) ff-only (11_mirror.md + 14 + go docs 02/04/05/06/07/10/12). No new Go modules, no new npm deps. `walhub mirror` CLI stub stays a stub (out of scope, documented). **Tests:** internal/mirror 97.0% (-race), server 95.6%, api 95.4%, repoimport 95.9%, store 95.1%; node --test 471 green; make vet + contract green; full short suite green; e2e green; push-budget test wires the shipped guard (cold 10/warm 9 ops, other collab families zero). Live-server proof: create -> first sync -> scheduled loop fire -> push refused (403/ng/real git client) -> summary/badge data (E15). In-browser render recorded OPEN: shared Chrome daemon network-guard blocks all private/loopback targets, private daemon forbidden. Closes #240 (do not merge from here).
Implements docs/features/11_mirror.md (plan R1 normative): mirror.json
sidecar + frozen-list amendment; create-from-URL flow (public upstream,
first-sync token memory-only); mirror-sync task kind + bucket lease +
registry-enumeration loop; followOnce-shaped converge (ff-only, rewind
refuse+narrate, force escape); funnel push refusal (HTTP+SSH, discovery
403, in-pipeline ng); import-vs-sync exclusion; failure counter +
backoff; computed next-fire; mirror badge + next-sync UI + Sync-now;
schedule presets; EVIDENCE E15. Decisions appended: (a) public-only v1,
(b) sidecar family + frozen list, (c) funnel placement, (f) ff-only
(11_mirror.md + 14_extensibility.md + go docs 02/04/05/06/07/10/12).
PUT /{o}/{r}/api/mirror create branch now probes the manifest first:
an unborn target gets 404 instead of an orphan sidecar that would 403
every future push to the name while every sync fails at Open (PUT is
config on a repo; the create twin owns repo creation). Rewind-refusal
narration + sidecar LastResult now pass through scrubText: ref names
are upstream-controlled and '=' is legal in a refname, so a hostile
upstream could otherwise smuggle credential-shaped text into task logs
and the outcome field (import S2 discipline). Tests: unborn-PUT 404
with no sidecar left behind; hostile-branch rewind asserts redaction
in both the sidecar and the task log tail.
Sign in to join this conversation.
No description provided.