Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
66de51c
feat: implement issue #1881 — auto-rebase still merges main into ever…
don-petry Sep 22, 2026
f904beb
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 22, 2026
31b9b02
fix(reviews): address review comments [skip ci-relay]
don-petry Sep 22, 2026
a5848ab
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 22, 2026
41fa32f
fix(bot): address bot feedback [skip ci-relay]
don-petry Sep 22, 2026
1c27f20
fix(reviews): address review comments [skip ci-relay]
don-petry Sep 22, 2026
c1660b3
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 23, 2026
3be30a5
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 23, 2026
c8b8135
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 23, 2026
08e9acc
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 23, 2026
056058c
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 24, 2026
273a62e
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 24, 2026
3454115
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 24, 2026
7436806
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 24, 2026
e608c40
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 25, 2026
2a9fa44
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 25, 2026
5f1e49d
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 25, 2026
9ea5e73
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 26, 2026
e968118
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 26, 2026
863812b
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 27, 2026
b52039d
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 27, 2026
125e2c0
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 29, 2026
6f83c32
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 29, 2026
84b4e08
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 30, 2026
234dc64
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Sep 30, 2026
57bccf7
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 1, 2026
48875d3
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 1, 2026
e5f83e6
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 1, 2026
cce6287
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 1, 2026
a700e18
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 1, 2026
939a821
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 1, 2026
90c53e9
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 1, 2026
4beda91
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 1, 2026
e882c29
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 2, 2026
ae805e0
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 2, 2026
30f6b21
Merge branch 'main' into dev-lead/issue-1881-20260922-1659
don-petry Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 122 additions & 7 deletions scripts/auto_rebase_health.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@
# audit. Repo-scoped — fleet-wide would need a cross-repo PAT this report
# does not carry.
#
# 4. Base-merge necessity (#1881) — a deterministic counter that splits the open
# BEHIND queue into base merges that are genuinely REQUIRED (being behind
# blocks the PR: the ruleset requires up-to-date branches, or the PR is queued
# to merge, or it carries the explicit request label) vs SKIPPABLE no-ops. The
# required/skippable split is derived from repository configuration (the
# ruleset's `strict_required_status_checks_policy`), so if strict is re-armed
# the counter follows with no code change. `skippable` is the no-op base-merge
# volume #1881 removes at the source — the second input to #1864 AC #4 alongside
# #1865's `reviews_carried_forward`. The gate that acts on this lives in the
# central auto-rebase reusable; this report only measures it.
#
# Layout (mirrors scripts/token_report.sh):
# * The count_*/summarize_*/fmt_*/render_report functions are PURE — they take
# JSON / scalars and write to stdout. Unit-tested in tests/auto_rebase_health.bats.
Expand Down Expand Up @@ -56,6 +67,16 @@ SENTINEL_MARKER='<!-- auto-rebase-conflict:'
REBASE_RESPONSE_MARKER='intent=rebase status='
REBASE_APPLIED_MARKER='intent=rebase status=applied'

# Label a human/PR can add to explicitly demand an up-to-date branch even when the
# ruleset does not require it — the "or a human/label requests it" arm of #1881 AC #1.
# Same label the central reusable's review-ready eligibility predicate already honours.
AUTO_REBASE_REQUEST_LABEL="${AUTO_REBASE_REQUEST_LABEL:-auto-rebase:ready}"
# Branch whose ruleset drives the up-to-date policy. Left empty by default so
# main() resolves the repo's ACTUAL default branch — an AGENT_REPO whose default
# branch is not `main` would otherwise have its rules read from the wrong branch.
# Set this env to pin a specific branch and skip the lookup.
DEFAULT_BRANCH="${DEFAULT_BRANCH:-}"

# ---------------------------------------------------------------------------
# Pure helpers (unit-tested; no network)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -112,6 +133,54 @@ summarize_merge_states() {
] | @tsv' <<< "$json"
}

# strict_from_branch_rules <rules_json>
# Reads whether the default branch's ruleset requires PRs to be up to date with
# base — the `strict_required_status_checks_policy` flag (#1864). Input is the JSON
# array from `gh api repos/{repo}/rules/branches/<branch>`. Echoes "true" iff some
# active required_status_checks rule sets that policy true, else "false". This is
# the config-derived condition of #1881 AC #2: if strict is re-armed later the flag
# flips with no code change. Absent/empty/unparseable input → "false".
strict_from_branch_rules() {
local json="${1:-}"
[ -n "$json" ] || json='[]'
local r
r="$(printf '%s' "$json" | jq -r '
[ .[]?
| select(.type == "required_status_checks")
| (.parameters.strict_required_status_checks_policy // false)
] | any' 2>/dev/null || echo false)"
echo "$r"
}

# summarize_base_merges <prs_json> <strict_enabled> [request_label]
# Emits TSV: required<TAB>skippable — over the open non-draft, non-Dependabot,
# mergeStateStatus==BEHIND PRs, the count whose base merge is genuinely REQUIRED vs
# a no-op that should be SKIPPED (#1881 AC #4). A BEHIND PR's base merge is required
# when being behind actually blocks it — ANY of:
# * strict_enabled == "true" (ruleset requires up-to-date branches), OR
# * the PR is queued to merge (autoMergeRequest non-null), OR
# * the PR carries the explicit request label (default AUTO_REBASE_REQUEST_LABEL).
# Otherwise the base merge is skippable churn. DIRTY PRs are deliberately NOT counted:
# they take the conflict/rebase path unchanged (AC #3). Absent/empty JSON → "0\t0".
summarize_base_merges() {
local json="${1:-}" strict="${2:-false}" label="${3:-$AUTO_REBASE_REQUEST_LABEL}"
[ -n "$json" ] || json='[]'
jq -r --arg strict "$strict" --arg label "$label" '
[ .[]
| select((.isDraft // false) | not)
| select(((.author?.login // "" | tostring) | test("dependabot"; "i")) | not)
| select(.mergeStateStatus == "BEHIND")
] | map(
($strict == "true")
or (.autoMergeRequest != null)
Comment thread
don-petry marked this conversation as resolved.
Resolved
or ([ (.labels // [])[] | (.name // "") ] | index($label) != null)
) as $flags |
[
([ $flags[] | select(.) ] | length),
([ $flags[] | select(. | not) ] | length)
] | @tsv' <<< "$json" 2>/dev/null || printf '0\t0'
}

# summarize_runs <runs_json>
# Emits TSV: total<TAB>success<TAB>failed over the auto-rebase run telemetry.
summarize_runs() {
Expand Down Expand Up @@ -144,13 +213,16 @@ fmt_rate() {
echo "$(( num * 100 / denom ))%"
}

# render_report <comments_json> <runs_json> <lookback_days> <behind_prs> [today] [prs_json] [pr_list_truncated] [pr_list_limit]
# render_report <comments_json> <runs_json> <lookback_days> <behind_prs> [today] [prs_json] [pr_list_truncated] [pr_list_limit] [strict_enabled] [request_label]
# Writes the full Markdown report to stdout. Pure: no network.
# pr_list_truncated=true renders a warning that BEHIND/DIRTY counts may undercount.
# strict_enabled ("true"/"false") is the config-derived up-to-date-branches policy;
# it drives the base-merge-necessity section (#1881).
render_report() {
local comments_json="${1:-[]}" runs_json="${2:-[]}"
local lookback="${3:-7}" behind="${4:-0}" today="${5:-}" prs_json="${6:-[]}"
local pr_list_truncated="${7:-false}" pr_list_limit="${8:-1000}"
local strict_enabled="${9:-false}" request_label="${10:-$AUTO_REBASE_REQUEST_LABEL}"
[ -n "$today" ] || today="$(date -u +%Y-%m-%d)"

local sentinels responses applied
Expand All @@ -162,6 +234,9 @@ render_report() {
local ms_behind ms_dirty
IFS=$'\t' read -r ms_behind ms_dirty < <(summarize_merge_states "$prs_json")

local bm_required bm_skippable
IFS=$'\t' read -r bm_required bm_skippable < <(summarize_base_merges "$prs_json" "$strict_enabled" "$request_label")

local fanout
fanout="$(estimate_fanout "$total" "$behind")"

Expand Down Expand Up @@ -199,7 +274,19 @@ render_report() {
fi
printf '> Snapshot of the current open-PR queue, scoped to **`%s`** — fleet-wide counts would ' "$WORKFLOW_REPO"
printf 'require a cross-repo PAT this report does not carry. Tracks the effectiveness of the '
printf 'AC1/AC2 auto-rebase fixes (petry-projects/.github#926) without a manual audit.\n'
printf 'AC1/AC2 auto-rebase fixes (petry-projects/.github#926) without a manual audit.\n\n'

printf '## Base-merge necessity (#1881)\n\n'
printf -- '- **Up-to-date-branches policy** (ruleset `strict_required_status_checks_policy`): `%s`\n' "$strict_enabled"
printf -- '- **Base merges required** (behind actually blocks: strict on, queued to merge, or `%s` label): %s\n' \
"$request_label" "$bm_required"
printf -- '- **Base merges skippable** (BEHIND with no such requirement — a no-op churn): %s\n' "$bm_skippable"
printf -- '- `base_merges_required=%s base_merges_skippable=%s`\n\n' "$bm_required" "$bm_skippable"
printf '> Deterministic counter over open non-draft non-Dependabot **BEHIND** PRs (DIRTY PRs keep the '
printf 'conflict/rebase path, so they are excluded). `skippable` is the no-op base-merge volume #1881 '
printf 'removes at the source; it is the second input to #1864 AC #4 alongside #1865 `reviews_carried_forward`. '
printf 'The policy flag is read from repository configuration — if `strict` is re-armed, every BEHIND PR '
printf 'becomes `required` with no code change.\n'
}

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -247,12 +334,13 @@ main() {
--paginate --jq '.workflow_runs | map({conclusion, created_at})' 2>/dev/null \
| jq -s 'add // []' 2>/dev/null || echo '[]')"

# 3. Open-PR snapshot — pulled once with the fields both the behind-PR
# multiplier and the AC7 merge-state (BEHIND/DIRTY) metric need. Best-effort;
# defaults to [] so the report still renders when the query fails.
# 3. Open-PR snapshot — pulled once with the fields the behind-PR multiplier, the
# AC7 merge-state (BEHIND/DIRTY) metric, and the #1881 base-merge-necessity
# counter (labels + autoMergeRequest) all need. Best-effort; defaults to [] so
# the report still renders when the query fails.
local prs_json pr_list_truncated pr_count
prs_json="$(gh pr list --repo "$WORKFLOW_REPO" --state open --limit "$PR_LIST_LIMIT" \
--json author,isDraft,mergeStateStatus 2>/dev/null || echo '[]')"
--json author,isDraft,mergeStateStatus,labels,autoMergeRequest 2>/dev/null || echo '[]')"
Comment thread
don-petry marked this conversation as resolved.
Resolved
[ -n "$prs_json" ] || prs_json='[]'
pr_count="$(printf '%s' "$prs_json" | jq 'length' 2>/dev/null || echo 0)"
if [ "$pr_count" -ge "$PR_LIST_LIMIT" ]; then
Expand All @@ -268,8 +356,35 @@ main() {
| jq '[.[] | select((.author?.login // "") | test("dependabot"; "i") | not)] | length' \
2>/dev/null || echo 0)"

# 4. Up-to-date-branches policy — read from the default branch's ruleset so the
# base-merge-necessity counter (#1881) is derived from config, not hardcoded.
# Best-effort: if the ruleset cannot be read the report degrades to strict=false
# with a warning (so a silent read failure cannot masquerade as "no requirement"),
# matching the token-degradation posture above.
#
# Resolve the branch first: when DEFAULT_BRANCH is not pinned via env, query the
# repo's ACTUAL default branch so an AGENT_REPO whose default is not `main` reads
# its rules from the right branch (#1887). Fall back to `main` if the lookup fails.
local default_branch="${DEFAULT_BRANCH:-}"
if [ -z "$default_branch" ]; then
default_branch="$(gh api "repos/${WORKFLOW_REPO}" --jq '.default_branch' 2>/dev/null || echo main)"
[ -n "$default_branch" ] || default_branch=main
fi

# Capture the command-substitution exit status into a variable first (rather than
# running it directly inside `if`) so a failure is not swallowed under `set -e`.
local rules_json strict_enabled api_status=0
rules_json="$(gh api "repos/${WORKFLOW_REPO}/rules/branches/${default_branch}" 2>/dev/null)" || api_status=$?
if [ "$api_status" -eq 0 ]; then
[ -n "$rules_json" ] || rules_json='[]'
else
rules_json='[]'
echo "::warning::Could not read branch rules for ${WORKFLOW_REPO}@${default_branch} — base-merge necessity assumes strict=false; skippable counts may be overstated." >&2
fi
strict_enabled="$(strict_from_branch_rules "$rules_json")"

local report
report="$(render_report "$comments_json" "$runs_json" "$LOOKBACK_DAYS" "$behind" "$today" "$prs_json" "$pr_list_truncated" "$PR_LIST_LIMIT")"
report="$(render_report "$comments_json" "$runs_json" "$LOOKBACK_DAYS" "$behind" "$today" "$prs_json" "$pr_list_truncated" "$PR_LIST_LIMIT" "$strict_enabled" "$AUTO_REBASE_REQUEST_LABEL")"

printf '%s\n' "$report"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
Expand Down
150 changes: 150 additions & 0 deletions tests/auto_rebase_health.bats
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,36 @@ setup() {
{"number":5,"mergeStateStatus":"BEHIND","isDraft":true,"author":{"login":"dave"}},
{"number":6,"mergeStateStatus":"DIRTY","isDraft":false,"author":{"login":"dependabot[bot]"}}
]'

# Branch-rules JSON as returned by `gh api repos/{repo}/rules/branches/main`.
# Contains a required_status_checks rule with strict=false (matches the current
# post-#1864 state: the ruleset no longer demands up-to-date branches).
RULES_STRICT_OFF_JSON='[
{"type":"pull_request","parameters":{}},
{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":false,"required_status_checks":[{"context":"lint"}]}}
]'
# Same shape but strict re-armed (the #1864 one-field revert).
RULES_STRICT_ON_JSON='[
{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"lint"}]}}
]'

# Base-merge-necessity PR set. Open non-draft non-Dependabot BEHIND PRs:
# #1 plain BEHIND (no queue, no label) → skippable when strict off
# #2 BEHIND + queued to merge (autoMergeRequest set) → required always
# #3 BEHIND + carries the explicit request label → required always
# #4 DIRTY (conflict path — never a base-merge candidate, AC #3)
# #5 BEHIND but DRAFT → excluded
# #6 BEHIND but Dependabot-authored → excluded
# #7 CLEAN → excluded (not BEHIND)
BASE_MERGE_PRS_JSON='[
{"number":1,"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"alice"},"labels":[],"autoMergeRequest":null},
{"number":2,"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"bob"},"labels":[],"autoMergeRequest":{"enabledAt":"2026-09-20T00:00:00Z"}},
{"number":3,"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"carol"},"labels":[{"name":"auto-rebase:ready"}],"autoMergeRequest":null},
{"number":4,"mergeStateStatus":"DIRTY","isDraft":false,"author":{"login":"dan"},"labels":[],"autoMergeRequest":null},
{"number":5,"mergeStateStatus":"BEHIND","isDraft":true,"author":{"login":"eve"},"labels":[],"autoMergeRequest":null},
{"number":6,"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"dependabot[bot]"},"labels":[],"autoMergeRequest":null},
{"number":7,"mergeStateStatus":"CLEAN","isDraft":false,"author":{"login":"frank"},"labels":[],"autoMergeRequest":null}
]'
}

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -135,6 +165,110 @@ setup() {
[ "$output" = "$(printf '1\t1')" ]
}

# ---------------------------------------------------------------------------
# strict_from_branch_rules — reads strict flag from repo config (AC #2)
# ---------------------------------------------------------------------------

@test "strict_from_branch_rules: strict policy on yields true" {
run strict_from_branch_rules "$RULES_STRICT_ON_JSON"
[ "$status" -eq 0 ]
[ "$output" = "true" ]
}

@test "strict_from_branch_rules: strict policy off yields false" {
run strict_from_branch_rules "$RULES_STRICT_OFF_JSON"
[ "$status" -eq 0 ]
[ "$output" = "false" ]
}

@test "strict_from_branch_rules: no required_status_checks rule yields false" {
run strict_from_branch_rules '[{"type":"pull_request","parameters":{}}]'
[ "$output" = "false" ]
}

@test "strict_from_branch_rules: empty/absent JSON yields false (no error)" {
run strict_from_branch_rules ""
[ "$status" -eq 0 ]
[ "$output" = "false" ]
}

@test "strict_from_branch_rules: unparseable input yields false (no error)" {
run strict_from_branch_rules "not-json"
[ "$status" -eq 0 ]
[ "$output" = "false" ]
}

# ---------------------------------------------------------------------------
# summarize_base_merges — required vs skippable base merges (#1881 AC #4)
# ---------------------------------------------------------------------------

@test "summarize_base_merges: strict off — only queued/labelled PRs are required" {
run summarize_base_merges "$BASE_MERGE_PRS_JSON" false
[ "$status" -eq 0 ]
# #2 queued + #3 labelled = 2 required; #1 plain BEHIND = 1 skippable.
# DIRTY(#4), draft(#5), dependabot(#6), CLEAN(#7) all excluded.
[ "$output" = "$(printf '2\t1')" ]
}

@test "summarize_base_merges: strict on — every eligible BEHIND PR is required" {
run summarize_base_merges "$BASE_MERGE_PRS_JSON" true
[ "$status" -eq 0 ]
# #1,#2,#3 all required; 0 skippable.
[ "$output" = "$(printf '3\t0')" ]
}

@test "summarize_base_merges: strict off, plain BEHIND PRs are all skippable" {
run summarize_base_merges '[
{"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"a"},"labels":[],"autoMergeRequest":null},
{"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"b"},"labels":[],"autoMergeRequest":null}
]' false
[ "$output" = "$(printf '0\t2')" ]
}

@test "summarize_base_merges: queued-to-merge PR is required even when strict off" {
run summarize_base_merges '[
{"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"a"},"labels":[],"autoMergeRequest":{"enabledAt":"x"}}
]' false
[ "$output" = "$(printf '1\t0')" ]
}

@test "summarize_base_merges: explicit request label makes a PR required when strict off" {
run summarize_base_merges '[
{"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"a"},"labels":[{"name":"auto-rebase:ready"}],"autoMergeRequest":null}
]' false
[ "$output" = "$(printf '1\t0')" ]
}

@test "summarize_base_merges: DIRTY PRs are not counted (conflict path unchanged, AC #3)" {
run summarize_base_merges '[
{"mergeStateStatus":"DIRTY","isDraft":false,"author":{"login":"a"},"labels":[],"autoMergeRequest":null}
]' false
[ "$output" = "$(printf '0\t0')" ]
}

@test "summarize_base_merges: excludes draft and Dependabot BEHIND PRs" {
run summarize_base_merges '[
{"mergeStateStatus":"BEHIND","isDraft":true,"author":{"login":"a"},"labels":[],"autoMergeRequest":null},
{"mergeStateStatus":"BEHIND","isDraft":false,"author":{"login":"dependabot[bot]"},"labels":[],"autoMergeRequest":null}
]' false
[ "$output" = "$(printf '0\t0')" ]
}

@test "summarize_base_merges: empty/absent JSON returns zeros (no error)" {
run summarize_base_merges "" false
[ "$status" -eq 0 ]
[ "$output" = "$(printf '0\t0')" ]
}

@test "summarize_base_merges: handles null/missing labels and author safely" {
run summarize_base_merges '[
{"mergeStateStatus":"BEHIND","isDraft":false,"author":null},
{"mergeStateStatus":"BEHIND","isDraft":false}
]' false
[ "$status" -eq 0 ]
[ "$output" = "$(printf '0\t2')" ]
}

# ---------------------------------------------------------------------------
# estimate_fanout — run_count × behind_prs
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -220,3 +354,19 @@ setup() {
[ "$status" -eq 0 ]
[[ "$output" != *"PR list capped"* ]]
}

@test "render_report: surfaces base-merge necessity section and machine counters" {
run render_report "$COMMENTS_JSON" "$RUNS_JSON" 7 3 2026-06-15 "$BASE_MERGE_PRS_JSON" false 1000 false
[ "$status" -eq 0 ]
[[ "$output" == *"Base-merge necessity"* ]]
# strict off → #2 queued + #3 labelled required, #1 skippable
[[ "$output" == *"base_merges_required=2"* ]]
[[ "$output" == *"base_merges_skippable=1"* ]]
}

@test "render_report: base-merge section reflects strict re-armed (all required)" {
run render_report "$COMMENTS_JSON" "$RUNS_JSON" 7 3 2026-06-15 "$BASE_MERGE_PRS_JSON" false 1000 true
[ "$status" -eq 0 ]
[[ "$output" == *"base_merges_required=3"* ]]
[[ "$output" == *"base_merges_skippable=0"* ]]
}
Loading