fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker, creates admin + bot users (dev-bot, review-bot), generates API tokens, creates repo, and pushes code — all automated - Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→ FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→ FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→ FORGE_BOT_USERNAMES (with backwards-compat fallbacks) - Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all() →forge_api_all() (with compat aliases) - Add forge_url field to project TOML; load-project.sh derives FORGE_API/FORGE_WEB from forge_url + repo - Update parse_repo_slug() to accept any host URL, not just codeberg - Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo) - Update all 58 files: agent scripts, formulas, docs, site HTML Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
39d30faf45
commit
a66bd91721
58 changed files with 863 additions and 628 deletions
|
|
@ -2,7 +2,7 @@
|
|||
# Review Agent
|
||||
|
||||
**Role**: AI-powered PR review — post structured findings and formal
|
||||
approve/request-changes verdicts to Codeberg.
|
||||
approve/request-changes verdicts to forge.
|
||||
|
||||
**Trigger**: `review-poll.sh` runs every 10 min via cron. It scans open PRs
|
||||
whose CI has passed and that lack a review for the current HEAD SHA, then
|
||||
|
|
@ -10,11 +10,11 @@ spawns `review-pr.sh <pr-number>`.
|
|||
|
||||
**Key files**:
|
||||
- `review/review-poll.sh` — Cron scheduler: finds unreviewed PRs with passing CI
|
||||
- `review/review-pr.sh` — Creates/reuses a tmux session (`review-{project}-{pr}`), injects PR diff, waits for Claude to write structured JSON output, posts markdown review + formal Codeberg review, auto-creates follow-up issues for pre-existing tech debt
|
||||
- `review/review-pr.sh` — Creates/reuses a tmux session (`review-{project}-{pr}`), injects PR diff, waits for Claude to write structured JSON output, posts markdown review + formal forge review, auto-creates follow-up issues for pre-existing tech debt
|
||||
|
||||
**Environment variables consumed**:
|
||||
- `CODEBERG_TOKEN` — Dev-agent token (must not be the same account as REVIEW_BOT_TOKEN)
|
||||
- `REVIEW_BOT_TOKEN` — Review-agent token for approvals (use human/admin account; branch protection: in approvals whitelist)
|
||||
- `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `FORGE_TOKEN` — Dev-agent token (must not be the same account as FORGE_REVIEW_TOKEN)
|
||||
- `FORGE_REVIEW_TOKEN` — Review-agent token for approvals (use human/admin account; branch protection: in approvals whitelist)
|
||||
- `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||
- `PRIMARY_BRANCH`, `WOODPECKER_REPO_ID`
|
||||
- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER`
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ source "$(dirname "$0")/../lib/ci-helpers.sh"
|
|||
REPO_ROOT="${PROJECT_REPO_ROOT}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
API_BASE="${CODEBERG_API}"
|
||||
API_BASE="${FORGE_API}"
|
||||
LOGFILE="$SCRIPT_DIR/review.log"
|
||||
MAX_REVIEWS=3
|
||||
REVIEW_IDLE_TIMEOUT=14400 # 4h: kill review session if idle
|
||||
|
|
@ -44,7 +44,7 @@ if [ -n "$REVIEW_SESSIONS" ]; then
|
|||
phase_file="/tmp/review-session-${PROJECT_NAME}-${pr_num}.phase"
|
||||
|
||||
# Check if PR is still open
|
||||
pr_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
pr_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${pr_num}" | jq -r '.state // "unknown"' 2>/dev/null) || true
|
||||
|
||||
if [ "$pr_state" != "open" ]; then
|
||||
|
|
@ -92,7 +92,7 @@ if [ -n "$REVIEW_SESSIONS" ]; then
|
|||
done <<< "$REVIEW_SESSIONS"
|
||||
fi
|
||||
|
||||
PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
PRS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls?state=open&limit=20" | \
|
||||
jq -r --arg branch "${PRIMARY_BRANCH}" '.[] | select(.base.ref == $branch) | select(.draft != true) | select(.title | test("^\\[?WIP[\\]:]"; "i") | not) | "\(.number) \(.head.sha) \(.head.ref)"')
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ inject_review_into_dev_session() {
|
|||
[ "${current_phase}" = "PHASE:awaiting_review" ] || return 0
|
||||
|
||||
local review_comment
|
||||
review_comment=$(codeberg_api_all "/issues/${pr_num}/comments" | \
|
||||
review_comment=$(forge_api_all "/issues/${pr_num}/comments" | \
|
||||
jq -r --arg sha "${pr_sha}" \
|
||||
'[.[] | select(.body | contains("<!-- reviewed: " + $sha))] | last // empty') || true
|
||||
if [ -z "${review_comment}" ] || [ "${review_comment}" = "null" ]; then
|
||||
|
|
@ -185,7 +185,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
|
|||
reviewed_sha=$(sed -n 's/^SHA://p' "$phase_file" 2>/dev/null | tr -d '[:space:]' || true)
|
||||
[ -n "$reviewed_sha" ] || continue
|
||||
|
||||
pr_json=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
pr_json=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${pr_num}" 2>/dev/null || true)
|
||||
[ -n "$pr_json" ] || continue
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
|
|||
pr_branch=$(printf '%s' "$pr_json" | jq -r '.head.ref // ""')
|
||||
if [ -z "$current_sha" ] || [ "$current_sha" = "$reviewed_sha" ]; then continue; fi
|
||||
|
||||
ci_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
ci_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/commits/${current_sha}/status" | jq -r '.state // "unknown"')
|
||||
|
||||
if ! ci_passed "$ci_state"; then
|
||||
|
|
@ -210,7 +210,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
|
|||
|
||||
if "${SCRIPT_DIR}/review-pr.sh" "$pr_num" 2>&1; then
|
||||
REVIEWED=$((REVIEWED + 1))
|
||||
FRESH_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
FRESH_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${pr_num}" | jq -r '.head.sha // ""') || true
|
||||
inject_review_into_dev_session "$pr_num" "${FRESH_SHA:-$current_sha}" "$pr_branch"
|
||||
else
|
||||
|
|
@ -227,7 +227,7 @@ while IFS= read -r line; do
|
|||
PR_SHA=$(echo "$line" | awk '{print $2}')
|
||||
PR_BRANCH=$(echo "$line" | awk '{print $3}')
|
||||
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
|
||||
|
||||
# Skip if CI is running/failed. Allow "success", no CI configured, or non-code PRs
|
||||
|
|
@ -240,8 +240,8 @@ while IFS= read -r line; do
|
|||
log " #${PR_NUM} CI=${CI_STATE} but no code files — proceeding"
|
||||
fi
|
||||
|
||||
# Check formal Codeberg reviews (not comment markers)
|
||||
HAS_REVIEW=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
# Check formal forge reviews (not comment markers)
|
||||
HAS_REVIEW=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${PR_NUM}/reviews" | \
|
||||
jq -r --arg sha "$PR_SHA" \
|
||||
'[.[] | select(.commit_id == $sha) | select(.state != "COMMENT")] | length')
|
||||
|
|
@ -259,7 +259,7 @@ while IFS= read -r line; do
|
|||
# Re-fetch current SHA: review-pr.sh fetches the PR independently and tags its
|
||||
# comment with whatever SHA it saw. If a commit arrived while review-pr.sh was
|
||||
# running those two SHA captures diverge and we would miss the comment.
|
||||
FRESH_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
FRESH_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API_BASE}/pulls/${PR_NUM}" | jq -r '.head.sha // ""') || true
|
||||
inject_review_into_dev_session "$PR_NUM" "${FRESH_SHA:-$PR_SHA}" "$PR_BRANCH"
|
||||
else
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true
|
|||
|
||||
PR_NUMBER="${1:?Usage: review-pr.sh <pr-number> [--force]}"
|
||||
FORCE="${2:-}"
|
||||
API="${CODEBERG_API}"
|
||||
API="${FORGE_API}"
|
||||
LOGFILE="${FACTORY_ROOT}/review/review.log"
|
||||
SESSION="review-${PROJECT_NAME}-${PR_NUMBER}"
|
||||
PHASE_FILE="/tmp/review-session-${PROJECT_NAME}-${PR_NUMBER}.phase"
|
||||
|
|
@ -37,7 +37,7 @@ if [ -f "$LOCKFILE" ]; then
|
|||
fi
|
||||
echo $$ > "$LOCKFILE"
|
||||
status "fetching metadata"
|
||||
PR_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" "${API}/pulls/${PR_NUMBER}")
|
||||
PR_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" "${API}/pulls/${PR_NUMBER}")
|
||||
PR_TITLE=$(printf '%s' "$PR_JSON" | jq -r '.title')
|
||||
PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""')
|
||||
PR_HEAD=$(printf '%s' "$PR_JSON" | jq -r '.head.ref')
|
||||
|
|
@ -50,16 +50,16 @@ if [ "$PR_STATE" != "open" ]; then
|
|||
cd "${PROJECT_REPO_ROOT}"; git worktree remove "$WORKTREE" --force 2>/dev/null || true
|
||||
rm -rf "$WORKTREE" "$PHASE_FILE" "$OUTPUT_FILE" 2>/dev/null || true; exit 0
|
||||
fi
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
|
||||
CI_NOTE=""; if ! ci_passed "$CI_STATE"; then
|
||||
ci_required_for_pr "$PR_NUMBER" && { log "SKIP: CI=${CI_STATE}"; exit 0; }
|
||||
CI_NOTE=" (not required — non-code PR)"; fi
|
||||
ALL_COMMENTS=$(codeberg_api_all "/issues/${PR_NUMBER}/comments")
|
||||
ALL_COMMENTS=$(forge_api_all "/issues/${PR_NUMBER}/comments")
|
||||
HAS_CMT=$(printf '%s' "$ALL_COMMENTS" | jq --arg s "$PR_SHA" \
|
||||
'[.[]|select(.body|contains("<!-- reviewed: "+$s+" -->"))]|length')
|
||||
[ "${HAS_CMT:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: reviewed ${PR_SHA:0:7}"; exit 0; }
|
||||
HAS_FML=$(codeberg_api_all "/pulls/${PR_NUMBER}/reviews" | jq --arg s "$PR_SHA" \
|
||||
HAS_FML=$(forge_api_all "/pulls/${PR_NUMBER}/reviews" | jq --arg s "$PR_SHA" \
|
||||
'[.[]|select(.commit_id==$s)|select(.state!="COMMENT")]|length')
|
||||
[ "${HAS_FML:-0}" -gt 0 ] && [ "$FORCE" != "--force" ] && { log "SKIP: formal review"; exit 0; }
|
||||
PREV_CONTEXT="" IS_RE_REVIEW=false PREV_SHA=""
|
||||
|
|
@ -81,7 +81,7 @@ if [ -n "$PREV_REV" ] && [ "$PREV_REV" != "null" ]; then
|
|||
fi
|
||||
fi
|
||||
status "fetching diff"
|
||||
curl -s -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
curl -s -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
"${API}/pulls/${PR_NUMBER}.diff" > "${REVIEW_TMPDIR}/full.diff"
|
||||
FSIZE=$(stat -c%s "${REVIEW_TMPDIR}/full.diff" 2>/dev/null || echo 0)
|
||||
DIFF=$(head -c "$MAX_DIFF" "${REVIEW_TMPDIR}/full.diff")
|
||||
|
|
@ -97,15 +97,15 @@ status "preparing review session"
|
|||
FORMULA=$(cat "${FACTORY_ROOT}/formulas/review-pr.toml")
|
||||
{
|
||||
printf 'You are the review agent for %s. Follow the formula to review PR #%s.\nYou MUST write PHASE:done to '\''%s'\'' when finished.\n\n' \
|
||||
"${CODEBERG_REPO}" "${PR_NUMBER}" "${PHASE_FILE}"
|
||||
"${FORGE_REPO}" "${PR_NUMBER}" "${PHASE_FILE}"
|
||||
printf '## PR Context\n**%s** (%s → %s) | SHA: %s | CI: %s%s\nRe-review: %s\n\n' \
|
||||
"$PR_TITLE" "$PR_HEAD" "$PR_BASE" "$PR_SHA" "$CI_STATE" "$CI_NOTE" "$IS_RE_REVIEW"
|
||||
printf '### Description\n%s\n\n### Changed Files\n%s\n\n### Diff%s\n```diff\n%s\n```\n' \
|
||||
"$PR_BODY" "$FILES" "$DNOTE" "$DIFF"
|
||||
[ -n "$PREV_CONTEXT" ] && printf '%s\n' "$PREV_CONTEXT"
|
||||
printf '\n## Formula\n%s\n\n## Environment\nREVIEW_OUTPUT_FILE=%s\nPHASE_FILE=%s\nCODEBERG_API=%s\nPR_NUMBER=%s\nFACTORY_ROOT=%s\n' \
|
||||
printf '\n## Formula\n%s\n\n## Environment\nREVIEW_OUTPUT_FILE=%s\nPHASE_FILE=%s\nFORGE_API=%s\nPR_NUMBER=%s\nFACTORY_ROOT=%s\n' \
|
||||
"$FORMULA" "$OUTPUT_FILE" "$PHASE_FILE" "$API" "$PR_NUMBER" "$FACTORY_ROOT"
|
||||
printf 'NEVER echo the actual token — always reference $CODEBERG_TOKEN or $REVIEW_BOT_TOKEN.\n'
|
||||
printf 'NEVER echo the actual token — always reference ${FORGE_TOKEN} or ${FORGE_REVIEW_TOKEN}.\n'
|
||||
} > "${REVIEW_TMPDIR}/prompt.md"
|
||||
PROMPT=$(cat "${REVIEW_TMPDIR}/prompt.md")
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ fi
|
|||
if [ -z "$REVIEW_JSON" ]; then
|
||||
log "ERROR: no valid review output"
|
||||
jq -n --arg b "## AI Review — Error\n<!-- review-error: ${PR_SHA} -->\nReview failed.\n---\n*${PR_SHA:0:7}*" \
|
||||
'{body: $b}' | curl -sf -o /dev/null -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||
'{body: $b}' | curl -sf -o /dev/null -X POST -H "Authorization: token ${FORGE_TOKEN}" \
|
||||
-H "Content-Type: application/json" "${API}/issues/${PR_NUMBER}/comments" -d @- || true
|
||||
matrix_send "review" "PR #${PR_NUMBER} review failed" 2>/dev/null || true; exit 1
|
||||
fi
|
||||
|
|
@ -163,7 +163,7 @@ COMMENT_BODY=$(printf '## AI %s\n<!-- reviewed: %s -->\n\n%s\n\n### Verdict\n**%
|
|||
printf '%s' "$COMMENT_BODY" > "${REVIEW_TMPDIR}/body.txt"
|
||||
jq -Rs '{body: .}' < "${REVIEW_TMPDIR}/body.txt" > "${REVIEW_TMPDIR}/comment.json"
|
||||
POST_RC=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${REVIEW_BOT_TOKEN}" -H "Content-Type: application/json" \
|
||||
-H "Authorization: token ${FORGE_REVIEW_TOKEN}" -H "Content-Type: application/json" \
|
||||
"${API}/issues/${PR_NUMBER}/comments" --data-binary @"${REVIEW_TMPDIR}/comment.json")
|
||||
[ "$POST_RC" != "201" ] && { log "ERROR: comment HTTP ${POST_RC}"; exit 1; }
|
||||
log "posted review comment"
|
||||
|
|
@ -171,19 +171,19 @@ log "posted review comment"
|
|||
REVENT="COMMENT"
|
||||
case "$VERDICT" in APPROVE) REVENT="APPROVED" ;; REQUEST_CHANGES|DISCUSS) REVENT="REQUEST_CHANGES" ;; esac
|
||||
if [ "$REVENT" = "APPROVED" ]; then
|
||||
BLOGIN=$(curl -sf -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
|
||||
BLOGIN=$(curl -sf -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
|
||||
"${API%%/repos*}/user" 2>/dev/null | jq -r '.login // empty' || true)
|
||||
[ -n "$BLOGIN" ] && codeberg_api_all "/pulls/${PR_NUMBER}/reviews" "$REVIEW_BOT_TOKEN" 2>/dev/null | \
|
||||
[ -n "$BLOGIN" ] && forge_api_all "/pulls/${PR_NUMBER}/reviews" "${FORGE_REVIEW_TOKEN}" 2>/dev/null | \
|
||||
jq -r --arg l "$BLOGIN" '.[]|select(.state=="REQUEST_CHANGES")|select(.user.login==$l)|.id' | \
|
||||
while IFS= read -r rid; do
|
||||
curl -sf -o /dev/null -X POST -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
|
||||
curl -sf -o /dev/null -X POST -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
|
||||
-H "Content-Type: application/json" "${API}/pulls/${PR_NUMBER}/reviews/${rid}/dismissals" \
|
||||
-d '{"message":"Superseded by approval"}' || true; log "dismissed review ${rid}"
|
||||
done || true
|
||||
fi
|
||||
jq -n --arg b "AI ${RTYPE}: **${VERDICT}** — ${REASON}" --arg e "$REVENT" --arg s "$PR_SHA" \
|
||||
'{body: $b, event: $e, commit_id: $s}' > "${REVIEW_TMPDIR}/formal.json"
|
||||
curl -s -o /dev/null -X POST -H "Authorization: token ${REVIEW_BOT_TOKEN}" \
|
||||
curl -s -o /dev/null -X POST -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \
|
||||
-H "Content-Type: application/json" "${API}/pulls/${PR_NUMBER}/reviews" \
|
||||
--data-binary @"${REVIEW_TMPDIR}/formal.json" >/dev/null 2>&1 || true
|
||||
log "formal ${REVENT} submitted"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue