177 lines
6.2 KiB
Bash
Executable File
177 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# audit-task.sh <owner>/<repo> <pr-number>
|
|
#
|
|
# Reads a Gitea PR, finds the linked issue (via "Resolves #N" in the PR body),
|
|
# asks claude to audit the diff against the issue's done criteria, and posts
|
|
# a structured review comment on the PR.
|
|
#
|
|
# Env knobs:
|
|
# MODEL default sonnet (the auditor's model)
|
|
# MAX_WALLCLOCK default 10m
|
|
# GITEA_URL default https://gitea.dannyhaslund.dk
|
|
# GITEA_TOKEN_FILE default /etc/agent/gitea-token
|
|
|
|
set -euo pipefail
|
|
|
|
REPO="${1:-}"
|
|
PR_NUM="${2:-}"
|
|
[[ -z "$REPO" || -z "$PR_NUM" ]] && { echo "usage: $0 <owner>/<repo> <pr-number>" >&2; exit 64; }
|
|
|
|
MODEL="${MODEL:-sonnet}"
|
|
MAX_WALLCLOCK="${MAX_WALLCLOCK:-10m}"
|
|
GITEA_URL="${GITEA_URL:-https://gitea.dannyhaslund.dk}"
|
|
GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-/etc/agent/gitea-token}"
|
|
LOG_ROOT="${LOG_ROOT:-/var/agent/logs}"
|
|
|
|
[[ -r "$GITEA_TOKEN_FILE" ]] || { echo "missing $GITEA_TOKEN_FILE" >&2; exit 65; }
|
|
GITEA_TOKEN="$(cat "$GITEA_TOKEN_FILE")"
|
|
|
|
mkdir -p "$LOG_ROOT"
|
|
LOG_FILE="$LOG_ROOT/audit-pr${PR_NUM}-$(date -u +%Y%m%dT%H%M%S).log"
|
|
log() { printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*" | tee -a "$LOG_FILE"; }
|
|
|
|
# ---------- 1. Fetch PR ----------
|
|
log "Fetching PR $REPO#$PR_NUM"
|
|
pr="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/v1/repos/$REPO/pulls/$PR_NUM")"
|
|
pr_title="$(jq -r .title <<<"$pr")"
|
|
pr_body="$(jq -r .body <<<"$pr")"
|
|
pr_head="$(jq -r .head.ref <<<"$pr")"
|
|
pr_base="$(jq -r .base.ref <<<"$pr")"
|
|
|
|
# ---------- 2. Find linked issue ----------
|
|
issue_num="$(grep -oiE '(resolves|closes|fixes)\s+#[0-9]+' <<<"$pr_body" | head -1 | grep -oE '[0-9]+' || true)"
|
|
if [[ -n "$issue_num" ]]; then
|
|
log "Linked issue: #$issue_num"
|
|
issue_body="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" \
|
|
"$GITEA_URL/api/v1/repos/$REPO/issues/$issue_num" | jq -r .body)"
|
|
else
|
|
log "WARN: no linked issue found in PR body; auditing against PR description only"
|
|
issue_body="(no linked issue; audit against PR description and general code quality)"
|
|
fi
|
|
|
|
# ---------- 3. Get diff ----------
|
|
DIFF_FILE="$(mktemp)"
|
|
trap 'rm -f "$DIFF_FILE" "$AUDIT_OUT" 2>/dev/null' EXIT
|
|
|
|
curl -fsS -H "Authorization: token $GITEA_TOKEN" \
|
|
"$GITEA_URL/api/v1/repos/$REPO/pulls/$PR_NUM.diff" > "$DIFF_FILE"
|
|
|
|
diff_size="$(wc -c < "$DIFF_FILE")"
|
|
log "Diff size: $diff_size bytes"
|
|
|
|
# Truncate to 32 KB if too large
|
|
if [[ $diff_size -gt 32768 ]]; then
|
|
log "Truncating diff to 32 KB for claude"
|
|
head -c 32768 "$DIFF_FILE" > "${DIFF_FILE}.trim"
|
|
echo -e "\n\n[... diff truncated at 32KB ...]" >> "${DIFF_FILE}.trim"
|
|
mv "${DIFF_FILE}.trim" "$DIFF_FILE"
|
|
fi
|
|
|
|
# ---------- 4. Audit via claude ----------
|
|
SCHEMA='{
|
|
"type": "object",
|
|
"required": ["verdict", "summary", "criteria"],
|
|
"properties": {
|
|
"verdict": { "type": "string", "enum": ["approve", "request_changes", "comment"] },
|
|
"summary": { "type": "string", "minLength": 10 },
|
|
"criteria": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["criterion", "status"],
|
|
"properties": {
|
|
"criterion": { "type": "string" },
|
|
"status": { "type": "string", "enum": ["pass", "fail", "unclear"] },
|
|
"note": { "type": "string" }
|
|
}
|
|
}
|
|
},
|
|
"issues_found": { "type": "array", "items": { "type": "string" } }
|
|
}
|
|
}'
|
|
|
|
PROMPT="You are the auditor agent in an autonomous coding pipeline. Review the pull request against the linked issue's Done criteria.
|
|
|
|
Be strict but fair:
|
|
- pass = the diff clearly satisfies the criterion
|
|
- fail = the diff clearly does not, or contradicts it
|
|
- unclear = cannot determine from the diff alone (e.g. requires running tests)
|
|
|
|
Set verdict:
|
|
- approve = all criteria pass, no significant issues
|
|
- comment = mostly fine but some criteria are unclear or minor issues
|
|
- request_changes = at least one criterion fails, or critical issues
|
|
|
|
issues_found is for code-quality concerns the issue did NOT specify but that
|
|
matter for merge: security holes, broken syntax, obvious bugs, dependency
|
|
problems. Do NOT bikeshed style.
|
|
|
|
Do not call any tools. Produce the JSON from the prompt text alone.
|
|
|
|
# Linked issue body
|
|
|
|
$issue_body
|
|
|
|
# PR title
|
|
|
|
$pr_title
|
|
|
|
# PR body
|
|
|
|
$pr_body
|
|
|
|
# Diff ($pr_head into $pr_base)
|
|
|
|
$(cat "$DIFF_FILE")
|
|
|
|
Return only the JSON matching the schema."
|
|
|
|
AUDIT_OUT="$(mktemp)"
|
|
log "Invoking claude (model=$MODEL, timeout=$MAX_WALLCLOCK)"
|
|
set +e
|
|
timeout --signal=INT --kill-after=30s "$MAX_WALLCLOCK" \
|
|
claude -p "$PROMPT" \
|
|
--model "$MODEL" \
|
|
--output-format json \
|
|
--json-schema "$SCHEMA" \
|
|
--allowedTools "" \
|
|
< /dev/null > "$AUDIT_OUT" 2>>"$LOG_FILE"
|
|
rc=$?
|
|
set -e
|
|
log "claude exited rc=$rc"
|
|
|
|
if [[ $rc -ne 0 ]]; then
|
|
log "ERROR — auditor failed; posting fallback comment"
|
|
curl -fsS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
|
|
-d "$(jq -nc --arg b "agent auditor: failed to produce an audit (claude rc=$rc). Log: \`$LOG_FILE\`" '{body:$b}')" \
|
|
"$GITEA_URL/api/v1/repos/$REPO/issues/$PR_NUM/comments" > /dev/null
|
|
exit 1
|
|
fi
|
|
|
|
# ---------- 5. Render review comment ----------
|
|
audit="$(jq -c '.structured_output' "$AUDIT_OUT")"
|
|
verdict="$(jq -r .verdict <<<"$audit")"
|
|
summary="$(jq -r .summary <<<"$audit")"
|
|
criteria_md="$(jq -r '.criteria // [] | map("- **" + (.status|ascii_upcase) + "** — " + .criterion + (if .note then " _" + .note + "_" else "" end)) | join("\n")' <<<"$audit")"
|
|
issues_md="$(jq -r '.issues_found // [] | if length == 0 then "_none_" else map("- " + .) | join("\n") end' <<<"$audit")"
|
|
|
|
# Verdict badge
|
|
case "$verdict" in
|
|
approve) badge=":white_check_mark: APPROVE" ;;
|
|
comment) badge=":speech_balloon: COMMENT" ;;
|
|
request_changes) badge=":no_entry: REQUEST_CHANGES" ;;
|
|
*) badge=":grey_question: $verdict" ;;
|
|
esac
|
|
|
|
review_body="$(printf '## Auditor review — %s\n\n%s\n\n### Criteria\n\n%s\n\n### Other issues found\n\n%s\n\n---\n_model: %s · log: `%s`_\n' \
|
|
"$badge" "$summary" "$criteria_md" "$issues_md" "$MODEL" "$LOG_FILE")"
|
|
|
|
# ---------- 6. Post comment ----------
|
|
log "Posting review (verdict=$verdict)"
|
|
curl -fsS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
|
|
-d "$(jq -nc --arg b "$review_body" '{body:$b}')" \
|
|
"$GITEA_URL/api/v1/repos/$REPO/issues/$PR_NUM/comments" > /dev/null
|
|
|
|
log "Done. verdict=$verdict"
|
|
echo "$verdict"
|