crates/storage-git-gix/parked-gix/.nornir/bench/forge-setup.sh
#!/usr/bin/env bash
# forge-setup — bring up the three servers the bake-off measures, from nothing.
#
# Split from `forge-bakeoff.sh` for one reason that is not tidiness: **gunnar
# holds an exclusive store lock while it serves.** `gunnar repo create` against
# a live data root fails with
#
# the store is locked by another writer (…/gunnar-store.lock); its owner is
# still running, or could not be shown to be gone
#
# which is `S-019` working as designed. So every gunnar repository has to exist
# before `gunnar serve` starts, while gitea and Forgejo take repositories over
# their HTTP API at any time. Doing it in the harness would have meant either
# stopping the server mid-run (and measuring a cold start) or quietly skipping
# the corpus.
#
# The two Go forges are containers, started through podman. Nothing here uses
# dwarves' RealRunner: it dispatches `RunKind::Container` to a fallback, and
# until 2026-08-03 that fallback was `DemoRunner`, which returns
# `ok("would run …")` having executed nothing.
set -u -o pipefail
B=${BAKEOFF_DIR:-/home/rickard/scratch/bakeoff}
G=${GUNNAR_BIN:-/home/rickard/scratch/cargo/gunnar-propaganda/release/gunnar}
CORPORA=${CORPORA:-"small large"}
die() { echo "FATAL: $*" >&2; exit 1; }
mkdir -p "$B/keys"
[ -f "$B/keys/id_ed25519" ] || ssh-keygen -t ed25519 -N '' -C bakeoff -f "$B/keys/id_ed25519" >/dev/null
chmod 600 "$B/keys/id_ed25519"
SSH="ssh -i $B/keys/id_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
# ── gunnar ──────────────────────────────────────────────────────────────────
pkill -f "gunnar serve --ssh-bind 127.0.0.1:12224" 2>/dev/null; sleep 1
rm -rf "$B/gunnar-data"; mkdir -p "$B/gunnar-data"
export GUNNAR_DATA_ROOT=$B/gunnar-data
"$G" namespace create bench >/dev/null || die "gunnar namespace create failed"
for r in $CORPORA; do
"$G" repo create "bench/$r" --hash sha1 --public >/dev/null || die "gunnar repo create bench/$r failed"
done
# The bootstrap admin (§5.1) is server-scoped, so no separate principal/grant is
# needed: the one key that pushes is the one key that reads.
nohup "$G" serve --ssh-bind 127.0.0.1:12224 --no-grpc --admin-key "@$B/keys/id_ed25519.pub" \
> "$B/gunnar.log" 2>&1 &
sleep 4
grep -q 'SSH-2.0-gunnar' <(timeout 5 bash -c 'exec 3<>/dev/tcp/127.0.0.1/12224 && head -c 30 <&3') \
|| die "gunnar did not answer on 12224: $(tail -3 "$B/gunnar.log")"
echo "gunnar up on 12224, repos: $CORPORA"
# ── the two Go forges ───────────────────────────────────────────────────────
# Rootless podman cannot bind :22 inside the container (the process runs as
# `git`, not root), so the built-in SSH server listens on 2222 and the published
# port maps to it. The first attempt at this died with
# `Unable to GetListener: listen tcp :22: bind: permission denied` — a container
# that exits during startup and a server that is merely slow look identical from
# the outside, which is why every step below is checked.
forge() { # name image port_http port_ssh envprefix
local name=$1 image=$2 ph=$3 ps=$4 pre=$5
podman rm -f "bakeoff-$name" >/dev/null 2>&1
# `rm -rf` as the invoking user CANNOT clear this: the files belong to the
# container's `git` uid, which maps to a subuid on the host. The first version
# of this script used plain rm, it failed silently, the old sqlite database
# survived, `admin user create` then failed as a duplicate, and setup carried
# on with a stale token. `podman unshare` runs inside the user namespace where
# those files are ours.
podman unshare rm -rf "$B/$name-data" 2>/dev/null; rm -rf "$B/$name-data" 2>/dev/null
mkdir -p "$B/$name-data"
podman run -d --name "bakeoff-$name" -p "$ph:3000" -p "$ps:2222" \
-e "${pre}__security__INSTALL_LOCK=true" -e "${pre}__database__DB_TYPE=sqlite3" \
-e "${pre}__server__DOMAIN=localhost" -e "${pre}__server__ROOT_URL=http://localhost:$ph/" \
-e "${pre}__server__SSH_DOMAIN=localhost" -e "${pre}__server__SSH_PORT=$ps" \
-e "${pre}__server__START_SSH_SERVER=true" -e "${pre}__server__SSH_LISTEN_PORT=2222" \
-v "$B/$name-data:/data:Z" "$image" >/dev/null || die "podman run $name failed"
local ok=0
for i in $(seq 1 60); do
sleep 2
[ "$(podman inspect -f '{{.State.Running}}' "bakeoff-$name" 2>/dev/null)" = "true" ] \
|| die "$name container EXITED during startup: $(podman logs --tail 5 "bakeoff-$name" 2>&1 | tail -3)"
[ "$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$ph/api/healthz")" = "200" ] && { ok=1; break; }
done
[ $ok -eq 1 ] || die "$name never became healthy on $ph"
local bin=gitea; [ "$name" = forgejo ] && bin=forgejo
podman exec -u git "bakeoff-$name" $bin admin user create --username bench --password benchpw123 \
--email bench@example.com --admin --must-change-password=false >/dev/null 2>&1
local tok; tok=$(podman exec -u git "bakeoff-$name" $bin admin user generate-access-token \
--username bench --scopes all --raw 2>&1 | tail -1)
# A LENGTH check here is hollow: `generate-access-token` prints its errors on
# stdout too, and "Command error: user already exists" is also 20+ characters.
# The only honest check is whether the token authenticates.
local who; who=$(curl -s -H "Authorization: token $tok" "http://localhost:$ph/api/v1/user" \
| sed -n 's/.*"login":"\([^"]*\)".*/\1/p')
[ "$who" = "bench" ] || die "$name access token does not authenticate (GET /user returned login='$who'); token was: ${tok:0:40}"
echo "$tok" > "$B/$name.token"
curl -s -o /dev/null -X POST -H "Authorization: token $tok" -H 'Content-Type: application/json' \
-d "{\"title\":\"bakeoff\",\"key\":\"$(cat "$B/keys/id_ed25519.pub")\"}" "http://localhost:$ph/api/v1/user/keys"
for r in $CORPORA; do
curl -s -o /dev/null -X POST -H "Authorization: token $tok" -H 'Content-Type: application/json' \
-d "{\"name\":\"$r\",\"private\":false,\"auto_init\":false}" "http://localhost:$ph/api/v1/user/repos"
# Read it back. `-o /dev/null` on the POST means a 401 and a 201 look the
# same, and that is exactly how the stale token got this far.
curl -s -H "Authorization: token $tok" "http://localhost:$ph/api/v1/repos/bench/$r" \
| grep -q "\"full_name\":\"bench/$r\"" || die "$name: repository bench/$r was not created"
done
# The real check: stock git must reach it. An HTTP 200 says the web app is up,
# not that the git side authenticates.
GIT_SSH_COMMAND="$SSH" git ls-remote "ssh://git@127.0.0.1:$ps/bench/${CORPORA%% *}.git" >/dev/null 2>&1 \
|| die "$name is healthy over HTTP but stock git cannot reach it on $ps"
echo "$name up on $ps ($(podman exec "bakeoff-$name" git --version))"
}
forge gitea docker.io/gitea/gitea:1.24 13000 12222 GITEA
forge forgejo codeberg.org/forgejo/forgejo:11 13001 12223 FORGEJO
echo "── all three servers answer stock git ──"