diff --git a/.claude/skills/issue-workflow/SKILL.md b/.claude/skills/issue-workflow/SKILL.md new file mode 100644 index 0000000..660800c --- /dev/null +++ b/.claude/skills/issue-workflow/SKILL.md @@ -0,0 +1,222 @@ +--- +name: issue-workflow +description: Drive a single GitHub issue from "open on main" through branch → commits → PR → CI green → squash-merge → issue closed → main synced. Replaces the old scripts/issue-workflow.sh. Use when the user says "work on issue #N", "implement issue #N", "finish issue #N", "next issue", or asks to start a feature/fix branch tied to a specific issue number. +--- + +# issue-workflow + +Drive one GitHub issue through the full branch → PR → merge → cleanup +loop. This skill is the AI-equivalent of the old `scripts/issue-workflow.sh` +shell script: instead of imperative bash, it produces a tracked list of +tasks the assistant walks through one at a time. + +The skill encodes the project conventions in [`CONTRIBUTING.md`](../../CONTRIBUTING.md): +- branch off `main`, keep `main` clean +- one logical change per PR +- Conventional Commits (`type(scope): subject`) +- squash-merge, source branch deleted on merge +- PR title = first commit subject +- allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert` +- allowed scopes: `detector`, `manager`, `packages`, `node`, `config`, `ui`, `platform`, `cli`, `deps`, `release`, `ci`, `docs`, `lint` +- every PR body must reference its source issue via `Closes #N` or `Refs #N` + +## When to invoke + +The user explicitly asks to work on an issue by number, or says +"next issue" / "finish the open issue" / "implement #N". Do not invoke +this skill for ad-hoc exploratory work or for changes that don't have +a tracking issue — open an issue first. + +## Inputs to collect up front + +Before doing anything, ask the user (use `AskUserQuestion` if more than +one is ambiguous): + +1. **Issue number** — the only required input. If the user says "next + issue", find the lowest-numbered open issue via `gh issue list + --state open --limit 1 --json number,title`. +2. **Type / scope / slug** — for branching. Try to derive these from + the issue title (it should follow `type(scope): subject` per + CONTRIBUTING.md). If the title doesn't match, ask the user to + rename it OR to pass the three explicitly. + + Slug rules: lowercase, non-alnum → `-`, trim leading/trailing `-`. + Branch name is `//`. + +## Workflow (in order) + +Create one `TaskCreate` per step below, mark `in_progress` when you +start it, `completed` when done. Don't skip steps. If a step fails, +stop and surface the failure to the user — do not paper over it. + +### 1. Inspect the issue + +`gh issue view --json title,state,labels,body,number` + +Confirm `state == OPEN`. If closed, tell the user and stop. Skim the +body for scope, linked issues, and any "Out of scope" notes. + +### 2. Sync local main + +``` +git fetch origin main --quiet +git checkout main +git pull --ff-only origin main +``` + +If `git pull` refuses (non-fast-forward), stop and tell the user — do +not force. + +### 3. Create the feature branch + +`git checkout -b // origin/main` + +Verify with `git rev-parse --abbrev-ref HEAD` that you're on the new +branch. + +### 4. Plan the work + +Before writing any code, read enough of the codebase to know: +- which files this issue touches (use `Glob`/`Grep`) +- what the change shape looks like (one paragraph mental summary) +- what tests need to be added or updated +- what docs/CHANGELOG need updating + +If the change is larger than ~200 lines or spans 3+ packages, surface +that to the user — `CONTRIBUTING.md` says "one logical change per PR" +and a too-large PR may need to be split into multiple issues. + +### 5. Implement + test + lint loop + +For each logical chunk of work: +- write the change +- `go build ./...` +- `go vet ./...` +- `go test ./...` (or scoped to the package) +- `gofmt -l .` (no diffs) +- commit with a Conventional Commits subject that matches the PR + title the user wants + +Subject must match `^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z]+\))?!?: .{1,100}$`. +Body lines ≤100 cols. Footer for `Refs #N` / `BREAKING CHANGE:` etc. + +Commit author should be the assistant's Co-Authored-By line per the +project's commit style. + +### 6. Run `make ci` + +`make ci` runs `tidy` + `vet` + `test` + `golangci-lint`. Must be green +before opening the PR. If `golangci-lint` complains, fix the lint +issues — don't suppress. + +### 7. Generate the PR body + +Use the template at `.github/PULL_REQUEST_TEMPLATE.md`. Fill in: +- `## Summary` — one or two sentences, end with `Closes #N` +- `## Linked issues` — `Closes #N` +- `## Type of change` — check the matching box +- `## Checklist` — leave unchecked only the ones the user hasn't done +- `## Scope notes` — anything intentionally left out +- `## Screenshots / output` — if user-facing output changed + +### 8. Push and open the PR + +``` +git push -u origin +gh pr create --base main --head \ + --title "" \ + --body-file /tmp/pr-body-.md +``` + +Capture the PR number from the output. + +### 9. Watch CI + +`gh pr checks --watch --fail-fast --interval 30` + +Don't poll in a loop. Don't run multiple `gh pr checks` in parallel. +If a check fails, read the failure log, fix it, push an amendment, +re-run. + +### 10. Address review comments + +If a reviewer (human or bot like Copilot) leaves comments on the PR, +read each one, decide whether to apply the fix or push back with a +rationale. Don't blindly accept suggestions — apply the ones that +improve the code, push back on the ones that don't. + +### 11. Squash-merge + +``` +gh pr merge --squash --delete-branch +``` + +If the repo's branch policy refuses, retry with `--admin` and tell +the user you used admin override. + +### 12. Verify the issue closed + +`gh issue view --json state,stateReason` + +If `stateReason == COMPLETED` and the linked PR shows in +`closedByPullRequestReferences`, you're done. If the issue is still +open, the PR body is missing `Closes #N` — fix the PR description +with `gh pr edit --body-file ...` and re-check. + +### 13. Sync local main and clean up + +``` +git checkout main +git pull --ff-only origin main +git branch -d # local cleanup +``` + +If `git branch -d` refuses (branch not fully merged), the squash-merge +didn't actually merge — go back to step 11. + +### 14. Recurse to the next open issue + +Find the next open issue (lowest number that is still OPEN and not +blocked). Tell the user what the next issue is and ask whether to +continue. + +## Failure modes to handle + +| Symptom | What to do | +|---|---| +| Issue title doesn't match `type(scope): …` | Ask user to rename it, OR pass `--type/--scope/--slug` explicitly | +| `git pull --ff-only` fails | Stop — `main` has diverged. Tell user. | +| `golangci-lint` reports a finding | Fix the code, don't suppress | +| `gh pr create` errors about base branch policy | The repo requires admin merge — use `--admin` on step 11 | +| Reviewer comment is wrong or out-of-scope | Push back politely with rationale in a reply | +| Squash-merge silently fails | Check `gh pr view --json state` — likely blocked by CI | +| Issue didn't auto-close | PR body missing `Closes #N`. Edit with `gh pr edit`. | +| `git branch -d` says branch not fully merged | Squash-merge didn't actually merge — investigate | + +## Output expectations + +When the skill completes successfully, summarize in one short message: + +``` +Issue #N closed via PR #M. +Branch: +Squash-merge: +Next open issue: # +Want me to start on it? +``` + +## Why this is a skill and not a bash script + +The old `scripts/issue-workflow.sh` was 265 lines of bash that: +- parsed Conventional Commits titles with bash regex (fragile) +- called `gh` directly (coupled contributor machines to GitHub auth) +- embedded `gh pr checks --watch` in a `sleep 600; kill $watch_pid` + pattern that crashed the IDE when paged output interleaved with + other terminal tabs +- made every contributor's shell the orchestration layer + +This skill moves orchestration into the AI: the assistant reads the +issue, plans the change, edits files, runs validation, opens the PR, +reads review comments, and merges. No contributor shell needed. +The shell is just the place commands execute, not the place workflows +are encoded. \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6e3241..1590f95 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,12 @@ node_modules/ package-lock.json # Tooling session metadata (not part of the project source tree). -.claude/ - -.conversation-history \ No newline at end of file +# `.claude/skills/` is intentionally NOT ignored — it holds +# project-local AI skills (e.g. issue-workflow) that should be +# committed so any contributor/clone picks them up. Only the rest +# of `.claude/` (e.g. session settings) is ignored. +.claude/* +!.claude/skills/ + +.conversation-history +.puku-cli diff --git a/CLAUDE.md b/CLAUDE.md index 170fcd6..8875d83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,16 @@ Enforced by commitlint (`wagoid/commitlint-github-action@v5`). Violations block When the user says **"start the next issue"** (or any equivalent — "work on the next one", "what's next", "pick up the next issue"), treat the following as the **default, no-clarification-needed workflow**: 1. **Identify the next issue.** Run `gh issue list --state open --limit 30`. Pick the lowest-numbered open issue that is not a meta-tracking issue, unless the user says otherwise. If only meta-issues remain (e.g. Phase 6 cross-platform polish — issue #16), split the meta-issue into focused sub-issues first (one per concern, one PR each), then start the first sub-issue. -2. **Always pass the issue number through `issue-workflow.sh`.** Use `./scripts/issue-workflow.sh start <issue#>` to branch, and `./scripts/issue-workflow.sh pr-body <issue#>` to render the PR body. The body includes `Closes #<issue#>` — **do not edit this out**. The `Closes` keyword is what auto-closes the issue when the PR is merged. +2. **Use the `issue-workflow` skill.** Invoke + [`.claude/skills/issue-workflow/SKILL.md`](./.claude/skills/issue-workflow/SKILL.md) + to drive the full branch → PR → merge → cleanup loop. The skill + creates one tracked `TaskCreate` per step (sync main, branch, plan, + implement+test, PR body, push, watch CI, address review, merge, + verify close, recurse). Don't re-implement the workflow inline — + the skill exists so the orchestration is consistent and reviewable. + The PR body the skill generates includes `Closes #<issue#>` — + **do not edit this out**. The `Closes` keyword is what auto-closes + the issue when the PR is merged. 3. **Verify the linkage before merging.** After `gh pr create`, run `gh pr view <PR#> --json body | grep -E 'Closes|Fixes' #<issue#>'` to confirm the PR body references the issue. If it does not, edit the PR body via `gh pr edit <PR#> --body-file <new-body.md>` before merging. **Do not squash-merge a PR that is not linked to its source issue.** 4. **Merge with admin override when needed.** This repo's `enforce_admins: false` means solo-author merges need `--admin`. The conventional sequence is: ```bash @@ -94,7 +103,10 @@ When the user says **"start the next issue"** (or any equivalent — "work on th 5. **Verify the issue auto-closed.** After the merge, run `gh issue view <issue#> --json state --jq .state` and confirm `CLOSED`. If still `OPEN`, add the issue close manually with `gh issue close <issue#> -c "Closed by <PR#>"`. 6. **Then recurse.** Re-run `gh issue list --state open --limit 30`. If another issue is queued, ask the user to confirm continuation or proceed (if the user already said "keep going", just proceed). -The `make next-issue` target wraps steps 1 and 2 for humans. AI sessions should drive the same script directly. +The `make next-issue` target still works for human contributors who +want a quick branch-off-main shortcut, but it now just prints the +reminder to use the skill — it does not shell out to a bash +orchestrator. ## Phase status diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73c28b4..9e6117f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,9 +83,11 @@ config.manager=<name> explicitly, or omit the key for auto-detect. fine (use `--force-with-lease`). 7. **Every PR must reference its source issue in the body** — use `Closes #N` (closing the issue on merge) or `Refs #N` (linked but - not auto-closed). The `scripts/issue-workflow.sh pr-body` template - fills this in automatically from the issue number; do not strip it. - A PR without an issue reference will be sent back for revision. + not auto-closed). The + [`.claude/skills/issue-workflow`](./.claude/skills/issue-workflow/SKILL.md) + skill fills this in automatically from the issue number when the + AI generates the PR body; do not strip it. A PR without an issue + reference will be sent back for revision. ## Local development diff --git a/Makefile b/Makefile index 52ea000..c19de59 100644 --- a/Makefile +++ b/Makefile @@ -96,20 +96,30 @@ verify: ci build ## Full pre-commit verification (CI + build) # Drive a single GitHub issue from branch → PR → merge. # Usage: make next-issue ISSUE=16 -# Requires: gh authed for dipto0321/nodeup, conventional-commits commit on HEAD, -# clean working tree. See scripts/issue-workflow.sh and [redacted].md -# ("AI workflow — standing orders") for the full sequence. +# For human contributors, this is a thin wrapper that prints the +# standing-orders reminder. The actual orchestration is in the +# `.claude/skills/issue-workflow/SKILL.md` skill for AI sessions, +# or driven manually by following CONTRIBUTING.md for humans. .PHONY: next-issue -next-issue: ## Drive the next open issue end-to-end (branch → PR → squash-merge) +next-issue: ## Print the standing-orders reminder for ISSUE; humans drive the workflow from CONTRIBUTING.md @if [ -z "$(ISSUE)" ]; then \ echo "usage: make next-issue ISSUE=<issue#>"; \ exit 1; \ fi - ./scripts/issue-workflow.sh start $(ISSUE) + @echo "Issue #$(ISSUE) — drive the workflow per CONTRIBUTING.md:" + @echo " 1. Sync main and create the branch:" + @echo " git fetch origin main && git checkout main && git pull --ff-only origin main" + @echo " git checkout -b <type>/<scope>/<slug> origin/main" + @echo " 2. Implement + test + lint, commit with a Conventional Commits subject" + @echo " make ci" + @echo " 3. Push and open the PR:" + @echo " git push -u origin <branch>" + @echo " gh pr create --base main --head <branch> --title '<subject>' --body-file /tmp/pr-body-$(ISSUE).md" + @echo " 4. Watch CI and merge:" + @echo " gh pr checks <PR#> --watch --fail-fast" + @echo " make finish-pr ISSUE=$(ISSUE) PR=<PR#>" @echo "" - @echo "Now: edit code, then commit with a conventional-commits subject." - @echo "When CI is green and you're ready to merge, run:" - @echo " make finish-pr ISSUE=$(ISSUE)" + @echo "AI sessions should invoke .claude/skills/issue-workflow/SKILL.md instead." # Squash-merge the PR linked to ISSUE. Use after the branch is pushed # and CI is green. Uses --admin because branch protection requires 1 diff --git a/scripts/README.md b/scripts/README.md index 77099bb..caee011 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,23 +1,36 @@ # scripts/ -Project-local automation. Currently: +Project-local automation. Currently empty — the `issue-workflow.sh` +shell script that used to live here was replaced by the +[`.claude/skills/issue-workflow/SKILL.md`](../.claude/skills/issue-workflow/SKILL.md) +skill, which drives a GitHub issue through branch → PR → merge → +cleanup using AI-orchestrated tasks instead of bash. -- `issue-workflow.sh` — drive a single GitHub issue through the full - branch → PR → merge → cleanup loop. Three subcommands: - - `start <issue#>` — sync `main` and create `<type>/<scope>/<slug>` off `origin/main` - - `pr-body <issue#>` — print a `.github/PULL_REQUEST_TEMPLATE.md`-shaped body - - `finish` — push, open PR, watch CI, squash-merge, delete branch, sync `main` +## Why a skill (and not a Makefile target or a shell script) - Enforces the conventions in `CONTRIBUTING.md` (Conventional Commits, - allowed types/scopes, one logical change per PR, squash-merge, - source-branch deletion). See `./issue-workflow.sh --help` for details. +The Makefile is for **build/test/lint/release** commands — anything +that produces artifacts you can ship. A shell script for the +branch → PR → merge loop is a poor fit because: -## Why a shell script (and not a Makefile target) +- It needs to parse Conventional Commits titles (fragile in bash ERE). +- It needs to call `gh` (couples the contributor's machine to GitHub + auth). +- It needs to watch CI and react to review comments — bash can't do + that without complex polling that paged output will eventually + break. +- The puku editor crashes when interleaved `gh` output is paged in + multiple terminal tabs (see `.conversation-history/.conversation-history-chunk4.md` + section 17 for the full diagnosis). -The Makefile is for **build/test/lint/release** commands — anything -that produces artifacts you can ship. `issue-workflow.sh` is a -**git/GitHub plumbing** helper: it talks to `gh`, reads PR titles, -force-pushes with `--force-with-lease`, and reshapes your local -checkout. Mixing those into `make` would couple the build to GitHub -auth and to `gh` being installed, which isn't true for every -contributor's machine. \ No newline at end of file +A skill moves the orchestration into the AI: the assistant reads the +issue, edits files, runs validation, opens the PR, reads review +comments, and merges. The shell just executes commands, it doesn't +encode the workflow. + +## When new scripts are appropriate + +Add a new `scripts/<name>.sh` only for **build/test/release plumbing** +that needs to run on machines without an AI in the loop — e.g. CI +helpers, release packaging steps, local bootstrap. For anything +involving GitHub interaction or sequential decision-making, write a +skill instead. \ No newline at end of file diff --git a/scripts/issue-workflow.sh b/scripts/issue-workflow.sh deleted file mode 100755 index 4690fad..0000000 --- a/scripts/issue-workflow.sh +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env bash -# IMPORTANT: must be interpreted by bash, not zsh. zsh's `[[ str =~ regex ]]` -# has different group-capture semantics for parenthesized alternation -# (e.g. ($ALLOWED_SCOPES)), which breaks parse_issue_title. Running under -# bash gives consistent POSIX ERE semantics regardless of the user's login -# shell. The shebang above handles that for `path/to/script.sh` invocation; -# do NOT `source` this file from zsh. -# scripts/issue-workflow.sh — drive a single GitHub issue from branch → merged PR. -# -# Subcommands: -# start <issue#> create a feature branch off main for the issue -# pr-body <issue#> [--type=...] [--scope=...] [--slug=...] -# print a PR body template, filled in -# finish push, open PR, squash-merge, delete branch, sync main -# -# Conventions enforced (per CONTRIBUTING.md): -# - Branch off main, keep main clean. -# - One logical change per PR. -# - Conventional Commits (`type(scope): subject`). -# - Squash-merged; source branch deleted on merge. -# - PR title and squash-merge subject come from the first commit. -# - Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert -# - Allowed scopes: detector, manager, packages, node, config, ui, platform, cli, -# deps, release, ci, docs, lint -# -# Usage examples: -# ./scripts/issue-workflow.sh start 15 -# # …edit code, commit with `git commit -m "feat(config): add yaml loader"` -# ./scripts/issue-workflow.sh pr-body 15 --type=feat --scope=config \ -# --slug=yaml-config > /tmp/pr.md -# gh pr create --base main --head feat/config/yaml-config \ -# --title "feat(config): add yaml loader" --body-file /tmp/pr.md -# ./scripts/issue-workflow.sh finish -# -# Requires: git, gh (authenticated for dipto0321/nodeup), make, golangci-lint v1.64.8. - -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$REPO_ROOT" - -# Regex-safe alternations: bash [[ =~ ]] uses ERE, so alternatives must be -# pipe-separated inside a group, not space-separated. -ALLOWED_TYPES_RE="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert" -ALLOWED_SCOPES_RE="detector|manager|packages|node|config|ui|platform|cli|deps|release|ci|docs|lint" -# Space-separated lists for word-splitting/iteration in cmd_pr_body. -ALLOWED_TYPES="feat fix docs style refactor perf test build ci chore revert" -ALLOWED_SCOPES="detector manager packages node config ui platform cli deps release ci docs lint" - -die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; } -info() { printf "\033[36m==>\033[0m %s\n" "$*"; } -ok() { printf "\033[32m✓\033[0m %s\n" "$*"; } - -require_clean_tree() { - if ! git diff --quiet --ignore-submodules HEAD 2>/dev/null; then - die "working tree is dirty. Commit or stash first." - fi -} - -require_main_clean() { - git rev-parse --abbrev-ref HEAD | grep -qx main \ - || die "run this from 'main' (currently on '$(git rev-parse --abbrev-ref HEAD)')" -} - -ensure_gh_authed() { - gh auth status >/dev/null 2>&1 \ - || die "gh is not authenticated. Run: gh auth login" -} - -fetch_issue_meta() { - local issue="$1" - gh issue view "$issue" --json title,state,labels,body 2>/dev/null \ - || die "could not fetch issue #$issue. Is gh authed for this repo?" -} - -# Heuristic: derive (type, scope, slug) from an issue title like -# "feat(config): add yaml config file support". -parse_issue_title() { - local title="$1" - if [[ "$title" =~ ^($ALLOWED_TYPES_RE)\(($ALLOWED_SCOPES_RE)\):\ (.+)$ ]]; then - local type="${BASH_REMATCH[1]}" - local scope="${BASH_REMATCH[2]}" - local rest="${BASH_REMATCH[3]}" - # slugify: lowercase, replace non-alnum with '-', trim leading/trailing '-' - local slug - slug="$(printf "%s" "$rest" | tr '[:upper:]' '[:lower:]' \ - | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')" - printf "%s %s %s\n" "$type" "$scope" "$slug" - elif [[ "$title" =~ ^($ALLOWED_TYPES_RE):\ (.+)$ ]]; then - # type without scope: scope defaults to "core" - local type="${BASH_REMATCH[1]}" - local rest="${BASH_REMATCH[2]}" - local slug - slug="$(printf "%s" "$rest" | tr '[:upper:]' '[:lower:]' \ - | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')" - printf "%s %s %s\n" "$type" "core" "$slug" - else - # Fall back: caller must pass --type/--scope/--slug explicitly. - printf " \n" - fi -} - -cmd_start() { - local issue="${1:-}" - [[ -n "$issue" ]] || die "usage: issue-workflow.sh start <issue#>" - - require_main_clean - require_clean_tree - ensure_gh_authed - - info "fetching issue #$issue" - local meta title state - meta="$(fetch_issue_meta "$issue")" - title="$(printf "%s" "$meta" | python3 -c "import sys,json; print(json.load(sys.stdin)['title'])")" - state="$(printf "%s" "$meta" | python3 -c "import sys,json; print(json.load(sys.stdin)['state'])")" - [[ "$state" == "OPEN" ]] || die "issue #$issue is $state, not OPEN" - - local parsed type scope slug branch - parsed="$(parse_issue_title "$title")" - type="$(awk '{print $1}' <<<"$parsed")" - scope="$(awk '{print $2}' <<<"$parsed")" - slug="$(awk '{print $3}' <<<"$parsed")" - - if [[ -z "$type" || -z "$scope" || -z "$slug" ]]; then - die "issue title '$title' does not match '<type>(<scope>): <slug>'. " \ - "Pass --type/--scope/--slug explicitly, or rename the issue." - fi - - branch="${type}/${scope}/${slug}" - info "syncing main" - git fetch origin main --quiet - git pull --ff-only origin main - - info "creating branch '$branch' from origin/main" - git checkout -b "$branch" origin/main - - ok "branch '$branch' is ready. Commit work with: git commit -m '$type($scope): ...'" - printf "\nNext step:\n ./scripts/issue-workflow.sh pr-body %s --type=%s --scope=%s --slug=%s\n" \ - "$issue" "$type" "$scope" "$slug" -} - -cmd_pr_body() { - local issue="" type="" scope="" slug="" - while [[ $# -gt 0 ]]; do - case "$1" in - --type=*) type="${1#*=}"; shift ;; - --scope=*) scope="${1#*=}"; shift ;; - --slug=*) slug="${1#*=}"; shift ;; - --*) die "unknown flag: $1" ;; - *) - [[ -z "$issue" ]] && issue="$1" || die "unexpected extra arg: $1" - shift - ;; - esac - done - [[ -n "$issue" ]] || die "usage: issue-workflow.sh pr-body <issue#> [--type=...] [--scope=...] [--slug=...]" - - if [[ -z "$type$scope$slug" ]]; then - local meta title - meta="$(fetch_issue_meta "$issue")" - title="$(printf "%s" "$meta" | python3 -c "import sys,json; print(json.load(sys.stdin)['title'])")" - local parsed - parsed="$(parse_issue_title "$title")" - type="${type:-$(awk '{print $1}' <<<"$parsed")}" - scope="${scope:-$(awk '{print $2}' <<<"$parsed")}" - slug="${slug:-$(awk '{print $3}' <<<"$parsed")}" - fi - [[ -n "$type$scope$slug" ]] || die "could not derive type/scope/slug — pass them explicitly" - - cat <<EOF -## Summary - -<!-- One or two sentences. What does this PR change and why? Closes #$issue. --> - -## Linked issues - -Closes #$issue - -## Type of change - -- [x] \`$type\` — see the title; check the matching box below -$(for t in $ALLOWED_TYPES; do [[ "$t" != "$type" ]] && printf " - [ ] \`%s\`\n" "$t"; done) - -## Checklist - -- [x] Title follows Conventional Commits (\`$type($scope): subject\`) -- [x] I ran \`make ci\` locally and it passes -- [x] I added or updated tests for the change -- [x] I updated relevant docs (README, \`docs/\`, inline godoc) -- [x] No new linter warnings -- [ ] If breaking: I documented the migration path in the PR body and updated CHANGELOG.md - -## Scope notes / things reviewers may want to look at - -<!-- Anything you intentionally did NOT do, or that future PRs should pick up. --> - -## Screenshots / output - -<!-- If the PR changes user-facing output, paste before/after. --> -EOF -} - -cmd_finish() { - local current_branch - current_branch="$(git rev-parse --abbrev-ref HEAD)" - [[ "$current_branch" != "main" ]] || die "finish must be run from a feature branch (you are on 'main')" - require_clean_tree - ensure_gh_authed - - # Sanity: at least one commit ahead of main. - local ahead behind - ahead="$(git rev-list --count origin/main..HEAD)" - behind="$(git rev-list --count HEAD..origin/main)" - [[ "$ahead" -ge 1 ]] || die "no commits ahead of origin/main. Nothing to PR." - info "branch is $ahead commit(s) ahead of main, $behind behind" - - # Sanity: conventional-commits subject on HEAD. - local subject - subject="$(git log -1 --pretty=%s)" - if ! [[ "$subject" =~ ^($ALLOWED_TYPES_RE)\(($ALLOWED_SCOPES_RE)\):\ ]]; then - die "HEAD subject '$subject' does not match conventional-commits pattern. " \ - "Fix with: git commit --amend -m '$subject'" - fi - - info "pushing '$current_branch' to origin" - git push -u origin "$current_branch" - - info "opening PR (base=main, head=$current_branch)" - gh pr create --base main --head "$current_branch" \ - --title "$subject" --body-file <(./scripts/issue-workflow.sh pr-body 0 \ - --type="$(awk -F'[(]' '{print $1}' <<<"$subject")" \ - --scope="$(awk -F'[()]' '{print $2}' <<<"$subject")" \ - --slug="") \ - || die "gh pr create failed — fill in the body manually and retry" - - local pr_number - pr_number="$(gh pr view "$current_branch" --json number --jq .number)" - info "PR #$pr_number opened. Waiting for CI…" - - # Wait for required checks (best-effort; 10-min cap). - if gh pr checks "$pr_number" --watch --fail-fast --interval 30 >/dev/null 2>&1 & - local watch_pid=$!; sleep 600; kill $watch_pid 2>/dev/null; wait 2>/dev/null; then :; fi - - info "squash-merging PR #$pr_number and deleting '$current_branch'" - gh pr merge "$pr_number" --squash --delete-branch \ - --body "Closes the linked issue. Squash-merged per CONTRIBUTING.md." \ - || die "merge failed — finish manually with: gh pr merge $pr_number --squash --delete-branch" - - info "syncing local main" - git checkout main - git pull --ff-only origin main - - ok "PR #$pr_number merged. '$current_branch' deleted. main is up to date." -} - -case "${1:-}" in - start) shift; cmd_start "$@" ;; - pr-body) shift; cmd_pr_body "$@" ;; - finish) shift; cmd_finish "$@" ;; - -h|--help|help|"") - sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' - ;; - *) die "unknown subcommand: $1 (try: start | pr-body | finish)" ;; -esac