- commit
- 719bd89270c4caf5f75ac373a87f7ef923289adf
- parent
- 2938c4b214517a73b983cf0d2bdfe656b904a9eb
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 99s, signed by cat
- kind
- docs risk score 0.5 of 3, Jev's estimate
- review
- #23 The guide says what forge is; internals says how
ci attestation
forge-attestation 1 type ci commit 719bd89270c4caf5f75ac373a87f7ef923289adf tree 295e51e94c0a2c004a7214dde504121e76353fcf script .forge/ci 91d640bd19a231e82d21cc0a10d907253d9b0f41 result pass exit 0 started 2026-10-19T06:25:00Z duration 99s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:3d8a823dd9ab1c8d9ccfa6b7d9a0b2cc2166d95e2f680ceafd4bf450ba836d89 295 lines |pushed wip/i18n |ea5e9f4 running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |ea5e9f4 pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |d85a3fe running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |d85a3fe fail in 1s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |b9cc80e running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |b9cc80e 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 7fdc7c9 -> /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 |e62125d running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |e62125d pass in 0s, signed |pushed main |6c644f1 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |6c644f1 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 f773404 -> /sites/dami/forge/guide/ |(node:11907) 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.e41UUc/wt/demo/root/.forge/www |(node:12073) 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.e41UUc/wt/demo/root/.forge/www |(node:12242) 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.e41UUc/wt/demo/root/.forge/www |(node:12528) 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 2s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.e41UUc/wt/demo/root/.forge/www |(node:13268) 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 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.e41UUc/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 | 80 | +++++++++++++++--------------- |
| M | server/ | 2 | +- |
| M | site/ | 8 | ++-- |
@@ -38,7 +38,7 @@ It adds what stagit doesn't have, borrowing from [args.io](https://args.io):
3838 - **Releases are signed files.** `forge release publish v1.0 <file>...`3939 puts a tag's binaries on `refs/releases/v1.0` with a `SHA256SUMS` signed4040 by your key; the forge checks both and serves the files.41-- **Keyboard navigation and clean URLs**, e.g. `/dami/greet/tree/main/greet.sh/`.41+- **Keyboard navigation and clean URLs**, e.g. `/cat/greet/tree/main/greet.sh/`.4242 - **Search and syntax highlighting.** `/` searches a repo's commits, reviews,4343 wiki and file paths from a static index, and file contents from a SQLite4444 index the browser reads in byte ranges; code is highlighted at build time.@@ -58,17 +58,17 @@ sh demo/make-demo.sh # throwaway forge in demo/root, real hooks over fi
5858 python3 -m http.server 4817 -d demo/root/.forge/www5959 ```6060 61-The demo creates two users (`dami`, `ana`) and a repo with:61+The demo creates two users and a repo with:6262 6363 - a landed review and a two-layer stack, with both layers rewritten once,6464 and a description, comments and approvals on it (one comment outdated by6565 the rewrite, one approval stale);66-- a contribution from `ana` whose CI fails and then passes, and which `main`66+- a contribution from the second user whose CI fails and then passes, and which `main`6767 turns away because nobody approved it;6868 - a draft that grows, then is rebased onto a newer `main` and reworked;6969 - an abandoned branch, a wiki, and a docs site with a preview;7070 - a release of `v0.1`: the script and a source tarball;71-- a standalone site that `ana` owns;71+- a standalone site that the second user owns;7272 - code in JavaScript, Go, CSS, Python and markdown fences, and a review of a7373 1200-line file with long lines.7474 @@ -135,12 +135,12 @@ Repos live at `/srv/git/<owner>/<repo>.git`, and `<owner>` owns them. Anyone
135135 can read a public repo; a private one, only the people below. The owner and136136 anyone listed in `<repo>.git/forge/collaborators` can push anything. Other137137 users who can read it can push branches and previews under their own name138-(`ana/...`) and add CI notes, which must fast-forward. Nobody can force-push138+(`fox/...`) and add CI notes, which must fast-forward. Nobody can force-push139139 `main` unless `forge.allowforce` is set.140140 141141 ```sh142-ssh git@git.example.com collaborators dami/notes add ana # rm to take it back; no argument lists them143-ssh git@git.example.com who dami/notes # its owner, collaborators and readers142+ssh git@git.example.com collaborators cat/notes add fox # rm to take it back; no argument lists them143+ssh git@git.example.com who cat/notes # its owner, collaborators and readers144144 ```145145 146146 Only the owner changes a repo's collaborators; its collaborators can list@@ -156,8 +156,8 @@ A user is added by the operator, with their SSH public key, or invited by
156156 a link that lets them give it themselves:157157 158158 ```sh159-forge user add ana 'ssh-ed25519 AAAA... ana@laptop'160-forge user invite fox [--ttl 7d] [--repo dami/notes --as reader|collaborator]159+forge user add fox 'ssh-ed25519 AAAA... fox@laptop'160+forge user invite fox [--ttl 7d] [--repo cat/notes --as reader|collaborator]161161 forge user invites [rm fox] # invites not taken up yet; rm makes a link dead162162 forge users # everyone: keys, passkeys and tokens; then the invites163163 ```@@ -183,7 +183,7 @@ several names split by spaces or commas. Owning repos doesn't make anyone
183183 an admin. Set it on the server, as git:184184 185185 ```sh186-git config --file /srv/git/.gitconfig --add forge.admins dami # in the container:186+git config --file /srv/git/.gitconfig --add forge.admins cat # in the container:187187 ssh slate docker exec -u git forge git config --file /srv/git/.gitconfig --add forge.admins cat188188 git config --file /srv/git/.gitconfig --get-all forge.admins # the list; --unset-all forge.admins empties it189189 ```@@ -192,7 +192,7 @@ An admin runs the people commands above over ssh, from their own machine,
192192 the same as on the server:193193 194194 ```sh195-ssh git@git.example.com invite fox [--ttl 7d] [--repo dami/notes --as reader|collaborator]195+ssh git@git.example.com invite fox [--ttl 7d] [--repo cat/notes --as reader|collaborator]196196 ssh git@git.example.com invites [rm fox]197197 ssh git@git.example.com users198198 ```@@ -220,17 +220,17 @@ ssh command on.
220220 `ssh git@git.example.com new <name>` creates a repo under your name, and so221221 does pushing to one that doesn't exist yet: `forge shell` sees the push before222222 git does, creates the repo the same way (same name rules, hooks installed),223-prints `created dami/notes`, and lets the push go on.223+prints `created cat/notes`, and lets the push go on.224224 225225 ```sh226-git push git@git.example.com:dami/notes.git main226+git push git@git.example.com:cat/notes.git main227227 ```228228 229229 The first branch pushed becomes the default if it isn't `main`. A push to a230230 missing repo under someone else's name, or a clone or fetch of one, fails231231 as before.232232 233-`ssh git@git.example.com rm dami/notes --yes` takes a repo of yours off the233+`ssh git@git.example.com rm cat/notes --yes` takes a repo of yours off the234234 forge (without `--yes` it says what would go); collaborators can't, and235235 `forge repo rm <owner>/<repo>` does the same as root. Its clones, pages in236236 either root, sites, previews, hostnames, release downloads and insights go@@ -265,10 +265,10 @@ history when the repo goes public later; Jev isn't asked about private
265265 repos.266266 267267 ```sh268-ssh git@git.example.com readers dami/notes add ana # rm to take it back; no argument lists them269-ssh git@git.example.com who dami/notes # who reads it, and who may push it270-ssh git@git.example.com private dami/greet271-ssh git@git.example.com public dami/notes [--force]268+ssh git@git.example.com readers cat/notes add fox # rm to take it back; no argument lists them269+ssh git@git.example.com who cat/notes # who reads it, and who may push it270+ssh git@git.example.com private cat/greet271+ssh git@git.example.com public cat/notes [--force]272272 ssh git@git.example.com ls # yours, and the private ones you may read273273 ```274274 @@ -319,7 +319,7 @@ or emails, and losing them is fixed by running ssh again:
319319 ssh git@git.example.com passkey [name] # a link that adds a passkey: once, within 10 minutes320320 ssh git@git.example.com passkeys [rm <id>]321321 ssh git@git.example.com sessions [rm <id>|--all]322-ssh git@git.example.com token [--repo dami/notes] [--ttl 30d] [name] # printed once322+ssh git@git.example.com token [--repo cat/notes] [--ttl 30d] [name] # printed once323323 ssh git@git.example.com tokens [rm <id>]324324 ```325325 @@ -341,7 +341,7 @@ ssh git@git.example.com tokens [rm <id>]
341341 - **Tokens** are for one repo or for every repo you can read when they are342342 used, for 30 days unless `--ttl` (at most 365d). Send one as343343 `Authorization: Bearer <token>`, or as the password of basic auth:344- `git clone https://dami:<token>@git.example.com/dami/notes.git`, a344+ `git clone https://cat:<token>@git.example.com/cat/notes.git`, a345345 credential helper, or the token alone as the user name.346346 347347 identity.db keeps only SHA-256 hashes of cookies, tokens, enrollment@@ -368,7 +368,7 @@ Push options set metadata: `forge push -t "Title" -d "Description" --draft --tar
368368 The description is one line (`\n` makes a line break, and so does a real369369 newline in the argument) and renders as markdown under the title, sanitized370370 like a README. Each push that creates or updates a review prints its link,371-`review #7: https://git.example.com/dami/greet/reviews/7/ (on #6 dami/flags)`,371+`review #7: https://git.example.com/cat/greet/reviews/7/ (on #6 cat/flags)`,372372 from `FORGE_BASE_URL` (just the path if that isn't set).373373 374374 ### Comments and approvals@@ -385,7 +385,7 @@ forge comment -m "Looks good" # the current branch's review
385385 forge comment 7 greet.sh:12 # a line comment; the message from $EDITOR or stdin386386 forge approve 7387387 forge land 7 # what the server gates check, then fast-forward388-ssh git@git.example.com land dami/greet 7 # the forge lands it itself; no clone needed388+ssh git@git.example.com land cat/greet 7 # the forge lands it itself; no clone needed389389 ```390390 391391 - **Comments** are files, `<repo>.git/forge/comments/<review>/<n>`: header@@ -460,7 +460,7 @@ minute. Caddy sends these posts to the private server before it looks in
460460 461461 ```sh462462 curl -H "Authorization: Bearer $token" -d 'body=Looks good' -d 'line=greet.sh:12' \463- https://git.example.com/dami/greet/reviews/7/comment463+ https://git.example.com/cat/greet/reviews/7/comment464464 ```465465 466466 ### CI attestations, and what they prove@@ -478,11 +478,11 @@ script .forge/ci 99a0d5e...
478478 result pass479479 exit 0480480 duration 41s481-runner cat@early-yak-00481+runner cat@laptop482482 system Darwin 25.5.0 arm64483483 env host484484 log sha256:... 120 lines485-|ok greet --name ana485+|ok greet --name fox486486 -----BEGIN SSH SIGNATURE-----487487 ```488488 @@ -559,7 +559,7 @@ commit whose tree is the built output.
559559 ```sh560560 git tag v1.0561561 forge release publish v1.0 dist/greet-linux-amd64 dist/greet-1.0.tar.gz562-forge release verify --url https://git.example.com/dami/greet/releases/download/v1.0/ \562+forge release verify --url https://git.example.com/cat/greet/releases/download/v1.0/ \563563 ~/Downloads/greet-linux-amd64 # SHA256SUMS and its .sig next to it564564 forge release ls # tag, file, size, sha256565565 forge release rm v1.0@@ -572,7 +572,7 @@ forge-release`, using the key that signs CI results. What it signs is
572572 573573 ```574574 forge-release 1575-repo dami/greet575+repo cat/greet576576 tag v1.0577577 <the bytes of SHA256SUMS>578578 ```@@ -589,7 +589,7 @@ the files and the old ones aren't kept reachable.
589589 release they say they are from: `--repo` and `--tag`, else `--url`, else590590 the path they were saved under if it is a download path (`wget -x`), else591591 the remote for the repo. It says which release it verified (`ok592-greet-linux-amd64: matches SHA256SUMS of dami/greet v1.0, signed by dami`).592+greet-linux-amd64: matches SHA256SUMS of cat/greet v1.0, signed by cat`).593593 The forge's keys come from `--signers <file>`, or `ssh <host> signers` with594594 `--host`, `forge.allowedsigners`, `forge.host` or the remote.595595 @@ -617,7 +617,7 @@ site (a private repo's into a store of its own, below), and hard-links its
617617 files into618618 `/<owner>/<repo>/releases/download/<tag>/` (a `/` in the tag becomes `~`).619619 `latest/` next to them holds the files of the newest tag that has a620-release, so `/dami/greet/releases/download/latest/greet-linux-amd64` keeps620+release, so `/cat/greet/releases/download/latest/greet-linux-amd64` keeps621621 working from one release to the next. Hard links, not a symlink as for a622622 site: rclone skips symlinks, and the [mirror](#the-pages-mirror) copies623623 these. The page build links them into the repo's new pages before it swaps@@ -656,10 +656,10 @@ Readers can also get them over ssh, and to anyone else these commands
656656 answer as for a missing repo:657657 658658 ```sh659-curl -fO -u ana:<token> https://git.example.com/dami/notes/releases/download/v1.0/tool # token --repo dami/notes660-ssh git@git.example.com release ls dami/notes661-ssh git@git.example.com release get dami/notes v1.0 tool > tool # also SHA256SUMS, SHA256SUMS.sig662-forge release verify --repo dami/notes --tag v1.0 tool659+curl -fO -u fox:<token> https://git.example.com/cat/notes/releases/download/v1.0/tool # token --repo cat/notes660+ssh git@git.example.com release ls cat/notes661+ssh git@git.example.com release get cat/notes v1.0 tool > tool # also SHA256SUMS, SHA256SUMS.sig662+forge release verify --repo cat/notes --tag v1.0 tool663663 ```664664 665665 A verified download proves what a verified attestation does, about these@@ -945,8 +945,8 @@ On slate, everything lives in `/srv/forge` (`src/`, `data/`, `forge.env`):
945945 946946 ```sh947947 deploy/slate/deploy.sh # ship HEAD (git archive), docker compose up -d --build948-ssh slate docker exec forge forge-admin user add ana 'ssh-ed25519 AAAA...'949-ssh slate docker exec forge forge-admin user invite ana --repo dami/notes --as collaborator # a link instead948+ssh slate docker exec forge forge-admin user add fox 'ssh-ed25519 AAAA...'949+ssh slate docker exec forge forge-admin user invite fox --repo cat/notes --as collaborator # a link instead950950 ssh slate docker exec forge forge-admin users951951 ```952952 @@ -975,7 +975,7 @@ older ones, so install a current Node first:
975975 ```sh976976 rsync -a --exclude node_modules --exclude demo/root --exclude demo/work ./ root@HOST:/opt/forge/977977 scp ~/.ssh/id_ed25519.pub root@HOST:/tmp/me.pub978-ssh root@HOST sh /opt/forge/server/provision.sh git.example.com dami /tmp/me.pub [pages.example.com]978+ssh root@HOST sh /opt/forge/server/provision.sh git.example.com cat /tmp/me.pub [pages.example.com]979979 ```980980 981981 Either way: every key in `authorized_keys` is forced through `forge shell`, so@@ -1067,7 +1067,7 @@ For the private ones, run the same loop with `select(.isPrivate)` and
10671067 credentials.10681068 10691069 Then point existing clones at the forge:1070-`git remote set-url origin git@git.example.com:dami/<repo>.git`.1070+`git remote set-url origin git@git.example.com:cat/<repo>.git`.10711071 10721072 A public repo is public everywhere: pages, sites, releases, HTTPS clones and10731073 the pages mirror. A private one is read over ssh, on its pages once you sign@@ -1330,7 +1330,7 @@ git config --global forge.host git@src.rest # for forge import
13301330 Day to day:13311331 13321332 ```sh1333-git switch -c dami/topic && git commit ...1333+git switch -c cat/topic && git commit ...13341334 forge attest --stack # CI for every commit on top of main13351335 forge push -t "Title" # branch + notes; becomes a review13361336 forge land # CI and approval checks, then fast-forward main@@ -1381,14 +1381,14 @@ as fresh as the last push; its times stay relative in the browser. The
13811381 events log is cut and left out of backups, so after a restore the list has13821382 only landed reviews until the next pushes. **topics**: the shelves (below)13831383 across every public repo; one owner's links to that shelf on their page1384-(`/dami/#games`, which opens on its tab), and one that several owners1384+(`/cat/#games`, which opens on its tab), and one that several owners13851385 share to `/topics/#games`, every public repo on its shelf, each naming its13861386 owner. Private repos, and owners with only private ones, are never on13871387 any of it.13881388 13891389 Each owner's page, and `/topics/`, show repos as rows on shelves by category,13901390 newest first. A repo's category is one line in the bare repo's `category`1391-file, next to `description` (`printf 'games\n' > /srv/git/dami/greet.git/category`).1391+file, next to `description` (`printf 'games\n' > /srv/git/cat/greet.git/category`).13921392 A repo without one goes on the shelf for what Jev says it is (a web app, a13931393 library), and failing that on "other". Links above the shelves show one shelf13941394 alone, only repos with a site, or sort by name.@@ -1,4 +1,4 @@
1-# Pages, plus clones: git clone https://git.example.com/dami/greet.git. Git's1+# Pages, plus clones: git clone https://git.example.com/cat/greet.git. Git's22 # paths, smart and dumb, go to git http-backend, which serves only repos with33 # git-daemon-export-ok (not private ones); nothing else inside a bare repo is44 # served. A path www has no file for goes to the private server (private@@ -61,7 +61,7 @@ table.dg-tokens td.v { font: var(--fw-regular) var(--text-xs) var(--mono); color
6161 <div class="dg-page">{{ ui.tabs('example', [{href: '#tabs', text: 'all', n: 12, cur: true}, {href: '#tabs', text: 'tools', n: 5}, {href: '#tabs', text: 'games', n: 4}, {href: '#tabs', text: 'other', n: 3}]) }}</div>6262 6363 {{ ui.heading('panels', null, 'where content sits: ui.panel') }}64-{% set head %}<span class="grow"><b>dami</b> <a class="key" href="#panels">Keep a range-diff's marker at the left edge</a></span><span class="end"><a class="sha" href="#panels">8318d80</a><span class="ci pass">✓</span><time>2h</time><a href="#panels">412 commits</a></span>{% endset %}64+{% set head %}<span class="grow"><b>cat</b> <a class="key" href="#panels">Keep a range-diff's marker at the left edge</a></span><span class="end"><a class="sha" href="#panels">8318d80</a><span class="ci pass">✓</span><time>2h</time><a href="#panels">412 commits</a></span>{% endset %}6565 {% call ui.panel(head) %}6666 <table class="list files nav"><tbody>6767 <tr><td class="file-icon">{{ ui.icon('folder') }}</td><td class="main"><a href="#panels">lib</a></td><td class="num size"></td></tr>@@ -96,8 +96,8 @@ table.dg-tokens td.v { font: var(--fw-regular) var(--text-xs) var(--mono); color
9696 {{ ui.heading('section heading', 4, 'a count, then a quieter note') }}9797 {% call ui.panel() %}9898 {% call ui.listTable(['commit', 'author', {text: '+', cls: 'num'}, {text: '-', cls: 'num'}, 'ci']) %}99-<tr><td class="main"><a href="#list">Parse --name and --shout</a></td><td class="nowrap">dami</td><td class="num i">+12</td><td class="num d">-3</td><td><span class="ci pass">✓</span></td></tr>100-<tr><td class="main"><a href="#list">Keep a range-diff's marker at the left edge</a></td><td class="nowrap">ana</td><td class="num i">+4</td><td class="num d">-1</td><td><span class="ci fail">✗</span></td></tr>99+<tr><td class="main"><a href="#list">Parse --name and --shout</a></td><td class="nowrap">cat</td><td class="num i">+12</td><td class="num d">-3</td><td><span class="ci pass">✓</span></td></tr>100+<tr><td class="main"><a href="#list">Keep a range-diff's marker at the left edge</a></td><td class="nowrap">fox</td><td class="num i">+4</td><td class="num d">-1</td><td><span class="ci fail">✗</span></td></tr>101101 {% endcall %}102102 {% endcall %}103103 @@ -116,7 +116,7 @@ table.dg-tokens td.v { font: var(--fw-regular) var(--text-xs) var(--mono); color
116116 {{ ui.heading('type') }}117117 {% call ui.panel() %}118118 <div class="dg-fam"><div class="big" style="font-family: var(--sans)">Open Sans: tabs, lists, subjects, buttons</div><div class="use"><code>--sans</code> all interface text</div></div>119-<div class="dg-fam"><div class="big" style="font-family: var(--mono)">Noto Sans Mono: 0f0215e dami/help +12</div><div class="use"><code>--mono</code> anything copied or compared: hashes, ids, branches, paths, counts, code, diffs, commands</div></div>119+<div class="dg-fam"><div class="big" style="font-family: var(--mono)">Noto Sans Mono: 0f0215e cat/help +12</div><div class="use"><code>--mono</code> anything copied or compared: hashes, ids, branches, paths, counts, code, diffs, commands</div></div>120120 <div class="dg-fam"><div class="big" style="font-family: var(--serif)">Noto Serif: what people wrote</div><div class="use"><code>--serif</code> rendered prose only</div></div>121121 {% endcall %}122122