# shellcheck source=scripts/pr-lib/github.sh source "$(cd "${BASH_SOURCE[0]%/*}" && pwd -P)/github.sh" || return 1 run_hosted_prepare_gates() { local pr="$1" local current_head="$2" local changelog_only="$3" local recent_sha="" local remote_record="${4:-}" remote_head remote_head_ref remote_is_cross_repository if [ -z "$remote_record" ]; then remote_record=$(read_pr_observation "$pr") || return 1 fi remote_head=$(pr_view_string_field "$remote_record" "headRefOid" "$pr" "Re-run prepare-init.") || return 1 remote_head_ref=$(printf '%s\n' "$remote_record" | jq -r .headRefName) remote_is_cross_repository=$(printf '%s\n' "$remote_record" | jq -r .isCrossRepository) if [ "$remote_head" != "$current_head" ]; then echo "PR head changed before hosted gate verification (expected $current_head, got $remote_head). Re-run prepare-init." return 1 fi # A docs-only final commit may reuse its immutable parent; this covers # release-owned cleanup without inferring PR identity from mutable branches. if [ -z "$recent_sha" ]; then local parent_sha local parent_delta if parent_sha=$(pr_git rev-parse "${current_head}^" 2>/dev/null) && parent_delta=$(pr_git diff --name-only "$parent_sha" "$current_head" 2>/dev/null) && file_list_is_docsish_only "$parent_delta"; then recent_sha="$parent_sha" fi fi local repo repo=$(printf '%s\n' "$remote_record" | jq -er '.baseRepository.nameWithOwner | select(type == "string" and length > 0)') || return 1 local scripts_dir="${script_parent_dir:-}" if [ -z "$scripts_dir" ]; then scripts_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) fi # A directory argv[1] keeps the imported module's standalone entrypoint inactive. local args=( "$scripts_dir" --repo "$repo" --sha "$current_head" --pr "$pr" --main-sha "$PR_MAIN_SHA" --output ".local/gates-hosted-checks.json" ) if [ -n "$recent_sha" ]; then args+=(--recent-sha "$recent_sha") fi if [ "$changelog_only" = "true" ]; then args+=(--changelog-only) fi if printf '%s\n' "$remote_record" | run_quiet_logged "hosted CI/Testbox gates" ".local/gates-hosted-checks.log" \ node --input-type=module -e ' import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; const { main } = await import(pathToFileURL(process.argv[1] + "/verify-pr-hosted-gates.mts").href); main(process.argv.slice(2), JSON.parse(readFileSync(0, "utf8"))); ' "${args[@]}"; then local reused_head reused_head=$(jq -er '.reusedFromSha // "" | strings' .local/gates-hosted-checks.json) || return 1 if [ -n "$reused_head" ]; then # Compare only main context incorporated into the candidate, not later main drift. local reused_base current_base reused_base=$(pr_git merge-base "$PR_MAIN_SHA" "$reused_head") || return 1 current_base=$(pr_git merge-base "$PR_MAIN_SHA" "$current_head") || return 1 if mainline_drift_requires_sync "$reused_base" "$reused_head" "$current_base"; then echo "Hosted CI reuse declined: candidate incorporated relevant main changes; require successful CI for $current_head." return 1 else local drift_status=$? if [ "$drift_status" -ne 1 ]; then echo "Hosted CI reuse failed: unable to evaluate mainline input changes." >&2 return 1 fi fi fi return 0 fi if rg -F -q "Missing successful recent CI workflow for $current_head. Observed: none" \ .local/gates-hosted-checks.log then if [ "$remote_is_cross_repository" = "true" ]; then cat <&2 return 1 fi if [ "$is_cross_repository" = "true" ]; then echo "PR #$pr comes from a fork; release-gate workflow dispatch requires a base-repository branch at $head_sha." >&2 return 1 fi if [ "$is_cross_repository" != "false" ]; then echo "PR #$pr is missing repository identity for workflow dispatch." >&2 return 1 fi mark_pr_operation_side_effects_if_available node "$script_parent_dir/pr-lib/ci-dispatch.mjs" \ "$pr" "$head_ref" "$head_sha" "$base_sha" false "$@" } mark_pr_operation_side_effects_if_available() { # scripts/pr sources operation-lock.sh first. Policy tests may source this # library alone, where advancing a lock phase is neither possible nor needed. if declare -F mark_pr_operation_side_effects_started >/dev/null; then mark_pr_operation_side_effects_started fi } pin_worktree_bundled_plugins_dir() { # Nested .worktrees/ checkouts resolve vitest tooling from the primary # checkout's node_modules; pin bundled plugin discovery to this worktree so # PR branches without the openclaw-root node_modules-boundary fix still test # their own extensions instead of the primary checkout's stale trees. export OPENCLAW_BUNDLED_PLUGINS_DIR="${OPENCLAW_BUNDLED_PLUGINS_DIR:-$PWD/extensions}" } resolve_pr_gates_remote_mode() { case "${OPENCLAW_PR_GATES_REMOTE:-}" in "") printf 'local\n' ;; testbox|crabbox-aws|github) if [ "${OPENCLAW_TESTBOX:-}" = "1" ]; then echo "OPENCLAW_PR_GATES_REMOTE=$OPENCLAW_PR_GATES_REMOTE conflicts with OPENCLAW_TESTBOX=1; select one gate mode." >&2 echo "Unset OPENCLAW_TESTBOX to use $OPENCLAW_PR_GATES_REMOTE gates, or unset OPENCLAW_PR_GATES_REMOTE to use completed hosted proof." >&2 return 2 fi printf '%s\n' "$OPENCLAW_PR_GATES_REMOTE" ;; *) echo "Unsupported OPENCLAW_PR_GATES_REMOTE=${OPENCLAW_PR_GATES_REMOTE} (supported: testbox, crabbox-aws, github)." >&2 return 1 ;; esac } prepare_local_gate_workspace() { pin_worktree_bundled_plugins_dir bootstrap_deps_if_needed } run_remote_testbox_full_test_gate() { local label="$1" local log_file="$2" local lease_label="$3" local remote_env=(CI=1 OPENCLAW_TESTBOX_REMOTE_RUN=1 PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false) local name value # Delegated Testbox commands do not inherit the caller's scheduling controls. for name in OPENCLAW_TEST_PROJECTS_PARALLEL OPENCLAW_VITEST_MAX_WORKERS; do [ -n "${!name:-}" ] || continue value=$(node --input-type=module -e ' import { pathToFileURL } from "node:url"; const { parsePositiveInt } = await import(pathToFileURL(process.argv[1] + "/lib/numeric-options.mjs").href); const value = process.argv[2].trim(); if (value) { try { console.log(parsePositiveInt(value, process.argv[3])); } catch (error) { console.error(error.message); process.exitCode = 2; } } ' "$script_parent_dir" "${!name}" "$name") || return 2 [ -z "$value" ] || remote_env+=("$name=$value") done # Same Blacksmith Testbox delegation shape check:changed uses; the worktree's # own wrapper syncs this prep tree (the canonical copy would sync the primary # checkout instead). run_quiet_logged "$label" "$log_file" \ node scripts/crabbox-wrapper.mjs run \ --provider blacksmith-testbox \ --blacksmith-org openclaw \ --blacksmith-workflow .github/workflows/ci-check-testbox.yml \ --blacksmith-job check \ --blacksmith-ref main \ --idle-timeout 90m \ --ttl 240m \ --timing-json \ --label "$lease_label" \ -- env "${remote_env[@]}" corepack pnpm test } read_remote_testbox_gate_stamp() { # crabbox --timing-json emits one single-line JSON report on stderr; pick the # last successful blacksmith-testbox report in the gate log as the stamp. local log_file="$1" jq -c -R ' fromjson? | select(type == "object") | select(.provider == "blacksmith-testbox" and .exitCode == 0 and ((.leaseId // "") | startswith("tbx_"))) ' "$log_file" | tail -n 1 } read_remote_testbox_gate_run_url() { # The delegated timing report currently omits actionsRunUrl, while the same # run prints the canonical Actions URL as a separate line. local log_file="$1" local pr_url="${PR_URL:-}" local expected_repo="${pr_url#https://github.com/}" expected_repo="${expected_repo%%/pull/*}" if [ -z "$expected_repo" ] || [ "$expected_repo" = "$pr_url" ]; then expected_repo="openclaw/openclaw" fi local url_prefix="https://github.com/$expected_repo/actions/runs/" local marker="GitHub Actions run: $url_prefix" awk -v marker="$marker" -v url_prefix="$url_prefix" ' index($0, marker) { suffix = substr($0, index($0, marker) + length(marker)) if (match(suffix, /^[0-9]+/)) { print url_prefix substr(suffix, RSTART, RLENGTH) exit } } ' "$log_file" } require_remote_testbox_gate_stamp() { # Runs inside $(...): report to stderr and fail the substitution so set -e # aborts the caller with the message visible. local log_file="$1" local stamp stamp=$(read_remote_testbox_gate_stamp "$log_file") if [ -z "$stamp" ]; then echo "Remote testbox gate passed but no successful blacksmith-testbox timing stamp was found in $log_file." >&2 return 1 fi local actions_run_url actions_run_url=$(read_remote_testbox_gate_run_url "$log_file") if [ -n "$actions_run_url" ] && [ "$(printf '%s\n' "$stamp" | jq -r '.actionsRunUrl // empty')" = "" ]; then stamp=$(printf '%s\n' "$stamp" | jq -c --arg actionsRunUrl "$actions_run_url" '. + {actionsRunUrl: $actionsRunUrl}') fi printf '%s\n' "$stamp" } require_active_org_admin_for_crabbox_gate() { local actor membership actor=$(pr_gh_writer_login) || return membership=$(pr_gh_plain api "orgs/openclaw/memberships/$actor" -H 'Cache-Control: max-age=0') || return if [ "$(printf '%s\n' "$membership" | jq -r .state)" != "active" ] || [ "$(printf '%s\n' "$membership" | jq -r .role)" != "admin" ]; then echo "OPENCLAW_PR_GATES_REMOTE=crabbox-aws requires an active openclaw organization admin." >&2 return 1 fi printf '%s\n' "$actor" } read_crabbox_gate_pr_binding() { local pr="$1" local expected_head="$2" local expected_base="${3:-}" local record record=$(pr_gh pr view "$pr" --json baseRefName,baseRefOid,headRefOid,isCrossRepository,state) || return 1 if [ "$(printf '%s\n' "$record" | jq -r .state)" != "OPEN" ] || [ "$(printf '%s\n' "$record" | jq -r .isCrossRepository)" != "false" ] || [ "$(printf '%s\n' "$record" | jq -r .baseRefName)" != "main" ] || [ "$(printf '%s\n' "$record" | jq -r .headRefOid)" != "$expected_head" ]; then echo "Crabbox AWS gate requires the requested open same-repository PR at exact head $expected_head." >&2 return 1 fi local base_sha base_sha=$(printf '%s\n' "$record" | jq -r .baseRefOid) if [[ ! "$base_sha" =~ ^[0-9a-f]{40}$ ]] || { [ -n "$expected_base" ] && [ "$base_sha" != "$expected_base" ]; }; then echo "Crabbox AWS gate PR base changed or is malformed." >&2 return 1 fi printf '%s\n' "$base_sha" } finalize_remote_crabbox_aws_gate() { local pr="$1" local head_sha="$2" resume_run="${3:-}" local base_sha log_file stamp run_id lease_id run_url local dispatch_args=(--backend crabbox --pending-gates) base_sha=$(read_crabbox_gate_pr_binding "$pr" "$head_sha") || return 1 if [ -n "$resume_run" ]; then # Preparation already resolved the retained immutable base; never reread a # moving base or take one from the observed check as recovery authority. base_sha="${4:-}" [[ "$base_sha" =~ ^[0-9a-f]{40}$ ]] || return 1 dispatch_args+=(--resume-crabbox-run "$resume_run") fi require_active_org_admin_for_crabbox_gate >/dev/null || return 1 if [ "${LAST_VERIFIED_HEAD_SHA:-}" != "$head_sha" ]; then [ -z "${PENDING_CRABBOX_STATE:-}" ] || return 1 # The publication owner has verified this hosted alias against the local # reviewed tree. Pending provenance must follow its actual public OID. write_gates_env_stamp "$pr" "${DOCS_ONLY:-false}" "${CHANGELOG_REQUIRED:-false}" \ remote_crabbox_aws_pending "$head_sha" "" "" aws "" "" "" || return 1 fi log_file=".local/gates-crabbox-aws.log" run_quiet_logged "protected-main Crabbox AWS exact-head gate" "$log_file" \ node "$script_parent_dir/pr-lib/ci-dispatch.mjs" \ "$pr" "$PR_HEAD" "$head_sha" "$base_sha" false "${dispatch_args[@]}" || return 1 stamp=$(jq -c -R \ --arg baseSha "$base_sha" \ --arg headSha "$head_sha" ' fromjson? | select( .backend == "crabbox" and .provider == "aws" and .target == "linux" and .baseSha == $baseSha and .headSha == $headSha and ((.runId // "") | startswith("run_")) and ((.leaseId // "") | startswith("cbx_")) and ((.actionsRunUrl // "") | startswith("https://github.com/openclaw/openclaw/actions/runs/")) ) ' "$log_file" | tail -n 1) if [ -z "$stamp" ]; then echo "Protected-main Crabbox publisher passed without trusted exact-head metadata." >&2 return 1 fi # Main may advance while a protected run is executing. Its immutable base is # verified by the check; admission still requires this open PR's exact head. read_crabbox_gate_pr_binding "$pr" "$head_sha" >/dev/null || return 1 if declare -F verify_correction_publication_authority >/dev/null; then verify_correction_publication_authority || return 1 fi run_id=$(printf '%s\n' "$stamp" | jq -r .runId) lease_id=$(printf '%s\n' "$stamp" | jq -r .leaseId) run_url=$(printf '%s\n' "$stamp" | jq -r .actionsRunUrl) write_gates_env_stamp \ "$pr" \ "${DOCS_ONLY:-false}" \ "${CHANGELOG_REQUIRED:-false}" \ "remote_crabbox_aws" \ "$head_sha" \ "$head_sha" \ "" \ "aws" \ "$run_id" \ "$lease_id" \ "$run_url" \ "$base_sha" \ "$(printf '%s\n' "$stamp" | jq -r .workflowSha)" \ "$(printf '%s\n' "$stamp" | jq -r .actionsRunAttempt)" } write_gates_env_stamp() { local pr="$1" local docs_only="$2" local changelog_required="$3" local gates_mode="$4" local last_verified_head="$5" local full_gates_head="$6" local hosted_gates_head="$7" local remote_provider="$8" local remote_run_id="$9" local remote_lease_id="${10}" local remote_run_url="${11}" local remote_base_sha="${12:-}" remote_workflow_sha="${13:-}" remote_attempt="${14:-}" local temporary temporary=$(mktemp .local/gates.env.XXXXXX) || return 1 # Shell-escape values; chain every write so a skipped optional block cannot # mask an earlier failure and publish a partial receipt. { printf '%s=%q\n' \ PR_NUMBER "$pr" \ DOCS_ONLY "$docs_only" \ CHANGELOG_REQUIRED "$changelog_required" \ GATES_MODE "$gates_mode" \ HOSTED_GATES_TARGET_HEAD_SHA "$hosted_gates_head" && if [ "$gates_mode" != github_pending ]; then printf '%s=%q\n' \ LAST_VERIFIED_HEAD_SHA "$last_verified_head" \ FULL_GATES_HEAD_SHA "$full_gates_head" \ REMOTE_GATES_PROVIDER "$remote_provider" \ REMOTE_GATES_RUN_ID "$remote_run_id" \ REMOTE_GATES_LEASE_ID "$remote_lease_id" \ REMOTE_GATES_RUN_URL "$remote_run_url" \ GATES_PASSED_AT "$(date -u +%Y-%m-%dT%H:%M:%SZ)" fi && if [ "$gates_mode" = remote_crabbox_aws ] && [ -n "$remote_base_sha" ]; then printf '%s=%q\n' \ REMOTE_GATES_BASE_SHA "$remote_base_sha" \ REMOTE_GATES_WORKFLOW_SHA "$remote_workflow_sha" \ REMOTE_GATES_ACTIONS_RUN_ATTEMPT "$remote_attempt" fi } > "$temporary" || { rm -f "$temporary"; return 1; } mv -f "$temporary" .local/gates.env || { rm -f "$temporary"; return 1; } } # Correction publication requires the native gate owner's exact candidate # stamp. An explicit pending Crabbox stamp is admission to its protected-main # publisher, not proof; retain that separately authorized route. require_correction_publication_gates() ( local pr="$1" head="$2" allow_pending="${3:-false}" local PR_NUMBER="" LAST_VERIFIED_HEAD_SHA="" FULL_GATES_HEAD_SHA="" local GATES_MODE="" HOSTED_GATES_TARGET_HEAD_SHA="" DOCS_ONLY="" local REMOTE_GATES_PROVIDER="" REMOTE_GATES_RUN_ID="" REMOTE_GATES_LEASE_ID="" REMOTE_GATES_RUN_URL="" require_artifact .local/gates.env || return 1 source .local/gates.env || return 1 local qualified_head="$LAST_VERIFIED_HEAD_SHA" [ "$PR_NUMBER" = "$pr" ] || return 1 if [ "$qualified_head" != "$head" ]; then # GraphQL can assign a hosted OID for the identical reviewed local tree. # The verified publication can precede the completed preparation stamp. local PR_HEAD="" PR_HEAD_SHA_BEFORE="" local PREP_PUBLICATION_LEASE_SHA="" PREP_PUBLICATION_HEAD_SHA="" PR_NUMBER="" require_artifact .local/prep-context.env || return 1 source .local/prep-context.env || return 1 resolve_prep_publication_target "$pr" "$head" || return 1 [ "$PREP_PUBLICATION_HEAD_SHA" = "$qualified_head" ] || { echo "Correction publication requires gates for the exact reviewed candidate." >&2 return 1 } fi case "$GATES_MODE" in full) [ "$FULL_GATES_HEAD_SHA" = "$qualified_head" ] || return 1 ;; docs_only|reused_docs_only) [ "$DOCS_ONLY" = true ] || return 1 ;; hosted_exact_or_recent_parent) [ "$HOSTED_GATES_TARGET_HEAD_SHA" = "$qualified_head" ] || return 1 ;; remote_testbox) [ "$FULL_GATES_HEAD_SHA" = "$qualified_head" ] && [ "$REMOTE_GATES_PROVIDER" = blacksmith-testbox ] && [[ "$REMOTE_GATES_LEASE_ID" == tbx_* ]] || return 1 ;; remote_crabbox_aws) [ "$FULL_GATES_HEAD_SHA" = "$qualified_head" ] && [ "$REMOTE_GATES_PROVIDER" = aws ] && [[ "$REMOTE_GATES_RUN_ID" == run_* ]] && [[ "$REMOTE_GATES_LEASE_ID" == cbx_* ]] && [[ "$REMOTE_GATES_RUN_URL" == https://github.com/openclaw/openclaw/actions/runs/* ]] || return 1 ;; remote_crabbox_aws_pending) [ "$allow_pending" = true ] && [ "$REMOTE_GATES_PROVIDER" = aws ] || return 1 require_active_org_admin_for_crabbox_gate >/dev/null || return 1 # The candidate is not hosted yet. Bind eligibility to the publication # lease, then let the existing publisher verify the newly hosted head. [ -n "${PREP_PUBLICATION_LEASE_SHA:-}" ] || return 1 read_crabbox_gate_pr_binding "$pr" "$PREP_PUBLICATION_LEASE_SHA" >/dev/null || return 1 ;; *) echo "Unrecognized correction gate mode: $GATES_MODE" >&2; return 1 ;; esac ) derive_prepare_gate_change_plan() { PREPARE_GATE_BASE_SHA=$(pr_git merge-base "$PR_MAIN_SHA" "${1:-HEAD}") || return 1 PREPARE_GATE_CHANGED_FILES=$(pr_git diff --name-only "$PREPARE_GATE_BASE_SHA" "${1:-HEAD}") || return 1 PREPARE_GATE_DOCS_ONLY=false if file_list_is_docsish_only "$PREPARE_GATE_CHANGED_FILES"; then PREPARE_GATE_DOCS_ONLY=true fi PREPARE_GATE_CHANGELOG_ONLY=false local changelog_mode changelog_mode=$(release_changelog_file_list_mode "$PREPARE_GATE_CHANGED_FILES") || return 1 PREPARE_GATE_CHANGELOG_UPDATE=false if [ "$changelog_mode" != "none" ]; then PREPARE_GATE_CHANGELOG_UPDATE=true fi if [ "$changelog_mode" = "only" ]; then PREPARE_GATE_CHANGELOG_ONLY=true fi PREPARE_GATE_CHANGELOG_REQUIRED=false if changelog_required_for_changed_files "$PREPARE_GATE_CHANGED_FILES"; then PREPARE_GATE_CHANGELOG_REQUIRED=true fi } prepare_gates() { local pr="$1" local remote_record="${2:-}" local gates_remote_mode gates_remote_mode=$(resolve_pr_gates_remote_mode) || return $? PR_MAIN_SHA="" enter_worktree "$pr" false || return 1 mark_pr_operation_side_effects_if_available refresh_prep_branch_for_reviewed_head "$pr" checkout_prep_branch "$pr" require_artifact .local/pr-meta.env # shellcheck disable=SC1091 source .local/pr-meta.env require_prepared_review "$pr" || return 1 local current_head current_head=$(pr_git rev-parse HEAD) || return 1 derive_prepare_gate_change_plan "$current_head" || return 1 local check_base="$PREPARE_GATE_BASE_SHA" local changed_files="$PREPARE_GATE_CHANGED_FILES" local docs_only="$PREPARE_GATE_DOCS_ONLY" local changelog_only="$PREPARE_GATE_CHANGELOG_ONLY" local changelog_required="$PREPARE_GATE_CHANGELOG_REQUIRED" local has_changelog_update="$PREPARE_GATE_CHANGELOG_UPDATE" local unsupported_changelog_fragments="" local changed_path while [ -n "$changed_files" ]; do changed_path="${changed_files%%$'\n'*}" if [ "$changed_path" = "$changed_files" ]; then changed_files="" else changed_files="${changed_files#*$'\n'}" fi [ -n "$changed_path" ] || continue case "$changed_path" in changelog/fragments/*) unsupported_changelog_fragments="${unsupported_changelog_fragments}${changed_path}"$'\n' ;; esac done if [ -n "$unsupported_changelog_fragments" ]; then echo "Unsupported changelog fragment files detected:" printf '%s\n' "$unsupported_changelog_fragments" echo "Move release-note context into the PR body or commit message and remove changelog/fragments files." exit 1 fi local changelog_mode if [ "$has_changelog_update" = "true" ]; then [ -n "$remote_record" ] || remote_record=$(read_pr_observation "$pr") || return 1 if ! changelog_mode=$(root_changelog_update_allowed_for_pr "$remote_record"); then echo "CHANGELOG.md is release-owned, along with CHANGELOG/.md and matching records; normal PRs should put release-note context in the PR body or commit message." echo "Use release/-main-closeout with the documented title and only that origin-tagged release's artifacts and necessary index update, or set OPENCLAW_ALLOW_ROOT_CHANGELOG_PR=1 for explicit release automation." exit 1 fi # Release artifacts retain their approved text, including historical PR references. validate_changelog_attribution_policy fi if [ "$changelog_required" = "true" ]; then local contrib="${PR_AUTHOR:-}" validate_changelog_merge_hygiene validate_changelog_entry_for_pr "$pr" "$contrib" else echo "Changelog not required for this changed-file set." fi local previous_last_verified_head="" local previous_full_gates_head="" local remote_gates_provider="" local remote_gates_run_id="" local remote_gates_lease_id="" local remote_gates_run_url="" if [ "$gates_remote_mode" != github ] && [ -s .local/gates.env ]; then # shellcheck disable=SC1091 source .local/gates.env previous_last_verified_head="${LAST_VERIFIED_HEAD_SHA:-}" previous_full_gates_head="${FULL_GATES_HEAD_SHA:-}" # Carried alongside FULL_GATES_HEAD_SHA: they describe how that exact-head # proof was produced; a fresh gate run below overwrites them. remote_gates_provider="${REMOTE_GATES_PROVIDER:-}" remote_gates_run_id="${REMOTE_GATES_RUN_ID:-}" remote_gates_lease_id="${REMOTE_GATES_LEASE_ID:-}" remote_gates_run_url="${REMOTE_GATES_RUN_URL:-}" fi local gates_mode="full" local hosted_gates_head="" local reuse_gates=false if [ "${OPENCLAW_TESTBOX:-}" != "1" ] && [ "$docs_only" = "true" ] && [ -n "$previous_last_verified_head" ] && pr_git merge-base --is-ancestor "$previous_last_verified_head" HEAD 2>/dev/null; then local delta_since_verified delta_since_verified=$(pr_git diff --name-only "$previous_last_verified_head"..HEAD) if [ -z "$delta_since_verified" ] || file_list_is_docsish_only "$delta_since_verified"; then reuse_gates=true fi fi if [ "$gates_remote_mode" = github ]; then gates_mode=github_pending hosted_gates_head="$current_head" echo "Required GitHub gates deferred for $current_head; no successful gate proof recorded." elif [ "${OPENCLAW_TESTBOX:-}" = "1" ]; then gates_mode="hosted_exact_or_recent_parent" remote_gates_provider="" remote_gates_run_id="" remote_gates_lease_id="" remote_gates_run_url="" if [ "$changelog_only" = "true" ]; then run_quiet_logged "git diff --check" ".local/gates-diff-check.log" pr_git diff --check "$PR_MAIN_SHA...HEAD" fi run_hosted_prepare_gates "$pr" "$current_head" "$changelog_only" "$remote_record" || return 1 hosted_gates_head="$current_head" elif [ "$reuse_gates" = "true" ]; then gates_mode="reused_docs_only" echo "Docs/changelog-only delta since last verified head $previous_last_verified_head; reusing prior gates." elif [ "$gates_remote_mode" = "crabbox-aws" ]; then require_active_org_admin_for_crabbox_gate >/dev/null gates_mode="remote_crabbox_aws_pending" previous_full_gates_head="" remote_gates_provider="aws" remote_gates_run_id="" remote_gates_lease_id="" remote_gates_run_url="" echo "Crabbox AWS proof is deferred until prepare-push verifies the exact remote head." else prepare_local_gate_workspace local build_command=(pnpm build) if [ "$gates_remote_mode" = local ] && [ "$docs_only" != true ]; then # Prepare the full suite's artifacts without changing check/test runtime inputs. build_command=(env OPENCLAW_BUILD_PRIVATE_QA=1 pnpm build) fi run_quiet_logged "pnpm build" ".local/gates-build.log" "${build_command[@]}" || return $? run_quiet_logged "pnpm check" ".local/gates-check.log" pnpm check --base "$check_base" if [ "$docs_only" = "true" ]; then gates_mode="docs_only" previous_full_gates_head="" remote_gates_provider="" remote_gates_run_id="" remote_gates_lease_id="" remote_gates_run_url="" echo "Docs-only change detected with high confidence; skipping pnpm test." elif [ "$gates_remote_mode" = "testbox" ]; then gates_mode="remote_testbox" echo "Running pnpm test on Blacksmith Testbox (OPENCLAW_PR_GATES_REMOTE=testbox)." run_remote_testbox_full_test_gate \ "pnpm test (blacksmith-testbox)" \ ".local/gates-test.log" \ "pr-$pr-gates" local remote_stamp remote_stamp=$(require_remote_testbox_gate_stamp ".local/gates-test.log") remote_gates_provider="blacksmith-testbox" remote_gates_run_id="" remote_gates_lease_id=$(printf '%s\n' "$remote_stamp" | jq -r '.leaseId') remote_gates_run_url=$(printf '%s\n' "$remote_stamp" | jq -r '.actionsRunUrl // ""') echo "Remote testbox gate stamp: $remote_gates_lease_id${remote_gates_run_url:+ ($remote_gates_run_url)}" previous_full_gates_head="$current_head" else gates_mode="full" if [ -n "${OPENCLAW_VITEST_MAX_WORKERS:-}" ]; then echo "Running pnpm test with OPENCLAW_VITEST_MAX_WORKERS=$OPENCLAW_VITEST_MAX_WORKERS." run_quiet_logged \ "pnpm test" \ ".local/gates-test.log" \ env OPENCLAW_VITEST_MAX_WORKERS="$OPENCLAW_VITEST_MAX_WORKERS" pnpm test else echo "Running pnpm test with host-aware scheduling defaults." run_quiet_logged "pnpm test" ".local/gates-test.log" pnpm test fi remote_gates_provider="" remote_gates_run_id="" remote_gates_lease_id="" remote_gates_run_url="" previous_full_gates_head="$current_head" fi fi require_prepared_review "$pr" || return 1 [ "$(pr_git rev-parse HEAD)" = "$current_head" ] || { echo "Candidate changed while gates ran; no gate stamp written." >&2 return 1 } write_gates_env_stamp \ "$pr" \ "$docs_only" \ "$changelog_required" \ "$gates_mode" \ "$current_head" \ "${previous_full_gates_head:-}" \ "$hosted_gates_head" \ "$remote_gates_provider" \ "$remote_gates_run_id" \ "$remote_gates_lease_id" \ "$remote_gates_run_url" echo "docs_only=$docs_only" echo "changelog_only=$changelog_only" echo "changelog_required=$changelog_required" echo "gates_mode=$gates_mode" echo "wrote=.local/gates.env" }