mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(pr): support pinned auto-merge landings (#114702)
* feat(pr): add pinned auto-merge mode * test(ci): remove duplicate codex prewarm route
This commit is contained in:
committed by
GitHub
parent
a1a507ddaf
commit
e879ce7e0e
+16
-3
@@ -177,8 +177,10 @@ Usage:
|
||||
scripts/pr prepare-run <PR>
|
||||
scripts/pr ci-dispatch <PR>
|
||||
scripts/pr merge-verify <PR>
|
||||
scripts/pr merge-run <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.
|
||||
@@ -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
|
||||
|
||||
+133
-9
@@ -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
|
||||
|
||||
@@ -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"]],
|
||||
[
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -155,11 +155,12 @@ describe("scripts/pr wrappers", () => {
|
||||
expect(script).toContain("scripts/pr review-init <PR>");
|
||||
expect(script).toContain("scripts/pr prepare-run <PR>");
|
||||
expect(script).toContain("scripts/pr ci-dispatch <PR>");
|
||||
expect(script).toContain("scripts/pr merge-run <PR>");
|
||||
expect(script).toContain("scripts/pr merge-run <PR> [--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", () => {
|
||||
|
||||
@@ -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"]],
|
||||
|
||||
Reference in New Issue
Block a user