feat: SHOC PR review runner, phase 1
Manually-dispatched GitHub Actions workflow that reviews SHOC pull requests in
a clean environment: exact-head checkout of shoc-frontend-new and shoc-backend,
clean build/test gates, a truthful evidence report, a single-shot Fireworks
review, deterministic output validation, and published artifacts. The runner
never writes to the product repositories or their pull requests.
The review checklists move here from the reviewers' local Cursor commands so
the instructions live outside both product repos.
Phase 1 does not provision a database, start either application, or run live
browser flows; the evidence report records those as NOT_RUN so a review cannot
claim them.
Security architecture: building a PR executes its author's code, so the
workflow is split. The gates job runs that code holding no Fireworks key and
revokes its App token first; the review job holds the key, executes no product
code, and re-checks out this repo fresh. Product checkouts live outside the
workspace, the App token is downscoped at mint time, gate results fail closed
on any duplicate key, changed files are read from git objects rather than the
filesystem, and the validator re-checks every claim against the gate table.
2026-07-29 12:04:27 -04:00
|
|
|
#!/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}"
|
|
|
|
|
|
2026-07-29 12:45:11 -04:00
|
|
|
# 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
|
feat: SHOC PR review runner, phase 1
Manually-dispatched GitHub Actions workflow that reviews SHOC pull requests in
a clean environment: exact-head checkout of shoc-frontend-new and shoc-backend,
clean build/test gates, a truthful evidence report, a single-shot Fireworks
review, deterministic output validation, and published artifacts. The runner
never writes to the product repositories or their pull requests.
The review checklists move here from the reviewers' local Cursor commands so
the instructions live outside both product repos.
Phase 1 does not provision a database, start either application, or run live
browser flows; the evidence report records those as NOT_RUN so a review cannot
claim them.
Security architecture: building a PR executes its author's code, so the
workflow is split. The gates job runs that code holding no Fireworks key and
revokes its App token first; the review job holds the key, executes no product
code, and re-checks out this repo fresh. Product checkouts live outside the
workspace, the App token is downscoped at mint time, gate results fail closed
on any duplicate key, changed files are read from git objects rather than the
filesystem, and the validator re-checks every claim against the gate table.
2026-07-29 12:04:27 -04:00
|
|
|
|
|
|
|
|
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"
|