Artifact dd0c3c8a11c5755ebb8ccc6fd1df0f5eb1d2ef5df80465c731b976ac6de0bda3:
- File docs/rfcs/0022-git-mirror-out.md — part of check-in [93b90dcaee] at 2026-10-15 00:10:57 on branch rfc-0022-git-mirror — RFC 0022: mirror a public repository to a Git host from its settings (user: zeitgeist, size: 6941) [annotate] [blame] [check-ins using] [more...]
- Feature Name:
git-mirror-out(a repository admin mirrors a public repository's trunk to GitHub or any Git host from its settings, pushed after each change, with a deploy key the forge makes and keeps) - Start Date: 2026-10-15
- Status: Draft. Four decisions settled by the owner on 2026-10-15 (see Decisions). Not built.
- Depends on: zit's
git export --autopush(zit#220: export new check-ins into a Git repository and push trunk fast-forward only); RFC 0008 (thealetter); RFC 0021 (agents holdaunder their operator's ceiling); the sealed-secret store (internal/oltp/secret.go) - Replaces: the operator script
/usr/local/sbin/mirror-to-githubon the forge host and its ten-minute timer (pikchr-studio today) - Ticket: #292. The inbound direction (a Git upstream into a quarry
store, zit
import-git --into) is #358 and is not this RFC.
Summary
A repository's settings gain a Mirror to Git section. A repository
admin gives a remote URL (an SSH URL such as
[email protected]:owner/repo.git) and a branch name (default main). On
enable the forge makes an ed25519 key pair for that repository, keeps
the private half sealed, and shows the public half to add to the host as
a deploy key with write access. After every change to trunk the forge
runs zit git export with that key, which exports the new check-ins and
pushes fast-forward only. The section shows the last pushed check-in,
when, and the last error in words. Turning the mirror off stops pushing
and discards the key. Only public repositories may be mirrored.
Decisions
Settled by the owner on 2026-10-15.
- Outbound only, now. This RFC pushes quarry repositories out. Following a Git upstream in (#358) is separate work.
- Any repository admin may set it up. The bar is
a, the same as webhooks and environments, so an agent holdinga(curator on quarry) may too. - Public repositories only. A private repository cannot be mirrored, and a repository that becomes private stops pushing (see Private).
- Author email is zit's rule. A commit carries the author's contact
email from the repository's user table, else
[email protected]. No new account setting; a person who wants the host to link their commits adds a contact email.
The setting
One row per repository: remote URL, push branch (default main),
enabled, the sealed private key, the public key, the last exported
check-in, when it was pushed, the last error and when. The remote URL is
validated as an SSH remote with no password in it (zit refuses a
password in an autopush URL, and an HTTPS URL would need a credential
this design does not keep). Every change is written to the repository's
settings log with its actor and via, as the other settings are.
The key is made on first enable with crypto/ed25519, the private half sealed with the same sealer that holds sync secrets, and shown only as its OpenSSH public line. It is never shown again in private form, never logged, and never leaves the forge except as the file the push reads for the length of one run (written 0600 in a per-run temporary directory and removed after). Disabling the mirror deletes the key; enabling again makes a new one, which the admin adds to the host again.
When it pushes
After trunk moves, not on a timer. The trigger is the store's own fold noticing a new trunk tip, which covers a direct push, an accepted landing and a merge the forge writes, with one path. A run is queued per repository and coalesced: several trunk moves while a run is going make one more run, not several. Only the node holding the repository's write lease runs it, so a two-node forge never pushes twice.
A run is zit git export MIRROR --autopush URL --push-branch BRANCH
against a Git repository the forge keeps under the repository's data
directory, with GIT_SSH_COMMAND naming the per-run key file and
IdentitiesOnly=yes. zit exports what is new and pushes fast-forward
only.
A failure is recorded and retried with backoff (one minute, doubling to an hour); it never blocks the forge or the repository. The last error is put in words for the page: "the host refused the deploy key: check it is added with write access", "the remote's main has history this repository does not: it was not pushed", "the host could not be reached".
No force
The forge never force-pushes. If the remote branch has history the export did not write (a first enable onto a non-empty repository, or someone pushed to the mirror directly), the run refuses and says so, and the mirror stays enabled and keeps refusing until the remote is reset by hand. There is no "replace the remote" control in this RFC.
Private
Enabling is refused on a private repository. A mirrored repository that is made private stops pushing at once: the next run is not started, the status says "the repository is private, so the mirror is paused", and the key is kept so that making it public again resumes. Nothing already pushed is withdrawn, and the page says that too.
What the page shows
The section shows the remote, the branch, on or off, the public key with a copy control, and the status line: last pushed check-in (linked) and when, or the last error and when. The repository's About shows "Mirrored to <host path>" with a link while a mirror is on and current. No help-text paragraphs; the error sentences carry what a person needs.
Build order
| Rung | Work |
|---|---|
| 1 | The setting row, key making and sealing, the settings section (enable, disable, URL, branch, public key), the private-repository refusal, the settings-log rows |
| 2 | The run: trigger on a trunk-tip change, per-repository coalescing, lease holder only, the per-run key file, zit git export --autopush, status and error words, backoff |
| 3 | Pause on private, the About line |
| 4 | Move pikchr-studio to the setting and retire mirror-to-github, its timer and the gitmirror user on the host |
Testing
- Enable on a public repository makes a key, stores it sealed, shows the public line, and logs the change; enable on a private one is refused; disable deletes the key; a remote URL with a password or an HTTPS URL is refused with the reason.
- A run against a local bare Git repository as the remote (an
sshstub that execsgit-receive-packlocally) exports and pushes after a trunk move, and a second move while it runs coalesces into one more run. - A remote with unrelated history is refused and recorded, and nothing is forced.
- The key file exists only during the run and is 0600; no log line carries it.
- A repository made private pauses; made public again resumes.
- An agent holding
amay enable it; a person withoutamay not.