feat(git-tools): add pull request edit wrapper (#1080)
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
coder3
2026-08-12 14:41:29 -05:00
parent ec260e678f
commit 4f0d3e6e25
3 changed files with 260 additions and 2 deletions
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
# pr-edit.sh - Edit a pull request on GitHub or Gitea
# Usage: pr-edit.sh -n <pr_number> [-t <title>] [-b <body>] [-B <base>] [--draft|--ready] [--login <name>]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=detect-platform.sh
source "$SCRIPT_DIR/detect-platform.sh"
PR_NUMBER=""
TITLE=""
BODY=""
BASE_BRANCH=""
DRAFT_MODE=""
LOGIN_OVERRIDE=""
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Edit a pull request on the current repository (Gitea or GitHub).
Options:
-n, --number NUMBER Pull request number (required)
-t, --title TITLE New title
-b, --body BODY New body/description
-B, --base BRANCH New base branch
--draft Mark the pull request as draft
--ready Mark the pull request ready for review
--login NAME Gitea tea login (must match the repository host)
-h, --help Show this help message
EOF
exit "${1:-1}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--number)
PR_NUMBER="${2:-}"
shift 2
;;
-t|--title)
TITLE="${2:-}"
shift 2
;;
-b|--body)
BODY="${2:-}"
shift 2
;;
-B|--base)
BASE_BRANCH="${2:-}"
shift 2
;;
--draft)
[[ "$DRAFT_MODE" != "ready" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 1; }
DRAFT_MODE="draft"
shift
;;
--ready)
[[ "$DRAFT_MODE" != "draft" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 1; }
DRAFT_MODE="ready"
shift
;;
--login)
LOGIN_OVERRIDE="${2:-}"
shift 2
;;
-h|--help)
usage 0
;;
*)
echo "Unknown option: $1" >&2
usage
;;
esac
done
[[ -n "$PR_NUMBER" ]] || { echo "Error: Pull request number is required (-n)" >&2; exit 1; }
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "Error: Pull request number must be a positive integer" >&2; exit 1; }
if [[ -z "$TITLE" && -z "$BODY" && -z "$BASE_BRANCH" && -z "$DRAFT_MODE" ]]; then
echo "Error: At least one edit option is required" >&2
exit 1
fi
PLATFORM=$(detect_platform)
case "$PLATFORM" in
github)
[[ -z "$LOGIN_OVERRIDE" ]] || { echo "Error: --login is only valid for Gitea" >&2; exit 1; }
if [[ -n "$TITLE" || -n "$BODY" || -n "$BASE_BRANCH" ]]; then
CMD=(gh pr edit "$PR_NUMBER")
[[ -n "$TITLE" ]] && CMD+=(--title "$TITLE")
[[ -n "$BODY" ]] && CMD+=(--body "$BODY")
[[ -n "$BASE_BRANCH" ]] && CMD+=(--base "$BASE_BRANCH")
"${CMD[@]}"
fi
if [[ "$DRAFT_MODE" == "draft" ]]; then
gh pr ready "$PR_NUMBER" --undo
elif [[ "$DRAFT_MODE" == "ready" ]]; then
gh pr ready "$PR_NUMBER"
fi
;;
gitea)
HOST=$(get_remote_host) || { echo "Error: Could not resolve Gitea host from remote" >&2; exit 1; }
REPO_SLUG=$(get_repo_slug) || { echo "Error: Could not resolve Gitea repo slug from remote" >&2; exit 1; }
if [[ -n "$LOGIN_OVERRIDE" ]]; then
GITEA_LOGIN="$LOGIN_OVERRIDE"
else
GITEA_LOGIN=$(get_gitea_login) || { echo "Error: Could not resolve Gitea login for remote host" >&2; exit 1; }
fi
# Bind the credential to the explicitly selected login and repository host.
TOKEN=$(get_gitea_token_for_login "$GITEA_LOGIN" "$HOST") || {
echo "Error: Could not resolve token for Gitea login '$GITEA_LOGIN' on '$HOST'" >&2
exit 1
}
PAYLOAD=$(TITLE="$TITLE" BODY="$BODY" BASE_BRANCH="$BASE_BRANCH" DRAFT_MODE="$DRAFT_MODE" python3 - <<'PY'
import json
import os
payload = {}
if os.environ["TITLE"]:
payload["title"] = os.environ["TITLE"]
if os.environ["BODY"]:
payload["body"] = os.environ["BODY"]
if os.environ["BASE_BRANCH"]:
payload["base"] = os.environ["BASE_BRANCH"]
if os.environ["DRAFT_MODE"]:
payload["draft"] = os.environ["DRAFT_MODE"] == "draft"
print(json.dumps(payload))
PY
)
curl -fsS -X PATCH \
-H "User-Agent: mosaic-pr-edit" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"https://${HOST}/api/v1/repos/${REPO_SLUG}/pulls/${PR_NUMBER}"
echo "Updated Gitea pull request #$PR_NUMBER using login '$GITEA_LOGIN'" >&2
;;
*)
echo "Error: Could not detect git platform" >&2
exit 1
;;
esac
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Regression harness for #701: -h/--help must exit 0, bad args must still exit nonzero.
#
# Covers the 7 wrappers whose usage() previously hard-coded `exit 1`, so every
# Covers wrappers whose usage() previously hard-coded `exit 1`, so every
# --help invocation exited nonzero and logged a phantom isError across fleet lanes.
# Asserts, per wrapper:
# 1. `--help` exits 0 and prints usage.
@@ -18,6 +18,7 @@ WRAPPERS=(
issue-list.sh
milestone-create.sh
pr-create.sh
pr-edit.sh
pr-list.sh
pr-merge.sh
)
@@ -47,7 +48,7 @@ for wrapper in "${WRAPPERS[@]}"; do
done
if [[ "$fail" -eq 0 ]]; then
echo "help-exit-code regression passed (7/7 wrappers)"
echo "help-exit-code regression passed (8/8 wrappers)"
fi
exit "$fail"
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# Regression harness for PR editing, including Gitea draft/ready and login binding.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-edit}"
REPO_DIR="$WORK_DIR/repo"
BIN_DIR="$WORK_DIR/bin"
HOME_DIR="$WORK_DIR/home"
XDG_DIR="$WORK_DIR/xdg"
LOG_FILE="$WORK_DIR/calls.log"
rm -rf "$WORK_DIR"
mkdir -p "$REPO_DIR" "$BIN_DIR" "$HOME_DIR" "$XDG_DIR/tea"
git -C "$REPO_DIR" init -q
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
git -C "$REPO_DIR" config mosaic.gitIdentity ""
cat > "$XDG_DIR/tea/config.yml" <<'YAML'
logins:
- name: usc
url: https://git.uscllc.com
token: fixture-usc-token
- name: mosaicstack
url: https://git.mosaicstack.dev
token: fixture-mosaic-token
YAML
cat > "$BIN_DIR/curl" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf 'curl' >> "$MOSAIC_TEST_LOG"
printf ' <%s>' "$@" >> "$MOSAIC_TEST_LOG"
printf '\n' >> "$MOSAIC_TEST_LOG"
printf '{"number":42,"draft":false}\n'
SH
cat > "$BIN_DIR/gh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf 'gh' >> "$MOSAIC_TEST_LOG"
printf ' <%s>' "$@" >> "$MOSAIC_TEST_LOG"
printf '\n' >> "$MOSAIC_TEST_LOG"
SH
chmod +x "$BIN_DIR/curl" "$BIN_DIR/gh" "$SCRIPT_DIR/pr-edit.sh"
run_wrapper() {
(
cd "$REPO_DIR"
PATH="$BIN_DIR:$PATH" \
HOME="$HOME_DIR" \
XDG_CONFIG_HOME="$XDG_DIR" \
MOSAIC_TEST_LOG="$LOG_FILE" \
"$SCRIPT_DIR/pr-edit.sh" "$@"
)
}
: > "$LOG_FILE"
# shellcheck disable=SC2016 # literal backticks prove argument-array body safety.
run_wrapper -n 42 --login usc --title 'New title' --body 'Body with `literal` bytes' --base develop --draft >/dev/null
python3 - "$LOG_FILE" <<'PY'
import json
import pathlib
import sys
line = pathlib.Path(sys.argv[1]).read_text()
assert "-X> <PATCH>" in line, line
assert "Authorization: token fixture-usc-token" in line, line
assert "https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls/42" in line, line
payload = line.split(" <-d> <", 1)[1].split("> <https://", 1)[0]
data = json.loads(payload)
assert data == {
"title": "New title",
"body": "Body with `literal` bytes",
"base": "develop",
"draft": True,
}, data
PY
: > "$LOG_FILE"
run_wrapper -n 42 --login usc --ready >/dev/null
grep -q '"draft": false' "$LOG_FILE"
: > "$LOG_FILE"
if run_wrapper -n 42 --login mosaicstack --draft >/dev/null 2>&1; then
echo "Expected a login bound to another host to fail closed" >&2
exit 1
fi
[[ ! -s "$LOG_FILE" ]] || { echo "Mismatched login unexpectedly reached curl" >&2; exit 1; }
if run_wrapper -n 42 --draft --ready >/dev/null 2>&1; then
echo "Expected --draft and --ready to be mutually exclusive" >&2
exit 1
fi
if run_wrapper -n 42 >/dev/null 2>&1; then
echo "Expected a no-op edit to fail" >&2
exit 1
fi
if ! run_wrapper --help 2>&1 | grep -q '^Usage:'; then
echo "Expected --help to exit zero and print usage" >&2
exit 1
fi
# GitHub uses gh's supported edit and ready/undo commands rather than raw defaults.
git -C "$REPO_DIR" remote set-url origin https://github.com/acme/widgets.git
: > "$LOG_FILE"
run_wrapper -n 7 --title 'GitHub title' --draft >/dev/null
grep -q 'gh <pr> <edit> <7> <--title> <GitHub title>' "$LOG_FILE"
grep -q 'gh <pr> <ready> <7> <--undo>' "$LOG_FILE"
echo "PR edit regression harness passed"