clank-together/deploy/forgejo/install-forgejo.sh
Clank Together 836610da8f Clank Together: coordinator, sandboxed verifier and agent client
Coordinator (accounts with approval, API tokens, tasks with leases, sandboxed MSVC verification against
the original exe, peer review by three agents for non-identical code, human review, serial merge queue),
the `td` contributor CLI with an MCP server for agents, pinned Ghidra integration, deployment (Caddy,
systemd, nftables, Forgejo) and the wowdecomp 15890 migration tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW
2026-10-18 01:29:16 +02:00

102 lines
5.8 KiB
Bash

#!/bin/sh
# Installs Forgejo as the git dashboard at https://<host>/git/ and makes the
# coordinator push every merge to it. Run as root from the unpacked source dir:
# sh deploy/forgejo/install-forgejo.sh <host> <admin-password-file>
# The password file holds the Forgejo admin password (one line); it is not printed.
set -eu
HOST="${1:?usage: install-forgejo.sh <host> <admin-password-file>}"
PWFILE="${2:?usage: install-forgejo.sh <host> <admin-password-file>}"
VERSION=15.0.10
# Forgejo release signing key ("Forgejo Releases <release@forgejo.org>").
SIGNING_KEY=EB114F5E6C0DC2BCDD183550A4B61A2DC5923710
ORG=${FJ_ORG:-clank}
REPO=${FJ_REPO:-game-decomp}
BOT=clank-bot
# "admin" is a reserved name in Forgejo.
ADMIN_USER=clank-admin
SRC=$(cd "$(dirname "$0")/../.." && pwd)
FJ="runuser -u git -- /usr/local/bin/forgejo --config /etc/forgejo/app.ini --work-path /var/lib/forgejo"
API=http://127.0.0.1:3000/api/v1
TD_DATA=/var/lib/togetherdecomp
echo "== forgejo $VERSION"
apt-get install -y -q gnupg curl git >/dev/null
if ! /usr/local/bin/forgejo --version 2>/dev/null | grep -q "version $VERSION[+ ]"; then
tmp=$(mktemp -d)
base="https://codeberg.org/forgejo/forgejo/releases/download/v$VERSION/forgejo-$VERSION-linux-amd64"
curl -fsSL "$base" -o "$tmp/forgejo"
curl -fsSL "$base.sha256" -o "$tmp/forgejo.sha256"
curl -fsSL "$base.asc" -o "$tmp/forgejo.asc"
(cd "$tmp" && echo "$(cut -d' ' -f1 forgejo.sha256) forgejo" | sha256sum -c -)
export GNUPGHOME="$tmp/gnupg"; mkdir -m 700 "$GNUPGHOME"
curl -fsSL "https://keys.openpgp.org/vks/v1/by-fingerprint/$SIGNING_KEY" | gpg --batch --quiet --import
# Releases are signed by a subkey; VALIDSIG ends with the primary key's fingerprint.
gpg --batch --status-fd 1 --verify "$tmp/forgejo.asc" "$tmp/forgejo" 2>/dev/null | grep -Eq "^\[GNUPG:\] VALIDSIG [0-9A-F]{40} .* $SIGNING_KEY\$" \
|| { echo "signature check FAILED"; exit 1; }
echo "signature ok"
install -m 0755 "$tmp/forgejo" /usr/local/bin/forgejo
rm -rf "$tmp"; unset GNUPGHOME
fi
echo "== user, dirs, config"
id git >/dev/null 2>&1 || adduser --system --group --home /var/lib/forgejo --shell /bin/bash --disabled-password git >/dev/null
mkdir -p /var/lib/forgejo/data /var/lib/forgejo/repositories /var/lib/forgejo/custom /etc/forgejo
chown -R git:git /var/lib/forgejo && chmod 750 /var/lib/forgejo
if [ ! -f /etc/forgejo/app.ini ]; then
sk=$(/usr/local/bin/forgejo generate secret SECRET_KEY)
it=$(/usr/local/bin/forgejo generate secret INTERNAL_TOKEN)
js=$(/usr/local/bin/forgejo generate secret JWT_SECRET)
sed -e "s|@HOST@|$HOST|g" -e "s|@SECRET_KEY@|$sk|" -e "s|@INTERNAL_TOKEN@|$it|" -e "s|@JWT_SECRET@|$js|" \
"$SRC/deploy/forgejo/app.ini.template" > /etc/forgejo/app.ini
fi
chown root:git /etc/forgejo /etc/forgejo/app.ini && chmod 750 /etc/forgejo && chmod 640 /etc/forgejo/app.ini
install -m 0644 "$SRC/deploy/forgejo/forgejo.service" /etc/systemd/system/forgejo.service
systemctl daemon-reload
systemctl enable --now forgejo.service
for i in $(seq 1 30); do curl -fs "$API/version" >/dev/null && break; sleep 1; done
curl -fs "$API/version" >/dev/null || { journalctl -u forgejo -n 30 --no-pager; exit 1; }
echo "== accounts"
ADMIN_PW=$(cat "$PWFILE")
if ! $FJ admin user list --admin | awk '{print $2}' | grep -qx "$ADMIN_USER"; then
$FJ admin user create --admin --username "$ADMIN_USER" --email "admin@$HOST" --password "$ADMIN_PW" --must-change-password=false >/dev/null
fi
if ! $FJ admin user list | awk '{print $2}' | grep -qx "$BOT"; then
$FJ admin user create --username "$BOT" --email "bot@$HOST" --random-password --must-change-password=false >/dev/null
fi
AUTH="$ADMIN_USER:$ADMIN_PW"
api() { curl -fsS -u "$AUTH" -H 'content-type: application/json' "$@"; }
echo "== organisation and repository"
api "$API/orgs/$ORG" >/dev/null 2>&1 || api -X POST "$API/orgs" -d "{\"username\":\"$ORG\",\"full_name\":\"Clank Together\",\"visibility\":\"public\",\"description\":\"Matching decompilations, built by people and their agents\"}" >/dev/null
if ! api "$API/repos/$ORG/$REPO" >/dev/null 2>&1; then
api -X POST "$API/orgs/$ORG/repos" -d "{\"name\":\"$REPO\",\"description\":\"Matching decompilation (written through the Clank Together coordinator)\",\"private\":false,\"default_branch\":\"main\"}" >/dev/null
fi
api -X PATCH "$API/repos/$ORG/$REPO" -d '{"has_pull_requests":false,"has_actions":false,"has_packages":false,"has_releases":false,"has_projects":false,"website":"https://'"$HOST"'/"}' >/dev/null
api -X PUT "$API/repos/$ORG/$REPO/collaborators/$BOT" -d '{"permission":"write"}' >/dev/null
echo "== push main and protect it"
# The bot token lives only in the coordinator repo's .git/config (data dir is 0700).
TDGIT="runuser -u togetherdecomp -- git -C $TD_DATA/repo"
if ! $TDGIT remote get-url forgejo >/dev/null 2>&1; then
TOKEN=$($FJ admin user generate-access-token --username "$BOT" --token-name "coordinator-$(date +%s)" --scopes write:repository --raw)
$TDGIT remote add forgejo "http://$BOT:$TOKEN@127.0.0.1:3000/$ORG/$REPO.git"
unset TOKEN
fi
$TDGIT push -q forgejo HEAD:refs/heads/main
api -X DELETE "$API/repos/$ORG/$REPO/branch_protections/main" >/dev/null 2>&1 || true
api -X POST "$API/repos/$ORG/$REPO/branch_protections" -d "{\"rule_name\":\"main\",\"enable_push\":true,\"enable_push_whitelist\":true,\"push_whitelist_usernames\":[\"$BOT\"],\"block_on_rejected_reviews\":false}" >/dev/null
echo "== coordinator settings"
runuser -u togetherdecomp -- /opt/node/bin/node -e '
const fs = require("fs"); const [file, host, org, repo] = process.argv.slice(1);
const c = JSON.parse(fs.readFileSync(file, "utf8"));
c.pushRemote = "forgejo";
c.gitCloneUrl = `https://${host}/git/${org}/${repo}.git`;
c.gitWebUrl = `https://${host}/git/${org}/${repo}`;
fs.writeFileSync(file, JSON.stringify(c, null, 2) + "\n");
' "$TD_DATA/server.json" "$HOST" "$ORG" "$REPO"
systemctl restart togetherdecomp
echo "== done: https://$HOST/git/$ORG/$REPO"