detached 4d330ea

docs(mirror): document push mirror setup, sync rules, CLI and events

commit
4d330eac1258dffb161b8644be0c38b0f9667c10
author
brice · · 2026-10-20 06:02 UTC
committer
brice · · 2026-10-20 10:02 UTC
parent
49d746d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +91 -1
M README.md +1 -0
@@ -132,6 +132,7 @@ Give the agent the [`skills/ermite`](skills/ermite/SKILL.md) skill so it knows t
132132 - [Configuration](docs/configuration.md): environment variables
133133 - [CLI](docs/cli.md): config file, commands, JSON output
134134 - [Git over HTTP](docs/git-http.md): URLs and authentication
135+- [Push mirror](docs/mirror.md): push repositories to a remote over SSH
135136 - [Tokens](docs/tokens.md): scopes, presets, branch-limited tokens
136137 - [Merge requests](docs/merge-requests.md)
137138 - [Issues](docs/issues.md)
@@ -132,6 +132,7 @@ Give the agent the [`skills/ermite`](skills/ermite/SKILL.md) skill so it knows t
132 - [Configuration](docs/configuration.md): environment variables132 - [Configuration](docs/configuration.md): environment variables
133 - [CLI](docs/cli.md): config file, commands, JSON output133 - [CLI](docs/cli.md): config file, commands, JSON output
134 - [Git over HTTP](docs/git-http.md): URLs and authentication134 - [Git over HTTP](docs/git-http.md): URLs and authentication
135+- [Push mirror](docs/mirror.md): push repositories to a remote over SSH
135 - [Tokens](docs/tokens.md): scopes, presets, branch-limited tokens136 - [Tokens](docs/tokens.md): scopes, presets, branch-limited tokens
136 - [Merge requests](docs/merge-requests.md)137 - [Merge requests](docs/merge-requests.md)
137 - [Issues](docs/issues.md)138 - [Issues](docs/issues.md)
M docs/ci-events.md +9 -0
@@ -62,3 +62,12 @@ ermite workflow run <file> --repo <repo> [--ref <branch|tag>] [-f name=value …
6262 - Start on a push that creates or deletes a branch or tag (also a merge with `--delete-branch`).
6363 - No filters; `[skip ci]` does not apply. A push of more than three tags starts none.
6464 - Payload: `ref`, `ref_type`, `pusher_type`, `repository`, `sender`; `create` adds `master_branch` and `description`.
65+
66+## Mirror events
67+
68+Not workflow triggers: `ermite watch` reports them when a [push mirror](mirror.md) changes health, once per incident.
69+
70+- `mirror.failed`: the mirror started failing or was paused. Payload: `repo`, `state` (`failing` or `paused`), `failure_class` (`diverged`, `auth_denied`, `remote_not_found`, `host_key`, `transient`, `internal`), `reason`.
71+- `mirror.recovered`: a failing or paused mirror pushed successfully. Payload: `repo`.
72+
73+The payload carries neither the remote URL nor the error detail; `ermite mirror status` shows the reason.
@@ -62,3 +62,12 @@ ermite workflow run <file> --repo <repo> [--ref <branch|tag>] [-f name=value …
62 - Start on a push that creates or deletes a branch or tag (also a merge with `--delete-branch`).62 - Start on a push that creates or deletes a branch or tag (also a merge with `--delete-branch`).
63 - No filters; `[skip ci]` does not apply. A push of more than three tags starts none.63 - No filters; `[skip ci]` does not apply. A push of more than three tags starts none.
64 - Payload: `ref`, `ref_type`, `pusher_type`, `repository`, `sender`; `create` adds `master_branch` and `description`.64 - Payload: `ref`, `ref_type`, `pusher_type`, `repository`, `sender`; `create` adds `master_branch` and `description`.
65+
66+## Mirror events
67+
68+Not workflow triggers: `ermite watch` reports them when a [push mirror](mirror.md) changes health, once per incident.
69+
70+- `mirror.failed`: the mirror started failing or was paused. Payload: `repo`, `state` (`failing` or `paused`), `failure_class` (`diverged`, `auth_denied`, `remote_not_found`, `host_key`, `transient`, `internal`), `reason`.
71+- `mirror.recovered`: a failing or paused mirror pushed successfully. Payload: `repo`.
72+
73+The payload carries neither the remote URL nor the error detail; `ermite mirror status` shows the reason.
M docs/cli.md +7 -1
@@ -75,6 +75,10 @@ ermite secret set|list|delete … # CI secrets (needs ERMITE_SECRETS_KEY
7575 ermite variable set|get|list|delete …
7676 ermite environment create|list|delete … --repo <repo>
7777
78+ermite mirror key show | rotate # SSH key to add to the remote (rotate: owner only)
79+ermite mirror config [--enabled] [--url …{repo}…] [--cron …] [--all-branches] | config show # global policy, owner only
80+ermite mirror add <repo> <url> [--cron …] [--all-branches] | remove <repo> | exclude <repo> | include <repo>
81+ermite mirror status [repo] | sync <repo>|--all [--force]
7882 ermite web login # one-time sign-in link (60 s)
7983 ermite web logout --all # sign every browser out
8084 ermite web passkey list | remove <id>
@@ -87,9 +91,11 @@ ermite completion bash|zsh|fish
8791
8892 Issues need a server with the `issues` feature and `ermite repo edit <repo> --issues owner` (or `--issues public`, which also opens a moderated form to visitors of a public repository); they are written with the `issue:write` scope. See [Issues](issues.md).
8993
94+Mirrors push repositories to a remote over SSH, never forcing. See [Push mirror](mirror.md).
95+
9096 ## Output
9197
92-- `--json` prints stable field names (listed in each `--help`) on `repo list|show|create|edit`, `issue create|list|show|comment|close|reopen`, `token create|list`, `run list|view|cancel|rerun`, `runner list` and `version`.
98+- `--json` prints stable field names (listed in each `--help`) on `repo list|show|create|edit`, `issue create|list|show|comment|close|reopen`, `token create|list`, `run list|view|cancel|rerun`, `runner list`, `mirror key|config|status` and `version`.
9399 - `watch --json` prints one object per line. `merge_request_mergeability` events carry `mergeability` (`state`, `behind`, `conflict_paths`…) and print as `repo!n mergeability: behind by 2`.
94100 - `mr merge --mode squash|merge` creates a commit signed by the server's own key (generated on first start); the output names its fingerprint, `ermite signing-key show` prints the key. Every mode is allowed by default; `repo edit --merge-modes fast-forward,squash` disables `merge`.
95101 - Errors are one line on stderr (`ermite: …`) with exit status 1.
@@ -75,6 +75,10 @@ ermite secret set|list|delete … # CI secrets (needs ERMITE_SECRETS_KEY
75 ermite variable set|get|list|delete …75 ermite variable set|get|list|delete …
76 ermite environment create|list|delete … --repo <repo>76 ermite environment create|list|delete … --repo <repo>
77 77
78+ermite mirror key show | rotate # SSH key to add to the remote (rotate: owner only)
79+ermite mirror config [--enabled] [--url …{repo}…] [--cron …] [--all-branches] | config show # global policy, owner only
80+ermite mirror add <repo> <url> [--cron …] [--all-branches] | remove <repo> | exclude <repo> | include <repo>
81+ermite mirror status [repo] | sync <repo>|--all [--force]
78 ermite web login # one-time sign-in link (60 s)82 ermite web login # one-time sign-in link (60 s)
79 ermite web logout --all # sign every browser out83 ermite web logout --all # sign every browser out
80 ermite web passkey list | remove <id>84 ermite web passkey list | remove <id>
@@ -87,9 +91,11 @@ ermite completion bash|zsh|fish
87 91
88 Issues need a server with the `issues` feature and `ermite repo edit <repo> --issues owner` (or `--issues public`, which also opens a moderated form to visitors of a public repository); they are written with the `issue:write` scope. See [Issues](issues.md).92 Issues need a server with the `issues` feature and `ermite repo edit <repo> --issues owner` (or `--issues public`, which also opens a moderated form to visitors of a public repository); they are written with the `issue:write` scope. See [Issues](issues.md).
89 93
94+Mirrors push repositories to a remote over SSH, never forcing. See [Push mirror](mirror.md).
95+
90 ## Output96 ## Output
91 97
92-- `--json` prints stable field names (listed in each `--help`) on `repo list|show|create|edit`, `issue create|list|show|comment|close|reopen`, `token create|list`, `run list|view|cancel|rerun`, `runner list` and `version`.98+- `--json` prints stable field names (listed in each `--help`) on `repo list|show|create|edit`, `issue create|list|show|comment|close|reopen`, `token create|list`, `run list|view|cancel|rerun`, `runner list`, `mirror key|config|status` and `version`.
93 - `watch --json` prints one object per line. `merge_request_mergeability` events carry `mergeability` (`state`, `behind`, `conflict_paths`…) and print as `repo!n mergeability: behind by 2`.99 - `watch --json` prints one object per line. `merge_request_mergeability` events carry `mergeability` (`state`, `behind`, `conflict_paths`…) and print as `repo!n mergeability: behind by 2`.
94 - `mr merge --mode squash|merge` creates a commit signed by the server's own key (generated on first start); the output names its fingerprint, `ermite signing-key show` prints the key. Every mode is allowed by default; `repo edit --merge-modes fast-forward,squash` disables `merge`.100 - `mr merge --mode squash|merge` creates a commit signed by the server's own key (generated on first start); the output names its fingerprint, `ermite signing-key show` prints the key. Every mode is allowed by default; `repo edit --merge-modes fast-forward,squash` disables `merge`.
95 - Errors are one line on stderr (`ermite: …`) with exit status 1.101 - Errors are one line on stderr (`ermite: …`) with exit status 1.
A docs/mirror.md +74 -0
@@ -0,0 +1,74 @@
1+# Push mirror
2+
3+Ermite can push its repositories to a remote Git host over SSH (GitHub, another forge…), to keep a read-only copy there. The push never forces and never deletes anything on the remote.
4+
5+## Set up
6+
7+1. The server needs `ERMITE_SECRETS_KEY`; it seals the mirror SSH key. Without it, syncs fail and nothing is stored.
8+2. Add the server's public mirror key to the remote account or repository, as a key allowed to push:
9+
10+ ```sh
11+ ermite mirror key show | gh ssh-key add - --title "ermite mirror"
12+ ```
13+
14+ `mirror key show` prints only the `authorized_keys` line. One key serves every mirror.
15+3. Set the global policy (owner only). The URL template holds `{repo}`, replaced by the repository name; the cron is five fields, in UTC:
16+
17+ ```sh
18+ ermite mirror config --url 'git@github.com:acme/{repo}.git' --cron '*/15 * * * *' --enabled
19+ ermite mirror config show
20+ ```
21+
22+ `config` changes only the flags you pass. `--all-branches` mirrors every branch instead of the default branch.
23+4. Adjust single repositories:
24+
25+ ```sh
26+ ermite mirror add <repo> <url> [--cron …] [--all-branches] # own URL (and schedule), even when the global policy is off
27+ ermite mirror exclude <repo> # leave the global policy
28+ ermite mirror include <repo> # back under it
29+ ermite mirror remove <repo> # drop the override
30+ ```
31+
32+ Without `--cron` or `--all-branches`, `add` inherits the global values.
33+
34+URLs are `git@host:path` or `ssh://[user@]host[:port]/path`, with no password. The cron may not fire more often than every 5 minutes.
35+
36+## What is pushed
37+
38+- The default branch and all tags; every branch with `--all-branches`.
39+- Never a force push, never a deletion: a branch or tag deleted here stays on the remote.
40+- If the remote has commits ermite lacks (diverged), the mirror is **paused**. To fix it, fast-forward ermite with the remote's commits (push them here), then run `ermite mirror sync <repo> --force`.
41+
42+## When it syncs
43+
44+- On the cron schedule. A push to ermite only marks the repository as changed; the next due cron tick syncs it. A tick with nothing new does nothing.
45+- `ermite mirror sync <repo>` or `ermite mirror sync --all` (owner only) syncs now; a repository needs admin rights.
46+- `--force` skips the no-op check and the 60 s throttle and lifts a pause. It never force-pushes.
47+- Overlapping syncs of a repository coalesce; only one push runs at a time per repository.
48+
49+## Limits against spam
50+
51+| Rule | Value |
52+|---|---|
53+| No-op | nothing changed since the last successful push: no connection |
54+| Manual sync | one per repository per 60 s, unless `--force` |
55+| Cron | at least 5 minutes between firings |
56+| Backoff after a transient failure | 5 min, doubling, up to 6 h |
57+| Paused | after 5 consecutive transient failures |
58+| Paused at once | diverged remote, key refused, remote not found, host key changed |
59+
60+A paused mirror is not retried by the schedule. Changing the mirror configuration of a repository clears its failures.
61+
62+## See what happens
63+
64+- `ermite mirror status [<repo>] [--json]`: state (`ok`, `failing`, `paused`, `disabled`), reason, last success, next run, failures.
65+- A `git push` to a repository whose mirror is failing or paused prints a `remote:` warning, to people with admin rights.
66+- The repository page shows a badge to owners and admins.
67+- `ermite watch` receives `mirror.failed` once when a mirror starts failing, and `mirror.recovered` when it succeeds again. See [events](ci-events.md#mirror-events).
68+
69+## Security
70+
71+- The private key is sealed at rest with `ERMITE_SECRETS_KEY`. It is never printed and never in logs; URLs are logged without credentials or user.
72+- Host keys are trusted on first use, kept in `<data dir>/mirror_known_hosts`. If a remote's key changes, the mirror is paused (`host_key`). After checking the change is legitimate, remove that host's line from the file and run `ermite mirror sync <repo> --force`.
73+- `ermite mirror key rotate` (owner only) makes a new key at once. Every mirror fails until the new key is added to the remotes.
74+- The scheduler runs in the server process: run a single ermite instance per data directory.
@@ -0,0 +1,74 @@
1+# Push mirror
2+
3+Ermite can push its repositories to a remote Git host over SSH (GitHub, another forge…), to keep a read-only copy there. The push never forces and never deletes anything on the remote.
4+
5+## Set up
6+
7+1. The server needs `ERMITE_SECRETS_KEY`; it seals the mirror SSH key. Without it, syncs fail and nothing is stored.
8+2. Add the server's public mirror key to the remote account or repository, as a key allowed to push:
9+
10+ ```sh
11+ ermite mirror key show | gh ssh-key add - --title "ermite mirror"
12+ ```
13+
14+ `mirror key show` prints only the `authorized_keys` line. One key serves every mirror.
15+3. Set the global policy (owner only). The URL template holds `{repo}`, replaced by the repository name; the cron is five fields, in UTC:
16+
17+ ```sh
18+ ermite mirror config --url 'git@github.com:acme/{repo}.git' --cron '*/15 * * * *' --enabled
19+ ermite mirror config show
20+ ```
21+
22+ `config` changes only the flags you pass. `--all-branches` mirrors every branch instead of the default branch.
23+4. Adjust single repositories:
24+
25+ ```sh
26+ ermite mirror add <repo> <url> [--cron …] [--all-branches] # own URL (and schedule), even when the global policy is off
27+ ermite mirror exclude <repo> # leave the global policy
28+ ermite mirror include <repo> # back under it
29+ ermite mirror remove <repo> # drop the override
30+ ```
31+
32+ Without `--cron` or `--all-branches`, `add` inherits the global values.
33+
34+URLs are `git@host:path` or `ssh://[user@]host[:port]/path`, with no password. The cron may not fire more often than every 5 minutes.
35+
36+## What is pushed
37+
38+- The default branch and all tags; every branch with `--all-branches`.
39+- Never a force push, never a deletion: a branch or tag deleted here stays on the remote.
40+- If the remote has commits ermite lacks (diverged), the mirror is **paused**. To fix it, fast-forward ermite with the remote's commits (push them here), then run `ermite mirror sync <repo> --force`.
41+
42+## When it syncs
43+
44+- On the cron schedule. A push to ermite only marks the repository as changed; the next due cron tick syncs it. A tick with nothing new does nothing.
45+- `ermite mirror sync <repo>` or `ermite mirror sync --all` (owner only) syncs now; a repository needs admin rights.
46+- `--force` skips the no-op check and the 60 s throttle and lifts a pause. It never force-pushes.
47+- Overlapping syncs of a repository coalesce; only one push runs at a time per repository.
48+
49+## Limits against spam
50+
51+| Rule | Value |
52+|---|---|
53+| No-op | nothing changed since the last successful push: no connection |
54+| Manual sync | one per repository per 60 s, unless `--force` |
55+| Cron | at least 5 minutes between firings |
56+| Backoff after a transient failure | 5 min, doubling, up to 6 h |
57+| Paused | after 5 consecutive transient failures |
58+| Paused at once | diverged remote, key refused, remote not found, host key changed |
59+
60+A paused mirror is not retried by the schedule. Changing the mirror configuration of a repository clears its failures.
61+
62+## See what happens
63+
64+- `ermite mirror status [<repo>] [--json]`: state (`ok`, `failing`, `paused`, `disabled`), reason, last success, next run, failures.
65+- A `git push` to a repository whose mirror is failing or paused prints a `remote:` warning, to people with admin rights.
66+- The repository page shows a badge to owners and admins.
67+- `ermite watch` receives `mirror.failed` once when a mirror starts failing, and `mirror.recovered` when it succeeds again. See [events](ci-events.md#mirror-events).
68+
69+## Security
70+
71+- The private key is sealed at rest with `ERMITE_SECRETS_KEY`. It is never printed and never in logs; URLs are logged without credentials or user.
72+- Host keys are trusted on first use, kept in `<data dir>/mirror_known_hosts`. If a remote's key changes, the mirror is paused (`host_key`). After checking the change is legitimate, remove that host's line from the file and run `ermite mirror sync <repo> --force`.
73+- `ermite mirror key rotate` (owner only) makes a new key at once. Every mirror fails until the new key is added to the remotes.
74+- The scheduler runs in the server process: run a single ermite instance per data directory.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 42

↑↓ moveenter openesc close