diff --git a/scripts/pr b/scripts/pr index 23558ce46a73..21481941d581 100755 --- a/scripts/pr +++ b/scripts/pr @@ -177,8 +177,10 @@ Usage: scripts/pr prepare-run scripts/pr ci-dispatch scripts/pr merge-verify - scripts/pr merge-run + scripts/pr merge-run [--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. @@ -268,7 +270,14 @@ main() { 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 | ci-dispatch | merge-verify | merge-run) + 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; } ;; *) @@ -388,7 +397,11 @@ main() { merge-run) local pr="${1-}" [ -n "$pr" ] || { usage; exit 2; } - merge_run "$pr" + local auto_merge=false + if [ "${2-}" = "--auto-merge" ] || [ "${OPENCLAW_PR_AUTO_MERGE:-}" = "1" ]; then + auto_merge=true + fi + merge_run "$pr" "$auto_merge" ;; *) usage diff --git a/scripts/pr-lib/merge.sh b/scripts/pr-lib/merge.sh index bf9c9c7fe3ad..00f043948fee 100644 --- a/scripts/pr-lib/merge.sh +++ b/scripts/pr-lib/merge.sh @@ -26,6 +26,13 @@ print_file_list_with_limit() { fi } +auto_merge_unavailable_error() { + local log_file="$1" + rg -q -i \ + 'auto[- ]merge.*(not allowed|not enabled|not available|unavailable|not configured|not supported|must be enabled)|(not allowed|not enabled|not available|unavailable|not configured|not supported).*auto[- ]merge' \ + "$log_file" +} + mainline_drift_requires_sync() { local mainline_base="$1" local prepared_head_sha="$2" @@ -186,6 +193,7 @@ merge_verify() { merge_run() { local pr="$1" + local auto_merge_requested="${2:-false}" enter_worktree "$pr" false local required @@ -257,17 +265,133 @@ merge_run() { ;; esac - if ! gh pr merge "$pr" \ - "$merge_flag" \ - --match-head-commit "$PREP_HEAD_SHA" \ - >.local/merge-output.log 2>&1 - then - print_relevant_log_excerpt .local/merge-output.log - exit 1 + if [ "$auto_merge_requested" = "true" ] && [ "$merge_method" != "squash" ]; then + echo "Auto-merge requires squash; unset OPENCLAW_PR_MERGE_METHOD or set it to squash." + exit 2 fi - local state - state=$(gh pr view "$pr" --json state --jq .state) + local merge_submitted=false + local state="" + # Auto-merge is only a post-verification landing strategy. Keep every + # artifact, exact-head, required-check, and drift check in merge_verify. + if [ "$auto_merge_requested" = "true" ]; then + local auto_meta + auto_meta=$(gh pr view "$pr" --json state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest) + local auto_head_sha + auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid) + if [ "$auto_head_sha" != "$PREP_HEAD_SHA" ]; then + echo "PR head changed before auto-merge enablement (expected $PREP_HEAD_SHA, got $auto_head_sha)." + exit 1 + fi + + local mergeable + local merge_state_status + local existing_auto_method + mergeable=$(printf '%s\n' "$auto_meta" | jq -r '.mergeable // "UNKNOWN"') + merge_state_status=$(printf '%s\n' "$auto_meta" | jq -r '.mergeStateStatus // "UNKNOWN"') + existing_auto_method=$(printf '%s\n' "$auto_meta" | jq -r '.autoMergeRequest.mergeMethod // ""') + + if [ "$mergeable" = "CONFLICTING" ]; then + echo "PR is not mergeable: GitHub reports merge conflicts." + exit 1 + fi + + if [ -n "$existing_auto_method" ]; then + echo "Auto-merge is already enabled with $existing_auto_method; re-arming it as pinned SQUASH." + if ! gh pr merge "$pr" --disable-auto >.local/merge-output.log 2>&1; then + print_relevant_log_excerpt .local/merge-output.log + exit 1 + fi + auto_meta=$(gh pr view "$pr" --json state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest) + auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid) + mergeable=$(printf '%s\n' "$auto_meta" | jq -r '.mergeable // "UNKNOWN"') + merge_state_status=$(printf '%s\n' "$auto_meta" | jq -r '.mergeStateStatus // "UNKNOWN"') + existing_auto_method=$(printf '%s\n' "$auto_meta" | jq -r '.autoMergeRequest.mergeMethod // ""') + if [ "$auto_head_sha" != "$PREP_HEAD_SHA" ]; then + echo "PR head changed while re-arming auto-merge (expected $PREP_HEAD_SHA, got $auto_head_sha)." + exit 1 + fi + if [ -n "$existing_auto_method" ]; then + echo "Auto-merge remained enabled after GitHub accepted the disable request." + exit 1 + fi + fi + + if [ "$mergeable" != "MERGEABLE" ] || [ "$merge_state_status" != "BEHIND" ]; then + echo "Auto-merge eligibility not met (mergeable=$mergeable, mergeStateStatus=$merge_state_status; expected MERGEABLE/BEHIND)." + echo "Falling back to the current immediate pinned merge behavior." + else + # GitHub's EnablePullRequestAutoMergeInput contract keeps expectedHeadOid + # as the head that must match to allow the eventual merge. + if gh pr merge "$pr" \ + --auto \ + --squash \ + --match-head-commit "$PREP_HEAD_SHA" \ + >.local/merge-output.log 2>&1 + then + auto_meta=$(gh pr view "$pr" --json state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest) + auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid) + state=$(printf '%s\n' "$auto_meta" | jq -r .state) + existing_auto_method=$(printf '%s\n' "$auto_meta" | jq -r '.autoMergeRequest.mergeMethod // ""') + if [ "$auto_head_sha" != "$PREP_HEAD_SHA" ]; then + echo "PR head changed while enabling auto-merge (expected $PREP_HEAD_SHA, got $auto_head_sha)." + exit 1 + elif [ "$state" = "MERGED" ]; then + merge_submitted=true + merge_label="squash auto-merge" + elif [ "$existing_auto_method" = "SQUASH" ]; then + echo "AUTO-MERGE ENABLED for PR #$pr at $PREP_HEAD_SHA." + echo "GitHub will land it via squash when required checks and branch up-to-dateness are satisfied." + return 0 + else + echo "GitHub accepted the auto-merge command but did not report a squash auto-merge request." + print_relevant_log_excerpt .local/merge-output.log + exit 1 + fi + else + auto_meta=$(gh pr view "$pr" --json state,headRefOid,autoMergeRequest) + auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid) + existing_auto_method=$(printf '%s\n' "$auto_meta" | jq -r '.autoMergeRequest.mergeMethod // ""') + if [ "$auto_head_sha" = "$PREP_HEAD_SHA" ] && [ -n "$existing_auto_method" ]; then + echo "Auto-merge enablement was inconclusive; clearing the observed $existing_auto_method request to fail closed." + if ! gh pr merge "$pr" --disable-auto >>.local/merge-output.log 2>&1; then + print_relevant_log_excerpt .local/merge-output.log + exit 1 + fi + auto_meta=$(gh pr view "$pr" --json state,headRefOid,autoMergeRequest) + auto_head_sha=$(printf '%s\n' "$auto_meta" | jq -r .headRefOid) + existing_auto_method=$(printf '%s\n' "$auto_meta" | jq -r '.autoMergeRequest.mergeMethod // ""') + if [ "$auto_head_sha" != "$PREP_HEAD_SHA" ] || [ -n "$existing_auto_method" ]; then + echo "Unable to prove the inconclusive auto-merge request was cleared at the verified head." + exit 1 + fi + echo "The inconclusive auto-merge request was cleared safely; re-run merge-run to retry." + exit 1 + fi + if ! auto_merge_unavailable_error .local/merge-output.log; then + print_relevant_log_excerpt .local/merge-output.log + exit 1 + fi + echo "GitHub auto-merge is unavailable for this repository or branch protection; falling back to the current immediate pinned merge behavior." + print_relevant_log_excerpt .local/merge-output.log + fi + fi + fi + + if [ "$merge_submitted" != "true" ]; then + if ! gh pr merge "$pr" \ + "$merge_flag" \ + --match-head-commit "$PREP_HEAD_SHA" \ + >.local/merge-output.log 2>&1 + then + print_relevant_log_excerpt .local/merge-output.log + exit 1 + fi + fi + + if [ -z "$state" ]; then + state=$(gh pr view "$pr" --json state --jq .state) + fi if [ "$state" != "MERGED" ]; then echo "Landing not finalized yet (state=$state), waiting up to 15 minutes..." local i diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 613dcb173ce7..c0a90dbdee80 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -1608,7 +1608,15 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ ["scripts/mobile-reauth.sh", ["test/scripts/auth-monitor.test.ts"]], ["scripts/committer", ["test/scripts/committer.test.ts"]], ["scripts/gh-read", ["test/scripts/gh-read.test.ts"]], - ["scripts/pr", ["test/scripts/pr-operation-lock.test.ts", "test/scripts/pr-wrappers.test.ts"]], + [ + "scripts/pr", + [ + "test/scripts/pr-merge.test.ts", + "test/scripts/pr-operation-lock.test.ts", + "test/scripts/pr-wrappers.test.ts", + ], + ], + ["scripts/pr-lib/merge.sh", ["test/scripts/pr-merge.test.ts"]], ["scripts/pr-lib/operation-lock.sh", ["test/scripts/pr-operation-lock.test.ts"]], ["scripts/pr-lib/process-group-runner.mjs", ["test/scripts/pr-operation-lock.test.ts"]], ["scripts/pr-merge", ["test/scripts/pr-wrappers.test.ts"]], @@ -2318,6 +2326,7 @@ const TOOLING_TEST_TARGETS = new Map([ ["test/scripts/plugin-prerelease-test-plan.test.ts"], ], ["test/scripts/pr-operation-lock.test.ts", ["test/scripts/pr-operation-lock.test.ts"]], + ["test/scripts/pr-merge.test.ts", ["test/scripts/pr-merge.test.ts"]], ["test/scripts/pr-wrappers.test.ts", ["test/scripts/pr-wrappers.test.ts"]], ["test/scripts/test-projects.test.ts", ["test/scripts/test-projects.test.ts"]], [ diff --git a/test/scripts/pr-merge.test.ts b/test/scripts/pr-merge.test.ts new file mode 100644 index 000000000000..63e98f8a9c2e --- /dev/null +++ b/test/scripts/pr-merge.test.ts @@ -0,0 +1,260 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; + +const tempDirs = useAutoCleanupTempDirTracker(afterEach); +const mergeScript = join(process.cwd(), "scripts/pr-lib/merge.sh"); +const headSha = "0123456789abcdef0123456789abcdef01234567"; +const landedSha = "fedcba9876543210fedcba9876543210fedcba98"; +const describePosix = process.platform === "win32" ? describe.skip : describe; + +type MergeScenario = { + auto?: boolean; + autoResult?: "enabled" | "inconclusive" | "unavailable"; + checks?: "fail" | "green"; + existingAutoMethod?: "" | "MERGE" | "REBASE" | "SQUASH"; + mergeStateStatus?: string; + mergeable?: string; +}; + +function runMerge(scenario: MergeScenario = {}) { + const root = tempDirs.make("openclaw-pr-merge-"); + const localDir = join(root, ".local"); + const calls = join(root, "gh-calls.log"); + const autoCalled = join(root, "auto-called"); + const autoState = join(root, "auto-state"); + mkdirSync(localDir, { recursive: true }); + writeFileSync( + join(localDir, "prep.env"), + `PREP_HEAD_SHA=${headSha}\nLOCAL_PREP_HEAD_SHA=${headSha}\n`, + ); + for (const artifact of ["review.md", "review.json", "prep.md"]) { + writeFileSync(join(localDir, artifact), "fixture\n"); + } + + const existingAutoMethod = scenario.existingAutoMethod ?? ""; + const preAutoMeta = JSON.stringify({ + state: "OPEN", + headRefOid: headSha, + mergeable: scenario.mergeable ?? "MERGEABLE", + mergeStateStatus: scenario.mergeStateStatus ?? "BEHIND", + autoMergeRequest: existingAutoMethod ? { mergeMethod: existingAutoMethod } : null, + }); + const postAutoMeta = JSON.stringify({ + state: "OPEN", + headRefOid: headSha, + mergeable: "MERGEABLE", + mergeStateStatus: "BEHIND", + autoMergeRequest: scenario.autoResult === "unavailable" ? null : { mergeMethod: "SQUASH" }, + }); + const disabledAutoMeta = JSON.stringify({ + state: "OPEN", + headRefOid: headSha, + mergeable: scenario.mergeable ?? "MERGEABLE", + mergeStateStatus: scenario.mergeStateStatus ?? "BEHIND", + autoMergeRequest: null, + }); + const checks = + scenario.checks === "fail" + ? [{ name: "CI", bucket: "fail", state: "FAILURE" }] + : [{ name: "CI", bucket: "pass", state: "SUCCESS" }]; + + const shell = ` +set -euo pipefail +source "$OPENCLAW_TEST_MERGE_SCRIPT" +enter_worktree() { :; } +require_artifact() { :; } +verify_prep_branch_matches_prepared_head() { :; } +mark_pr_operation_side_effects_started() { :; } +mainline_drift_requires_sync() { return 1; } +print_relevant_log_excerpt() { cat "$1"; } +repo_root() { printf '%s\\n' "$OPENCLAW_TEST_ROOT"; } +remove_worktree_if_present() { :; } +delete_local_branch_if_safe() { :; } +pr_meta_json() { + printf '%s\\n' '{"state":"OPEN","isDraft":false,"headRefOid":"${headSha}"}' +} +git() { + if [ "\${1-}" = "merge-base" ]; then + if [ "$OPENCLAW_TEST_MERGE_STATE_STATUS" = "BEHIND" ]; then + return 1 + fi + return 0 + fi + return 0 +} +gh() { + printf '%s\\n' "$*" >> "$OPENCLAW_TEST_GH_CALLS" + case "$1 $2" in + "pr checks") + case " $* " in + *" --json "*) printf '%s\\n' "$OPENCLAW_TEST_CHECKS_JSON" ;; + esac + ;; + "pr view") + case "$*" in + *"--json state,isDraft"*) + printf '%s\\n' '{"state":"OPEN","isDraft":false}' + ;; + *"--json state,headRefOid,mergeable,mergeStateStatus,autoMergeRequest"*) + if [ -e "$OPENCLAW_TEST_AUTO_STATE" ] && [ "$(cat "$OPENCLAW_TEST_AUTO_STATE")" = "enabled" ]; then + printf '%s\\n' "$OPENCLAW_TEST_POST_AUTO_META" + elif [ -e "$OPENCLAW_TEST_AUTO_STATE" ]; then + printf '%s\\n' "$OPENCLAW_TEST_DISABLED_AUTO_META" + else + printf '%s\\n' "$OPENCLAW_TEST_PRE_AUTO_META" + fi + ;; + *"--json state,headRefOid,autoMergeRequest"*) + if [ -e "$OPENCLAW_TEST_AUTO_STATE" ] && [ "$(cat "$OPENCLAW_TEST_AUTO_STATE")" = "disabled" ]; then + printf '%s\\n' "$OPENCLAW_TEST_DISABLED_AUTO_META" + else + printf '%s\\n' "$OPENCLAW_TEST_POST_AUTO_META" + fi + ;; + *"--json state --jq .state"*) printf 'MERGED\\n' ;; + *"--json mergeCommit"*) printf '%s\\n' "$OPENCLAW_TEST_LANDED_SHA" ;; + *"--json commits"*) printf '1\\n' ;; + *"--json headRefName,headRepository"*) + printf '%s\\n' '{"headRefName":"feature","headRepository":{"name":"openclaw"},"headRepositoryOwner":{"login":"openclaw"},"isCrossRepository":false,"maintainerCanModify":true}' + ;; + *"--json url"*) printf 'https://github.com/openclaw/openclaw/pull/123\\n' ;; + *) printf '%s\\n' '{"state":"OPEN"}' ;; + esac + ;; + "pr merge") + case " $* " in + *" --disable-auto "*) + printf 'disabled\\n' > "$OPENCLAW_TEST_AUTO_STATE" + ;; + *" --auto "*) + : > "$OPENCLAW_TEST_AUTO_CALLED" + printf 'enabled\\n' > "$OPENCLAW_TEST_AUTO_STATE" + if [ "$OPENCLAW_TEST_AUTO_RESULT" = "unavailable" ]; then + echo 'GraphQL: Pull request auto merge is not allowed for this repository' >&2 + return 1 + fi + if [ "$OPENCLAW_TEST_AUTO_RESULT" = "inconclusive" ]; then + echo 'transport closed after mutation' >&2 + return 1 + fi + ;; + esac + ;; + "repo view") printf 'openclaw/openclaw\\n' ;; + "pr comment") printf 'https://github.com/openclaw/openclaw/pull/123#issuecomment-1\\n' ;; + "api "*) : ;; + *) echo "unexpected gh invocation: $*" >&2; return 2 ;; + esac +} +merge_run 123 "$OPENCLAW_TEST_AUTO_REQUESTED" +`; + + const result = spawnSync("bash", ["-c", shell], { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + OPENCLAW_TEST_AUTO_CALLED: autoCalled, + OPENCLAW_TEST_AUTO_REQUESTED: scenario.auto ? "true" : "false", + OPENCLAW_TEST_AUTO_RESULT: scenario.autoResult ?? "enabled", + OPENCLAW_TEST_AUTO_STATE: autoState, + OPENCLAW_TEST_CHECKS_JSON: JSON.stringify(checks), + OPENCLAW_TEST_DISABLED_AUTO_META: disabledAutoMeta, + OPENCLAW_TEST_GH_CALLS: calls, + OPENCLAW_TEST_LANDED_SHA: landedSha, + OPENCLAW_TEST_MERGE_SCRIPT: mergeScript, + OPENCLAW_TEST_MERGE_STATE_STATUS: scenario.mergeStateStatus ?? "BEHIND", + OPENCLAW_TEST_POST_AUTO_META: postAutoMeta, + OPENCLAW_TEST_PRE_AUTO_META: preAutoMeta, + OPENCLAW_TEST_ROOT: root, + }, + }); + return { + ...result, + calls: existsSync(calls) ? readFileSync(calls, "utf8") : "", + }; +} + +describePosix("scripts/pr merge-run", () => { + it("does not enable auto-merge when exact-head required CI is failing", () => { + const result = runMerge({ auto: true, checks: "fail" }); + + expect(result.status).toBe(1); + expect(result.stdout).toContain("Required checks are failing."); + expect(result.calls).not.toContain("pr merge"); + }); + + it("fails a conflicting PR without attempting auto-merge", () => { + const result = runMerge({ + auto: true, + mergeable: "CONFLICTING", + mergeStateStatus: "DIRTY", + }); + + expect(result.status).toBe(1); + expect(result.stdout).toContain("GitHub reports merge conflicts"); + expect(result.calls).not.toContain("pr merge"); + }); + + it("keeps the default immediate pinned squash merge unchanged", () => { + const result = runMerge({ mergeStateStatus: "CLEAN" }); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(result.calls).toContain(`pr merge 123 --squash --match-head-commit ${headSha}`); + expect(result.calls).not.toContain("--auto"); + expect(result.stdout).toContain("merge-run complete for PR #123"); + }); + + it("enables squash auto-merge only for a verified mergeable BEHIND head", () => { + const result = runMerge({ auto: true }); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(result.calls).toContain(`pr merge 123 --auto --squash --match-head-commit ${headSha}`); + expect(result.calls.match(/^pr merge /gmu)).toHaveLength(1); + expect(result.stdout).toContain("AUTO-MERGE ENABLED"); + expect(result.stdout).toContain("required checks and branch up-to-dateness"); + }); + + it("falls back to the immediate merge when BEHIND is not the only obstacle", () => { + const result = runMerge({ auto: true, mergeStateStatus: "BLOCKED" }); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(result.calls).not.toContain("--auto"); + expect(result.calls).toContain(`pr merge 123 --squash --match-head-commit ${headSha}`); + expect(result.stdout).toContain("expected MERGEABLE/BEHIND"); + expect(result.stdout).toContain("Falling back"); + }); + + it("re-arms an existing auto-merge request with the verified head", () => { + const result = runMerge({ auto: true, existingAutoMethod: "MERGE" }); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(result.calls).toContain("pr merge 123 --disable-auto"); + expect(result.calls).toContain(`pr merge 123 --auto --squash --match-head-commit ${headSha}`); + expect(result.stdout).toContain("re-arming it as pinned SQUASH"); + expect(result.stdout).toContain("AUTO-MERGE ENABLED"); + }); + + it("clears an inconclusive auto-merge request instead of trusting its method", () => { + const result = runMerge({ auto: true, autoResult: "inconclusive" }); + + expect(result.status).toBe(1); + expect(result.calls).toContain(`pr merge 123 --auto --squash --match-head-commit ${headSha}`); + expect(result.calls).toContain("pr merge 123 --disable-auto"); + expect(result.stdout).toContain("clearing the observed SQUASH request"); + expect(result.stdout).toContain("cleared safely"); + }); + + it("reports unavailable auto-merge and falls back to the immediate pinned merge", () => { + const result = runMerge({ auto: true, autoResult: "unavailable" }); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(result.calls).toContain(`pr merge 123 --auto --squash --match-head-commit ${headSha}`); + expect(result.calls).toContain(`pr merge 123 --squash --match-head-commit ${headSha}`); + expect(result.stdout).toContain("auto-merge is unavailable"); + expect(result.stdout).toContain("falling back"); + }); +}); diff --git a/test/scripts/pr-wrappers.test.ts b/test/scripts/pr-wrappers.test.ts index f9e58d0a88be..9585756da880 100644 --- a/test/scripts/pr-wrappers.test.ts +++ b/test/scripts/pr-wrappers.test.ts @@ -155,11 +155,12 @@ describe("scripts/pr wrappers", () => { expect(script).toContain("scripts/pr review-init "); expect(script).toContain("scripts/pr prepare-run "); expect(script).toContain("scripts/pr ci-dispatch "); - expect(script).toContain("scripts/pr merge-run "); + expect(script).toContain("scripts/pr merge-run [--auto-merge]"); + expect(script).toContain("OPENCLAW_PR_AUTO_MERGE=1 is equivalent"); expect(script).toContain('review_init "$pr"'); expect(script).toContain('prepare_run "$pr"'); expect(script).toContain('ci_dispatch "$pr"'); - expect(script).toContain('merge_run "$pr"'); + expect(script).toContain('merge_run "$pr" "$auto_merge"'); expect(script).toContain('require_main_target_pr "${1-}"'); expect(script).toContain("only support PRs targeting main"); }); @@ -261,6 +262,8 @@ describe("scripts/pr wrappers", () => { expect(script).toContain("--merge"); expect(script).toContain("--rebase"); expect(script).toContain('echo "Merged via $merge_label."'); + expect(script).toContain("--auto"); + expect(script).toContain('--match-head-commit "$PREP_HEAD_SHA"'); }); it("keeps prepare wrapper modes delegated to the main PR helper", () => { diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index af8c429ce207..6ed2cfa70b5a 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -1727,8 +1727,13 @@ describe("scripts/test-projects changed-target routing", () => { ["scripts/gh-read", ["test/scripts/gh-read.test.ts"]], [ "scripts/pr", - ["test/scripts/pr-operation-lock.test.ts", "test/scripts/pr-wrappers.test.ts"], + [ + "test/scripts/pr-merge.test.ts", + "test/scripts/pr-operation-lock.test.ts", + "test/scripts/pr-wrappers.test.ts", + ], ], + ["scripts/pr-lib/merge.sh", ["test/scripts/pr-merge.test.ts"]], ["scripts/pr-lib/operation-lock.sh", ["test/scripts/pr-operation-lock.test.ts"]], ["scripts/pr-lib/process-group-runner.mjs", ["test/scripts/pr-operation-lock.test.ts"]], ["scripts/pr-merge", ["test/scripts/pr-wrappers.test.ts"]],