clone

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

An admin sets the home page's motd over ssh

cat committed 38873d0
ssh motd sets the line under the home page's header for admins
(forge.admins), and forge motd does the same on the server:

    ssh git@src.rest motd "notes, tools and games"
    ssh git@src.rest motd            # print it
    ssh git@src.rest motd --clear    # no line at all
    ssh git@src.rest motd --default  # "a personal forge on plain git"

It is one line of plain text, trimmed, 72 characters at most, without
control characters (lib/motd.ts checks it; the page escapes it). It goes
in .forge/motd, which backups carry, and the index pages are rebuilt.
motd is one of the admins' commands, so to anyone else it is git-shell's
unknown command, byte for byte, and their help doesn't list it.
commit
38873d01038d33e362cc24c6ba0c4dbf0733a257
parent
a92a26928f539747da142a4a61470cc01ea3182a
author
cat <admin@dev.site>
ci
✓ passed in 86s, signed by cat
kind
feat risk score 0.8 of 3, Jev's estimate
review
#21 A home page of profiles, recent and topics; motd for admins; /login/ in panels
ci attestation
forge-attestation 1
type ci
commit 38873d01038d33e362cc24c6ba0c4dbf0733a257
tree d9cc1852fe205797e1e81a0dfc58465c0d220aa9
script .forge/ci 2dbcda6e044dc4b05503abde8369e2528413d3c7
result pass
exit 0
started 2026-10-19T04:43:51Z
duration 86s
runner (not shown here; refs/notes/ci has it)
system Darwin 25.5.0 arm64
env host
log sha256:f613a97fec5ceca5393c792d0b7458ead71b3a3a65bc488bfb9283b0dc8cb752 282 lines
|pushed wip/i18n
|de82f46 running .forge/ci: greet.sh: say what it is
|  | sh: /bin/sh
|  | ok   greet
|de82f46 pass in 0s, signed
|pushed main
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|63c302e running .forge/ci: wave at the world
|  | sh: /bin/sh
|  | FAIL greet : got 'hello, world o/', want 'hello, world'
|63c302e fail in 0s, signed
|forge: 1 commit(s) did not pass
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|290b5e8 running .forge/ci: update the test for the wave
|  | sh: /bin/sh
|  | ok   greet
|290b5e8 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 ebc4321 -> /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
|b211f9f running .forge/ci: web, server and Python versions of greet
|  | sh: /bin/sh
|  | ok   greet
|b211f9f pass in 0s, signed
|pushed main
|f34d561 running .forge/ci: messages: notes for the later regions
|  | sh: /bin/sh
|  | ok   greet
|f34d561 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 607dcb7 -> /sites/dami/forge/guide/
|(node:82000) 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.lyM7Pj/wt/demo/root/.forge/www
|(node:82155) 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.lyM7Pj/wt/demo/root/.forge/www
|(node:82325) 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.lyM7Pj/wt/demo/root/.forge/www
|(node:82485) 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.lyM7Pj/wt/demo/root/.forge/www
|(node:83205) 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.lyM7Pj/wt/demo/root/.forge/www
|235 of 235 pages render the same without 11ty
|guide: 8 files, links ok
|ok

Signature: good, cat

5 files changed, +139 -6
MREADME.md16+++++++++-
Mbin/forge47+++++++++++++++++++++++++++---
Atest/scenarios/motd-admin.sh36+++++++++++++++++++++++
Atest/scenarios/motd-hidden.sh29+++++++++++++++++++
Atest/scenarios/motd-limits.sh17+++++++++++
M README.md +14 -2
M bin/forge +43 -4
@@ -24,6 +24,7 @@
2424 # forge user invite <name> [--ttl 7d] [--repo <owner>/<repo> --as reader|collaborator] a one-time link to join2525 # forge user invites [rm <name>] invites not taken up yet2626 # forge users everyone: keys, passkeys; and who is invited27+# forge motd [<text>|--clear|--default] the line under the header on /2728 # forge readers|collaborators <owner>/<repo> [add|rm <user>] who may read or push a repo2829 # forge keys sync2930 # forge repo new [--private] <owner>/<name> [description]
@@ -57,8 +58,8 @@ NS=forge-ci
5758 RNS=forge-release5859 ZERO=00000000000000000000000000000000000000005960 # ssh commands for the forge's admins only (is_admin): the operator's60-# forge user invite, user invites and users.61-ADMIN_CMDS='invite invites users'61+# forge user invite, user invites and users, and motd.62+ADMIN_CMDS='invite invites users motd'6263 CI_SCRIPT=.forge/ci6364 6465 die() { printf 'forge: %s\n' "$*" >&2; exit 1; }
@@ -1508,6 +1509,42 @@ can_read() {
15081509 return 115091510 }15101511 1512+# forge motd [<text>|--clear|--default] (and ssh motd, for admins): the1513+# quiet line under the header on the home page. Without arguments it prints1514+# it. Text is one line of plain text, at most 72 characters, trimmed; the1515+# page escapes it (lib/motd.ts checks it). --clear shows no line, --default1516+# the default one. It is kept in .forge/motd, which backups carry: no file1517+# is the default, an empty one no line. A change rebuilds the index pages.1518+cmd_motd() {1519+ if [ -n "${FORGE_USER:-}" ]; then mu="usage: motd"; else mu="usage: forge motd"; fi1520+ f=$FORGE_STATE/motd1521+ if [ $# -eq 0 ]; then1522+ if [ ! -f "$f" ]; then1523+ node "$FORGE_HOME/lib/motd.ts" default1524+ say "(the default; motd <text> sets another)"1525+ elif [ -s "$f" ]; then cat "$f"1526+ else say "(none: the home page shows no line; motd <text> sets one)"; fi1527+ return 01528+ fi1529+ case $1 in1530+ --clear | --default)1531+ [ $# -eq 1 ] || die "$mu [<text>|--clear|--default]"1532+ if [ "$1" = --clear ]; then : >"$f"; else rm -f "$f"; fi1533+ ;;1534+ -*) die "$mu [<text>|--clear|--default]" ;;1535+ *)1536+ text=$(node "$FORGE_HOME/lib/motd.ts" check "$*") || exit 11537+ printf '%s\n' "$text" >"$f.tmp" && mv "$f.tmp" "$f"1538+ ;;1539+ esac1540+ rebuild - motd1541+ case $1 in1542+ --clear) say "the home page shows no line now" ;;1543+ --default) say "the home page shows the default line now" ;;1544+ *) say "the home page says: $text" ;;1545+ esac1546+}1547+15111548 # is_admin <user>: is user one of the forge's admins? They are the values15121549 # of forge.admins in the server's git config, $FORGE_ROOT/.gitconfig (the15131550 # git user's own; several values, or names split by spaces or commas).
@@ -2184,10 +2221,11 @@ cmd_shell_cmd() {
21842221 release) sc_release "$@" ;;21852222 rm) sc_rm "$@" ;;21862223 passkey | passkeys | sessions | tokens) credentials "$c" "$@" ;;2187- invite | invites | users)2224+ invite | invites | users | motd)21882225 # Reached only through the admins' git-shell-commands (cmd_shell).21892226 is_admin "$user" || die "unknown command $c"21902227 case $c in2228+ motd) cmd_motd "$@" ;;21912229 invite) user_invite "$@" ;;21922230 invites) node "$FORGE_HOME/lib/identity.ts" invites "$@" ;;21932231 users) [ $# -eq 0 ] || die "usage: users"; node "$FORGE_HOME/lib/identity.ts" users ;;
@@ -2206,7 +2244,7 @@ cmd_shell_cmd() {
22062244 ;;22072245 *) printf 'commands:\n new [--private] <name> [description] create %s/<name>\n ls list your repos, and private ones you may read\n signers keys that may sign CI results (for forge verify)\n site-url <owner>/<repo> <site> [branch] where a site deploy is served\n reviews <owner>/<repo> open and draft reviews (forge reviews)\n land <owner>/<repo> <id|branch> land a review here, with the same gates as a push\n comment, approve, approvals used by forge comment, approve and land\n private <owner>/<repo> only its owner, collaborators and readers can see it\n public <owner>/<repo> [--force] anyone can, once its whole history passes the secret check\n readers <owner>/<repo> [add|rm <user>] who may read it while it is private\n collaborators <owner>/<repo> [add|rm <user>] who may push anything to it; only its owner changes the list\n who <owner>/<repo> its owner, collaborators and readers\n release ls <owner>/<repo> its releases: tag, file, size, sha256\n release get <owner>/<repo> <tag> <file> a release file, on stdout\n rm <owner>/<repo> --yes remove a repo of yours (the admin can restore it for 14 days)\n passkey [name] a one-time link that adds a passkey, to sign in on the web\n passkeys [rm <id>] your passkeys\n sessions [rm <id>|--all] where you are signed in on the web; rm signs one out, --all all\n token [--repo <owner>/<repo>] [--ttl 30d] [name] a token for curl and HTTPS clones, shown once\n tokens [rm <id>] your tokens\na push to %s/<name>.git creates that repo when it is missing\n' "$user" "$user"22082246 if is_admin "$user"; then2209- printf 'for admins of the forge:\n invite <name> [--ttl 7d] [--repo <owner>/<repo> --as reader|collaborator] a one-time link that makes someone a user\n invites [rm <name>] invites not taken up yet; rm makes a link dead\n users everyone here: keys, passkeys, tokens; then the invites\n'2247+ printf 'for admins of the forge:\n invite <name> [--ttl 7d] [--repo <owner>/<repo> --as reader|collaborator] a one-time link that makes someone a user\n invites [rm <name>] invites not taken up yet; rm makes a link dead\n users everyone here: keys, passkeys, tokens; then the invites\n motd [<text>|--clear|--default] the line under the header on the home page: print it, set it (72 characters at most), hide it, or go back to the default\n'22102248 fi22112249 ;;22122250 esac
@@ -3769,6 +3807,7 @@ server)
37693807 ;;37703808 user) cmd_user "$@" ;;37713809 users) [ $# -eq 0 ] || die "usage: forge users"; load_config; node "$FORGE_HOME/lib/identity.ts" users ;;3810+motd) load_config; cmd_motd "$@" ;;37723811 readers | collaborators) cmd_names "$cmd" "$@" ;;37733812 keys) [ "${1:-}" = sync ] || die "usage: forge keys sync"; cmd_keys_sync ;;37743813 repo) cmd_repo "$@" ;;
A test/scenarios/motd-admin.sh +36 -0
A test/scenarios/motd-hidden.sh +29 -0
A test/scenarios/motd-limits.sh +17 -0