mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-10 20:07:49 +02:00
d8c91c6267
* fix(ship): restore always-loaded PR-title-version invariant to skeleton
The v1.54.0.0 carve moved the 'PR title MUST start with v$NEW_VERSION' rule
out of the always-loaded ship skeleton and entirely into the lazily-loaded
pr-body.md section. The agent only set the version prefix if it happened to
read that section before creating the PR, so PRs landed with bare titles.
Restore a one-line invariant (+ helper reference) to ship/SKILL.md.tmpl right
before the {{SECTION:pr-body}} pointer, mirroring the AUQ always-loaded
precedent. Full procedure stays sectioned. Regenerated all hosts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ship): guard PR-title-version rule + pull_request_target safety
Two free gate tests so a future carve or workflow refactor can't silently
regress:
- ship-pr-title-version-always-loaded: asserts the invariant lives in the
always-loaded ship/SKILL.md skeleton (not only sections/), and that the
skeleton+sections union keeps BOTH the create and the existing-PR update
title paths. Modeled on test/auq-format-always-loaded.test.ts.
- pr-title-sync-workflow-safety: static tripwire that fails CI if
pr-title-sync.yml checks out PR-head code or inlines an attacker-controlled
${{ github.event.pull_request.* }} field inside a run: block (the two
pull_request_target footguns actionlint cannot catch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): pr-title-sync covers fork PRs via hardened pull_request_target
Under plain pull_request the GITHUB_TOKEN is read-only on fork PRs, so the
title-sync backstop could never edit a fork/agent PR title. Switch to
pull_request_target (write token in base context) and make it safe:
- Check out the base repo only (no ref:) — execute trusted infra, never
fork-head code.
- All attacker-controlled PR fields (title, head repo, head sha) pass via
env: and are referenced as shell-quoted "$VAR", never inlined into run:.
- Read the PR-head VERSION as data (raw media type) from the head repo at the
head sha; guard the assignment under set -e.
- Same-repo read failure fails loudly; fork miss warns and skips (the backstop
stays green without going silently optional).
- Never echo the raw fork title (Actions parses ::workflow-command:: from stdout).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ship): expand binDir path in pr-body Linked Spec block
ship/sections/pr-body.md.tmpl:98-99 used ${ctx.paths.binDir}, but the
gen-skill-docs generator only resolves {{TOKEN}} syntax in .tmpl files — the
${...} JS-template-literal form is substituted only inside .ts resolver files.
So the token passed through literally into the generated pr-body.md, leaving the
agent with an unexpandable ${ctx.paths.binDir}/gstack-paths command in the
Linked Spec auto-detect block. Use the hardcoded helper path, consistent with
every other path reference in this section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(test): fold ship PR-title skeleton guard into carve-guard registry
main shipped a generalized carve-guard system (PR #1907) that is now the single
source of truth for carved-skill skeleton invariants. Register the PR-title rule
there instead of a standalone test: ship's mustStayInSkeleton asserts v$NEW_VERSION
+ the rewrite helper stay always-loaded, and mustMoveToSection asserts both the
create and update PR paths stay carved into pr-body.md (present in the union, out of
the skeleton). Delete the standalone ship-pr-title-version-always-loaded test it
replaces. The CI-workflow safety tripwire stays standalone (not a carve concern).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v1.57.3.0)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
206 lines
11 KiB
Cheetah
206 lines
11 KiB
Cheetah
## Step 18: Documentation sync (via subagent, before PR creation)
|
|
|
|
**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation.
|
|
|
|
**Sequencing:** This step runs AFTER Step 17 (Push) and BEFORE Step 19 (Create PR). The PR is created once from final HEAD with the `## Documentation` section baked into the initial body. No create-then-re-edit dance.
|
|
|
|
**Subagent prompt:**
|
|
|
|
> You are executing the /document-release workflow after a code push. Read the full skill file `${HOME}/.claude/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: `<branch>`, base: `<base>`.
|
|
>
|
|
> After completing the workflow, output a single JSON object on the LAST LINE of your response (no other text after it):
|
|
> `{"files_updated":["README.md","CLAUDE.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":"<markdown block for PR body's ## Documentation section>"}`
|
|
>
|
|
> If no documentation files needed updating, output:
|
|
> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null}`
|
|
|
|
**Parent processing:**
|
|
|
|
1. Parse the LAST line of the subagent's output as JSON.
|
|
2. Store `documentation_section` — Step 19 embeds it in the PR body (or omits the section if null).
|
|
3. If `files_updated` is non-empty, print: `Documentation synced: {files_updated.length} files updated, committed as {commit_sha}`.
|
|
4. If `files_updated` is empty, print: `Documentation is current — no updates needed.`
|
|
|
|
**If the subagent fails or returns invalid JSON:** Print a warning and proceed to Step 19 without a `## Documentation` section. Do not block /ship on subagent failure. The user can run `/document-release` manually after the PR lands.
|
|
|
|
---
|
|
|
|
## Step 19: Create PR/MR
|
|
|
|
**Idempotency check:** Check if a PR/MR already exists for this branch.
|
|
|
|
**If GitHub:**
|
|
```bash
|
|
gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): \(.url)" else "NO_PR" end' 2>/dev/null || echo "NO_PR"
|
|
```
|
|
|
|
**If GitLab:**
|
|
```bash
|
|
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.**
|
|
|
|
**Always update the PR title to start with `v$NEW_VERSION`.** PR titles use the workspace-aware format `v<NEW_VERSION> <type>: <summary>` — 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<X.Y.Z.W>` 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.
|
|
|
|
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
|
|
|
The PR/MR body should contain these sections:
|
|
|
|
```
|
|
## Summary
|
|
<Summarize ALL changes being shipped. Run `git log <base>..HEAD --oneline` to enumerate
|
|
every commit. Exclude the VERSION/CHANGELOG metadata commit (that's this PR's bookkeeping,
|
|
not a substantive change). Group the remaining commits into logical sections (e.g.,
|
|
"**Performance**", "**Dead Code Removal**", "**Infrastructure**"). Every substantive commit
|
|
must appear in at least one section. If a commit's work isn't reflected in the summary,
|
|
you missed it.>
|
|
|
|
## Test Coverage
|
|
<coverage diagram from Step 7, or "All new code paths have test coverage.">
|
|
<If Step 7 ran: "Tests: {before} → {after} (+{delta} new)">
|
|
|
|
## Pre-Landing Review
|
|
<findings from Step 9 code review, or "No issues found.">
|
|
|
|
## Design Review
|
|
<If design review ran: "Design Review (lite): N findings — M auto-fixed, K skipped. AI Slop: clean/N issues.">
|
|
<If no frontend files changed: "No frontend files changed — design review skipped.">
|
|
|
|
## Eval Results
|
|
<If evals ran: suite names, pass/fail counts, cost dashboard summary. If skipped: "No prompt-related files changed — evals skipped.">
|
|
|
|
## Greptile Review
|
|
<If Greptile comments were found: bullet list with [FIXED] / [FALSE POSITIVE] / [ALREADY FIXED] tag + one-line summary per comment>
|
|
<If no Greptile comments found: "No Greptile comments.">
|
|
<If no PR existed during Step 10: omit this section entirely>
|
|
|
|
## Scope Drift
|
|
<If scope drift ran: "Scope Check: CLEAN" or list of drift/creep findings>
|
|
<If no scope drift: omit this section>
|
|
|
|
## Plan Completion
|
|
<If plan file found: completion checklist summary from Step 8>
|
|
<If no plan file: "No plan file detected.">
|
|
<If plan items deferred: list deferred items>
|
|
|
|
## Linked Spec
|
|
<Auto-detect: look for /spec archives matching this branch via:
|
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug)"
|
|
CURRENT_BRANCH=$(git branch --show-current)
|
|
SPEC_ARCHIVES="$GSTACK_STATE_ROOT/projects/$SLUG/specs"
|
|
# Find newest archive whose spec_branch frontmatter matches current branch (or one of its
|
|
# parents — if spec spawned worktree spec/<slug>-$$, the spawned worktree IS where /ship runs).
|
|
SPEC_FILE=$(grep -l "^spec_branch: $CURRENT_BRANCH$" "$SPEC_ARCHIVES"/*.md 2>/dev/null | head -1)
|
|
[ -z "$SPEC_FILE" ] && exit # no spec; omit this section entirely
|
|
SPEC_ISSUE=$(grep "^spec_issue_number:" "$SPEC_FILE" | cut -d' ' -f2)
|
|
[ -z "$SPEC_ISSUE" ] && exit # spec archive exists but no issue number; omit
|
|
|
|
# CONDITIONAL Closes #N (codex F4): only add when Plan Completion above is "complete".
|
|
# If the plan completion gate from Step 8 reports any deferred or failed items, emit:
|
|
# "Linked to #$SPEC_ISSUE (partial delivery — NOT auto-closing; close manually after follow-up)"
|
|
# If Plan Completion is fully complete, emit:
|
|
# "Closes #$SPEC_ISSUE"
|
|
# and include the Closes #N line in the PR body so GitHub auto-closes on merge.>
|
|
|
|
<Format:
|
|
Closes #<N>
|
|
|
|
This PR delivers the spec at <archive path relative to repo root>.
|
|
Spec filed: <spec_filed_at from frontmatter>>
|
|
|
|
<If partial delivery, emit instead:
|
|
Linked to #<N> (partial delivery — not auto-closing).
|
|
Deferred items: <list from Plan Completion>.
|
|
Close #<N> manually after follow-up lands.>
|
|
|
|
<If no /spec archive matches this branch: omit this entire section.>
|
|
|
|
## Verification Results
|
|
<If verification ran: summary from Step 8.1 (N PASS, M FAIL, K SKIPPED)>
|
|
<If skipped: reason (no plan, no server, no verification section)>
|
|
<If not applicable: omit this section>
|
|
|
|
## TODOS
|
|
<If items marked complete: bullet list of completed items with version>
|
|
<If no items completed: "No TODO items completed in this PR.">
|
|
<If TODOS.md created or reorganized: note that>
|
|
<If TODOS.md doesn't exist and user skipped: omit this section>
|
|
|
|
## Documentation
|
|
<Embed the `documentation_section` string returned by Step 18's subagent here, verbatim.>
|
|
<If Step 18 returned `documentation_section: null` (no docs updated), omit this section entirely.>
|
|
|
|
## Test plan
|
|
- [x] All Rails tests pass (N runs, 0 failures)
|
|
- [x] All Vitest tests pass (N tests)
|
|
|
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
```
|
|
|
|
#### Redaction scan (PR body + title) — runs before create AND edit
|
|
|
|
The PR body is world-readable on a public repo. Scan-at-sink before sending:
|
|
write the composed body to a temp file, scan THAT file with the shared engine,
|
|
and pass the same file to `gh`/`glab`. Wrap any Codex / Greptile / eval output
|
|
sections in tool-attributed fences (` ```codex-review ` / ` ```greptile `) so the
|
|
engine WARN-degrades the example credentials those tools quote instead of blocking
|
|
the PR (a live-format credential inside the fence still blocks).
|
|
|
|
```bash
|
|
REDACT_VIS=$(~/.claude/skills/gstack/bin/gstack-config get redact_repo_visibility 2>/dev/null)
|
|
[ -z "$REDACT_VIS" ] && REDACT_VIS=$(gh repo view --json visibility -q .visibility 2>/dev/null | tr 'A-Z' 'a-z')
|
|
REDACT_VIS="${REDACT_VIS:-unknown}"
|
|
PR_BODY_FILE=$(mktemp)
|
|
cat > "$PR_BODY_FILE" <<'PR_BODY_EOF'
|
|
<PR body from above>
|
|
PR_BODY_EOF
|
|
~/.claude/skills/gstack/bin/gstack-redact --from-file "$PR_BODY_FILE" --repo-visibility "$REDACT_VIS" --self-email "$(git config user.email 2>/dev/null)" --json
|
|
case $? in
|
|
3) echo "BLOCKED — credential in PR body. Rotate + redact, do not create the PR."; exit 1 ;;
|
|
2) echo "MEDIUM findings — confirm per finding (sterner on public) before proceeding." ;;
|
|
esac
|
|
# Also scan the title (short, single-line):
|
|
printf '%s' "v$NEW_VERSION <type>: <summary>" | ~/.claude/skills/gstack/bin/gstack-redact --repo-visibility "$REDACT_VIS" --json
|
|
```
|
|
|
|
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).
|
|
|
|
**If GitHub:** create from the SCANNED file (exact bytes scanned = bytes sent):
|
|
|
|
```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 <base> --title "v$NEW_VERSION <type>: <summary>" --body-file "$PR_BODY_FILE"
|
|
rm -f "$PR_BODY_FILE"
|
|
```
|
|
|
|
**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 <base> -t "v$NEW_VERSION <type>: <summary>" -d "$(cat <<'EOF'
|
|
<MR body from above>
|
|
EOF
|
|
)"
|
|
```
|
|
|
|
**If neither CLI is available:**
|
|
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
|
|
|
|
**Output the PR/MR URL** — then proceed to Step 20.
|
|
|
|
---
|