mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
ce0fafefce
* fix(tooling): avoid shared tsx cache startup stalls Use the shared preloader before tsx initializes so maintained build, check, and test commands retain memory transforms without scanning other checkouts' disk caches. Preserve changed-cwd forks and copied tooling closures. * test: align command contracts with tooling bootstrap
447 lines
16 KiB
Bash
Executable File
447 lines
16 KiB
Bash
Executable File
#!/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
|
|
|
|
# This is the single source of truth for the canonical-wrapper trust boundary.
|
|
# Advisory commands may run a mismatched local wrapper only with the explicit
|
|
# developer opt-in; landing commands must always use canonical/origin-main code.
|
|
# Classification is independent of serialization: ci-dispatch remains locked
|
|
# because GitHub exposes neither dispatch deduplication nor a correlation ID.
|
|
# PR_SUBCOMMAND_CLASSIFICATIONS_BEGIN
|
|
pr_subcommand_classification() {
|
|
case "$1" in
|
|
ls | ci-dispatch)
|
|
printf 'advisory\n'
|
|
;;
|
|
gc | lock-recover | 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)
|
|
printf 'landing\n'
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
# PR_SUBCOMMAND_CLASSIFICATIONS_END
|
|
|
|
dev_wrapper_opt_in=0
|
|
if [ "${OPENCLAW_PR_DEV_WRAPPER:-}" = "1" ]; then
|
|
dev_wrapper_opt_in=1
|
|
fi
|
|
if [ "${1-}" = "--dev-wrapper" ]; then
|
|
dev_wrapper_opt_in=1
|
|
export OPENCLAW_PR_DEV_WRAPPER=1
|
|
shift
|
|
fi
|
|
requested_subcommand="${1-}"
|
|
|
|
# Select trusted wrapper code independently from the canonical repository root;
|
|
# a linked wrapper may be removed by merge-run or gc before supervision ends.
|
|
script_self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
|
script_parent_dir="$(dirname "$script_self")"
|
|
canonical_repo_root="$script_parent_dir/.."
|
|
pr_wrapper_components=(
|
|
scripts/pr
|
|
scripts/pr-lib
|
|
scripts/lib/plain-gh.sh
|
|
scripts/lib/plain-gh.mjs
|
|
scripts/lib/direct-run.mjs
|
|
scripts/lib/tsx-cli-shim.mjs
|
|
scripts/lib/local-check-runtime.mts
|
|
scripts/tsx.mjs
|
|
scripts/verify-pr-hosted-gates.mjs
|
|
scripts/verify-pr-hosted-gates.mts
|
|
scripts/watch-pr-ci.mjs
|
|
scripts/watch-pr-ci.mts
|
|
)
|
|
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 -- "${pr_wrapper_components[@]/#/:(top)}"; 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 "${pr_wrapper_components[@]/#/HEAD:}" 2>/dev/null || true
|
|
)
|
|
canonical_wrapper_revision=$(
|
|
git -C "$canonical_repo_root" rev-parse "${pr_wrapper_components[@]/#/HEAD:}" 2>/dev/null || true
|
|
)
|
|
canonical_wrapper_clean=1
|
|
if ! git -C "$canonical_repo_root" diff --quiet HEAD -- "${pr_wrapper_components[@]/#/:(top)}"; 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 "${pr_wrapper_components[@]/#/refs/remotes/origin/main:}" 2>/dev/null || true
|
|
)
|
|
if [ -z "$linked_wrapper_revision" ] ||
|
|
[ -z "$anchor_wrapper_revision" ] ||
|
|
[ "$linked_wrapper_revision" != "$anchor_wrapper_revision" ]; then
|
|
requested_classification=$(pr_subcommand_classification "$requested_subcommand" 2>/dev/null || true)
|
|
if [ "$dev_wrapper_opt_in" = "1" ] && [ "$requested_classification" = "advisory" ]; then
|
|
if [ "${OPENCLAW_PR_DEV_WRAPPER_BANNER_SHOWN:-}" != "1" ]; then
|
|
local_head_revision=$(git -C "$script_parent_dir" rev-parse HEAD 2>/dev/null || printf 'unknown')
|
|
echo "WARNING: running local scripts/pr revision $local_head_revision via dev-wrapper opt-in." >&2
|
|
echo "subcommand '$requested_subcommand' is classified advisory." >&2
|
|
echo "The local wrapper differs from the canonical checkout and origin/main; landing subcommands remain refused." >&2
|
|
export OPENCLAW_PR_DEV_WRAPPER_BANNER_SHOWN=1
|
|
fi
|
|
else
|
|
if [ "$dev_wrapper_opt_in" = "1" ] && [ -n "$requested_classification" ]; then
|
|
echo "subcommand '$requested_subcommand' is classified $requested_classification; dev-wrapper opt-in is unavailable." >&2
|
|
fi
|
|
# Worktrees routinely sit on a base that predates (or carries) wrapper
|
|
# changes relative to main. When the canonical checkout is byte-identical
|
|
# to the fetched origin/main anchor, exec-ing it runs exactly the trusted
|
|
# anchor code; announce the substitution so it is never silent.
|
|
if [ -n "$anchor_wrapper_revision" ] &&
|
|
[ "$canonical_wrapper_revision" = "$anchor_wrapper_revision" ] &&
|
|
[ "$canonical_wrapper_clean" = "1" ]; then
|
|
echo "scripts/pr wrapper in this worktree differs from origin/main; running the canonical checkout's wrapper (matches the origin/main trust anchor): $canonical_repo_root" >&2
|
|
exec "$canonical_self" "$@"
|
|
fi
|
|
# HEAD blobs are authoritative here: the uncommitted-wrapper guard above
|
|
# already exited for any staged or unstaged edit to these paths, so
|
|
# the working tree matches HEAD and this list matches what was rejected.
|
|
differing_wrapper_components=()
|
|
for wrapper_component in "${pr_wrapper_components[@]}"; do
|
|
linked_component_revision=$(git -C "$script_parent_dir" rev-parse "HEAD:$wrapper_component" 2>/dev/null || true)
|
|
anchor_component_revision=$(git -C "$script_parent_dir" rev-parse "refs/remotes/origin/main:$wrapper_component" 2>/dev/null || true)
|
|
if [ -z "$linked_component_revision" ] ||
|
|
[ -z "$anchor_component_revision" ] ||
|
|
[ "$linked_component_revision" != "$anchor_component_revision" ]; then
|
|
differing_wrapper_components+=("$wrapper_component")
|
|
fi
|
|
done
|
|
echo "scripts/pr implementation differs between this worktree and the canonical checkout, and does not match origin/main." >&2
|
|
echo "differing wrapper components vs origin/main: ${differing_wrapper_components[*]}" >&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
|
|
fi
|
|
|
|
is_locked_pr_command() {
|
|
# Advisory trust classification permits local dogfood, but dispatches still
|
|
# serialize with landing operations because the remote mutation is not atomic.
|
|
if [ "$1" = "ci-dispatch" ]; then
|
|
return 0
|
|
fi
|
|
local classification
|
|
classification=$(pr_subcommand_classification "$1") || return 1
|
|
[ "$classification" = "landing" ] || return 1
|
|
# gc manages per-PR locks itself; lock-recover performs an exact-OID CAS.
|
|
[ "$1" != "gc" ] && [ "$1" != "lock-recover" ]
|
|
}
|
|
|
|
is_main_only_pr_command() {
|
|
case "$1" in
|
|
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
|
|
# operation-lock.sh consumes the one-shot marker when it installs the
|
|
# leader-only completion trap, before PR command tools can inherit it.
|
|
:
|
|
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" "$canonical_repo_root" "$script_self" "$@"
|
|
fi
|
|
fi
|
|
|
|
# shellcheck disable=SC1091
|
|
source "$script_parent_dir/lib/plain-gh.sh"
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage:
|
|
scripts/pr [--dev-wrapper] <subcommand> ...
|
|
scripts/pr ls
|
|
scripts/pr gc [--dry-run]
|
|
scripts/pr lock-recover <PR> <OWNER_OID> --confirmed-no-running-tools
|
|
scripts/pr review-init <PR>
|
|
scripts/pr review-checkout-main <PR>
|
|
scripts/pr review-checkout-pr <PR>
|
|
scripts/pr review-claim <PR>
|
|
scripts/pr review-guard <PR>
|
|
scripts/pr review-artifacts-init <PR>
|
|
scripts/pr review-validate-artifacts <PR>
|
|
scripts/pr review-tests <PR> <test-file> [<test-file> ...]
|
|
scripts/pr prepare-init <PR>
|
|
scripts/pr prepare-validate-commit <PR>
|
|
scripts/pr prepare-gates <PR>
|
|
scripts/pr prepare-push <PR>
|
|
scripts/pr prepare-sync-head <PR>
|
|
scripts/pr prepare-run <PR>
|
|
scripts/pr ci-dispatch <PR>
|
|
scripts/pr merge-verify <PR>
|
|
scripts/pr merge-run <PR> [--auto-merge]
|
|
OPENCLAW_PR_MERGE_METHOD=merge|rebase preserves the PR commit series.
|
|
--auto-merge enables pinned squash auto-merge for a verified BEHIND head.
|
|
OPENCLAW_PR_AUTO_MERGE=1 is equivalent.
|
|
|
|
--dev-wrapper permits a mismatched local wrapper only for subcommands
|
|
classified advisory. OPENCLAW_PR_DEV_WRAPPER=1 is equivalent.
|
|
|
|
Required commands: git, gh, jq, rg (ripgrep), pnpm, node.
|
|
USAGE
|
|
}
|
|
|
|
require_cmds() {
|
|
local missing=()
|
|
local cmd
|
|
for cmd in git gh 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+=("real-gh")
|
|
else
|
|
export OPENCLAW_GH_BIN
|
|
fi
|
|
|
|
if [ "${#missing[@]}" -gt 0 ]; then
|
|
echo "Missing required command(s): ${missing[*]}" >&2
|
|
if [[ " ${missing[*]} " = *" rg "* ]]; then
|
|
echo "Install ripgrep and retry: https://github.com/BurntSushi/ripgrep#installation" >&2
|
|
fi
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_main_target_pr() {
|
|
local pr="$1"
|
|
local base base_json
|
|
base_json=$(read_pr_view_json "$pr" "baseRefName") || exit 1
|
|
base=$(pr_view_string_field "$base_json" "baseRefName" "$pr" "Retry the scripts/pr command.") || exit 1
|
|
if [ "$base" != "main" ]; then
|
|
echo "scripts/pr prepare and merge commands only support PRs targeting main; PR #$pr targets $base." >&2
|
|
echo "Use the reviewed release-branch landing flow for non-main PRs." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# 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; }
|
|
;;
|
|
merge-run)
|
|
[ "$#" -ge 1 ] && [ "$#" -le 2 ] || { usage; exit 2; }
|
|
if [ "$#" -eq 2 ] && [ "$2" != "--auto-merge" ]; then
|
|
usage
|
|
exit 2
|
|
fi
|
|
;;
|
|
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 | ci-dispatch | merge-verify)
|
|
[ "$#" -ge 1 ] || { usage; exit 2; }
|
|
;;
|
|
*)
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
require_cmds
|
|
|
|
if is_main_only_pr_command "$cmd"; then
|
|
require_main_target_pr "${1-}"
|
|
fi
|
|
|
|
if is_locked_pr_command "$cmd"; then
|
|
local locked_pr="${1-}"
|
|
acquire_pr_operation_lock "$locked_pr"
|
|
begin_pr_operation_validation_phase
|
|
# Temp-backed shell redirections must fail while the supervisor can auto-release.
|
|
validate_pr_temp_storage
|
|
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"
|
|
;;
|
|
ci-dispatch)
|
|
local pr="${1-}"
|
|
[ -n "$pr" ] || { usage; exit 2; }
|
|
ci_dispatch "$pr"
|
|
;;
|
|
merge-verify)
|
|
local pr="${1-}"
|
|
[ -n "$pr" ] || { usage; exit 2; }
|
|
merge_verify "$pr"
|
|
;;
|
|
merge-run)
|
|
local pr="${1-}"
|
|
[ -n "$pr" ] || { usage; exit 2; }
|
|
local auto_merge=false
|
|
if [ "${2-}" = "--auto-merge" ] || [ "${OPENCLAW_PR_AUTO_MERGE:-}" = "1" ]; then
|
|
auto_merge=true
|
|
fi
|
|
merge_run "$pr" "$auto_merge"
|
|
;;
|
|
*)
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|