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>
80 lines
2.5 KiB
Bash
80 lines
2.5 KiB
Bash
#!/usr/bin/env bash
|
|
# Reforge lifecycle step 3 (docs/reforge.md): file the run agenda as issues
|
|
# in the relevant sandbox repos. The issue rows live in
|
|
# $REFORGE_CONFIG_DIR/issues.tsv (one per line):
|
|
#
|
|
# repo|title|body
|
|
#
|
|
# ('#' and blank lines are ignored; body is single-line markdown.) Keep
|
|
# this file as the machine-readable twin of your agenda.md — titles should
|
|
# carry an agenda item id (e.g. "[A1] ...") for traceability.
|
|
#
|
|
# Idempotent: an issue whose exact title already exists in the repo (any
|
|
# state) is skipped, so re-running converges.
|
|
#
|
|
# reforge-kickoff
|
|
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}
|
|
TOKEN_FILE=${REFORGE_ADMIN_TOKEN_FILE:-$TOKENS_DIR/${ADMIN_USER}.token}
|
|
|
|
API="$FORGE_URL/api/v1"
|
|
TOKEN=$(cat "$TOKEN_FILE")
|
|
ISSUES_FILE="$CONFIG_DIR/issues.tsv"
|
|
[ -r "$ISSUES_FILE" ] || { echo "kickoff: no readable issues file at $ISSUES_FILE" >&2; exit 1; }
|
|
|
|
WORK=$(mktemp -d)
|
|
trap 'rm -rf "$WORK"' EXIT
|
|
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"
|
|
}
|
|
|
|
filed=0 skipped=0 failed=0
|
|
mapfile -t ROWS < <(grep -Ev '^[[:space:]]*(#|$)' "$ISSUES_FILE")
|
|
current_repo=
|
|
for row in "${ROWS[@]}"; do
|
|
IFS='|' read -r repo title body <<<"$row"
|
|
|
|
# one issue-list fetch per repo (titles for the idempotency check)
|
|
if [ "$repo" != "$current_repo" ]; then
|
|
current_repo=$repo
|
|
if [ "$(api GET "/repos/$ORG/$repo/issues?state=all&type=issues&limit=50")" = 200 ]; then
|
|
jq -r '.[].title' <"$RESP" >"$WORK/titles" || : >"$WORK/titles"
|
|
else
|
|
: >"$WORK/titles"
|
|
fi
|
|
fi
|
|
|
|
if grep -qxF "$title" "$WORK/titles"; then
|
|
skipped=$((skipped + 1))
|
|
continue
|
|
fi
|
|
|
|
payload=$(jq -n --arg t "$title" --arg b "$body" '{title: $t, body: $b}')
|
|
code=$(api POST "/repos/$ORG/$repo/issues" "$payload")
|
|
if [ "$code" = 201 ]; then
|
|
echo " filed $repo: $title"
|
|
filed=$((filed + 1))
|
|
else
|
|
echo " FAILED $repo: $title (HTTP $code)" >&2
|
|
cat "$RESP" >&2
|
|
failed=$((failed + 1))
|
|
fi
|
|
done
|
|
|
|
echo
|
|
echo "kickoff: $filed filed, $skipped already present, $failed failed."
|
|
[ "$failed" = 0 ]
|