set_review_mode() { local mode="$1" # Security: shell-escape values to prevent command injection when sourced. printf '%s=%q\n' \ REVIEW_MODE "$mode" \ REVIEW_MODE_SET_AT "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ > .local/review-mode.env } review_artifacts_helper_path() { local scripts_dir="${script_parent_dir:-}" if [ -z "$scripts_dir" ]; then scripts_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) fi printf '%s/pr-lib/review-artifacts.mjs\n' "$scripts_dir" } review_claim() { local pr="$1" mark_pr_operation_side_effects_started # Claim logs are per-PR review state: keeping them in the PR worktree leaves the # shared canonical checkout with no scripts/pr-owned .local, so a stray artifact # there can never be mistaken for this flow's output. Claiming still works on a # cold PR because enter_worktree provisions both the worktree and .local. enter_worktree "$pr" false local reviewer="" local max_attempts=3 local attempt for attempt in $(seq 1 "$max_attempts"); do local user_log user_log=".local/review-claim-user-attempt-$attempt.log" if reviewer=$(gh api user --jq .login 2>"$user_log"); then printf "%s\n" "$reviewer" >"$user_log" break fi echo "Claim reviewer lookup failed (attempt $attempt/$max_attempts)." print_relevant_log_excerpt "$user_log" if [ "$attempt" -lt "$max_attempts" ]; then sleep 2 fi done if [ -z "$reviewer" ]; then echo "Failed to resolve reviewer login after $max_attempts attempts." return 1 fi for attempt in $(seq 1 "$max_attempts"); do local claim_log claim_log=".local/review-claim-assignee-attempt-$attempt.log" if gh pr edit "$pr" --add-assignee "$reviewer" >"$claim_log" 2>&1; then echo "review claim succeeded: @$reviewer assigned to PR #$pr" return 0 fi echo "Claim assignee update failed (attempt $attempt/$max_attempts)." print_relevant_log_excerpt "$claim_log" if [ "$attempt" -lt "$max_attempts" ]; then sleep 2 fi done echo "Failed to assign @$reviewer to PR #$pr after $max_attempts attempts." return 1 } review_checkout_main() { local pr="$1" enter_worktree "$pr" false mark_pr_operation_side_effects_started git fetch origin main git checkout --detach origin/main set_review_mode main echo "review mode set to main baseline" echo "branch=$(git branch --show-current)" echo "head=$(git rev-parse --short HEAD)" } review_checkout_pr() { local pr="$1" enter_worktree "$pr" false mark_pr_operation_side_effects_started git fetch origin "pull/$pr/head:pr-$pr" --force git checkout --detach "pr-$pr" set_review_mode pr echo "review mode set to PR head" echo "branch=$(git branch --show-current)" echo "head=$(git rev-parse --short HEAD)" } review_guard() { local pr="$1" enter_worktree "$pr" false require_artifact .local/review-mode.env require_artifact .local/pr-meta.env # shellcheck disable=SC1091 source .local/review-mode.env # shellcheck disable=SC1091 source .local/pr-meta.env if [ "${PR_NUMBER:-}" != "$pr" ]; then echo "Review guard failed: .local/pr-meta.env describes PR #${PR_NUMBER:-unknown}, not #$pr. Re-run: scripts/pr review-init $pr" exit 1 fi local branch branch=$(git branch --show-current) local head_sha head_sha=$(git rev-parse HEAD) case "${REVIEW_MODE:-}" in main) local expected_main_sha expected_main_sha=$(git rev-parse origin/main) if [ "$head_sha" != "$expected_main_sha" ]; then echo "Review guard failed: expected HEAD at origin/main ($expected_main_sha) for main baseline mode, got $head_sha" exit 1 fi ;; pr) if [ -z "${PR_HEAD_SHA:-}" ]; then echo "Review guard failed: missing PR_HEAD_SHA in .local/pr-meta.env" exit 1 fi if [ "$head_sha" != "$PR_HEAD_SHA" ]; then echo "Review guard failed: expected HEAD at PR_HEAD_SHA ($PR_HEAD_SHA), got $head_sha" exit 1 fi ;; *) echo "Review guard failed: unknown review mode '${REVIEW_MODE:-}'" exit 1 ;; esac echo "review guard passed" echo "mode=$REVIEW_MODE" echo "branch=$branch" echo "head=$head_sha" } review_artifacts_init() { local pr="$1" enter_worktree "$pr" false require_artifact .local/pr-meta.env require_artifact .local/pr-meta.json mark_pr_operation_side_effects_started local meta_number head_sha meta_number=$(jq -r '.number' .local/pr-meta.json) head_sha=$(jq -r '.headRefOid' .local/pr-meta.json) # Bash regex, not rg: this guard runs inside fork-PR CI test harnesses on # GitHub-hosted runners without ripgrep, where a missing rg (exit 127) would # misreport a valid head SHA as an identity mismatch. if [ "$meta_number" != "$pr" ] || ! [[ "$head_sha" =~ ^[0-9a-f]{40}$ ]]; then echo "Review artifacts init failed: .local/pr-meta.json describes PR #$meta_number at '$head_sha', not PR #$pr. Re-run: scripts/pr review-init $pr" exit 1 fi # Take the first line in the shell, not through `head`: pipefail turns the # helper's EPIPE into a spurious failure once the template outgrows the pipe. local identity_line identity_line=$(node "$(review_artifacts_helper_path)" markdown "$meta_number" "$head_sha") identity_line=${identity_line%%$'\n'*} if [ -f .local/review.json ] && [ -f .local/review.md ] && jq -e --argjson number "$meta_number" --arg head "$head_sha" \ '.pr.number == $number and .pr.headSha == $head' .local/review.json >/dev/null 2>&1 && [ "$(head -n1 .local/review.md)" = "$identity_line" ] then echo "review artifacts already stamped for PR #$meta_number at $head_sha" echo "files=.local/review.md .local/review.json" return 0 fi # Artifacts on disk were authored for another PR or a superseded head. Keep them # instead of deleting: a mid-review head change is legitimate and the prose is # worth salvaging, but only a freshly stamped template may gate this landing. # mktemp -d allocates the archive slot atomically so a retry, a concurrent init, # or a repeated clock second cannot overwrite an earlier preserved review. local superseded_dir="" ext for ext in json md; do [ -f ".local/review.$ext" ] || continue if [ -z "$superseded_dir" ]; then mkdir -p .local/superseded superseded_dir=$(mktemp -d ".local/superseded/$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX") fi mv ".local/review.$ext" "$superseded_dir/review.$ext" echo "moved aside .local/review.$ext -> $superseded_dir/review.$ext (not authored for PR #$meta_number at $head_sha)" done node "$(review_artifacts_helper_path)" markdown "$meta_number" "$head_sha" > .local/review.md node "$(review_artifacts_helper_path)" template "$meta_number" "$head_sha" > .local/review.json echo "review artifact templates are ready" echo "files=.local/review.md .local/review.json" } validate_review_artifact_data() { # pr-meta.json is the identity authority the review artifacts are stamped against, # so it must itself be anchored: review_guard binds pr-meta.env to the guarded PR # and the checked-out head, and this ties pr-meta.json to pr-meta.env. Without it a # wholly foreign but self-consistent .local set still gates the landing. local meta_number meta_head meta_number=$(jq -r '.number' .local/pr-meta.json) meta_head=$(jq -r '.headRefOid' .local/pr-meta.json) if ! ( # shellcheck disable=SC1091 source .local/pr-meta.env [ "$meta_number" = "${PR_NUMBER:-}" ] && [ "$meta_head" = "${PR_HEAD_SHA:-}" ] ); then echo "Review artifact identity mismatch: .local/pr-meta.json describes PR #$meta_number at $meta_head, which does not match .local/pr-meta.env. Re-run: scripts/pr review-init" return 1 fi if ! node "$(review_artifacts_helper_path)" validate \ .local/review.json \ .local/review.md \ .local/pr-meta.json then return 1 fi } require_ready_review_recommendation() { if ! jq -e '.recommendation == "READY FOR /prepare-pr"' .local/review.json >/dev/null; then echo "PR preparation requires a validated READY FOR /prepare-pr review recommendation." return 1 fi } review_validate_artifacts() { local pr="$1" # Callers use an OR-list to keep pre-mutation failures reversible; Bash disables # errexit within that context, so every artifact and exact-head guard must propagate. enter_worktree "$pr" false || return 1 require_artifact .local/review.md || return 1 require_artifact .local/review.json || return 1 require_artifact .local/pr-meta.env || return 1 require_artifact .local/pr-meta.json || return 1 review_guard "$pr" || return 1 if [ "${REVIEW_MODE:-}" != "pr" ]; then echo "Review artifact validation requires the reviewed PR head, not main-baseline mode." return 1 fi validate_review_artifact_data || return 1 echo "review artifacts validated" print_review_stdout_summary } review_tests() { local pr="$1" shift if [ "$#" -lt 1 ]; then echo "Usage: scripts/pr review-tests [ ...]" exit 2 fi enter_worktree "$pr" false review_guard "$pr" local target for target in "$@"; do if [ ! -f "$target" ]; then echo "Missing test target file: $target" exit 1 fi done mark_pr_operation_side_effects_started bootstrap_deps_if_needed local run_log=".local/review-tests-run.log" run_quiet_logged "pnpm test" "$run_log" pnpm test -- "$@" local missing_run=() for target in "$@"; do local base base=$(basename "$target") if ! rg -F -q "$target" "$run_log" && ! rg -F -q "$base" "$run_log"; then missing_run+=("$target") fi done if [ "${#missing_run[@]}" -gt 0 ]; then echo "These requested targets were not observed in vitest run output:" printf ' - %s\n' "${missing_run[@]}" exit 1 fi # Security: shell-escape values to prevent command injection when sourced. printf '%s=%q\n' \ REVIEW_TESTS_AT "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ REVIEW_TEST_TARGET_COUNT "$#" \ > .local/review-tests.env echo "review tests passed and were observed in output" } review_init() { local pr="$1" mark_pr_operation_side_effects_started enter_worktree "$pr" true local json pr_url json=$(pr_meta_json "$pr") write_pr_meta_files "$json" pr_url=$(printf '%s\n' "$json" | jq -r .url) git fetch origin "pull/$pr/head:pr-$pr" --force local mb mb=$(git merge-base origin/main "pr-$pr") # Security: shell-escape values to prevent command injection when sourced. printf '%s=%q\n' \ PR_NUMBER "$pr" \ MERGE_BASE "$mb" \ REVIEW_STARTED_AT "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ > .local/review-context.env set_review_mode main printf '%s\n' "$json" | jq '{number,title,url,state,isDraft,author:.author.login,base:.baseRefName,head:.headRefName,headSha:.headRefOid,headRepo:.headRepository.nameWithOwner,additions,deletions,files:.changedFiles}' echo "worktree=$PWD" echo "pr_url=$pr_url" echo "merge_base=$mb" echo "branch=$(git branch --show-current)" echo "wrote=.local/pr-meta.json .local/pr-meta.env .local/review-context.env .local/review-mode.env" cat <