The private store holds files extracted from release refs, which a backup has, so like .forge/store it is left out (it is under .forge/private, which derived_state leaves out already) and a restore rebuilds it: restore_finish links a private repo's downloads into the private root through release_store, which extracts them into the private store again. A scenario pins both, and the README says so.
- commit
- 42f0525d2830eb35c14937ab5160e120de0a2f25
- parent
- 1312bf52289179e55b97c1d7c2ec6c644d0496a5
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 101s, signed by cat
- kind
- fix risk score 1.2 of 3, Jev's estimate
- review
- #4 Keep private repos' release trees in a store only the forge reads
ci attestation
forge-attestation 1 type ci commit 42f0525d2830eb35c14937ab5160e120de0a2f25 tree e2b8032ad4cbeb76eb76ab6186574c0017b143e6 script .forge/ci e6f038ae810b87f90a4be115a762a9c91dd76a01 result pass exit 0 started 2026-10-18T22:36:59Z duration 101s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:dc341546931fe1b4ef34ddf945949efaec7caa6daae5c7ec7c15505fe6336343 238 lines |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |d470ca9 running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |d470ca9 pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |863c781 running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |863c781 fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |9cfe594 running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |9cfe594 pass in 0s, signed |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |warning: You appear to have cloned an empty repository. |site home: deployed c117d4d -> /sites/ana/dotfiles/home/ |created ana/notes |remote: review #1: /ana/notes/reviews/1/ |pushed ana/todo |landing #1 A todo list: ana/todo -> main |c09638c running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |c09638c pass in 0s, signed |pushed main |60e0502 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |60e0502 pass in 0s, signed |remote: review #7: /dami/greet/reviews/7/ |pushed dami/regions |created dami/forge |warning: You appear to have cloned an empty repository. |site guide: deployed ad69003 -> /sites/dami/forge/guide/ |(node:55869) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/dotfiles in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.YT3Kbf/wt/demo/root/.forge/www |(node:56002) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/notes in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.YT3Kbf/wt/demo/root/.forge/www |(node:56143) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/forge in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.YT3Kbf/wt/demo/root/.forge/www |(node:56281) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/greet in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.YT3Kbf/wt/demo/root/.forge/www |(node:56955) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built - in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.YT3Kbf/wt/demo/root/.forge/www |226 of 226 pages render the same without 11ty |guide: 4 files, links ok |ok
Signature: good, cat
| M | README.md | 6 | +++-- |
| M | bin/ | 16 | +++++++----- |
| A | test/ | 40 | ++++++++++++++++++++++++++++++ |
@@ -936,7 +936,8 @@ Everything in `/srv/git` that can't be rebuilt:
936936 937937 Left out, because a restore rebuilds it or every boot rewrites it: the pages in938938 `.forge/www` and `.forge/private`, the extracted sites and releases in939-`.forge/store` (both are git refs),939+`.forge/store` and private repos' releases in `.forge/private/store` (all940+are git refs),940941 `.forge/cache`, `authorized_keys`, `allowed_signers`, `git-shell-commands/`,941942 hooks, `.forge/secrets` and `.forge/backup.env` (both written from `forge.env`942943 at boot), and logs, locks and scratch files.@@ -1074,7 +1075,8 @@ What you set up yourself, once:
10741075 `/srv/git`. It restores the files, rebuilds what a backup leaves out (what10751076 `forge server init` and `forge keys sync` write, hooks in every repo, pinned10761077 review revisions, extracted sites, release downloads, a private repo's in1077-the private root, with the mtimes they had, so the mirror uploads none1078+the private root, extracted into `.forge/private/store` again, with the1079+mtimes they had, so the mirror uploads none10781080 again) and runs `forge10791081 build`. identity.db is the backup's copy or litestream's replica, whichever10801082 changed last (see [above](#identitydb-backed-up-as-it-changes)). It refuses@@ -2900,10 +2900,11 @@ stage_repo() {
29002900 }29012901 29022902 # .forge entries a backup leaves out: rebuilt (www, store, cache,2903-# allowed_signers, private pages), regenerated at boot or by commands2904-# (secrets, litestream.yml), or logs, locks and scratch. The lake is staged2905-# by itself (stage_lake), and identity.db by stage_identity; its WAL and2906-# litestream's state are never copied.2903+# allowed_signers, private: private pages and the private store),2904+# regenerated at boot or by commands (secrets, litestream.yml), or logs,2905+# locks and scratch. The lake is staged by itself (stage_lake), and2906+# identity.db by stage_identity; its WAL and litestream's state are never2907+# copied.29072908 derived_state() {29082909 case $1 in29092910 www | store | cache | backup | secrets | allowed_signers | config.restored | restore.tmp | lake | private | \@@ -3202,9 +3203,10 @@ restore_finish() {
32023203 awk '$2 == "commit" { print "update refs/forge/keep/" $1 " " $1 }' | git update-ref --stdin32033204 # A private repo's sites stay unserved (sites_public), as before the backup.32043205 if sites_public "$GIT_DIR"; then redeploy_sites; fi3205- # Releases are refs too: a public repo's go to www, a private repo's to3206- # the private root. The files get their commit's time again, so the3207- # mirror sees nothing new.3206+ # Releases are refs too: a public repo's go to www, linked from3207+ # .forge/store, and a private repo's to the private root, extracted3208+ # into the private store again (release_store). The files get their3209+ # commit's time again, so the mirror sees nothing new.32083210 releases_into "$(pages_root "$GIT_DIR")"32093211 git update-server-info32103212 unset GIT_DIR@@ -0,0 +1,40 @@
1+# A backup leaves .forge/private/store out, like .forge/store, since the2+# release refs it is extracted from are in the backup; a restore extracts a3+# private repo's releases into it again and links its downloads from there,4+# and a public repo's from .forge/store.5+. "$(dirname "$0")/../lib.sh"6+user dami7+repo dami/open8+repo --private dami/secret9+mkdir "$T/f"10+printf 'tool\n' >"$T/f/tool"11+export FORGE_USER=dami12+for r in open secret; do13+ clone dami "dami/$r" "$T/w/$r"14+ at "$T/w/$r" commit "first $r"15+ at "$T/w/$r" git tag v116+ ok push_as dami "$T/w/$r" origin main17+ ok at "$T/w/$r" "$FORGE" release publish v1 "$T/f/tool"18+done19+unset FORGE_USER20+open=$(git --git-dir="$FORGE_ROOT/dami/open.git" rev-parse 'refs/releases/v1^{tree}')21+secret=$(git --git-dir="$FORGE_ROOT/dami/secret.git" rev-parse 'refs/releases/v1^{tree}')22+exists "$FORGE_ROOT/.forge/private/store/$secret/tool"23+24+ok forge backup --dry-run25+st=$FORGE_ROOT/.forge/backup/stage26+missing "$st/.forge/private"27+missing "$st/.forge/store"28+exists "$st/dami/secret.git/HEAD"29+30+r=$T/restored31+ok env FORGE_ROOT="$r" FORGE_WWW="$r/.forge/www" FORGE_CACHE="$T/cache2" "$FORGE" restore --from "$st"32+inode() { ls -i "$1" | awk '{ print $1 }'; }33+dl=dami/secret/releases/download34+ok cmp "$T/f/tool" "$r/.forge/private/store/$secret/tool"35+eq "$(inode "$r/.forge/private/www/$dl/v1/tool")" "$(inode "$r/.forge/private/store/$secret/tool")"36+eq "$(inode "$r/.forge/private/www/$dl/latest/tool")" "$(inode "$r/.forge/private/store/$secret/tool")"37+missing "$r/.forge/store/$secret"38+eq "$(ls -ld "$r/.forge/private" | cut -c1-10)" drwx------39+eq "$(inode "$r/.forge/www/dami/open/releases/download/v1/tool")" "$(inode "$r/.forge/store/$open/tool")"40+missing "$r/.forge/private/store/$open"