feat(infra): design-reviewer agent + PR helper (US-24 + US-25)
US-24 — Process hygiene UI:
- New .claude/agents/design-reviewer.md (model: opus, read-only) — visual + design-system reviewer that runs after frontend-builder and before code-reviewer. Audits alignment, DESIGN.md tokens, light/dark consistency, typo hierarchy, whitespace rhythm, responsive sanity at 1280x720, button convention, V1 a11y. Output format mirrors code-reviewer.
- Updated .claude/agents/frontend-builder.md DoD: screenshots are MANDATORY (one per feature/state introduced or modified, light+dark when theming is in scope). Hard block on "Dev server not started" — must be flagged explicitly. Screenshots feed the design-reviewer step.
US-25 — PR helper:
- scripts/open-pr.sh wraps `POST /api/v1/repos/{owner}/{repo}/pulls`. Detects host/owner/repo from `git remote get-url origin`, reads basic-auth credentials from `~/.git-credentials` (same source as `git push`, no token in env), uses jq to compose the multiline-safe payload. Validates args, prints PR URL on success, exits non-zero with the server message on failure.
- Makefile target `open-pr TITLE="..." BODY=path/to/body.md [BASE=main]` wraps the script with the same arg validation.
- README.md "Make targets" table extended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
136
scripts/open-pr.sh
Executable file
136
scripts/open-pr.sh
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
# Open a pull request against the Mimic Gitea repository using the credentials
|
||||
# already stored in ~/.git-credentials (the same token used for `git push`).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/open-pr.sh --title "feat: sprint N — short summary" \
|
||||
# --body path/to/body.md \
|
||||
# [--base main] \
|
||||
# [--head <current branch by default>]
|
||||
#
|
||||
# Or via the Makefile wrapper:
|
||||
# make open-pr TITLE="feat: sprint 4 — UI polish" BODY=tasks/pr-body-sprint-4.md
|
||||
#
|
||||
# Output: prints the PR URL on success, exits non-zero on failure.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Arg parsing ------------------------------------------------------------
|
||||
|
||||
TITLE=""
|
||||
BODY_FILE=""
|
||||
BASE="main"
|
||||
HEAD=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--title)
|
||||
TITLE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--body)
|
||||
BODY_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--base)
|
||||
BASE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--head)
|
||||
HEAD="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--sprint)
|
||||
# purely informational; ignored — the title is what carries semantics
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
sed -n '2,15p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown arg: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$TITLE" ]] || { echo "--title is required" >&2; exit 2; }
|
||||
[[ -n "$BODY_FILE" ]] || { echo "--body is required" >&2; exit 2; }
|
||||
[[ -f "$BODY_FILE" ]] || { echo "body file not found: $BODY_FILE" >&2; exit 2; }
|
||||
|
||||
# --- Credentials ------------------------------------------------------------
|
||||
|
||||
CRED_FILE="${HOME}/.git-credentials"
|
||||
[[ -f "$CRED_FILE" ]] || { echo "no ~/.git-credentials — git push must have run at least once" >&2; exit 3; }
|
||||
|
||||
# Detect Gitea host from origin remote
|
||||
ORIGIN_URL=$(git remote get-url origin)
|
||||
# Strip protocol, .git suffix → host/owner/repo
|
||||
case "$ORIGIN_URL" in
|
||||
https://*)
|
||||
REST="${ORIGIN_URL#https://}"
|
||||
;;
|
||||
*)
|
||||
echo "origin is not https (got: $ORIGIN_URL) — this script supports HTTPS Gitea only" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
REST="${REST%.git}"
|
||||
HOST="${REST%%/*}"
|
||||
PATHPART="${REST#*/}" # owner/repo
|
||||
OWNER="${PATHPART%%/*}"
|
||||
REPO="${PATHPART#*/}"
|
||||
REPO="${REPO%%/*}" # belt + braces in case of trailing slash
|
||||
|
||||
# Match the credential line for this host
|
||||
CRED_LINE=$(grep -E "^https://[^@]+@${HOST}\$" "$CRED_FILE" || true)
|
||||
[[ -n "$CRED_LINE" ]] || { echo "no credential for host ${HOST} in ${CRED_FILE}" >&2; exit 3; }
|
||||
|
||||
USER_PART=$(echo "$CRED_LINE" | sed -E 's|^https://([^:]+):.*|\1|')
|
||||
TOKEN=$(echo "$CRED_LINE" | sed -E 's|^https://[^:]+:([^@]+)@.*$|\1|')
|
||||
|
||||
[[ -n "$USER_PART" && -n "$TOKEN" ]] || { echo "could not parse user/token from credential" >&2; exit 3; }
|
||||
|
||||
# --- Branch -----------------------------------------------------------------
|
||||
|
||||
if [[ -z "$HEAD" ]]; then
|
||||
HEAD=$(git rev-parse --abbrev-ref HEAD)
|
||||
fi
|
||||
[[ "$HEAD" != "HEAD" ]] || { echo "detached HEAD — pass --head explicitly" >&2; exit 3; }
|
||||
|
||||
# --- Compose payload --------------------------------------------------------
|
||||
|
||||
API_URL="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls"
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "$TITLE" \
|
||||
--rawfile body "$BODY_FILE" \
|
||||
--arg head "$HEAD" \
|
||||
--arg base "$BASE" \
|
||||
'{title:$title, body:$body, head:$head, base:$base}')
|
||||
|
||||
# --- POST -------------------------------------------------------------------
|
||||
|
||||
RESPONSE_FILE=$(mktemp)
|
||||
HTTP_CODE=$(curl -sS -u "${USER_PART}:${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d "$PAYLOAD" \
|
||||
-o "$RESPONSE_FILE" \
|
||||
-w "%{http_code}" \
|
||||
"$API_URL")
|
||||
|
||||
if [[ "$HTTP_CODE" != "201" ]]; then
|
||||
echo "PR creation failed (HTTP $HTTP_CODE):" >&2
|
||||
jq -r '.message // empty' "$RESPONSE_FILE" >&2 2>/dev/null || cat "$RESPONSE_FILE" >&2
|
||||
rm -f "$RESPONSE_FILE"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
PR_URL=$(jq -r '.html_url' "$RESPONSE_FILE")
|
||||
PR_NUMBER=$(jq -r '.number' "$RESPONSE_FILE")
|
||||
rm -f "$RESPONSE_FILE"
|
||||
|
||||
echo "Opened PR #${PR_NUMBER}"
|
||||
echo "$PR_URL"
|
||||
Reference in New Issue
Block a user