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
165
scripts/reforge-smoke.sh
Normal file
165
scripts/reforge-smoke.sh
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env bash
|
||||
# End-to-end smoke test of the sandbox forge toolchain (docs/architecture.md).
|
||||
# Run after every reset, before starting a run: exercises the full issue ->
|
||||
# branch -> PR -> review-gated merge flow against the working repo with the
|
||||
# provisioned role accounts, asserting both the happy path AND the gates:
|
||||
#
|
||||
# 1. backend-dev files an issue
|
||||
# 2. backend-dev pushes a feature branch
|
||||
# 3. backend-dev opens a PR referencing the issue
|
||||
# 4. merging WITHOUT approval is BLOCKED (branch protection)
|
||||
# 5. direct push to main is REJECTED (branch protection)
|
||||
# 6. reviewer leaves a comment review
|
||||
# 7. security-lead approves
|
||||
# 8. merge after approval succeeds
|
||||
# 9. main contains the change
|
||||
#
|
||||
# Leaves the issue/PR in the working repo as a record; exits non-zero if
|
||||
# any step fails.
|
||||
#
|
||||
# reforge-smoke
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
REPO=${REFORGE_REPO_NAME:-sandbox-project}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
API="$FORGE_URL/api/v1"
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
RESP="$WORK/resp"
|
||||
STAMP=$(date +%s)
|
||||
|
||||
tok() { cat "$TOKENS_DIR/$1.token"; }
|
||||
|
||||
api() { # user method path [json-body] -> echoes HTTP code, body in $RESP
|
||||
local user=$1 method=$2 path=$3 data=${4:-}
|
||||
local args=(
|
||||
-sS -o "$RESP" -w '%{http_code}' -X "$method"
|
||||
-H "Authorization: token $(tok "$user")"
|
||||
-H 'Content-Type: application/json'
|
||||
)
|
||||
if [ -n "$data" ]; then args+=(--data "$data"); fi
|
||||
curl "${args[@]}" "$API$path"
|
||||
}
|
||||
|
||||
pass=0 fail=0
|
||||
PASS() {
|
||||
echo " PASS $*"
|
||||
pass=$((pass + 1))
|
||||
}
|
||||
FAIL() {
|
||||
echo " FAIL $*"
|
||||
fail=$((fail + 1))
|
||||
}
|
||||
die() {
|
||||
FAIL "$*"
|
||||
cat "$RESP" >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── wait for the API ────────────────────────────────────────────────
|
||||
ready=
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -sf "$FORGE_URL/api/healthz" >/dev/null 2>&1; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$ready" = 1 ] || {
|
||||
echo "smoke: forge at $FORGE_URL is not healthy" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "== 1. backend-dev files an issue"
|
||||
code=$(api backend-dev POST "/repos/$ORG/$REPO/issues" \
|
||||
"{\"title\":\"smoke $STAMP: verify PR flow\",\"body\":\"Tooling smoke test — issue/branch/PR/review/merge round-trip.\"}")
|
||||
[ "$code" = 201 ] || die "issue create (HTTP $code)"
|
||||
issue=$(jq .number <"$RESP")
|
||||
PASS "issue #$issue created"
|
||||
|
||||
echo "== 2. backend-dev pushes a feature branch"
|
||||
clone_url="http://backend-dev:$(tok backend-dev)@${FORGE_URL#http://}/$ORG/$REPO.git"
|
||||
git clone --quiet "$clone_url" "$WORK/repo"
|
||||
git -C "$WORK/repo" checkout --quiet -b "smoke/$STAMP"
|
||||
echo "smoke $STAMP" >"$WORK/repo/smoke-$STAMP.txt"
|
||||
git -C "$WORK/repo" add "smoke-$STAMP.txt"
|
||||
git -C "$WORK/repo" -c user.name=backend-dev -c user.email=backend-dev@sandbox.invalid \
|
||||
commit --quiet -m "smoke: add smoke-$STAMP.txt (#$issue)"
|
||||
if git -C "$WORK/repo" push --quiet origin "smoke/$STAMP"; then
|
||||
PASS "branch smoke/$STAMP pushed"
|
||||
else
|
||||
FAIL "branch push"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== 3. backend-dev opens a PR"
|
||||
code=$(api backend-dev POST "/repos/$ORG/$REPO/pulls" \
|
||||
"{\"title\":\"smoke $STAMP: PR flow\",\"head\":\"smoke/$STAMP\",\"base\":\"main\",\"body\":\"Closes #$issue\"}")
|
||||
[ "$code" = 201 ] || die "PR create (HTTP $code)"
|
||||
pr=$(jq .number <"$RESP")
|
||||
PASS "PR #$pr opened"
|
||||
|
||||
echo "== 4. merge WITHOUT approval must be blocked"
|
||||
code=$(api backend-dev POST "/repos/$ORG/$REPO/pulls/$pr/merge" '{"Do":"merge"}')
|
||||
case $code in
|
||||
200) FAIL "unapproved merge WENT THROUGH — branch protection is not active" ;;
|
||||
*) PASS "unapproved merge blocked (HTTP $code)" ;;
|
||||
esac
|
||||
|
||||
echo "== 5. direct push to main must be rejected"
|
||||
git -C "$WORK/repo" checkout --quiet main
|
||||
echo "direct $STAMP" >"$WORK/repo/direct-$STAMP.txt"
|
||||
git -C "$WORK/repo" add "direct-$STAMP.txt"
|
||||
git -C "$WORK/repo" -c user.name=backend-dev -c user.email=backend-dev@sandbox.invalid \
|
||||
commit --quiet -m "smoke: direct push probe"
|
||||
if git -C "$WORK/repo" push --quiet origin main 2>/dev/null; then
|
||||
FAIL "direct push to main was ACCEPTED — branch protection is not active"
|
||||
else
|
||||
PASS "direct push to main rejected"
|
||||
fi
|
||||
|
||||
echo "== 6. reviewer leaves a comment review"
|
||||
code=$(api reviewer POST "/repos/$ORG/$REPO/pulls/$pr/reviews" \
|
||||
'{"event":"COMMENT","body":"smoke: quality/alignment lens present (comment review)."}')
|
||||
if [ "$code" = 200 ] || [ "$code" = 201 ]; then
|
||||
PASS "reviewer comment review posted"
|
||||
else
|
||||
FAIL "reviewer review (HTTP $code)"
|
||||
fi
|
||||
|
||||
echo "== 7. security-lead approves"
|
||||
code=$(api security-lead POST "/repos/$ORG/$REPO/pulls/$pr/reviews" \
|
||||
'{"event":"APPROVED","body":"smoke: approved by the security gate."}')
|
||||
if [ "$code" = 200 ] || [ "$code" = 201 ]; then
|
||||
PASS "security-lead approval posted"
|
||||
else
|
||||
die "security-lead approval (HTTP $code)"
|
||||
fi
|
||||
|
||||
echo "== 8. merge after approval"
|
||||
code=$(api backend-dev POST "/repos/$ORG/$REPO/pulls/$pr/merge" '{"Do":"merge"}')
|
||||
if [ "$code" = 200 ]; then
|
||||
PASS "PR #$pr merged"
|
||||
else
|
||||
die "approved merge (HTTP $code)"
|
||||
fi
|
||||
|
||||
echo "== 9. main contains the change"
|
||||
git -C "$WORK/repo" fetch --quiet origin main
|
||||
if git -C "$WORK/repo" cat-file -e "FETCH_HEAD:smoke-$STAMP.txt" 2>/dev/null; then
|
||||
PASS "smoke-$STAMP.txt present on main"
|
||||
else
|
||||
FAIL "merged file missing from main"
|
||||
fi
|
||||
|
||||
# Soft check — informational, not a gate: "Closes #N" should have
|
||||
# auto-closed the issue on merge to the default branch.
|
||||
api backend-dev GET "/repos/$ORG/$REPO/issues/$issue" >/dev/null
|
||||
echo " INFO issue #$issue state after merge: $(jq -r .state <"$RESP")"
|
||||
|
||||
echo
|
||||
echo "smoke: $pass passed, $fail failed."
|
||||
[ "$fail" = 0 ]
|
||||
Loading…
Add table
Add a link
Reference in a new issue