From 08749372bee2a93963cfb221211071c952db1e95 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 30 Apr 2026 23:05:50 -0700 Subject: [PATCH] feat(skills): /ship and /document-release always prefix PR titles with v MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ship/SKILL.md.tmpl Step 19: idempotency block now always rewrites titles to start with v$NEW_VERSION via the new helper. Removes the "custom title kept intentionally" loophole that let unprefixed titles persist forever. Adds a post-edit self-check that re-fetches the title and retries once if the edit didn't stick. Inline comments on the create-PR snippets at lines 867 and 876 make the rule unmissable. document-release/SKILL.md.tmpl Step 9: new "PR/MR title sync" sub-step calls the same helper after the body update. Catches the case where Step 8 bumped VERSION after /ship had already created the PR — title now follows VERSION instead of going stale. Golden fixtures regenerated for claude/codex/factory ship variants. Co-Authored-By: Claude Opus 4.7 --- document-release/SKILL.md | 48 ++++++++++++++++++++++ document-release/SKILL.md.tmpl | 48 ++++++++++++++++++++++ ship/SKILL.md | 13 +++++- ship/SKILL.md.tmpl | 13 +++++- test/fixtures/golden/claude-ship-SKILL.md | 13 +++++- test/fixtures/golden/codex-ship-SKILL.md | 13 +++++- test/fixtures/golden/factory-ship-SKILL.md | 13 +++++- 7 files changed, 156 insertions(+), 5 deletions(-) diff --git a/document-release/SKILL.md b/document-release/SKILL.md index 7d049b19..575c3501 100644 --- a/document-release/SKILL.md +++ b/document-release/SKILL.md @@ -1018,6 +1018,54 @@ rm -f /tmp/gstack-pr-body-$$.md 7. If `gh pr edit` / `glab mr update` fails: warn "Could not update PR/MR body — documentation changes are in the commit." and continue. +**PR/MR title sync (idempotent, always-on):** + +PR titles must always start with `v` — same rule as `/ship`. If Step 8 bumped VERSION after `/ship` had already created the PR, the title is now stale. This sub-step fixes it. + +1. Read the current VERSION: + +```bash +V=$(cat VERSION 2>/dev/null | tr -d '[:space:]') +``` + +If `VERSION` does not exist or is empty, skip this sub-step entirely. + +2. Read the current PR/MR title: + +**If GitHub:** +```bash +CURRENT_TITLE=$(gh pr view --json title -q .title 2>/dev/null || true) +``` + +**If GitLab:** +```bash +CURRENT_TITLE=$(glab mr view -F json 2>/dev/null | jq -r .title 2>/dev/null || true) +``` + +If `CURRENT_TITLE` is empty (no open PR/MR), skip with message "No PR/MR found — skipping title sync." + +3. Compute the corrected title using the shared helper (single source of truth — same one `/ship` uses): + +```bash +NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$V" "$CURRENT_TITLE") +``` + +The helper handles three cases: title already correct (no-op), title has a different `v` prefix (replace it), or title has no version prefix (prepend one). + +4. If `NEW_TITLE` differs from `CURRENT_TITLE`, update it: + +**If GitHub:** +```bash +gh pr edit --title "$NEW_TITLE" +``` + +**If GitLab:** +```bash +glab mr update -t "$NEW_TITLE" +``` + +5. If the edit command fails: warn "Could not update PR/MR title — documentation changes are still in the commit." and continue. Do not block on title sync failure. + **Structured doc health summary (final output):** Output a scannable summary showing every documentation file's status: diff --git a/document-release/SKILL.md.tmpl b/document-release/SKILL.md.tmpl index 0fd08eac..8e2b7059 100644 --- a/document-release/SKILL.md.tmpl +++ b/document-release/SKILL.md.tmpl @@ -342,6 +342,54 @@ rm -f /tmp/gstack-pr-body-$$.md 7. If `gh pr edit` / `glab mr update` fails: warn "Could not update PR/MR body — documentation changes are in the commit." and continue. +**PR/MR title sync (idempotent, always-on):** + +PR titles must always start with `v` — same rule as `/ship`. If Step 8 bumped VERSION after `/ship` had already created the PR, the title is now stale. This sub-step fixes it. + +1. Read the current VERSION: + +```bash +V=$(cat VERSION 2>/dev/null | tr -d '[:space:]') +``` + +If `VERSION` does not exist or is empty, skip this sub-step entirely. + +2. Read the current PR/MR title: + +**If GitHub:** +```bash +CURRENT_TITLE=$(gh pr view --json title -q .title 2>/dev/null || true) +``` + +**If GitLab:** +```bash +CURRENT_TITLE=$(glab mr view -F json 2>/dev/null | jq -r .title 2>/dev/null || true) +``` + +If `CURRENT_TITLE` is empty (no open PR/MR), skip with message "No PR/MR found — skipping title sync." + +3. Compute the corrected title using the shared helper (single source of truth — same one `/ship` uses): + +```bash +NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$V" "$CURRENT_TITLE") +``` + +The helper handles three cases: title already correct (no-op), title has a different `v` prefix (replace it), or title has no version prefix (prepend one). + +4. If `NEW_TITLE` differs from `CURRENT_TITLE`, update it: + +**If GitHub:** +```bash +gh pr edit --title "$NEW_TITLE" +``` + +**If GitLab:** +```bash +glab mr update -t "$NEW_TITLE" +``` + +5. If the edit command fails: warn "Could not update PR/MR title — documentation changes are still in the commit." and continue. Do not block on title sync failure. + **Structured doc health summary (final output):** Output a scannable summary showing every documentation file's status: diff --git a/ship/SKILL.md b/ship/SKILL.md index 1030ef99..9a884b14 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -2760,7 +2760,14 @@ glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. -**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. +**Always update the PR title to start with `v$NEW_VERSION`.** PR titles use the workspace-aware format `v : ` — version ALWAYS first, no exceptions, no "custom title kept intentionally" escape hatch. The shared helper `bin/gstack-pr-title-rewrite.sh` is the single source of truth for the rule. + +1. Read the current title: `CURRENT=$(gh pr view --json title -q .title)` (or `glab mr view -F json | jq -r .title`). +2. Compute the corrected title: `NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$NEW_VERSION" "$CURRENT")`. The helper handles three cases: title already correct (no-op), title has a different `v` prefix (replace it), or title has no version prefix (prepend one). +3. If `NEW_TITLE` differs from `CURRENT`, run `gh pr edit --title "$NEW_TITLE"` (or `glab mr update -t "$NEW_TITLE"`). +4. **Self-check:** re-fetch the title and assert it starts with `v$NEW_VERSION `. If it does not, retry the edit once. If still wrong, surface the failure to the user. + +This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version, and forces the format on PRs that were created without it. Print the existing URL and continue to Step 20. @@ -2830,6 +2837,8 @@ you missed it.> **If GitHub:** ```bash +# PR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF @@ -2839,6 +2848,8 @@ EOF **If GitLab:** ```bash +# MR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index b6a19bcb..470068fd 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -794,7 +794,14 @@ glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. -**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. +**Always update the PR title to start with `v$NEW_VERSION`.** PR titles use the workspace-aware format `v : ` — version ALWAYS first, no exceptions, no "custom title kept intentionally" escape hatch. The shared helper `bin/gstack-pr-title-rewrite.sh` is the single source of truth for the rule. + +1. Read the current title: `CURRENT=$(gh pr view --json title -q .title)` (or `glab mr view -F json | jq -r .title`). +2. Compute the corrected title: `NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$NEW_VERSION" "$CURRENT")`. The helper handles three cases: title already correct (no-op), title has a different `v` prefix (replace it), or title has no version prefix (prepend one). +3. If `NEW_TITLE` differs from `CURRENT`, run `gh pr edit --title "$NEW_TITLE"` (or `glab mr update -t "$NEW_TITLE"`). +4. **Self-check:** re-fetch the title and assert it starts with `v$NEW_VERSION `. If it does not, retry the edit once. If still wrong, surface the failure to the user. + +This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version, and forces the format on PRs that were created without it. Print the existing URL and continue to Step 20. @@ -864,6 +871,8 @@ you missed it.> **If GitHub:** ```bash +# PR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF @@ -873,6 +882,8 @@ EOF **If GitLab:** ```bash +# MR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF diff --git a/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md index 1030ef99..9a884b14 100644 --- a/test/fixtures/golden/claude-ship-SKILL.md +++ b/test/fixtures/golden/claude-ship-SKILL.md @@ -2760,7 +2760,14 @@ glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. -**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. +**Always update the PR title to start with `v$NEW_VERSION`.** PR titles use the workspace-aware format `v : ` — version ALWAYS first, no exceptions, no "custom title kept intentionally" escape hatch. The shared helper `bin/gstack-pr-title-rewrite.sh` is the single source of truth for the rule. + +1. Read the current title: `CURRENT=$(gh pr view --json title -q .title)` (or `glab mr view -F json | jq -r .title`). +2. Compute the corrected title: `NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$NEW_VERSION" "$CURRENT")`. The helper handles three cases: title already correct (no-op), title has a different `v` prefix (replace it), or title has no version prefix (prepend one). +3. If `NEW_TITLE` differs from `CURRENT`, run `gh pr edit --title "$NEW_TITLE"` (or `glab mr update -t "$NEW_TITLE"`). +4. **Self-check:** re-fetch the title and assert it starts with `v$NEW_VERSION `. If it does not, retry the edit once. If still wrong, surface the failure to the user. + +This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version, and forces the format on PRs that were created without it. Print the existing URL and continue to Step 20. @@ -2830,6 +2837,8 @@ you missed it.> **If GitHub:** ```bash +# PR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF @@ -2839,6 +2848,8 @@ EOF **If GitLab:** ```bash +# MR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index 40a03b38..32d68710 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -2375,7 +2375,14 @@ glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. -**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. +**Always update the PR title to start with `v$NEW_VERSION`.** PR titles use the workspace-aware format `v : ` — version ALWAYS first, no exceptions, no "custom title kept intentionally" escape hatch. The shared helper `bin/gstack-pr-title-rewrite.sh` is the single source of truth for the rule. + +1. Read the current title: `CURRENT=$(gh pr view --json title -q .title)` (or `glab mr view -F json | jq -r .title`). +2. Compute the corrected title: `NEW_TITLE=$($GSTACK_ROOT/bin/gstack-pr-title-rewrite.sh "$NEW_VERSION" "$CURRENT")`. The helper handles three cases: title already correct (no-op), title has a different `v` prefix (replace it), or title has no version prefix (prepend one). +3. If `NEW_TITLE` differs from `CURRENT`, run `gh pr edit --title "$NEW_TITLE"` (or `glab mr update -t "$NEW_TITLE"`). +4. **Self-check:** re-fetch the title and assert it starts with `v$NEW_VERSION `. If it does not, retry the edit once. If still wrong, surface the failure to the user. + +This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version, and forces the format on PRs that were created without it. Print the existing URL and continue to Step 20. @@ -2445,6 +2452,8 @@ you missed it.> **If GitHub:** ```bash +# PR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF @@ -2454,6 +2463,8 @@ EOF **If GitLab:** ```bash +# MR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index c361b59c..8b8e479e 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -2751,7 +2751,14 @@ glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. -**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. +**Always update the PR title to start with `v$NEW_VERSION`.** PR titles use the workspace-aware format `v : ` — version ALWAYS first, no exceptions, no "custom title kept intentionally" escape hatch. The shared helper `bin/gstack-pr-title-rewrite.sh` is the single source of truth for the rule. + +1. Read the current title: `CURRENT=$(gh pr view --json title -q .title)` (or `glab mr view -F json | jq -r .title`). +2. Compute the corrected title: `NEW_TITLE=$($GSTACK_ROOT/bin/gstack-pr-title-rewrite.sh "$NEW_VERSION" "$CURRENT")`. The helper handles three cases: title already correct (no-op), title has a different `v` prefix (replace it), or title has no version prefix (prepend one). +3. If `NEW_TITLE` differs from `CURRENT`, run `gh pr edit --title "$NEW_TITLE"` (or `glab mr update -t "$NEW_TITLE"`). +4. **Self-check:** re-fetch the title and assert it starts with `v$NEW_VERSION `. If it does not, retry the edit once. If still wrong, surface the failure to the user. + +This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version, and forces the format on PRs that were created without it. Print the existing URL and continue to Step 20. @@ -2821,6 +2828,8 @@ you missed it.> **If GitHub:** ```bash +# PR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF @@ -2830,6 +2839,8 @@ EOF **If GitLab:** ```bash +# MR title MUST start with v$NEW_VERSION — enforced on every run, no exceptions. +# (See Step 19 idempotency block + bin/gstack-pr-title-rewrite.sh for the rule.) glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF