Every directory under the forge's root got an owner page, and forge user add makes one for each new user, so /<name>/ said who had an account. Owner pages are built only for owners with at least one public repo now, and a build of the index (or of one repo, or @owner) takes away the page of an owner who no longer has one, whose last repo went private or was removed. Scenario: no page for a user with no repos or only private ones, and one that goes when the last public repo does.
- commit
- 51330a11d4f4f0236a1ee7b446832936c7f0beaa
- parent
- 56690dd0e278f446809ace61f4728dce4b13c321
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 162s, signed by cat
- kind
- feat risk score 2.0 of 3, Jev's estimate
- review
- #30 Security: sign-in, invites, owner pages, lake, caps and the edge
ci attestation
forge-attestation 1 type ci commit 51330a11d4f4f0236a1ee7b446832936c7f0beaa tree 31ae4dfd1e299092acfc96efe3ea3b129d5bed36 script .forge/ci 8d2000a416f8160bcf710d98b72af9eb76287f58 result pass exit 0 started 2026-10-19T07:50:26Z duration 162s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:e3cfb6eeb9991fa5e207c3ab5860b866030bb8da6a075c108701115fe27b15f3 320 lines |pushed wip/i18n |6b42563 running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |6b42563 pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |de64b24 running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |de64b24 fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |f4cc753 running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |f4cc753 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 79515b5 -> /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 |c5e560a running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |c5e560a pass in 0s, signed |pushed main |cb833f5 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |cb833f5 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: mkdir -p guide/fonts && cp site/assets/fonts/*.woff2 guide/fonts/ |site guide: deployed 987b7dd -> /sites/dami/forge/guide/ |(node:63639) 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.vHNYYF/wt/demo/root/.forge/www |(node:63845) 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 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.vHNYYF/wt/demo/root/.forge/www |(node:64067) 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.vHNYYF/wt/demo/root/.forge/www |(node:64277) 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 4s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.vHNYYF/wt/demo/root/.forge/www |(node:65154) 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.vHNYYF/wt/demo/root/.forge/www |235 of 235 pages render the same without 11ty |guide: 8 files, links ok |ok
Signature: good, cat
| M | README.md | 4 | +++- |
| M | bin/ | 16 | ++++++++++++++++ |
| M | lib/ | 6 | +++++- |
| A | test/ | 24 | ++++++++++++++++++++++++ |
@@ -132,7 +132,9 @@ it:
132132 `git`).133133 134134 Repos live at `/srv/git/<owner>/<repo>.git`, and `<owner>` owns them. Anyone135-can read a public repo; a private one, only the people below. The owner and135+can read a public repo; a private one, only the people below. An owner's136+page, `/<owner>/`, is there only while they have a public repo, so a user's137+name alone isn't published. The owner and136138 anyone listed in `<repo>.git/forge/collaborators` can push anything. Other137139 users who can read it can push branches and previews under their own name138140 (`fox/...`) and add CI notes, which must fast-forward. Nobody can force-push@@ -2897,6 +2897,9 @@ cmd_build() {
28972897 (trap - EXIT && export GIT_DIR="$FORGE_ROOT/$only.git" && OWNER=${only%%/*} NAME=${only#*/} && releases_into "$out") ||28982898 say "forge: linking the release files of $only failed"28992899 fi2900+ # Owner pages were built if the index was (the swap moves it).2901+ owners_built=02902+ [ ! -f "$out/index.html" ] || owners_built=129002903 # Swap generated pages in. sites/, previews/ and hosts/ belong to the hooks. A29012904 # single-repo build refreshes owner index pages but only replaces that repo.29022905 for e in "$out"/*; do@@ -2915,6 +2918,19 @@ cmd_build() {
29152918 swap "$e" "$FORGE_WWW/$b"29162919 fi29172920 done2921+ # An owner without a public repo has no page (lib/forge.ts): one whose2922+ # last went private or away loses theirs.2923+ if [ -n "$only" ] && [ "$owners_built" = 1 ]; then2924+ for p in "$FORGE_WWW"/*/index.html; do2925+ [ -f "$p" ] || continue2926+ o=$(basename "$(dirname "$p")")2927+ case $o in assets | sites | previews | hosts | insights | design | topics) continue ;; esac2928+ case $only in @*) [ "$o" = "${only#@}" ] || continue ;; esac2929+ [ ! -f "$out/$o/index.html" ] || continue2930+ rm -f "$p" "$FORGE_WWW/$o/activity.json"2931+ rmdir "$FORGE_WWW/$o" 2>/dev/null || true2932+ done2933+ fi29182934 # Made private while this was building: its pages go after all. Public:29192935 # its private pages go, if it had any.29202936 if [ "$only" != - ] && [ -f "$FORGE_ROOT/$only.git/HEAD" ]; then@@ -470,7 +470,11 @@ export function loadForge(env: NodeJS.ProcessEnv = process.env): Forge {
470470 471471 // The index's panels; a private build has no index.472472 const lately = priv ? { items: [], active: {} } : recent(cfg, summaries);473- const owners = ownerNames.filter((o) => !cfg.only?.startsWith('@') || cfg.only === `@${o}`).map((owner) => ownerPage(cfg, owner, summaries.filter((s) => s.owner === owner)));473+ // Only owners with a public repo have a page: a user's name alone (forge474+ // user add makes their directory) is nobody's business.475+ const owners = ownerNames476+ .filter((o) => (!cfg.only?.startsWith('@') || cfg.only === `@${o}`) && summaries.some((s) => s.owner === o))477+ .map((owner) => ownerPage(cfg, owner, summaries.filter((s) => s.owner === owner)));474478 for (const o of owners) {475479 if (!priv && o.activity) raw.push({ path: `/${o.owner}/activity.json`, data: Buffer.from(JSON.stringify(o.activity.data) + '\n') });476480 }@@ -0,0 +1,24 @@
1+# Only owners with a public repo have a page. A user with no repos, or2+# only private ones, has none, so /<name>/ doesn't say who has an account;3+# an owner whose last public repo goes private loses theirs at the next4+# build, as one whose repo is removed does.5+. "$(dirname "$0")/../lib.sh"6+user dami7+user ana8+user bob9+repo dami/open10+repo --private bob/hidden11+build12+exists "$FORGE_WWW/dami/index.html"13+missing "$FORGE_WWW/ana"14+missing "$FORGE_WWW/bob"15+lacks "$FORGE_WWW/index.html" 'ana|bob'16+repo ana/notes17+build ana/notes18+exists "$FORGE_WWW/ana/index.html"19+sync_builds20+ok ssh_as ana private ana/notes21+missing "$FORGE_WWW/ana"22+exists "$FORGE_WWW/dami/index.html"23+ok ssh_as dami rm dami/open --yes24+missing "$FORGE_WWW/dami"