clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

Leave the private store out of backups, and rebuild it on restore

cat committed 42f0525
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

3 files changed, +53 -9
MREADME.md6+++--
Mbin/forge16+++++++-----
Atest/scenarios/release-private-store-backup.sh40++++++++++++++++++++++++++++++
M README.md +4 -2
M bin/forge +9 -7
@@ -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
A test/scenarios/release-private-store-backup.sh +40 -0
@@ -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"