| M | README.md | 12 | ++++++- |
| M | bin/ | 53 | +++++++++++++++++++++++++++--- |
| M | guide/ | 2 | +- |
| M | lib/ | 4 | ++ |
| M | lib/ | 2 | +- |
| M | lib/ | 4 | +- |
| M | site/ | 2 | +- |
| M | site/ | 2 | +- |
| M | site/ | 2 | +- |
| M | site/ | 2 | +- |
| A | test/ | 49 | ++++++++++++++++++++++++++++ |
| M | test/ | 5 | ++- |
@@ -361,7 +361,7 @@ script .forge/ci 99a0d5e...
361361 result pass362362 exit 0363363 duration 41s364-runner dami@mbp364+runner cat@early-yak-00365365 system Darwin 25.5.0 arm64366366 env host367367 log sha256:... 120 lines@@ -369,6 +369,14 @@ log sha256:... 120 lines
369369 -----BEGIN SSH SIGNATURE-----370370 ```371371 372+`runner` is your forge name and a made-up name for the machine:373+`git config forge.user`, else the name `allowed_signers` gives your signing374+key; and `git config forge.machine`, else two words and two hex digits375+picked once from a hash of the hostname and a random salt, then kept in your376+global git config. Never the hostname or your local account, since the377+statement is published; site deploys and releases name their builder the378+same way (`Built-By`). Pages don't show either: they show who signed.379+372380 The statement is appended to `refs/notes/ci`, and `forge push` merges and373381 pushes those notes with the branch. Results are keyed by **tree**, so a374382 reworded or reordered commit with an identical tree keeps its result without@@ -680,7 +688,7 @@ Tables: `events` (pushes, comments, approvals, builds), `builds` and
680688 `build_repos` (each line of `metrics.jsonl`), `reviews` (each line of a681689 review log: created, revisions, approvals, deleted), `review_ends` (landed682690 or closed, and when) and `ci` (each attestation: commit, tree, result,683-duration, runner, signer). A review counts as landed when its branch is691+duration, runner, signer; `ci.parquet` leaves the runner out). A review counts as landed when its branch is684692 deleted with its tip in the default branch, as `forge land` leaves it; one685693 whose tip reaches the default branch while the branch stays counts from686694 when the lake saw it.@@ -149,6 +149,49 @@ stack_base() {
149149 done150150 }151151 152+# whoami_at: who made an attestation, site deploy or release, and where, as153+# the lines publish it: <forge user>@<machine>. The forge user is154+# git config forge.user, else the name allowed_signers gives the signing155+# key (forge.allowedsigners), else unknown; never the local account name.156+# The machine is git config forge.machine, which is a made-up name the157+# first time: two words and two hex digits picked by a hash of the158+# hostname and a random salt, stored in the global git config, so it stays159+# the same on this machine and says nothing about it. Pages show neither:160+# who signed is what they show.161+whoami_at() { printf '%s@%s' "$(forge_user)" "$(machine)"; }162+163+forge_user() {164+ u=$(git config --get forge.user 2>/dev/null || true)165+ if [ -z "$u" ]; then166+ k=$(signing_key 2>/dev/null || true)167+ case $k in *.pub) ;; *) [ -f "$k.pub" ] && k=$k.pub ;; esac168+ s=$(git config --get forge.allowedsigners 2>/dev/null || true)169+ case $s in "~/"*) s=$HOME/${s#"~/"} ;; esac170+ if [ -f "$k" ] && [ -f "$s" ]; then171+ blob=$(awk '{ print $2; exit }' "$k")172+ [ -z "$blob" ] || u=$(awk -v b="$blob" '{ for (i = 2; i <= NF; i++) if ($i == b) { split($1, p, ","); print p[1]; exit } }' "$s")173+ fi174+ fi175+ printf '%s' "${u:-unknown}"176+}177+178+machine() {179+ m=$(git config --get forge.machine 2>/dev/null || true)180+ if [ -z "$m" ]; then181+ h=$( (hostname 2>/dev/null; od -An -N16 -tx1 /dev/urandom) | { shasum -a 256 2>/dev/null || sha256sum; } | cut -c1-6)182+ m=$(printf '%s\n' "$h" | awk '{183+ a = "amber brisk calm deft eager fair gentle hazel ivory jolly keen lucid mellow nimble olive plain quiet rosy sage tidy umber vivid wry young zesty bold clear dusky early fern grand humble"184+ n = "heron otter finch badger lark marten wren stoat ibis vole puffin tern egret hare lynx newt plover raven robin seal shrike swift teal thrush viper whelk yak zebu crane dove kestrel moth"185+ split(a, A, " "); split(n, N, " ")186+ x = index("0123456789abcdef", substr($0, 1, 1)) * 16 + index("0123456789abcdef", substr($0, 2, 1))187+ y = index("0123456789abcdef", substr($0, 3, 1)) * 16 + index("0123456789abcdef", substr($0, 4, 1))188+ printf "%s-%s-%s", A[x % 32 + 1], N[y % 32 + 1], substr($0, 5, 2)189+ }')190+ git config --global forge.machine "$m" 2>/dev/null || true191+ fi192+ printf '%s' "$m"193+}194+152195 signing_key() {153196 k=${FORGE_SIGNING_KEY:-$(git config --get forge.signingkey || git config --get user.signingkey || true)}154197 if [ -z "$k" ]; then@@ -262,7 +305,7 @@ attest_one() {
262305 echo "exit $code"263306 echo "started $started"264307 echo "duration ${dur}s"265- echo "runner $(id -un)@$(hostname -s 2>/dev/null || hostname)"308+ echo "runner $(whoami_at)"266309 echo "system $(uname -srm)"267310 echo "env $envdesc"268311 echo "log sha256:$(sha256 "$tmp/log") $(wc -l <"$tmp/log" | tr -d ' ') lines"@@ -454,8 +497,8 @@ publish_dir() {
454497 say "site $name: unchanged"455498 return 0456499 fi457- msg=$(printf 'site %s: %s\n\nSource-Commit: %s\nSource-Ref: %s\nBuild: %s\nBuilt-By: %s@%s\n' \458- "$name" "$(git log -1 --format=%s "$src" 2>/dev/null || echo "$dir")" "$src" "$br" "$build" "$(id -un)" "$(hostname -s 2>/dev/null || hostname)")500+ msg=$(printf 'site %s: %s\n\nSource-Commit: %s\nSource-Ref: %s\nBuild: %s\nBuilt-By: %s\n' \501+ "$name" "$(git log -1 --format=%s "$src" 2>/dev/null || echo "$dir")" "$src" "$br" "$build" "$(whoami_at)")459502 # Signed with the key that signs CI results, so the forge can show who460503 # deployed it.461504 commit=@@ -584,8 +627,8 @@ release_publish() {
584627 for f in SHA256SUMS SHA256SUMS.sig; do printf '100644 blob %s\t%s\n' "$(git hash-object -w -- "$tmp/$f")" "$f" >>"$tmp/tree"; done585628 tree=$(git mktree <"$tmp/tree")586629 rm -rf "$tmp"587- msg=$(printf 'release %s: %s\n\nTag: %s\nSource-Commit: %s\nBuilt-By: %s@%s\n' \588- "$tag" "$(git log -1 --format=%s "$src")" "$tag" "$src" "$(id -un)" "$(hostname -s 2>/dev/null || hostname)")630+ msg=$(printf 'release %s: %s\n\nTag: %s\nSource-Commit: %s\nBuilt-By: %s\n' \631+ "$tag" "$(git log -1 --format=%s "$src")" "$tag" "$src" "$(whoami_at)")589632 commit=$(printf '%s\n' "$msg" | git -c gpg.format=ssh -c "user.signingkey=$key" commit-tree -S "$tree" 2>/dev/null) ||590633 commit=$(printf '%s\n' "$msg" | git commit-tree "$tree")591634 r=$(remote)@@ -146,7 +146,7 @@ script .forge/ci 99a0d5ea2eee...<span class="n">2</span>
146146 exit 0147147 started 2026-10-18T02:00:14Z148148 duration 0s149-runner dami@Mac<span class="n">4</span>149+runner cat@early-yak-00<span class="n">4</span>150150 system Darwin 25.5.0 arm64151151 env host152152 log sha256:6cc79019... 4 lines@@ -543,6 +543,10 @@ export const filters = {
543543 },544544 // A repo card's color: a hue from its name, the same on every build.545545 hue: (name: string): number => [...name].reduce((h, c) => (h * 31 + c.charCodeAt(0)) % 360, 7),546+ // An attestation as a page shows it: the runner's value left out, since a547+ // machine name can say who someone is; the note in refs/notes/ci has the548+ // statement as signed, for checking the signature.549+ statement: (payload: string): string => payload.replace(/^runner .*$/m, 'runner (not shown here; refs/notes/ci has it)'),546550 short: (sha: string | null | undefined): string => String(sha ?? '').slice(0, 7),547551 // Unix seconds (review logs) or an ISO date.548552 ago(ts: number | string | null): string {@@ -97,7 +97,7 @@ async function exportScope(lake: Lake, s: Scope, publicRepos: string[]): Promise
9797 SELECT repo, review, kind, ts, "user", rev, sha, base, tree, branch, title, NULL::BOOLEAN AS observed FROM lake.reviews WHERE ${s.where}9898 UNION ALL SELECT repo, review, kind, ts, "user", NULL, sha, NULL, NULL, NULL, NULL, observed FROM lake.review_ends WHERE ${s.where}9999 ORDER BY repo, review, ts`);100- await copy('ci.parquet', `SELECT repo, "commit", tree, result, exit, started, duration_s, runner, system, env, signer, verified FROM lake.ci WHERE ${s.where} ORDER BY started`);100+ await copy('ci.parquet', `SELECT repo, "commit", tree, result, exit, started, duration_s, system, env, signer, verified FROM lake.ci WHERE ${s.where} ORDER BY started`);101101 out.files = FILES.slice(1).map((f) => [f, fs.statSync(path.join(tmp, f)).size]);102102 fs.writeFileSync(path.join(tmp, 'insights.json'), JSON.stringify(out) + '\n');103103 @@ -199,7 +199,7 @@ export function ci(status: CiStatus | null | undefined, { long = false }: { long
199199 const a = status.latest;200200 const who = a.verification.signer ?? 'an unknown key';201201 const word = WORD[status.state];202- if (long) return `<span class="ci ${status.state}">${MARK[status.state]} ${esc(word)}</span> <span class="desc">on ${esc(a.runner ?? 'unknown')} in ${esc(a.duration ?? '?')}, signed by ${esc(who)}</span>`;203- const detail = `${word} on ${a.runner ?? 'unknown'} in ${a.duration ?? '?'}, signed by ${who}${a.via ? ', same tree as ' + a.via.slice(0, 7) : ''}`;202+ if (long) return `<span class="ci ${status.state}">${MARK[status.state]} ${esc(word)}</span> <span class="desc">in ${esc(a.duration ?? '?')}, signed by ${esc(who)}</span>`;203+ const detail = `${word} in ${a.duration ?? '?'}, signed by ${who}${a.via ? ', same tree as ' + a.via.slice(0, 7) : ''}`;204204 return `<span class="ci ${status.state}" title="${esc(detail)}">${MARK[status.state]}</span>`;205205 }@@ -89,7 +89,7 @@
8989 <tr><td><a href="{{ base }}events.parquet">events.parquet</a></td><td>pushes, comments, approvals and builds as they happened</td></tr>9090 <tr><td><a href="{{ base }}builds.parquet">builds.parquet</a></td><td>each page build: time spent, pages written</td></tr>9191 <tr><td><a href="{{ base }}reviews.parquet">reviews.parquet</a></td><td>each review's lifecycle: created, revisions, approvals, landed or closed</td></tr>92-<tr><td><a href="{{ base }}ci.parquet">ci.parquet</a></td><td>each CI attestation: commit, tree, result, duration, runner, signer</td></tr>92+<tr><td><a href="{{ base }}ci.parquet">ci.parquet</a></td><td>each CI attestation: commit, tree, result, duration, system, signer</td></tr>9393 </table>9494 <pre class="cmd">duckdb -c "SELECT result, count(*), median(duration_s) FROM '{{ forge.config.baseUrl or 'https://<this forge>' }}{{ base }}ci.parquet' GROUP BY result"</pre>9595 </div>@@ -31,7 +31,7 @@ dl.facts dd { margin: 0; overflow-wrap: anywhere; }
3131 </dl>3232 {% if c.ci %}3333 <details class="att"><summary>CI attestation</summary>34-<pre>{{ c.ci.latest.payload }}</pre>34+<pre>{{ c.ci.latest.payload | statement }}</pre>3535 <p class="desc">Signature: {% if c.ci.latest.verification.ok %}good, {{ c.ci.latest.verification.signer }}{% if c.ci.latest.verification.key %} {{ c.ci.latest.verification.key }}{% endif %}{% else %}{{ c.ci.latest.verification.reason }}{% endif %}</p>3636 </details>3737 {% endif %}@@ -34,7 +34,7 @@ pagination:
3434 </table>3535 <p><a href="{{ r.files.sums }}" download>SHA256SUMS</a> {% if r.files.signed == 'good' %}<span class="ci pass" title="SHA256SUMS.sig verified against the forge's keys">✓ signed by {{ r.files.signer }}</span>{% elif r.files.signed %}<span class="ci fail">signature {{ r.files.signed }}</span>{% else %}<span class="desc">signature not checked</span>{% endif %} <span class="desc">(<a href="{{ r.files.sig }}" download>.sig</a>; <code>forge release verify <file></code>)</span>3636 <br>CI of {{ r.tag.sha | short }}: {{ r.ci | ciHtml(true) | safe }}37-<br><span class="desc">published {{ r.files.date | ago | safe }}{{ (' by ' ~ r.files.builtBy) if r.files.builtBy }}{% if r.files.source and r.files.source != r.tag.sha %}, from {{ r.files.source | short }}, where the tag was then{% endif %}</span></p>37+<br><span class="desc">published {{ r.files.date | ago | safe }}{% if r.files.source and r.files.source != r.tag.sha %}, from {{ r.files.source | short }}, where the tag was then{% endif %}</span></p>3838 </div>3939 {% endif %}4040 <div class="notes">@@ -20,7 +20,7 @@ forge site deploy docs</pre>
2020 <table class="list stack">2121 <tbody>2222 {% for h in s.history %}23-<tr><td class="date">{{ h.date | date }}</td><td class="main">{{ h.subject }}{% if loop.first %} <span class="status open">live</span>{% endif %}</td><td class="sha hide-sm">{% if h.source %}{{ h.source | commitLink(p.repo) | safe }}{% endif %}</td><td class="hide-sm desc">{{ h.builtBy or '' }}</td><td>{% if h.signed == 'good' %}<span class="ci pass" title="signed by {{ h.signer }}">✓ {{ h.signer }}</span>{% elif h.signed %}<span class="ci fail">{{ h.signed }}</span>{% else %}<span class="desc">unsigned</span>{% endif %}</td></tr>23+<tr><td class="date">{{ h.date | date }}</td><td class="main">{{ h.subject }}{% if loop.first %} <span class="status open">live</span>{% endif %}</td><td class="sha hide-sm">{% if h.source %}{{ h.source | commitLink(p.repo) | safe }}{% endif %}</td><td>{% if h.signed == 'good' %}<span class="ci pass" title="signed by {{ h.signer }}">✓ {{ h.signer }}</span>{% elif h.signed %}<span class="ci fail">{{ h.signed }}</span>{% else %}<span class="desc">unsigned</span>{% endif %}</td></tr>2424 {% endfor %}2525 </tbody>2626 </table>@@ -0,0 +1,49 @@
1+# What attestations, site deploys and releases publish about who made them:2+# the forge name the signing key has in allowed_signers (or forge.user) and3+# a made-up machine name kept in the global git config (or forge.machine);4+# never the hostname or the local account. Pages show neither, only who5+# signed.6+. "$(dirname "$0")/../lib.sh"7+user dami8+repo dami/greet9+clone dami dami/greet "$T/w/d"10+mkdir -p "$T/w/d/.forge"11+printf '#!/bin/sh\ntrue\n' >"$T/w/d/.forge/ci"12+chmod +x "$T/w/d/.forge/ci"13+at "$T/w/d" commit "first"14+export FORGE_USER=dami15+host=$(hostname -s 2>/dev/null || hostname)16+me=$(id -un)17+runner() { git -C "$T/w/d" notes --ref=ci show "$1" | sed -n 's/^runner //p' | tail -n 1; }18+19+ok at "$T/w/d" "$FORGE" attest20+r=$(runner HEAD)21+case $r in dami@[a-z]*-[a-z]*-[0-9a-f][0-9a-f]) ;; *) fail "runner is '$r'" ;; esac22+case $r in *"$host"* | "$me"@*) [ "$me" = dami ] || fail "runner '$r' names the host or the account" ;; esac23+m=$(git config --global --get forge.machine)24+eq "dami@$m" "$r"25+# The same name next time.26+at "$T/w/d" commit "second"27+ok at "$T/w/d" "$FORGE" attest28+eq "$(runner HEAD)" "$r"29+# forge.user and forge.machine win.30+at "$T/w/d" git config forge.user someone31+at "$T/w/d" git config forge.machine lab-box32+at "$T/w/d" commit "third"33+ok at "$T/w/d" "$FORGE" attest34+eq "$(runner HEAD)" someone@lab-box35+at "$T/w/d" git config --unset forge.user36+at "$T/w/d" git config --unset forge.machine37+38+# A release names its builder the same way.39+at "$T/w/d" git tag v140+mkdir "$T/f" && printf 'x\n' >"$T/f/tool"41+ok push_as dami "$T/w/d" origin main refs/notes/ci42+ok at "$T/w/d" "$FORGE" release publish v1 "$T/f/tool"43+eq "$(git --git-dir="$FORGE_ROOT/dami/greet.git" log -1 --format='%(trailers:key=Built-By,valueonly)' refs/releases/v1 | tr -d '\n')" "$r"44+45+# Pages show who signed, not the runner or the builder.46+build dami/greet47+lacks "$FORGE_WWW/dami/greet/commit/$(git -C "$T/w/d" rev-parse HEAD~1)/index.html" "$m"48+has "$FORGE_WWW/dami/greet/commit/$(git -C "$T/w/d" rev-parse HEAD~1)/index.html" 'signed by dami'49+lacks "$FORGE_WWW/dami/greet/releases/index.html" "$m"@@ -31,8 +31,9 @@ has "$p" "<td class=\"sum mono desc\" title=\"sha256\">$(t_sha256 "$T/f/greet-li
3131 has "$p" '<a href="/dami/greet/releases/download/v1/notes.txt" download>notes.txt</a>'3232 has "$p" '<a href="/dami/greet/releases/download/v1/SHA256SUMS" download>SHA256SUMS</a> <span class="ci pass"[^>]*>✓ signed by dami</span>'3333 has "$p" 'CI of [0-9a-f]{7}: <span class="ci pass">'34-# Built-By is whoever ran forge release publish, here the one running the tests.35-has "$p" "published .* by $(id -un)@"34+# Who built it isn't shown: who signed SHA256SUMS is.35+has "$p" 'published '36+lacks "$p" 'published [^<]* by '3637 eq "$(grep -c 'class="downloads"' "$p")" 13738 has "$p" 'id="v0"'3839 # A Releases tab (key e), current on this page; the home page links the