#!/usr/bin/env bash # Failure-safe rescue: when a run FAILED after a dev agent already pushed commits, the normal # Publish step never ran and the work would be stranded on the branch with no PR (issue #33). # This opens a PR for the pushed branch so nothing is silently lost. It is strictly best-effort: # every failure here is swallowed (the caller also appends `|| true`) so it can never itself break # the run. Comment-only roles (pm/qa) push nothing, so they are skipped. # # Required env (provided by the workflow step): # GT TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA TOKEN_OPS # NAME MODE NUM TITLE BRANCH GITHUB_SERVER_URL GITHUB_REPOSITORY set +e # Only dev agents (mode=pr) ever push a branch to rescue. [ "${MODE:-}" = "pr" ] || { echo "rescue: comment-mode agent, nothing to rescue"; exit 0; } [ -n "${BRANCH:-}" ] || { echo "rescue: no branch known, skipping"; exit 0; } # Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot. case "$NAME" in pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";; lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; intern) TOK="$TOKEN_INTERN";; *) TOK="";; esac [ -z "$TOK" ] && TOK="$GT" # Trigger token: the @pm hand-back below must FIRE a new run, which the built-in token cannot. TTOK="$TOK"; [ "$TTOK" = "$GT" ] && TTOK="" API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json") git fetch -q origin 2>/dev/null || true # Nothing to rescue unless the branch exists on the remote with commits beyond main. ahead=$(git rev-list --count "origin/main..origin/$BRANCH" 2>/dev/null || echo 0) if [ "${ahead:-0}" -eq 0 ]; then echo "rescue: no pushed commits on origin/$BRANCH beyond main — nothing to rescue" exit 0 fi echo "rescue: origin/$BRANCH is $ahead commit(s) ahead of main — ensuring a PR exists" # Idempotent: Gitea ignores ?head=, so match the head branch client-side. resp=$(curl -sS "${hdr[@]}" "$API/pulls?state=open&limit=50" \ | jq -r --arg br "$BRANCH" 'if type=="array" then (map(select(.head.ref==$br)) | .[0] // empty) else empty end' 2>/dev/null) url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null) if [ -z "$url" ]; then body=$(printf 'The run failed before it could publish, but pushed work exists on this branch — opening a PR so it is not lost.\n\n---\nResolves #%s (auto-rescued after a failed run)' "$NUM") resp=$(curl -sS -X POST "${hdr[@]}" "$API/pulls" \ -d "$(jq -nc --arg t "@$NAME: $TITLE" --arg h "$BRANCH" --arg b "$body" \ '{title:$t, head:$h, base:"main", body:$b}')") echo "rescue PR create ($BRANCH): $resp" url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null) prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null) else prnum=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null) fi if [ -n "$url" ]; then # Status note on the issue (marked — must not trigger)… curl -sS -X POST "${hdr[@]}" "$API/issues/$NUM/comments" \ -d "$(jq -nc --arg b "$(printf '⚠️ The run failed, but the pushed work was not lost — a PR was opened for branch \`%s\`:\n- %s\n\n' "$BRANCH" "$url")" '{body:$b}')" \ -w '\nrescue comment -> HTTP %{http_code}\n' || true # …then hand the rescued PR back into the flow: without this, the pm→qa choreography would stall # here (the normal "PR ready" trigger never fired). Unmarked + PAT so it starts @pm's run. if [ -n "$TTOK" ]; then curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \ "$API/issues/$NUM/comments" \ -d "$(jq -nc --arg b "@pm — PR #${prnum:-?} was auto-rescued after a failed run (issue #$NUM). Please route it for review." '{body:$b}')" \ -w '\nrescue trigger @pm -> HTTP %{http_code}\n' || true fi else echo "rescue: could not open/find a PR for $BRANCH" fi exit 0