#!/usr/bin/env bash set -euo pipefail # This wrapper parses GitHub CLI JSON. Caller shells may force ANSI color globally. export NO_COLOR=1 export CLICOLOR=0 export CLICOLOR_FORCE=0 export FORCE_COLOR=0 unset COLORTERM # If invoked from a linked worktree copy of this script, re-exec the canonical # script from the repository root so behavior stays consistent across worktrees. script_self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" script_parent_dir="$(dirname "$script_self")" if common_git_dir=$(git -C "$script_parent_dir" rev-parse --path-format=absolute --git-common-dir 2>/dev/null); then canonical_repo_root="$(dirname "$common_git_dir")" canonical_self="$canonical_repo_root/scripts/$(basename "${BASH_SOURCE[0]}")" if [ "$script_self" != "$canonical_self" ] && [ -x "$canonical_self" ]; then if ! git -C "$script_parent_dir" diff --quiet HEAD -- \ ":(top)scripts/pr" ":(top)scripts/pr-lib" ":(top)scripts/lib/plain-gh.sh"; then echo "scripts/pr wrapper files have uncommitted changes in this worktree." >&2 echo "Refusing to run unreviewed wrapper code from: $script_parent_dir" >&2 exit 1 fi linked_wrapper_revision=$( git -C "$script_parent_dir" rev-parse \ HEAD:scripts/pr \ HEAD:scripts/pr-lib \ HEAD:scripts/lib/plain-gh.sh 2>/dev/null || true ) canonical_wrapper_revision=$( git -C "$canonical_repo_root" rev-parse \ HEAD:scripts/pr \ HEAD:scripts/pr-lib \ HEAD:scripts/lib/plain-gh.sh 2>/dev/null || true ) canonical_wrapper_clean=1 if ! git -C "$canonical_repo_root" diff --quiet HEAD -- \ ":(top)scripts/pr" ":(top)scripts/pr-lib" ":(top)scripts/lib/plain-gh.sh"; then canonical_wrapper_clean=0 fi if [ -n "$linked_wrapper_revision" ] && [ "$linked_wrapper_revision" = "$canonical_wrapper_revision" ] && [ "$canonical_wrapper_clean" = "1" ]; then exec "$canonical_self" "$@" fi # The canonical checkout can be parked on another branch or carry local # edits (release trains move it); maintainer-controlled origin/main is the # trust anchor then. Run THIS worktree's committed wrapper, without # substitution, when it exactly matches origin/main. # refs/remotes/... explicitly: bare "origin/main" is a DWIM name that a # local branch or tag named origin/main could shadow, spoofing the anchor. anchor_wrapper_revision=$( git -C "$script_parent_dir" rev-parse \ refs/remotes/origin/main:scripts/pr \ refs/remotes/origin/main:scripts/pr-lib \ refs/remotes/origin/main:scripts/lib/plain-gh.sh 2>/dev/null || true ) if [ -z "$linked_wrapper_revision" ] || [ -z "$anchor_wrapper_revision" ] || [ "$linked_wrapper_revision" != "$anchor_wrapper_revision" ]; then echo "scripts/pr implementation differs between this worktree and the canonical checkout, and does not match origin/main." >&2 echo "Refusing to silently substitute canonical wrapper code from: $canonical_repo_root" >&2 echo "Run scripts/pr from a checkout whose wrapper matches the canonical checkout or a fetched origin/main." >&2 exit 1 fi fi fi is_locked_pr_command() { case "$1" in review-init | review-checkout-main | review-checkout-pr | review-claim | review-guard | review-artifacts-init | review-validate-artifacts | review-tests | prepare-init | prepare-validate-commit | prepare-gates | prepare-push | prepare-sync-head | prepare-run | merge-verify | merge-run) return 0 ;; *) return 1 ;; esac } is_supervised_pr_process() { [ "${OPENCLAW_PR_DEDICATED_PROCESS_GROUP:-}" = "1" ] && [ "${OPENCLAW_PR_LOCK_NOTIFY_FD:-}" = "3" ] && [ "${OPENCLAW_PR_LOCK_SUPERVISOR_PID:-}" = "$PPID" ] } if [ "${1-}" = "gc" ] || is_locked_pr_command "${1-}"; then if is_supervised_pr_process; then # Do not leak the one-shot marker to tools or nested wrapper calls. unset OPENCLAW_PR_DEDICATED_PROCESS_GROUP else unset OPENCLAW_PR_DEDICATED_PROCESS_GROUP unset OPENCLAW_PR_LOCK_NOTIFY_FD unset OPENCLAW_PR_LOCK_SUPERVISOR_PID command -v node >/dev/null 2>&1 || { echo "Missing required command: node" >&2; exit 1; } exec node "$script_parent_dir/pr-lib/process-group-runner.mjs" "$script_parent_dir/.." "$script_self" "$@" fi fi # shellcheck disable=SC1091 source "$script_parent_dir/lib/plain-gh.sh" usage() { cat < --confirmed-no-running-tools scripts/pr review-init scripts/pr review-checkout-main scripts/pr review-checkout-pr scripts/pr review-claim scripts/pr review-guard scripts/pr review-artifacts-init scripts/pr review-validate-artifacts scripts/pr review-tests [ ...] scripts/pr prepare-init scripts/pr prepare-validate-commit scripts/pr prepare-gates scripts/pr prepare-push scripts/pr prepare-sync-head scripts/pr prepare-run scripts/pr merge-verify scripts/pr merge-run OPENCLAW_PR_MERGE_METHOD=merge|rebase preserves the PR commit series. USAGE } require_cmds() { local missing=() local cmd for cmd in git jq rg pnpm node; do if ! command -v "$cmd" >/dev/null 2>&1; then missing+=("$cmd") fi done if ! OPENCLAW_GH_BIN="$(resolve_plain_gh_bin)"; then missing+=("gh") else export OPENCLAW_GH_BIN fi if [ "${#missing[@]}" -gt 0 ]; then echo "Missing required command(s): ${missing[*]}" exit 1 fi } gh() { gh_plain "$@" } # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/worktree.sh" # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/operation-lock.sh" # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/common.sh" # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/changelog.sh" # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/gates.sh" # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/push.sh" # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/review.sh" # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/prepare-core.sh" # shellcheck disable=SC1091 source "$script_parent_dir/pr-lib/merge.sh" main() { if [ "$#" -lt 1 ]; then usage exit 2 fi local cmd="${1-}" shift || true if [ "$cmd" = "lock-recover" ]; then local pr="${1-}" local owner_oid="${2-}" local confirmation="${3-}" [ -n "$pr" ] && [ -n "$owner_oid" ] && [ "$#" -eq 3 ] || { usage; exit 2; } recover_pr_operation_lock "$pr" "$owner_oid" "$confirmation" return fi case "$cmd" in ls) ;; gc) [ "$#" -eq 0 ] || { [ "$#" -eq 1 ] && [ "$1" = "--dry-run" ]; } || { usage exit 2 } ;; review-tests) [ "$#" -ge 2 ] || { usage; exit 2; } ;; review-init | review-checkout-main | review-checkout-pr | review-claim | review-guard | review-artifacts-init | review-validate-artifacts | prepare-init | prepare-validate-commit | prepare-gates | prepare-push | prepare-sync-head | prepare-run | merge-verify | merge-run) [ "$#" -ge 1 ] || { usage; exit 2; } ;; *) usage exit 2 ;; esac require_cmds if is_locked_pr_command "$cmd"; then local locked_pr="${1-}" acquire_pr_operation_lock "$locked_pr" trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 131' QUIT trap 'exit 143' TERM fi case "$cmd" in ls) list_pr_worktrees ;; gc) local dry_run=false if [ "$#" -eq 1 ]; then dry_run=true fi gc_pr_worktrees "$dry_run" ;; review-init) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } review_init "$pr" ;; review-checkout-main) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } review_checkout_main "$pr" ;; review-checkout-pr) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } review_checkout_pr "$pr" ;; review-claim) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } review_claim "$pr" ;; review-guard) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } review_guard "$pr" ;; review-artifacts-init) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } review_artifacts_init "$pr" ;; review-validate-artifacts) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } review_validate_artifacts "$pr" ;; review-tests) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } shift || true review_tests "$pr" "$@" ;; prepare-init) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } prepare_init "$pr" ;; prepare-validate-commit) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } prepare_validate_commit "$pr" ;; prepare-gates) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } prepare_gates "$pr" ;; prepare-push) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } prepare_push "$pr" ;; prepare-sync-head) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } prepare_sync_head "$pr" ;; prepare-run) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } prepare_run "$pr" ;; merge-verify) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } merge_verify "$pr" ;; merge-run) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } merge_run "$pr" ;; *) usage exit 2 ;; esac } main "$@"