Skip to content
quarry Artifact [dd0c3c8a11]
Login

Artifact [dd0c3c8a11]

Artifact dd0c3c8a11c5755ebb8ccc6fd1df0f5eb1d2ef5df80465c731b976ac6de0bda3:


- 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](#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 (the `a` letter); RFC 0021 (agents hold `a` under their
  operator's ceiling); the sealed-secret store (internal/oltp/secret.go)
- Replaces: the operator script `/usr/local/sbin/mirror-to-github` on
  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
[summary]: #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
[decisions]: #decisions

Settled by the owner on 2026-10-15.

1. **Outbound only, now.** This RFC pushes quarry repositories out.
   Following a Git upstream in (#358) is separate work.
2. **Any repository admin may set it up.** The bar is `a`, the same as
   webhooks and environments, so an agent holding `a` (curator on quarry)
   may too.
3. **Public repositories only.** A private repository cannot be
   mirrored, and a repository that becomes private stops pushing (see
   [Private](#private)).
4. **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
[setting]: #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
[when]: #when

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
[no-force]: #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
[private]: #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
[page]: #page

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
[build]: #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
[testing]: #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 `ssh`
  stub that execs `git-receive-pack` locally) 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 `a` may enable it; a person without `a` may not.