feat: extract reforge engine into a standalone consumable flake
The forgejo-sandbox / reforge harness, lifted out of the machine config into a host-agnostic, generic engine anyone can consume with Nix. Two layers: - engine (this repo) — nixosModules.reforge stands up the sandbox forge, provisions role accounts + tokens, enforces branch protection, and puts the reforge-* CLI + forgejo-mcp on PATH. Carries no project specifics. - run config — per-project manifest/charter/agenda/issues an adopter fills in; scaffold one with the `reforge` flake template. Portability fixes vs the in-config version: - forgejo-mcp resolved from $REFORGE_MCP_BIN or PATH, never a named host (kills the nixosConfigurations.omni hardcode). - all instance data + paths parameterized via REFORGE_* env, baked into the reforge-scripts wrappers from module options (configDir, agentsDir, refsDir, org, port, tokenOwner, ...). - option namespace neutral (reforge.* not omni.packs.*); settings policies carry no absolute /etc/nixos paths. - role briefs + orchestrator playbook genericized: all project specifics point at the charter; refs corpus optional. Validated: nix flake check (eval) + builds of forgejo-mcp, reforge-scripts, and a module-eval check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
df0fd9a9ba
32 changed files with 2698 additions and 0 deletions
151
scripts/reforge-seed.sh
Normal file
151
scripts/reforge-seed.sh
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/env bash
|
||||
# Seed the sandbox forge for a reforge run (docs/reforge.md). The repo set,
|
||||
# pinned bases and declared targets live in the run manifest,
|
||||
# $REFORGE_CONFIG_DIR/manifest.txt:
|
||||
#
|
||||
# - kind=fork -> clone of the upstream at base_ref (branch, tag, or
|
||||
# commit SHA; full history), pushed as `main`, then
|
||||
# protected (no direct push, security-lead approval
|
||||
# required).
|
||||
# - kind=original -> empty placeholder repo, unprotected until a first
|
||||
# scaffold exists.
|
||||
# - charter -> in-forge copy of $REFORGE_CONFIG_DIR/charter.md
|
||||
# (README.md) + agenda.md (AGENDA.md).
|
||||
#
|
||||
# Targets are never pushed into the sandbox — the acceptance diff against
|
||||
# them is reforge-compare.
|
||||
#
|
||||
# Idempotent: every step is check-before-create / skip-if-pushed, so
|
||||
# re-running converges. NOTE: changing base_ref does NOT re-seed an
|
||||
# existing repo — reset first (reforge-reset reset).
|
||||
#
|
||||
# reforge-seed
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
ADMIN_USER=${REFORGE_ADMIN_USER:-sandbox-admin}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
CONFIG_DIR=${REFORGE_CONFIG_DIR:?set REFORGE_CONFIG_DIR to your run config dir (manifest.txt, charter.md, agenda.md)}
|
||||
TOKEN_FILE=${REFORGE_ADMIN_TOKEN_FILE:-$TOKENS_DIR/${ADMIN_USER}.token}
|
||||
|
||||
API="$FORGE_URL/api/v1"
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
MANIFEST="$CONFIG_DIR/manifest.txt"
|
||||
[ -r "$MANIFEST" ] || { echo "seed: no readable manifest at $MANIFEST" >&2; exit 1; }
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
# Loud abort: a mid-run failure (bad pin, unreachable upstream) must not
|
||||
# scroll by unnoticed — the run is incomplete until "converged" prints.
|
||||
trap 'echo; echo "seed: ABORTED — fix the error above and re-run (completed repos are skipped)." >&2' ERR
|
||||
RESP="$WORK/resp"
|
||||
|
||||
api() { # method path [json-body] -> echoes HTTP code, body in $RESP
|
||||
local method=$1 path=$2 data=${3:-}
|
||||
local args=(
|
||||
-sS -o "$RESP" -w '%{http_code}' -X "$method"
|
||||
-H "Authorization: token $TOKEN"
|
||||
-H 'Content-Type: application/json'
|
||||
)
|
||||
if [ -n "$data" ]; then args+=(--data "$data"); fi
|
||||
curl "${args[@]}" "$API$path"
|
||||
}
|
||||
ok() { case $1 in 200 | 201 | 204) return 0 ;; *) return 1 ;; esac }
|
||||
must() { # code context
|
||||
if ! ok "$1"; then
|
||||
echo "seed: $2 failed (HTTP $1):" >&2
|
||||
cat "$RESP" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
auth_url() { # name
|
||||
echo "http://$ADMIN_USER:$TOKEN@${FORGE_URL#http://}/$ORG/$1.git"
|
||||
}
|
||||
|
||||
ensure_repo() { # name description
|
||||
if [ "$(api GET "/repos/$ORG/$1")" = 404 ]; then
|
||||
must "$(api POST "/orgs/$ORG/repos" \
|
||||
"{\"name\":\"$1\",\"private\":true,\"auto_init\":false,\"default_branch\":\"main\",\"description\":\"$2\"}")" \
|
||||
"create repo $ORG/$1"
|
||||
echo " created repo $ORG/$1"
|
||||
fi
|
||||
}
|
||||
|
||||
protect_main() { # name (same rule the module puts on the working repo)
|
||||
if [ "$(api GET "/repos/$ORG/$1/branch_protections/main")" = 404 ]; then
|
||||
must "$(api POST "/repos/$ORG/$1/branch_protections" \
|
||||
'{"branch_name":"main","rule_name":"main","enable_push":false,"required_approvals":1,"enable_approvals_whitelist":true,"approvals_whitelist_username":["security-lead"],"block_on_rejected_reviews":true,"dismiss_stale_approvals":true}')" \
|
||||
"protect $ORG/$1 main"
|
||||
echo " protected main"
|
||||
fi
|
||||
}
|
||||
|
||||
seed_fork() { # name upstream ref
|
||||
local name=$1 url=$2 ref=$3 sha
|
||||
echo "-- $name <- $url @ $ref"
|
||||
ensure_repo "$name" "clean base of $url (no downstream work)"
|
||||
|
||||
if [ -n "$(git ls-remote "$(auth_url "$name")" refs/heads/main)" ]; then
|
||||
echo " main already seeded, skipping (reset to re-seed at a new base_ref)"
|
||||
else
|
||||
if [ "$ref" = auto ]; then
|
||||
ref=$(git ls-remote --symref "$url" HEAD |
|
||||
awk '/^ref:/ {sub("refs/heads/", "", $2); print $2}')
|
||||
fi
|
||||
echo " cloning at $ref…"
|
||||
if git clone --quiet --single-branch --branch "$ref" "$url" "$WORK/$name" 2>/dev/null; then
|
||||
: # branch or tag — single-branch history
|
||||
else
|
||||
# bare commit SHA — needs a full clone, then detached checkout
|
||||
git clone --quiet "$url" "$WORK/$name"
|
||||
git -C "$WORK/$name" checkout --quiet "$ref"
|
||||
fi
|
||||
sha=$(git -C "$WORK/$name" rev-parse HEAD)
|
||||
git -C "$WORK/$name" push --quiet "$(auth_url "$name")" "HEAD:refs/heads/main"
|
||||
rm -rf "${WORK:?}/$name"
|
||||
echo " seeded main = $sha ($ref)"
|
||||
fi
|
||||
|
||||
protect_main "$name"
|
||||
}
|
||||
|
||||
echo "== charter: in-forge copy of the standard + run agenda =="
|
||||
ensure_repo charter "project charter — the standard changes are judged against, plus the run agenda"
|
||||
if [ -z "$(git ls-remote "$(auth_url charter)" refs/heads/main)" ]; then
|
||||
git init --quiet -b main "$WORK/charter"
|
||||
cp "$CONFIG_DIR/charter.md" "$WORK/charter/README.md"
|
||||
cp "$CONFIG_DIR/agenda.md" "$WORK/charter/AGENDA.md"
|
||||
git -C "$WORK/charter" add README.md AGENDA.md
|
||||
git -C "$WORK/charter" \
|
||||
-c user.name="$ADMIN_USER" -c user.email="$ADMIN_USER@sandbox.invalid" \
|
||||
commit --quiet -m "charter: the standard + run agenda"
|
||||
git -C "$WORK/charter" push --quiet "$(auth_url charter)" main
|
||||
rm -rf "${WORK:?}/charter"
|
||||
echo " seeded charter (README.md + AGENDA.md <- $CONFIG_DIR)"
|
||||
fi
|
||||
protect_main charter
|
||||
|
||||
echo
|
||||
echo "== stack repos (from the manifest) =="
|
||||
mapfile -t ROWS < <(grep -Ev '^[[:space:]]*(#|$)' "$MANIFEST")
|
||||
for row in "${ROWS[@]}"; do
|
||||
IFS='|' read -r name kind upstream base_ref _target_url _target_ref <<<"$row"
|
||||
case $kind in
|
||||
fork)
|
||||
seed_fork "$name" "$upstream" "$base_ref"
|
||||
;;
|
||||
original)
|
||||
echo "-- $name (placeholder)"
|
||||
ensure_repo "$name" "placeholder — original software, to be rebuilt"
|
||||
;;
|
||||
*)
|
||||
echo "seed: unknown kind '$kind' for $name in $MANIFEST" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo
|
||||
echo "seed: converged. Repos: $FORGE_URL/$ORG"
|
||||
Loading…
Add table
Add a link
Reference in a new issue