diff --git a/ship/sections/pr-body.md b/ship/sections/pr-body.md index 90888b7a9..4be361e90 100644 --- a/ship/sections/pr-body.md +++ b/ship/sections/pr-body.md @@ -41,13 +41,20 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body-file "$PR_BODY_FILE"` (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. **Run the same redaction scan-at-sink (PR body + title) as the create path (Step 19) before editing — scan the temp file, then `gh pr edit --body-file` from it.** +If an **open** PR/MR already exists: **update** the PR body using GitHub's REST API (`gh api -X PATCH "repos/$REPO_NWO/pulls/$PR_NUMBER" --input -`) or `glab mr update -d ...` (GitLab). Do not use `gh pr edit` for GitHub updates; it can hit deprecated Projects-classic GraphQL fields on some repos. 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. **Run the same redaction scan-at-sink (PR body + title) as the create path (Step 19) before editing — scan the temp file, then send those exact bytes through the REST PATCH path.** **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"`). +3. If `NEW_TITLE` differs from `CURRENT`, update GitHub via REST: + ```bash + PR_NUMBER=$(gh pr view --json number -q .number) + REPO_NWO=$(gh repo view --json nameWithOwner -q .nameWithOwner) + jq -n --arg title "$NEW_TITLE" '{title:$title}' \ + | gh api -X PATCH "repos/$REPO_NWO/pulls/$PR_NUMBER" --input - + ``` + For GitLab, run `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. @@ -177,7 +184,17 @@ printf '%s' "v$NEW_VERSION : " | ~/.claude/skills/gstack/bin/gsta ``` HIGH blocks (exit 3, no skip). MEDIUM → AskUserQuestion (PII subset offers -`--auto-redact`). Same scan runs before the `gh pr edit --body` path (Step 17). +`--auto-redact`). Same scan runs before the GitHub REST PATCH body path (Step 17). + +**If a GitHub PR already exists:** update from the SCANNED file with REST (exact bytes scanned = bytes sent): + +```bash +PR_NUMBER=$(gh pr view --json number -q .number) +REPO_NWO=$(gh repo view --json nameWithOwner -q .nameWithOwner) +jq -n --rawfile body "$PR_BODY_FILE" '{body:$body}' \ + | gh api -X PATCH "repos/$REPO_NWO/pulls/$PR_NUMBER" --input - +rm -f "$PR_BODY_FILE" +``` **If GitHub:** create from the SCANNED file (exact bytes scanned = bytes sent): diff --git a/ship/sections/pr-body.md.tmpl b/ship/sections/pr-body.md.tmpl index 22ac0d668..188d7b96c 100644 --- a/ship/sections/pr-body.md.tmpl +++ b/ship/sections/pr-body.md.tmpl @@ -39,13 +39,20 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body-file "$PR_BODY_FILE"` (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. **Run the same redaction scan-at-sink (PR body + title) as the create path (Step 19) before editing — scan the temp file, then `gh pr edit --body-file` from it.** +If an **open** PR/MR already exists: **update** the PR body using GitHub's REST API (`gh api -X PATCH "repos/$REPO_NWO/pulls/$PR_NUMBER" --input -`) or `glab mr update -d ...` (GitLab). Do not use `gh pr edit` for GitHub updates; it can hit deprecated Projects-classic GraphQL fields on some repos. 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. **Run the same redaction scan-at-sink (PR body + title) as the create path (Step 19) before editing — scan the temp file, then send those exact bytes through the REST PATCH path.** **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"`). +3. If `NEW_TITLE` differs from `CURRENT`, update GitHub via REST: + ```bash + PR_NUMBER=$(gh pr view --json number -q .number) + REPO_NWO=$(gh repo view --json nameWithOwner -q .nameWithOwner) + jq -n --arg title "$NEW_TITLE" '{title:$title}' \ + | gh api -X PATCH "repos/$REPO_NWO/pulls/$PR_NUMBER" --input - + ``` + For GitLab, run `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. @@ -175,7 +182,17 @@ printf '%s' "v$NEW_VERSION : " | ~/.claude/skills/gstack/bin/gsta ``` HIGH blocks (exit 3, no skip). MEDIUM → AskUserQuestion (PII subset offers -`--auto-redact`). Same scan runs before the `gh pr edit --body` path (Step 17). +`--auto-redact`). Same scan runs before the GitHub REST PATCH body path (Step 17). + +**If a GitHub PR already exists:** update from the SCANNED file with REST (exact bytes scanned = bytes sent): + +```bash +PR_NUMBER=$(gh pr view --json number -q .number) +REPO_NWO=$(gh repo view --json nameWithOwner -q .nameWithOwner) +jq -n --rawfile body "$PR_BODY_FILE" '{body:$body}' \ + | gh api -X PATCH "repos/$REPO_NWO/pulls/$PR_NUMBER" --input - +rm -f "$PR_BODY_FILE" +``` **If GitHub:** create from the SCANNED file (exact bytes scanned = bytes sent): diff --git a/test/helpers/carve-guards.ts b/test/helpers/carve-guards.ts index 127d7fbae..cd49922c5 100644 --- a/test/helpers/carve-guards.ts +++ b/test/helpers/carve-guards.ts @@ -119,7 +119,7 @@ export const CARVE_GUARDS: Record = { // ...while the full create/update procedure stays carved into pr-body.md // (out of the skeleton, present in the union). Asserts BOTH PR paths // survive: the create path and the idempotent update path. - mustMoveToSection: ['gh pr create --base', 'gh pr edit --title'], + mustMoveToSection: ['gh pr create --base', 'gh api -X PATCH'], // ship is operational (multi-STOP, not a plan review); no single post-STOP gate. gateAfterStop: undefined, }, diff --git a/test/ship-template-redaction.test.ts b/test/ship-template-redaction.test.ts index 030d26c1e..f3f02cf69 100644 --- a/test/ship-template-redaction.test.ts +++ b/test/ship-template-redaction.test.ts @@ -32,8 +32,9 @@ describe("/ship redaction wiring", () => { test("creates from the scanned temp file (exact bytes)", () => { expect(TMPL).toMatch(/gh pr create[\s\S]{0,120}--body-file "\$PR_BODY_FILE"/); }); - test("edit path also scans before sending", () => { - expect(TMPL).toMatch(/gh pr edit --body-file "\$PR_BODY_FILE"/); + test("REST edit path also scans before sending", () => { + expect(TMPL).toMatch(/gh api -X PATCH "repos\/\$REPO_NWO\/pulls\/\$PR_NUMBER" --input -/); + expect(TMPL).not.toContain("gh pr edit --body-file"); expect(TMPL).toMatch(/same redaction scan-at-sink.*before editing/i); }); test("HIGH blocks the PR (exit 3), no skip", () => {