mirror of
https://github.com/Sea-Haven-Industries/shoc-pr-review-runner.git
synced 2026-10-02 01:23:16 +00:00
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.
73 lines
3.3 KiB
Bash
Executable file
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"
|