clone

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

A shelf's link followed on its own owner page opens that shelf

cat committed e86770c
commit
e86770c361a7be928ca7eeec361bb105ff8bd646
parent
9bf849c7c2becd5b19a9b70b113d37d1e9a1d428
author
cat <admin@dev.site>
ci
✓ passed in 98s, signed by cat
kind
fix risk score 1.2 of 3, Jev's estimate
review
#26 A shelf link followed on its owner page opens that shelf
ci attestation
forge-attestation 1
type ci
commit e86770c361a7be928ca7eeec361bb105ff8bd646
tree 7c22ecfa65a1d84c72a887df62c90e3813740515
script .forge/ci 91d640bd19a231e82d21cc0a10d907253d9b0f41
result pass
exit 0
started 2026-10-19T05:30:07Z
duration 98s
runner (not shown here; refs/notes/ci has it)
system Darwin 25.5.0 arm64
env host
log sha256:4c30364a294b838727618c6b6ef73582104af4a944b2329781b2493f240c7aa6 289 lines
|pushed wip/i18n
|afe7ee5 running .forge/ci: greet.sh: say what it is
|  | sh: /bin/sh
|  | ok   greet
|afe7ee5 pass in 0s, signed
|pushed main
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|de25128 running .forge/ci: wave at the world
|  | sh: /bin/sh
|  | FAIL greet : got 'hello, world o/', want 'hello, world'
|de25128 fail in 0s, signed
|forge: 1 commit(s) did not pass
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|506024e running .forge/ci: update the test for the wave
|  | sh: /bin/sh
|  | ok   greet
|506024e 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 e80a319 -> /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
|af8a24d running .forge/ci: web, server and Python versions of greet
|  | sh: /bin/sh
|  | ok   greet
|af8a24d pass in 0s, signed
|pushed main
|9521fcb running .forge/ci: messages: notes for the later regions
|  | sh: /bin/sh
|  | ok   greet
|9521fcb 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 13ccf03 -> /sites/dami/forge/guide/
|(node:49670) 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 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.1DUlIV/wt/demo/root/.forge/www
|(node:49852) 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.1DUlIV/wt/demo/root/.forge/www
|(node:50155) 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.1DUlIV/wt/demo/root/.forge/www
|(node:50411) 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.1DUlIV/wt/demo/root/.forge/www
|(node:51128) 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.1DUlIV/wt/demo/root/.forge/www
|235 of 235 pages render the same without 11ty
|guide: 8 files, links ok
|ok

Signature: good, cat

4 files changed, +36 -15
Msite/assets/ui.ts15++++++++++++---
Mtest/page.ts25++++++++++++++++---------
Mtest/scenarios/shelves-fragment.sh9+++++++--
Mtest/scenarios/times-relative.sh2+-
M site/assets/ui.ts +12 -3
M test/page.ts +16 -9
@@ -4,10 +4,10 @@
44 // hidden, text, moving children, and selectors made of tags, classes and55 // attributes, descendants and lists.66 //7-// node test/page.ts shelves <page.html> [#fragment] [click <selector>]...7+// node test/page.ts shelves <page.html> [#fragment] [click <selector> | go <#fragment>]...88 //9-// After loading the page at the fragment and clicking each selector in10-// turn, prints the shelf panels that show, a line each:9+// After loading the page at the fragment, then clicking each selector or10+// going to each fragment on the page (hashchange) in turn, prints the shelf panels that show, a line each:1111 // `<head strip>: <repo> <repo>...`, the repos as their rows show them.1212 import fs from 'node:fs';1313 import vm from 'node:vm';
@@ -197,9 +197,11 @@ function load(file: string, hash: string) {
197197 },198198 });199199 const noop = (): void => {};200+ const location = { hash };200201 const page = vm.createContext({201202 document: doc,202- location: { hash },203+ window: { addEventListener: (type: string, f: (e: object) => void) => (handlers['window ' + type] ??= []).push(f) },204+ location,203205 localStorage: { getItem: () => null, setItem: noop },204206 matchMedia: () => ({ matches: false }),205207 setInterval: noop,
@@ -221,7 +223,11 @@ function load(file: string, hash: string) {
221223 if (!target) throw new Error(`test/page.ts: nothing on the page matches ${selector}`);222224 for (const f of handlers.click ?? []) f({ type: 'click', target, preventDefault: noop });223225 };224- return { doc, click };226+ const go = (to: string): void => {227+ location.hash = to;228+ for (const f of handlers['window hashchange'] ?? []) f({ type: 'hashchange' });229+ };230+ return { doc, click, go };225231 }226232 227233 const shown = (e: El): boolean => !e.closest('[hidden]');
@@ -229,14 +235,15 @@ const words = (s: string): string => s.replace(/\s+/g, ' ').trim();
229235 230236 const [mode, file, ...rest] = process.argv.slice(2);231237 if (mode !== 'shelves' || !file) {232- console.error('usage: node test/page.ts shelves <page.html> [#fragment] [click <selector>]...');238+ console.error('usage: node test/page.ts shelves <page.html> [#fragment] [click <selector> | go <#fragment>]...');233239 process.exit(2);234240 }235241 const hash = rest[0]?.startsWith('#') ? rest.shift()! : '';236-const { doc, click } = load(file, hash);242+const { doc, click, go } = load(file, hash);237243 for (let i = 0; i < rest.length; i += 2) {238- if (rest[i] !== 'click' || !rest[i + 1]) throw new Error(`test/page.ts: expected click <selector>, got ${rest.slice(i).join(' ')}`);239- click(rest[i + 1]);244+ if (rest[i] === 'click' && rest[i + 1]) click(rest[i + 1]);245+ else if (rest[i] === 'go' && rest[i + 1]) go(rest[i + 1]);246+ else throw new Error(`test/page.ts: expected click <selector> or go <#fragment>, got ${rest.slice(i).join(' ')}`);240247 }241248 for (const sec of doc.querySelectorAll('section.shelf').filter(shown)) {242249 const names = sec.querySelectorAll('.card').filter(shown).map((c) => words(c.querySelector('.card-name')!.textContent).replace(' / ', '/'));
M test/scenarios/shelves-fragment.sh +7 -2
M test/scenarios/times-relative.sh +1 -1