a1b0691a1e
Build & push image / build-and-push (pull_request) Successful in 9s
The dogfood swarm reviewed PR #1; folding in the warranted findings (graded via the gadfly MCP — 18 real / 18 false-positive across the 4 completed reviewers): - entrypoint.sh: finalize a never-written status file when run.sh skips the binary (empty diff / no key / missing binary). The pre-seed stayed {started:0, done:false}, so the board showed that model "waiting to start" forever and the N/N counter never completed — breaking the board's own "tell when everything is finished" invariant. (glm-5.2, correctness — the strongest finding.) - main.go: recover() in the per-lens goroutine. A panic previously crashed the whole binary (killing every other lens's output) and left the lens stuck "running" on the board. Now it's recorded as an errored result and the lens is marked finished. (glm-5.2 + minimax-m3.) - status-board.sh: coerce a non-numeric GADFLY_STATUS_POLL_SECS back to 12. Under `set -uo pipefail` a bad `sleep "$POLL"` failed silently and the loop spun, hammering the Gitea API. (glm-5.2, error-handling.) The remaining real findings (sanitizer collision, page-10 pagination, markdown-injection via PR-controlled lens names, cosmetic blank line) were graded trivial and left as-is — documented in the finding notes. gofmt clean, go vet quiet, go build + go test -race green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
5.9 KiB
Bash
Executable File
138 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Live status board for a Gadfly review.
|
|
#
|
|
# Each model process (the cmd/gadfly binary) publishes its per-lens progress to
|
|
# $GADFLY_STATUS_DIR/<model>.json as lenses go queued -> running -> finished.
|
|
# This script polls that directory and upserts ONE consolidated PR comment that
|
|
# aggregates every model's per-lens status, so a human (or an agent watching the
|
|
# PR) can see the whole swarm's progress at a glance and know when it's done —
|
|
# instead of staring at N separate "⏳ Reviewing…" placeholders.
|
|
#
|
|
# It is advisory and best-effort: a failed render/post is logged and retried on
|
|
# the next tick; nothing here can fail the review or block a merge. It runs in
|
|
# the background from entrypoint.sh and exits once the $GADFLY_STATUS_DIR/.done
|
|
# sentinel appears (the entrypoint touches it after all model lanes finish),
|
|
# after one final render.
|
|
#
|
|
# Required env:
|
|
# GITEA_API https://HOST/api/v1/repos/OWNER/REPO
|
|
# GITEA_TOKEN token with repo write access (posts the comment)
|
|
# PR pull request number
|
|
# GADFLY_STATUS_DIR directory holding the per-model <model>.json files
|
|
# Optional:
|
|
# GADFLY_STATUS_POLL_SECS render/upsert interval (default 12)
|
|
set -uo pipefail
|
|
|
|
: "${GITEA_API:?GITEA_API required}"
|
|
: "${GITEA_TOKEN:?GITEA_TOKEN required}"
|
|
: "${PR:?PR required}"
|
|
: "${GADFLY_STATUS_DIR:?GADFLY_STATUS_DIR required}"
|
|
|
|
POLL="${GADFLY_STATUS_POLL_SECS:-12}"
|
|
# Guard against a non-numeric poll interval: with `set -uo pipefail` (no set -e)
|
|
# a bad `sleep "$POLL"` would fail silently and the `while :` loop would spin,
|
|
# hammering the Gitea API. Coerce anything non-integer (or <1) back to 12.
|
|
case "$POLL" in ''|*[!0-9]*) POLL=12 ;; esac
|
|
[ "$POLL" -ge 1 ] 2>/dev/null || POLL=12
|
|
DONE_FILE="${GADFLY_STATUS_DIR}/.done"
|
|
MARKER="<!-- gadfly-status-board -->"
|
|
API_TIMEOUT="--connect-timeout 20 --max-time 30"
|
|
BOARD_ID="" # cached comment id, so we PATCH in place instead of re-searching
|
|
|
|
say() { echo "[gadfly-status-board] $*" >&2; }
|
|
|
|
command -v jq >/dev/null 2>&1 || { say "jq not found; status board disabled"; exit 0; }
|
|
|
|
# render_section FILE -> markdown for one model (its header + per-lens bullets).
|
|
# Reads the JSON the binary writes; tolerates a half-written/missing file by
|
|
# emitting nothing (jq exits non-zero -> caller skips it this tick).
|
|
render_section() {
|
|
jq -r '
|
|
def icon(state; errored):
|
|
if state == "finished" then (if errored then "⚠️" else "✅" end)
|
|
elif state == "running" then "🔄"
|
|
else "⏸️" end;
|
|
def lensline:
|
|
"- " + icon(.state; (.errored // false)) + " **" + .name + "** — " +
|
|
( if .state == "finished" then (if (.errored // false) then "could not complete" else (.verdict // "done") end)
|
|
elif .state == "running" then "running"
|
|
else "queued" end );
|
|
( [.lenses[] | select(.state == "finished")] | length ) as $fin
|
|
| ( .lenses | length ) as $tot
|
|
| ( if .done then "✅ done"
|
|
elif $tot == 0 then "⏳ waiting to start"
|
|
else "⏳ " + ($fin|tostring) + "/" + ($tot|tostring) + " lenses" end ) as $sum
|
|
| "#### `" + .model + "` · " + .provider + " — " + $sum + "\n"
|
|
+ ( if $tot == 0 then "- ⏸️ _no lenses reported yet_"
|
|
else ([.lenses[] | lensline] | join("\n")) end )
|
|
' "$1" 2>/dev/null
|
|
}
|
|
|
|
# render_body -> the full consolidated comment body (marker + header + sections).
|
|
render_body() {
|
|
local f sections="" total=0 done=0 ts
|
|
shopt -s nullglob
|
|
local files=("${GADFLY_STATUS_DIR}"/*.json)
|
|
shopt -u nullglob
|
|
for f in "${files[@]}"; do
|
|
local sec
|
|
sec="$(render_section "$f")" || continue
|
|
[ -z "$sec" ] && continue
|
|
total=$((total + 1))
|
|
if [ "$(jq -r 'if .done then 1 else 0 end' "$f" 2>/dev/null)" = "1" ]; then
|
|
done=$((done + 1))
|
|
fi
|
|
sections="${sections}${sec}"$'\n\n'
|
|
done
|
|
ts="$(date -u '+%Y-%m-%d %H:%M:%SZ')"
|
|
if [ "$total" -eq 0 ]; then
|
|
sections="_Waiting for reviewers to start…_"$'\n'
|
|
fi
|
|
printf '%s\n## 🪰 Gadfly — live review status\n\n%d/%d reviewers finished · updated %s\n\n%s\n<sub>Live status board. Findings are posted in each model'\''s own comment. Advisory only — does not block merge.</sub>' \
|
|
"$MARKER" "$done" "$total" "$ts" "$sections"
|
|
}
|
|
|
|
# find_existing -> id of the board comment if it already exists (paginate by
|
|
# marker). Used once, to recover the comment across container restarts.
|
|
find_existing() {
|
|
local page=1 cmts id
|
|
while [ "$page" -le 10 ]; do
|
|
cmts="$(curl $API_TIMEOUT -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
|
"${GITEA_API}/issues/${PR}/comments?limit=50&page=${page}" 2>/dev/null || echo '[]')"
|
|
[ "$(echo "$cmts" | jq 'length' 2>/dev/null || echo 0)" = "0" ] && break
|
|
id="$(echo "$cmts" | jq -r --arg m "$MARKER" \
|
|
'.[] | select(.body != null and (.body | startswith($m))) | .id' 2>/dev/null | head -n1)"
|
|
[ -n "$id" ] && { echo "$id"; return; }
|
|
page=$((page + 1))
|
|
done
|
|
echo ""
|
|
}
|
|
|
|
# upsert BODY — PATCH the cached/known board comment, else POST a new one and
|
|
# cache its id. A failed PATCH (e.g. comment deleted) clears the cache so the
|
|
# next tick re-discovers or re-creates it.
|
|
upsert() {
|
|
local body="$1" post_body resp
|
|
post_body="$(jq -n --arg b "$body" '{body:$b}')"
|
|
[ -z "$BOARD_ID" ] && BOARD_ID="$(find_existing)"
|
|
if [ -n "$BOARD_ID" ]; then
|
|
if ! curl $API_TIMEOUT -fsS -X PATCH -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
|
|
"${GITEA_API}/issues/comments/${BOARD_ID}" -d "$post_body" >/dev/null 2>&1; then
|
|
say "patch of comment ${BOARD_ID} failed; will re-discover"
|
|
BOARD_ID=""
|
|
fi
|
|
else
|
|
resp="$(curl $API_TIMEOUT -fsS -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
|
|
"${GITEA_API}/issues/${PR}/comments" -d "$post_body" 2>/dev/null || echo '{}')"
|
|
BOARD_ID="$(echo "$resp" | jq -r '.id // ""' 2>/dev/null)"
|
|
fi
|
|
}
|
|
|
|
say "starting (poll ${POLL}s, dir ${GADFLY_STATUS_DIR})"
|
|
while :; do
|
|
upsert "$(render_body)"
|
|
[ -f "$DONE_FILE" ] && break
|
|
sleep "$POLL"
|
|
done
|
|
say "done"
|