shoc-pr-review-runner/scripts/resolve-pr-head.sh
Adam Moussa 3c541cc7b4
fix: never report an unread CI signal as missing CI
The first live run reported the reviewed PR's CI as "missing" while it was
actually green, and the model cited that as a reason to withhold approval.

Reading check-runs needs the App's checks:read permission, which the App did
not have, so the call 403'd and the fallback turned an authorization failure
into the factual claim "this PR has no CI". That is the exact failure this
runner exists to prevent, applied to a governance signal instead of a gate.

Distinguish the two: an unreadable signal is now recorded as "unknown", the
evidence report says unknown is not evidence of absent or failing CI, and the
skill tells the reviewer that a signal the runner could not read is not a
finding. Request checks:read at token mint so the signal is readable at all.

Also correct the App verification snippet in the README: listing an
installation's selected repositories needs the installation token, so the
documented /user/installations call does not work for an org admin.
2026-07-29 12:35:36 -04:00

73 lines
3.3 KiB
Bash
Executable file

#!/usr/bin/env bash
# Resolve a PR's exact head (spec §10.1) and record its metadata + CI status.
#
# Usage: resolve-pr-head.sh <side: frontend|backend> <repo owner/name> <pr-number>
# Reads: GH_TOKEN (read-only App installation token)
# Writes: $ARTIFACTS_DIR/<side>-pr.json (metadata consumed by evidence + agent)
# <side>_sha / <side>_short_sha to $GITHUB_OUTPUT when present.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "$SCRIPT_DIR/lib.sh"
side="${1:?side required}"
repo="${2:?repo required}"
pr="${3:?pr number required}"
require_env GH_TOKEN
pr_json="$(gh api "repos/$repo/pulls/$pr")" || die "failed to fetch $repo PR #$pr"
state="$(jq -r '.state' <<<"$pr_json")"
head_sha="$(jq -r '.head.sha // empty' <<<"$pr_json")"
head_repo="$(jq -r '.head.repo.full_name // empty' <<<"$pr_json")"
[ -n "$head_sha" ] || die "$repo PR #$pr has an empty head"
[ "$state" = "open" ] || log "WARNING: $repo PR #$pr state is '$state', not open"
[ "$head_repo" = "$repo" ] || die "$repo PR #$pr head lives in '$head_repo' (fork heads are not supported)"
short_sha="${head_sha:0:7}"
# CI status on the exact head: green / red / pending / missing / unknown.
#
# A failed API call must NEVER be reported as "missing": that would state as a
# fact ("this PR has no CI") what is actually an unread signal, which is the
# exact failure mode this runner exists to prevent. Reading check-runs needs the
# App's `checks: read` permission; without it the call 403s and the honest
# answer is "unknown".
checks_err="$ARTIFACTS_DIR/$side-checks-error.txt"
if checks_json="$(gh api "repos/$repo/commits/$head_sha/check-runs" \
--jq '{total: .total_count, runs: [.check_runs[] | {name, status, conclusion}]}' 2>"$checks_err")"; then
ci_status="$(jq -r '
if .total == 0 then "missing"
elif ([.runs[] | select(.status != "completed")] | length) > 0 then "pending"
elif ([.runs[] | select(.conclusion != "success" and .conclusion != "neutral" and .conclusion != "skipped")] | length) > 0 then "red"
else "green" end' <<<"$checks_json")"
rm -f "$checks_err"
else
reason="$(tr -d '\000-\037' <"$checks_err" | cut -c1-160)"
log "WARNING: could not read check-runs for $repo@$short_sha: $reason"
ci_status="unknown (check-runs unreadable; the review App likely lacks 'checks: read')"
checks_json='{"total":0,"runs":[],"unreadable":true}'
fi
jq -n \
--arg side "$side" --arg repo "$repo" --argjson pr "$pr" \
--arg title "$(jq -r '.title' <<<"$pr_json")" \
--arg head_ref "$(jq -r '.head.ref' <<<"$pr_json")" \
--arg base_ref "$(jq -r '.base.ref' <<<"$pr_json")" \
--arg head_sha "$head_sha" --arg short_sha "$short_sha" \
--arg state "$state" --arg ci_status "$ci_status" \
--argjson mergeable "$(jq '.mergeable' <<<"$pr_json")" \
--argjson checks "$checks_json" \
'{side: $side, repo: $repo, pr: $pr, title: $title, head_ref: $head_ref,
base_ref: $base_ref, head_sha: $head_sha, short_sha: $short_sha,
state: $state, mergeable: $mergeable, ci_status: $ci_status, checks: $checks}' \
>"$ARTIFACTS_DIR/$side-pr.json"
out="${GITHUB_OUTPUT:-/dev/stdout}"
{
printf '%s_sha=%s\n' "$side" "$head_sha"
printf '%s_short_sha=%s\n' "$side" "$short_sha"
} >>"$out"
log "$side: $repo#$pr head=$short_sha base=$(jq -r '.base.ref' <<<"$pr_json") ci=$ci_status"