framework tools/git: issue-reopen + pr-close R1/R4 conversions (P1)
Same contract as issue-close (7a38eed1): -b/--body canonical comment
flag with -c/--comment alias; usage errors stderr + exit 2 (unknown
option, missing required, value-less flags); GitHub-path provider
failures normalized to exit 1.
Both new suites enrolled in framework-shell CI (enumeration guard OK,
population 76). pr-close's suite uses FAILING provider stubs (exit 99):
its API fallback treats a successful curl as a closed PR, so exit-0
stubs let sandbox arms succeed (measured); post-sandbox contact
assertions are correspondingly scoped, parser arms remain zero-contact.
Mirrored to the brain tree; all usage-contract suites green in both.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
# issue-reopen.sh - Reopen a closed issue on GitHub or Gitea
|
||||
# Usage: issue-reopen.sh -i <issue_number> [-c <comment>]
|
||||
# Usage: issue-reopen.sh -i <issue_number> [-b <comment>]
|
||||
# (-c/--comment is a backward-compatible alias for -b/--body; R1/R4 2026-08-28)
|
||||
|
||||
set -e
|
||||
|
||||
@@ -11,35 +12,49 @@ source "$SCRIPT_DIR/detect-platform.sh"
|
||||
ISSUE_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2,
|
||||
# distinct from provider, credential, and verification failures (exit 1), so a
|
||||
# caller or stop gate can tell an invocation defect from a delivery blocker.
|
||||
usage_error() {
|
||||
echo "Error: $*" >&2
|
||||
echo "Usage: issue-reopen.sh -i <issue_number> [-b <comment>] (see --help)" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
[[ $# -ge 2 ]] || usage_error "option $1 requires a value"
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
-b|--body|-c|--comment)
|
||||
# R1 (2026-08-28): --body is the canonical flag; -c/--comment stays
|
||||
# a backward-compatible alias.
|
||||
[[ $# -ge 2 ]] || usage_error "option $1 requires a value"
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-reopen.sh -i <issue_number> [-c <comment>]"
|
||||
echo "Usage: issue-reopen.sh -i <issue_number> [-b <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -c, --comment Comment to add when reopening (optional)"
|
||||
echo " -b, --body Comment to add when reopening (optional; canonical)"
|
||||
echo " -c, --comment Alias for --body"
|
||||
echo " -h, --help Show this help"
|
||||
echo ""
|
||||
echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential/verification failure."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
usage_error "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
usage_error "issue number is required (-i/--issue)"
|
||||
fi
|
||||
|
||||
detect_platform >/dev/null
|
||||
@@ -80,10 +95,22 @@ gitea_issue_reopen_api() {
|
||||
}
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
# R4: normalize provider failures to exit 1 (gh's own usage errors exit 2
|
||||
# and would collide with the reserved usage-error status).
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
gh_rc=0
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT" || gh_rc=$?
|
||||
if [[ "$gh_rc" -ne 0 ]]; then
|
||||
echo "Error: GitHub comment before reopen failed (gh exit $gh_rc)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
gh_rc=0
|
||||
gh issue reopen "$ISSUE_NUMBER" || gh_rc=$?
|
||||
if [[ "$gh_rc" -ne 0 ]]; then
|
||||
echo "Error: GitHub issue reopen failed (gh exit $gh_rc)" >&2
|
||||
exit 1
|
||||
fi
|
||||
gh issue reopen "$ISSUE_NUMBER"
|
||||
echo "Reopened GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
REPO_ARGS=$(get_gitea_repo_args || true)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
# pr-close.sh - Close a pull request without merging on GitHub or Gitea
|
||||
# Usage: pr-close.sh -n <pr_number> [-c <comment>]
|
||||
# Usage: pr-close.sh -n <pr_number> [-b <comment>]
|
||||
# (-c/--comment is a backward-compatible alias for -b/--body; R1/R4 2026-08-28)
|
||||
|
||||
set -e
|
||||
|
||||
@@ -11,44 +12,70 @@ source "$SCRIPT_DIR/detect-platform.sh"
|
||||
PR_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2,
|
||||
# distinct from provider, credential, and verification failures (exit 1), so a
|
||||
# caller or stop gate can tell an invocation defect from a delivery blocker.
|
||||
usage_error() {
|
||||
echo "Error: $*" >&2
|
||||
echo "Usage: pr-close.sh -n <pr_number> [-b <comment>] (see --help)" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
[[ $# -ge 2 ]] || usage_error "option $1 requires a value"
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
-b|--body|-c|--comment)
|
||||
# R1 (2026-08-28): --body is the canonical flag; -c/--comment stays
|
||||
# a backward-compatible alias.
|
||||
[[ $# -ge 2 ]] || usage_error "option $1 requires a value"
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-close.sh -n <pr_number> [-c <comment>]"
|
||||
echo "Usage: pr-close.sh -n <pr_number> [-b <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -c, --comment Comment before closing (optional)"
|
||||
echo " -b, --body Comment before closing (optional; canonical)"
|
||||
echo " -c, --comment Alias for --body"
|
||||
echo " -h, --help Show this help"
|
||||
echo ""
|
||||
echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential/verification failure."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
usage_error "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)"
|
||||
exit 1
|
||||
usage_error "PR number is required (-n/--number)"
|
||||
fi
|
||||
|
||||
detect_platform >/dev/null
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
# R4: normalize provider failures to exit 1 (gh's own usage errors exit 2
|
||||
# and would collide with the reserved usage-error status).
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
gh pr comment "$PR_NUMBER" --body "$COMMENT"
|
||||
gh_rc=0
|
||||
gh pr comment "$PR_NUMBER" --body "$COMMENT" || gh_rc=$?
|
||||
if [[ "$gh_rc" -ne 0 ]]; then
|
||||
echo "Error: GitHub PR comment before close failed (gh exit $gh_rc)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
gh_rc=0
|
||||
gh pr close "$PR_NUMBER" || gh_rc=$?
|
||||
if [[ "$gh_rc" -ne 0 ]]; then
|
||||
echo "Error: GitHub PR close failed (gh exit $gh_rc)" >&2
|
||||
exit 1
|
||||
fi
|
||||
gh pr close "$PR_NUMBER"
|
||||
echo "Closed GitHub PR #$PR_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
# Usage-error contract for issue-reopen.sh (R1/R4, 2026-08-28).
|
||||
#
|
||||
# R4: usage errors print to STDERR and exit 2, distinct from provider,
|
||||
# credential, and verification failures (exit 1). R1: -b/--body is the
|
||||
# canonical comment flag; -c/--comment remains a compatible alias.
|
||||
# The comment is OPTIONAL here (an issue may close without one), so unlike
|
||||
# issue-comment there is no missing-comment arm.
|
||||
#
|
||||
# Arms:
|
||||
# 1. --help and -h exit 0 and print usage.
|
||||
# 2. Unknown option exits 2 with the message on stderr.
|
||||
# 3. Missing required -i exits 2 (stderr).
|
||||
# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr).
|
||||
# 5. -b and -c both pass parsing (sandboxed runner: the run then fails
|
||||
# at credential resolution, nonzero and NOT 2) — no real token is
|
||||
# ever read and no provider is contacted.
|
||||
# 6. No arm performs any provider request (PATH shims record every
|
||||
# invocation; the probe log must stay empty).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-reopen-usage}"
|
||||
BIN_DIR="$WORK_DIR/bin"
|
||||
PROBE_LOG="$WORK_DIR/provider-probes.log"
|
||||
OUT_FILE="$WORK_DIR/out.log"
|
||||
ERR_FILE="$WORK_DIR/err.log"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
: > "$PROBE_LOG"
|
||||
|
||||
for tool in gh tea curl; do
|
||||
cat > "$BIN_DIR/$tool" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
echo "$tool \$*" >> "$PROBE_LOG"
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$BIN_DIR/$tool"
|
||||
done
|
||||
|
||||
run_wrapper() {
|
||||
( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-reopen.sh" "$@" )
|
||||
}
|
||||
|
||||
# Hermetic variant: neutralizes every identity/credential source the wrapper
|
||||
# consults so parse-acceptance arms fail at credential resolution in ANY cwd
|
||||
# repo (see test-issue-comment-usage-contract.sh for the measured incident).
|
||||
run_wrapper_sandboxed() {
|
||||
mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg"
|
||||
(
|
||||
cd "$WORK_DIR"
|
||||
PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \
|
||||
MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \
|
||||
"$SCRIPT_DIR/issue-reopen.sh" "$@"
|
||||
)
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
echo "--- stderr ---" >&2
|
||||
cat "$ERR_FILE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
expect_rc() { # expect_rc <want> <desc> <args...>
|
||||
local want="$1" desc="$2" rc=0
|
||||
shift 2
|
||||
run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$?
|
||||
[[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want"
|
||||
}
|
||||
|
||||
expect_stderr() { # expect_stderr <pattern> <desc>
|
||||
grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'"
|
||||
}
|
||||
|
||||
# 1. Help exits 0 and prints usage.
|
||||
expect_rc 0 "--help exits 0" --help
|
||||
grep -q "Usage: issue-reopen.sh" "$OUT_FILE" || fail "--help did not print usage"
|
||||
expect_rc 0 "-h exits 0" -h
|
||||
|
||||
# 2. Unknown option: rc 2, stderr.
|
||||
expect_rc 2 "unknown option exits 2" --bogus
|
||||
expect_stderr "unknown option" "unknown option names itself on stderr"
|
||||
|
||||
# 3. Missing required issue number: rc 2, stderr.
|
||||
expect_rc 2 "missing -i exits 2"
|
||||
expect_stderr "issue number is required" "missing -i message on stderr"
|
||||
|
||||
# 4. Value-less flags: rc 2 with "requires a value" on stderr.
|
||||
for flag in -i -b -c --issue --body --comment; do
|
||||
expect_rc 2 "value-less $flag exits 2" "$flag"
|
||||
expect_stderr "requires a value" "value-less $flag message on stderr"
|
||||
done
|
||||
|
||||
# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact.
|
||||
if [[ -s "$PROBE_LOG" ]]; then
|
||||
echo "FAIL: a parser-failure arm contacted a provider:" >&2
|
||||
cat "$PROBE_LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Alias acceptance under the sandbox: both -b and -c carry a value past
|
||||
# parsing; the run fails at credential resolution nonzero and NOT 2.
|
||||
for flag in -b -c; do
|
||||
rc=0
|
||||
run_wrapper_sandboxed -i 5 "$flag" "closing note" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$?
|
||||
[[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox"
|
||||
[[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error"
|
||||
done
|
||||
|
||||
# 6. Sandbox arms may issue DETECTION reads only (tea login list via the
|
||||
# stub); no gh/curl write or read may occur.
|
||||
if grep -Ev '^(gh|tea|curl) login list' "$PROBE_LOG" | grep -q .; then
|
||||
echo "FAIL: a sandbox arm performed a non-detection provider request:" >&2
|
||||
grep -Ev '^(gh|tea|curl) login list' "$PROBE_LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -qE '^(gh|curl)' "$PROBE_LOG"; then
|
||||
echo "FAIL: gh or curl was invoked during a sandbox arm:" >&2
|
||||
grep -E '^(gh|curl)' "$PROBE_LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "issue-reopen.sh usage-contract regression passed (R1/R4)"
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# Usage-error contract for pr-close.sh (R1/R4, 2026-08-28).
|
||||
#
|
||||
# R4: usage errors print to STDERR and exit 2, distinct from provider,
|
||||
# credential, and verification failures (exit 1). R1: -b/--body is the
|
||||
# canonical comment flag; -c/--comment remains a compatible alias.
|
||||
# The comment is OPTIONAL here (an issue may close without one), so unlike
|
||||
# issue-comment there is no missing-comment arm.
|
||||
#
|
||||
# Arms:
|
||||
# 1. --help and -h exit 0 and print usage.
|
||||
# 2. Unknown option exits 2 with the message on stderr.
|
||||
# 3. Missing required -i exits 2 (stderr).
|
||||
# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr).
|
||||
# 5. -b and -c both pass parsing (sandboxed runner: the run then fails
|
||||
# at credential resolution, nonzero and NOT 2) — no real token is
|
||||
# ever read and no provider is contacted.
|
||||
# 6. No arm performs any provider request (PATH shims record every
|
||||
# invocation; the probe log must stay empty).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-close-usage}"
|
||||
BIN_DIR="$WORK_DIR/bin"
|
||||
PROBE_LOG="$WORK_DIR/provider-probes.log"
|
||||
OUT_FILE="$WORK_DIR/out.log"
|
||||
ERR_FILE="$WORK_DIR/err.log"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
: > "$PROBE_LOG"
|
||||
|
||||
# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an
|
||||
# API fallback that treats a successful curl as a closed PR, so exit-0
|
||||
# stubs would let the sandbox arms "succeed" (measured 2026-08-28).
|
||||
for tool in gh tea curl; do
|
||||
cat > "$BIN_DIR/$tool" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
echo "$tool \$*" >> "$PROBE_LOG"
|
||||
exit 99
|
||||
STUB
|
||||
chmod +x "$BIN_DIR/$tool"
|
||||
done
|
||||
|
||||
run_wrapper() {
|
||||
( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/pr-close.sh" "$@" )
|
||||
}
|
||||
|
||||
# Hermetic variant: neutralizes every identity/credential source the wrapper
|
||||
# consults so parse-acceptance arms fail at credential resolution in ANY cwd
|
||||
# repo (see test-issue-comment-usage-contract.sh for the measured incident).
|
||||
run_wrapper_sandboxed() {
|
||||
mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg"
|
||||
(
|
||||
cd "$WORK_DIR"
|
||||
PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \
|
||||
MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \
|
||||
"$SCRIPT_DIR/pr-close.sh" "$@"
|
||||
)
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
echo "--- stderr ---" >&2
|
||||
cat "$ERR_FILE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
expect_rc() { # expect_rc <want> <desc> <args...>
|
||||
local want="$1" desc="$2" rc=0
|
||||
shift 2
|
||||
run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$?
|
||||
[[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want"
|
||||
}
|
||||
|
||||
expect_stderr() { # expect_stderr <pattern> <desc>
|
||||
grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'"
|
||||
}
|
||||
|
||||
# 1. Help exits 0 and prints usage.
|
||||
expect_rc 0 "--help exits 0" --help
|
||||
grep -q "Usage: pr-close.sh" "$OUT_FILE" || fail "--help did not print usage"
|
||||
expect_rc 0 "-h exits 0" -h
|
||||
|
||||
# 2. Unknown option: rc 2, stderr.
|
||||
expect_rc 2 "unknown option exits 2" --bogus
|
||||
expect_stderr "unknown option" "unknown option names itself on stderr"
|
||||
|
||||
# 3. Missing required PR number: rc 2, stderr.
|
||||
expect_rc 2 "missing -n exits 2"
|
||||
expect_stderr "PR number is required" "missing -n message on stderr"
|
||||
|
||||
# 4. Value-less flags: rc 2 with "requires a value" on stderr.
|
||||
for flag in -n -b -c --number --body --comment; do
|
||||
expect_rc 2 "value-less $flag exits 2" "$flag"
|
||||
expect_stderr "requires a value" "value-less $flag message on stderr"
|
||||
done
|
||||
|
||||
# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact.
|
||||
if [[ -s "$PROBE_LOG" ]]; then
|
||||
echo "FAIL: a parser-failure arm contacted a provider:" >&2
|
||||
cat "$PROBE_LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Alias acceptance under the sandbox: both -b and -c carry a value past
|
||||
# parsing; the run fails at credential resolution nonzero and NOT 2.
|
||||
for flag in -b -c; do
|
||||
rc=0
|
||||
run_wrapper_sandboxed -n 5 "$flag" "closing note" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$?
|
||||
[[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox"
|
||||
[[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error"
|
||||
done
|
||||
|
||||
# 6. Post-sandbox provider assertions are intentionally NOT applied here:
|
||||
# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a
|
||||
# comment parses, then falls back to the API. Hermeticity for this
|
||||
# wrapper comes from the FAILING stubs (exit 99), not from non-contact —
|
||||
# the arm above proves only parse acceptance and non-usage classification.
|
||||
# Parser-failure arms (1-4) remain zero-contact (asserted at 4b).
|
||||
|
||||
echo "pr-close.sh usage-contract regression passed (R1/R4)"
|
||||
Reference in New Issue
Block a user