Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 13 additions & 13 deletions plugins/me/skills/create-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@ name: create-pr
description: Create PR — commit, push, PR, wait for merge.
---

Execute each line literally (scripts MUST be run, not reimplemented):
Run the wrapper. Do not reimplement its git/gh checks.

```bash
S="${CLAUDE_PLUGIN_ROOT}/skills/create-pr/scripts"
# If on main/master: checkout -b <type>/<short> first
"$S/preflight-check.sh" # syncs if behind base
git add <files> && git commit -m "type(scope): msg"
git push -u origin HEAD
gh pr create --title "$(git log -1 --pretty=%s)" --body-file /tmp/pr_body.md
# Auto merge: only if user explicitly requests it
# gh pr merge --auto --squash
# REQUIRED: invoke via Monitor tool — streams per-check events + terminal event.
# Monitor({command: "\"$S/wait-for-merge.sh\"", description: "PR checks", timeout_ms: 1800000, persistent: false})
printf '%s\n' "<full PR body>" | "$S/create-pr.sh" "type(scope): msg" -- <files>
# If auto merge was requested:
printf '%s\n' "<full PR body>" | "$S/create-pr.sh" --auto-merge "type(scope): msg" -- <files>
```

Write the PR body to a file and pass it with `--body-file`. Do not put setup commands or PR body text inside --title.
The wrapper handles preflight, safe `/tmp/pr_body.md` creation, commit, push, PR creation, optional auto-merge, and
merge waiting.

If user requests auto merge: `gh pr merge --auto --squash` → invoke `"$S/wait-for-merge.sh"` via the Monitor tool. Each `check: <name>: <bucket>` line streams as a notification; the terminal event has one of these prefixes — branch on it:
Branch only on terminal prefixes:

- `NOOP:` → nothing to PR
- `MERGED:` → done
- `AWAITING_REVIEW:` → CI green, needs reviewer
- `CI_FAILED: <url> run-id=<id>` → `gh run view <run-id> --log-failed` → `me:fix-pr` once → re-enable `gh pr merge --auto --squash` → re-invoke Monitor. Stop if unclear or still failing.
- `CI_FAILED: <url> run-id=<id>` → inspect failed log, use `me:fix-pr` once, then retry auto-merge
- `CLOSED:` → stop.
PR body: fill PR template if exists, else summary+changes+tests. Write /tmp/pr_body.md in ONE Write call (compose the full body first); never Write/Edit it twice — re-Read before any second write.

PR body: fill PR template if exists, else summary+changes+tests. Pipe it once to the wrapper; never Write/Edit
`/tmp/pr_body.md`.
67 changes: 67 additions & 0 deletions plugins/me/skills/create-pr/scripts/create-pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BODY_PATH="${CREATE_PR_BODY_PATH:-/tmp/pr_body.md}"
AUTO_MERGE=0

usage() {
cat >&2 <<'EOF'
Usage: printf '%s\n' '<PR body>' | create-pr.sh [--auto-merge] '<commit message>' -- <files...>
EOF
}

if [[ "${1:-}" == "--auto-merge" ]]; then
AUTO_MERGE=1
shift
fi

MESSAGE="${1:-}"
[[ -n "$MESSAGE" ]] || { usage; exit 2; }
shift

[[ "${1:-}" == "--" ]] || { usage; exit 2; }
shift
[[ "$#" -gt 0 ]] || { usage; exit 2; }

write_body() {
local tmp
mkdir -p "$(dirname "$BODY_PATH")"
tmp="$(mktemp "${BODY_PATH}.XXXXXX")"
cat > "$tmp"
mv "$tmp" "$BODY_PATH"
}

PREFLIGHT_OUTPUT=$("$SCRIPT_DIR/preflight-check.sh")
printf '%s\n' "$PREFLIGHT_OUTPUT"
PREFLIGHT_LAST=$(printf '%s\n' "$PREFLIGHT_OUTPUT" | tail -n 1)

case "$PREFLIGHT_LAST" in
NOOP:*|MERGED:*)
exit 0
;;
esac

write_body
git add -- "$@"
if git diff --cached --quiet; then
echo "NOOP: no staged changes to commit"
exit 0
else
git commit -m "$MESSAGE"
fi

git push -u origin HEAD

if gh pr view --json url >/dev/null 2>&1; then
PR_URL=$(gh pr view --json url --jq .url)
echo "PR_EXISTS: $PR_URL"
else
TITLE="$(git log -1 --pretty=%s)"
gh pr create --title "$TITLE" --body-file "$BODY_PATH"
fi

if [[ "$AUTO_MERGE" -eq 1 ]]; then
gh pr merge --auto --squash
"$SCRIPT_DIR/wait-for-merge.sh"
fi
19 changes: 18 additions & 1 deletion plugins/me/skills/create-pr/scripts/preflight-check.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
# preflight-check.sh — exit 0: ready | exit 1: blocking | exit 2: env error
# preflight-check.sh — exit 0: terminal/readiness | exit 1: blocking | exit 2: env error
# Terminal/readiness prefixes:
# OK
# NOOP: <reason>
# MERGED: <url>

git rev-parse --git-dir >/dev/null 2>&1 || { echo "ERROR: Not a git repo" >&2; exit 2; }
BASE="${1:-$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || echo "")}"
Expand All @@ -13,5 +17,18 @@ if [[ $(git rev-list HEAD..origin/"$BASE" --count 2>/dev/null || echo 0) -gt 0 ]
git push -u origin HEAD || { echo "Push failed" >&2; exit 1; }
fi

PR_STATE=$(gh pr view --json state --jq .state 2>/dev/null || true)
PR_URL=$(gh pr view --json url --jq .url 2>/dev/null || true)
if [[ "$PR_STATE" == "MERGED" && -n "$PR_URL" ]]; then
echo "MERGED: $PR_URL"
exit 0
fi

AHEAD=$(git rev-list "origin/$BASE"..HEAD --count 2>/dev/null || echo 0)
if [[ -z "$(git status --porcelain)" && "$AHEAD" -eq 0 && -z "$PR_URL" ]]; then
echo "NOOP: no local changes or commits ahead of origin/$BASE"
exit 0
fi

git merge-tree --write-tree HEAD "origin/$BASE" >/dev/null 2>&1 || { echo "ERROR: Conflicts with origin/$BASE" >&2; exit 1; }
echo "OK"
106 changes: 106 additions & 0 deletions tests/me/create-pr-e2e.bats
Original file line number Diff line number Diff line change
@@ -1,12 +1,118 @@
#!/usr/bin/env bats
# create-pr e2e test - intentionally failing to test CI recovery flow

setup() {
TEST_TEMP_DIR="$(mktemp -d -t create-pr-test.XXXXXX)"
export TEST_TEMP_DIR
export STUB_LOG="${TEST_TEMP_DIR}/calls.log"
export PATH="${TEST_TEMP_DIR}/bin:${PATH}"
mkdir -p "${TEST_TEMP_DIR}/bin"
touch "$STUB_LOG"

cat > "${TEST_TEMP_DIR}/bin/git" <<'STUB'
#!/usr/bin/env bash
set -euo pipefail
echo "git $*" >> "$STUB_LOG"
case "$*" in
"rev-parse --git-dir") echo ".git" ;;
"symbolic-ref --short HEAD") echo "feature/create-pr-wrapper" ;;
"fetch origin main") ;;
"rev-list HEAD..origin/main --count") echo "${GIT_BEHIND:-0}" ;;
"rev-list origin/main..HEAD --count") echo "${GIT_AHEAD:-1}" ;;
"merge-tree --write-tree HEAD origin/main") ;;
"status --porcelain") [[ "${GIT_DIRTY:-0}" == "1" ]] && echo " M changed.txt" || true ;;
"add -- plugins/me/skills/create-pr/SKILL.md") touch "${TEST_TEMP_DIR}/staged" ;;
"diff --cached --quiet") [[ -f "${TEST_TEMP_DIR}/staged" ]] && exit 1 || exit 0 ;;
"commit -m feat(test): wrapper") rm -f "${TEST_TEMP_DIR}/staged"; touch "${TEST_TEMP_DIR}/committed" ;;
"push -u origin HEAD") ;;
"log -1 --pretty=%s") echo "feat(test): wrapper" ;;
*) ;;
esac
STUB
chmod +x "${TEST_TEMP_DIR}/bin/git"

cat > "${TEST_TEMP_DIR}/bin/gh" <<'STUB'
#!/usr/bin/env bash
set -euo pipefail
echo "gh $*" >> "$STUB_LOG"
case "$*" in
"repo view --json defaultBranchRef -q .defaultBranchRef.name") echo "main" ;;
"pr view --json state --jq .state")
if [[ "${GH_EXISTING_PR:-none}" == "merged" || -f "${TEST_TEMP_DIR}/merged" ]]; then
echo "MERGED"
elif [[ "${GH_EXISTING_PR:-none}" == "open" || -f "${TEST_TEMP_DIR}/pr_created" ]]; then
echo "OPEN"
else
exit 1
fi
;;
"pr view --json url --jq .url")
if [[ "${GH_EXISTING_PR:-none}" != "none" || -f "${TEST_TEMP_DIR}/pr_created" ]]; then
echo "https://example.test/pr/1"
else
exit 1
fi
;;
"pr view --json url") [[ "${GH_EXISTING_PR:-none}" != "none" || -f "${TEST_TEMP_DIR}/pr_created" ]] || exit 1 ;;
"pr checks --json name,bucket,link") echo '[{"name":"CI","bucket":"pass","link":"https://example.test/runs/1234567890"}]' ;;
pr\ create*) touch "${TEST_TEMP_DIR}/pr_created"; echo "https://example.test/pr/1" ;;
"pr merge --auto --squash") touch "${TEST_TEMP_DIR}/auto_merge"; echo "auto merge enabled" ;;
"pr merge --squash") touch "${TEST_TEMP_DIR}/merged"; echo "merged" ;;
*) ;;
esac
STUB
chmod +x "${TEST_TEMP_DIR}/bin/gh"
}

teardown() {
rm -rf "$TEST_TEMP_DIR"
}

assert_log_excludes() {
local pattern="$1"
if grep -q "$pattern" "$STUB_LOG"; then
echo "Unexpected command in log: $pattern" >&2
cat "$STUB_LOG" >&2
return 1
fi
}

@test "create-pr: wait-for-merge script exists and is executable" {
local script="${BATS_TEST_DIRNAME}/../../plugins/me/skills/create-pr/scripts/wait-for-merge.sh"
[ -f "$script" ]
[ -x "$script" ]
}

@test "create-pr: wrapper commits selected files, creates PR, enables auto merge, and waits" {
local script="${BATS_TEST_DIRNAME}/../../plugins/me/skills/create-pr/scripts/create-pr.sh"
local body="${TEST_TEMP_DIR}/pr_body.md"

run env CREATE_PR_BODY_PATH="$body" bash -c \
"printf '## Summary\n- wrapper\n' | '$script' --auto-merge 'feat(test): wrapper' -- plugins/me/skills/create-pr/SKILL.md"

[ "$status" -eq 0 ]
[[ "$output" == *"MERGED: https://example.test/pr/1"* ]]
[ "$(cat "$body")" = $'## Summary\n- wrapper' ]
grep -q "git add -- plugins/me/skills/create-pr/SKILL.md" "$STUB_LOG"
grep -q "git commit -m feat(test): wrapper" "$STUB_LOG"
grep -q "git push -u origin HEAD" "$STUB_LOG"
grep -q "gh pr create --title feat(test): wrapper --body-file $body" "$STUB_LOG"
grep -q "gh pr merge --auto --squash" "$STUB_LOG"
grep -q "gh pr merge --squash" "$STUB_LOG"
}

@test "create-pr: wrapper stops before PR creation when preflight reports no diff" {
local script="${BATS_TEST_DIRNAME}/../../plugins/me/skills/create-pr/scripts/create-pr.sh"

run env GIT_AHEAD=0 GIT_DIRTY=0 bash -c \
"printf '## Summary\n- noop\n' | '$script' 'feat(test): wrapper' -- plugins/me/skills/create-pr/SKILL.md"

[ "$status" -eq 0 ]
[[ "$output" == "NOOP:"* ]]
assert_log_excludes "git push -u origin HEAD"
assert_log_excludes "gh pr create"
}

@test "fix-pr: skill exists under the new name" {
local skill="${BATS_TEST_DIRNAME}/../../plugins/me/skills/fix-pr/SKILL.md"

Expand Down
2 changes: 2 additions & 0 deletions tests/me/me-specific.bats
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ load ../helpers/bats_helper
# create-pr skill tests
@test "me: create-pr skill exists with required components" {
[ -f "${PROJECT_ROOT}/plugins/me/skills/create-pr/SKILL.md" ]
[ -f "${PROJECT_ROOT}/plugins/me/skills/create-pr/scripts/create-pr.sh" ]
[ -f "${PROJECT_ROOT}/plugins/me/skills/create-pr/scripts/preflight-check.sh" ]
[ -f "${PROJECT_ROOT}/plugins/me/skills/create-pr/scripts/wait-for-merge.sh" ]
}

@test "me: create-pr scripts are executable" {
[ -x "${PROJECT_ROOT}/plugins/me/skills/create-pr/scripts/create-pr.sh" ]
[ -x "${PROJECT_ROOT}/plugins/me/skills/create-pr/scripts/preflight-check.sh" ]
[ -x "${PROJECT_ROOT}/plugins/me/skills/create-pr/scripts/wait-for-merge.sh" ]
}
Expand Down
Loading