pr-merge --base-line: gated intra-line exception (B5) (#1471)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Co-authored-by: marcie <[email protected]>
This commit was merged in pull request #1471.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# pr-merge.sh - Merge pull requests on Gitea or GitHub
|
# pr-merge.sh - Merge pull requests on Gitea or GitHub
|
||||||
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--expect-head SHA] [--no-ci-expected] [--co-author-trailers --escalate-to PRINCIPAL]
|
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--expect-head SHA] [--no-ci-expected] [--base-line BRANCH] [--co-author-trailers --escalate-to PRINCIPAL]
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -30,6 +30,12 @@ Options:
|
|||||||
-d, --delete-branch Delete the head branch after merge
|
-d, --delete-branch Delete the head branch after merge
|
||||||
--dry-run Run metadata/login preflight without merging
|
--dry-run Run metadata/login preflight without merging
|
||||||
--expect-head SHA Refuse unless the PR head matches this full commit SHA
|
--expect-head SHA Refuse unless the PR head matches this full commit SHA
|
||||||
|
--base-line BRANCH Documented intra-line exception (B5, ruled
|
||||||
|
2026-08-29): authorize a merge whose base is
|
||||||
|
neither main nor next (stacked PR lines). The
|
||||||
|
value must MATCH the PR base; all other gates
|
||||||
|
(queue guard, head pin, CI) still run and the
|
||||||
|
exception is recorded in the merge audit.
|
||||||
--no-ci-expected Assert the target repository has no CI: forward --no-ci-expected to the queue guard (requires repository admin)
|
--no-ci-expected Assert the target repository has no CI: forward --no-ci-expected to the queue guard (requires repository admin)
|
||||||
--co-author-trailers Build verified trailers from linked PR commit authors
|
--co-author-trailers Build verified trailers from linked PR commit authors
|
||||||
--escalate-to NAME Named principal for an unresolved-author BLOCK
|
--escalate-to NAME Named principal for an unresolved-author BLOCK
|
||||||
@@ -46,6 +52,7 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
|
BASE_LINE_OVERRIDE=""
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
-n|--number)
|
-n|--number)
|
||||||
@@ -64,6 +71,11 @@ while [[ $# -gt 0 ]]; do
|
|||||||
DRY_RUN=true
|
DRY_RUN=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--base-line)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Error: --base-line requires a branch name." >&2; exit 1; }
|
||||||
|
BASE_LINE_OVERRIDE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--expect-head)
|
--expect-head)
|
||||||
if [[ $# -lt 2 ]]; then
|
if [[ $# -lt 2 ]]; then
|
||||||
echo "Error: --expect-head requires one full commit SHA." >&2
|
echo "Error: --expect-head requires one full commit SHA." >&2
|
||||||
@@ -172,8 +184,17 @@ if [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" == 2 ]]; then
|
|||||||
else
|
else
|
||||||
repo_decl_warn_absent_irreversible "pr-merge"
|
repo_decl_warn_absent_irreversible "pr-merge"
|
||||||
if [[ "$BASE_BRANCH" != "main" && "$BASE_BRANCH" != "next" ]]; then
|
if [[ "$BASE_BRANCH" != "main" && "$BASE_BRANCH" != "next" ]]; then
|
||||||
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' or 'next' (found '$BASE_BRANCH')." >&2
|
if [[ -n "$BASE_LINE_OVERRIDE" && "$BASE_LINE_OVERRIDE" != "$BASE_BRANCH" ]]; then
|
||||||
exit 1
|
echo "Error: --base-line '$BASE_LINE_OVERRIDE' does not match the PR base '$BASE_BRANCH' (refusing; the exception must name the real base)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$BASE_LINE_OVERRIDE" == "$BASE_BRANCH" ]]; then
|
||||||
|
echo "audit: base-line exception — merge into '$BASE_BRANCH' authorized by explicit --base-line (B5 ruling 2026-08-29); queue guard, head pin, and CI gates unchanged." >&2
|
||||||
|
else
|
||||||
|
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' or 'next' (found '$BASE_BRANCH')." >&2
|
||||||
|
echo " A ruled intra-line merge may pass --base-line '$BASE_BRANCH' (same gates; the exception is recorded)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ -z "$HEAD_BRANCH" || -z "$HEAD_REPO" || ! "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
|
if [[ -z "$HEAD_BRANCH" || -z "$HEAD_REPO" || ! "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# B5 (ruled 2026-08-29, orch-01-adopted): pr-merge --base-line — the
|
||||||
|
# documented intra-line exception. Arms:
|
||||||
|
# 1. Base neither main nor next, no flag: policy refusal (rc 1), queue
|
||||||
|
# guard NOT invoked, hint names the exception.
|
||||||
|
# 2. Base neither main nor next, matching --base-line: authorized; the
|
||||||
|
# queue guard IS invoked with the same args (rc from the stub proves
|
||||||
|
# gates still run) and the audit line is emitted.
|
||||||
|
# 3. Mismatched --base-line (different branch than the PR base): refusal
|
||||||
|
# rc 1 with the mismatch named; queue guard NOT invoked.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-baseline}"
|
||||||
|
FIXTURE_DIR="$WORK_DIR/tools/git"
|
||||||
|
CALL_LOG="$WORK_DIR/queue-call.log"
|
||||||
|
OUT_LOG="$WORK_DIR/out.log"
|
||||||
|
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
mkdir -p "$FIXTURE_DIR"
|
||||||
|
cp "$SCRIPT_DIR/pr-merge.sh" "$FIXTURE_DIR/pr-merge.sh"
|
||||||
|
cp "$SCRIPT_DIR/detect-platform.sh" "$FIXTURE_DIR/detect-platform.sh"
|
||||||
|
|
||||||
|
cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' '{"baseRefName":"mosaic-cli-p0","baseRepository":"mosaicstack/stack","headRefName":"mosaic-cli-p2-socket-res","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"mosaicstack/stack"}'
|
||||||
|
SH
|
||||||
|
|
||||||
|
cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" > "${MOSAIC_QUEUE_CALL_LOG:?}"
|
||||||
|
exit 42
|
||||||
|
SH
|
||||||
|
chmod +x "$FIXTURE_DIR"/*.sh
|
||||||
|
|
||||||
|
run_case() { # run_case <extra-args...>
|
||||||
|
: > "$CALL_LOG"
|
||||||
|
set +e
|
||||||
|
(
|
||||||
|
cd "$WORK_DIR"
|
||||||
|
export MOSAIC_QUEUE_CALL_LOG="$CALL_LOG"
|
||||||
|
"$FIXTURE_DIR/pr-merge.sh" -n 123 "$@"
|
||||||
|
) >"$OUT_LOG" 2>&1
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. No flag: policy refusal, no gate invocation.
|
||||||
|
run_case
|
||||||
|
[[ "$rc" -eq 1 ]] || { echo "FAIL arm1: rc=$rc want 1" >&2; cat "$OUT_LOG" >&2; exit 1; }
|
||||||
|
[[ ! -s "$CALL_LOG" ]] || { echo "FAIL arm1: queue guard ran without authorization" >&2; exit 1; }
|
||||||
|
grep -q "only for PRs targeting" "$OUT_LOG" || { echo "FAIL arm1: policy message missing" >&2; exit 1; }
|
||||||
|
grep -q -- "--base-line 'mosaic-cli-p0'" "$OUT_LOG" || { echo "FAIL arm1: hint missing" >&2; exit 1; }
|
||||||
|
|
||||||
|
# 2. Matching flag: authorized, audit line emitted, gates RUN (stub rc 42).
|
||||||
|
run_case --base-line mosaic-cli-p0
|
||||||
|
[[ "$rc" -eq 42 ]] || { echo "FAIL arm2: rc=$rc want 42 (gate stub rc must propagate)" >&2; cat "$OUT_LOG" >&2; exit 1; }
|
||||||
|
[[ -s "$CALL_LOG" ]] || { echo "FAIL arm2: queue guard NOT invoked despite authorization" >&2; exit 1; }
|
||||||
|
grep -q -- '-B mosaic-cli-p2-socket-res' "$CALL_LOG" || { echo "FAIL arm2: guard args wrong" >&2; cat "$CALL_LOG" >&2; exit 1; }
|
||||||
|
grep -q "base-line exception" "$OUT_LOG" || { echo "FAIL arm2: audit line missing" >&2; exit 1; }
|
||||||
|
|
||||||
|
# 3. Mismatched flag: refusal, mismatch named, no gate invocation.
|
||||||
|
run_case --base-line some-other-line
|
||||||
|
[[ "$rc" -eq 1 ]] || { echo "FAIL arm3: rc=$rc want 1" >&2; cat "$OUT_LOG" >&2; exit 1; }
|
||||||
|
[[ ! -s "$CALL_LOG" ]] || { echo "FAIL arm3: queue guard ran on a refused merge" >&2; exit 1; }
|
||||||
|
grep -q "does not match the PR base" "$OUT_LOG" || { echo "FAIL arm3: mismatch message missing" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "pr-merge --base-line exception regression passed (B5)"
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user