Fix broken Go/Java code examples in iac/concepts docs #5874
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Pre-merge Review (update-review) | |
| # Re-entrant pinned-review refresh, dispatched by the explicit hashtag | |
| # `#update-review` on an `@claude` mention. Hashtag-driven routing means | |
| # this workflow only fires when the user explicitly asks for a review | |
| # refresh -- bare `@claude` mentions go to claude.yml (off-the-shelf tag | |
| # mode) and `@claude #new-review` goes to claude-new.yml (regenerate). | |
| # The compound-mention contract is documented inline in the prompt. | |
| # | |
| # A second entry point exists for the stale-review auto-refresh: the | |
| # auto-refresh job in claude-code-review.yml dispatches this workflow | |
| # (workflow_dispatch, auto=true) when a synchronize push passes the | |
| # deterministic auto-refresh-gate.py check — every hunk lands on a line | |
| # range carried by a 🚨 Outstanding finding and the push is small. The run | |
| # then follows update.md Case 1 (fix-response) with a synthesized mention | |
| # body; there is no human mention author. | |
| on: | |
| issue_comment: | |
| types: [created] | |
| pull_request_review_comment: | |
| types: [created] | |
| issues: | |
| types: [opened, assigned] | |
| pull_request_review: | |
| types: [submitted] | |
| # Dispatched by claude-code-review.yml's auto-refresh job (auto=true). | |
| # Humans with write access can also dispatch manually from the Actions | |
| # UI as an alternative to commenting `@claude #update-review`. | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number whose pinned review to refresh' | |
| required: true | |
| type: string | |
| head_sha: | |
| description: 'PR head SHA the dispatcher evaluated; the run no-ops if the PR head has moved past it (superseded by a newer push)' | |
| required: false | |
| type: string | |
| default: '' | |
| auto: | |
| description: 'true when dispatched by the auto-refresh gate (attributes the run to auto-refresh instead of a human mention author)' | |
| required: false | |
| type: boolean | |
| default: false | |
| jobs: | |
| claude-update: | |
| # Trigger requires: | |
| # 1. `@claude` mention. | |
| # 2. `#update-review` hashtag. | |
| # 3. NOT `#new-review` -- if both hashtags are present, the more | |
| # decisive #new-review wins (handled by claude-new.yml's filter; | |
| # this workflow excludes itself in that case). | |
| # 4. Author is not claude[bot] itself -- the pinned-review footer | |
| # contains literal "@claude" instructions which would otherwise | |
| # re-trigger on every review post. | |
| if: | | |
| ((github.event_name == 'workflow_dispatch') || | |
| (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(github.event.comment.body, '#update-review') && !contains(github.event.comment.body, '#new-review') && github.event.comment.user.login != 'claude[bot]') || | |
| (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(github.event.comment.body, '#update-review') && !contains(github.event.comment.body, '#new-review') && github.event.comment.user.login != 'claude[bot]') || | |
| (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(github.event.review.body, '#update-review') && !contains(github.event.review.body, '#new-review') && github.event.review.user.login != 'claude[bot]') || | |
| (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && (contains(github.event.issue.body, '#update-review') || contains(github.event.issue.title, '#update-review')) && !contains(github.event.issue.body, '#new-review') && !contains(github.event.issue.title, '#new-review') && github.event.issue.user.login != 'claude[bot]')) | |
| # One update per PR at a time; a newer trigger (another push through the | |
| # auto-refresh gate, or a fresh #update-review mention) cancels the | |
| # in-flight run — the newest head wins. Also the once-per-push dedup for | |
| # the auto path: stacked synchronize events collapse here. | |
| concurrency: | |
| group: claude-update-${{ github.event.inputs.pr_number || github.event.pull_request.number || github.event.issue.number }} | |
| cancel-in-progress: true | |
| runs-on: ubuntu-latest | |
| environment: production | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: read | |
| id-token: write | |
| actions: read # Required for Claude to read CI results on PRs | |
| steps: | |
| # Resolve the PR head SHA before checkout so the working tree | |
| # reflects PR content, not the base branch. Without this, Vale | |
| # below runs against base prose and produces empty findings. | |
| # `issues` events have no PR head; the SHA stays empty and | |
| # checkout falls back to default behavior (the Vale step is | |
| # gated on is_pr=true anyway, so it skips on issues). | |
| - name: Resolve PR head SHA | |
| id: head | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| case "${{ github.event_name }}" in | |
| issue_comment) | |
| PR="${{ github.event.issue.number }}" | |
| IS_PR="${{ github.event.issue.pull_request != null }}" | |
| ;; | |
| pull_request_review_comment|pull_request_review) | |
| PR="${{ github.event.pull_request.number }}" | |
| IS_PR="true" | |
| ;; | |
| workflow_dispatch) | |
| PR="${{ github.event.inputs.pr_number }}" | |
| IS_PR="true" | |
| ;; | |
| *) | |
| PR=""; IS_PR="false" | |
| ;; | |
| esac | |
| if [ "$IS_PR" = "true" ] && [ -n "$PR" ]; then | |
| SHA=$(gh pr view "$PR" --repo "${{ github.repository }}" --json headRefOid --jq .headRefOid) | |
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Staleness guard for dispatched runs: the dispatcher pins the head | |
| # SHA it evaluated. If the PR has moved on since (another push beat | |
| # this run out of the queue), the whole run no-ops — the newer | |
| # push's own gate evaluation owns the refresh. The concurrency | |
| # group handles overlapping runs; this handles the | |
| # already-finished-dispatching race. | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ | |
| && [ -n "${{ github.event.inputs.head_sha }}" ] \ | |
| && [ "$SHA" != "${{ github.event.inputs.head_sha }}" ]; then | |
| echo "superseded=true" >> "$GITHUB_OUTPUT" | |
| echo "PR #$PR head moved from ${{ github.event.inputs.head_sha }} to $SHA — superseded; no-op" | |
| fi | |
| # ESC runs before checkout so the bot token can authenticate the | |
| # checkout — pushes made by claude-code-action later in the workflow | |
| # then go out as pulumi-bot rather than github-actions[bot], | |
| # which is what lets downstream workflows (build-and-deploy, social | |
| # review, etc.) fire on those commits. | |
| # The three setup steps below skip on superseded dispatched runs | |
| # (steps.head sets superseded=true when the PR head moved past the | |
| # dispatcher's pinned SHA) — the run is a no-op, so don't pay for | |
| # secrets, checkout, or toolchain install. | |
| - name: Fetch secrets from ESC | |
| id: esc-secrets | |
| if: steps.head.outputs.superseded != 'true' | |
| uses: pulumi/esc-action@v3 | |
| - name: Checkout repository | |
| if: steps.head.outputs.superseded != 'true' | |
| uses: actions/checkout@v7 | |
| with: | |
| token: ${{ steps.esc-secrets.outputs.PULUMI_BOT_TOKEN }} | |
| ref: ${{ steps.head.outputs.sha }} | |
| fetch-depth: 1 | |
| # Install mise-managed tools (Vale, Node, etc.) so the prose-lint | |
| # step below has the pinned vale binary on PATH. | |
| - name: Install mise-managed tools | |
| if: steps.head.outputs.superseded != 'true' | |
| uses: jdx/mise-action@v4 | |
| with: | |
| cache: true | |
| - name: Check repository write access | |
| id: check-access | |
| if: steps.head.outputs.superseded != 'true' | |
| run: | | |
| # workflow_dispatch runs skip the collaborator-permission lookup: | |
| # triggering a dispatch already requires write access (humans via | |
| # the Actions UI) or comes from the auto-refresh gate in | |
| # claude-code-review.yml, which only fires for same-repo branches | |
| # (pushable only with write access). There is no mention author; | |
| # auto-gated runs attribute to "auto-refresh", manual dispatches | |
| # to the dispatching actor. | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| if [ "${{ github.event.inputs.auto }}" = "true" ]; then | |
| AUTHOR="auto-refresh" | |
| else | |
| AUTHOR="${{ github.actor }}" | |
| fi | |
| echo "has_write_access=true" >> $GITHUB_OUTPUT | |
| echo "author=$AUTHOR" >> $GITHUB_OUTPUT | |
| echo "✓ workflow_dispatch by $AUTHOR — write access implied by dispatch permissions" | |
| exit 0 | |
| fi | |
| # Use the actual repository the workflow is running in, not a hardcoded | |
| # upstream name. The GITHUB_TOKEN is only scoped to this repo, so a | |
| # hardcoded owner/repo would always return "none" in fork-based testing | |
| # and in repo transfers. | |
| REPO_FULL="${{ github.repository }}" | |
| # Determine the author based on event type | |
| if [ "${{ github.event_name }}" = "issue_comment" ]; then | |
| AUTHOR="${{ github.event.comment.user.login }}" | |
| elif [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then | |
| AUTHOR="${{ github.event.comment.user.login }}" | |
| elif [ "${{ github.event_name }}" = "pull_request_review" ]; then | |
| AUTHOR="${{ github.event.review.user.login }}" | |
| elif [ "${{ github.event_name }}" = "issues" ]; then | |
| AUTHOR="${{ github.event.issue.user.login }}" | |
| else | |
| AUTHOR="unknown" | |
| fi | |
| # GitHub App bots are not collaborators, so the permission API | |
| # below returns "none" for them. Trusted bots that drive review | |
| # refreshes on this repo are whitelisted by name instead. | |
| # `workprentice` is Joe Duffy's docs-automation identity (the Docs | |
| # Groundskeeper agent) and is trusted like an internal author: it | |
| # opens PRs, addresses the pinned-review findings, and then mentions | |
| # `@claude #update-review` to re-verify them — which is exactly what | |
| # this workflow does. Without this, that mention hits the | |
| # collaborator lookup, resolves to "none", and the refresh silently | |
| # no-ops. The author string differs by source: this workflow reads | |
| # the comment/review/issue `user.login` (`workprentice[bot]`) while | |
| # claude-code-review.yml reads `gh pr view` (`app/workprentice`), so | |
| # both forms are listed. Keep this list in sync with the matching | |
| # checks in claude-code-review.yml and claude-triage.yml. | |
| if [[ "$AUTHOR" == "github-copilot[bot]" || "$AUTHOR" == "eon-pulumi-agent[bot]" \ | |
| || "$AUTHOR" == "workprentice[bot]" || "$AUTHOR" == "app/workprentice" ]]; then | |
| echo "has_write_access=true" >> $GITHUB_OUTPUT | |
| echo "author=$AUTHOR" >> $GITHUB_OUTPUT | |
| echo "✓ Bot $AUTHOR is whitelisted for review refreshes" | |
| exit 0 | |
| fi | |
| # Get user's permission level (admin, write, read, or none) | |
| PERMISSION=$(curl -s \ | |
| -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/repos/$REPO_FULL/collaborators/$AUTHOR/permission" \ | |
| | jq -r '.permission // "none"') | |
| # Allow admin or write access | |
| if [[ "$PERMISSION" == "admin" || "$PERMISSION" == "write" ]]; then | |
| echo "has_write_access=true" >> $GITHUB_OUTPUT | |
| echo "author=$AUTHOR" >> $GITHUB_OUTPUT | |
| echo "✓ User $AUTHOR has $PERMISSION access to $REPO_FULL" | |
| else | |
| echo "has_write_access=false" >> $GITHUB_OUTPUT | |
| echo "author=$AUTHOR" >> $GITHUB_OUTPUT | |
| echo "✗ User $AUTHOR has $PERMISSION access to $REPO_FULL (insufficient permissions)" | |
| fi | |
| - name: Resolve PR context | |
| id: pr-context | |
| if: steps.check-access.outputs.has_write_access == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Determine PR / issue number, whether it's a PR, and whether | |
| # a pinned Claude review already exists. The skill needs all | |
| # three to decide between Case 1/2/3 in update.md and the | |
| # initial-review fallback path. | |
| PR_NUMBER="" | |
| IS_PR="false" | |
| case "${{ github.event_name }}" in | |
| issue_comment) | |
| PR_NUMBER="${{ github.event.issue.number }}" | |
| if [ "${{ github.event.issue.pull_request != null }}" = "true" ]; then | |
| IS_PR="true" | |
| fi | |
| ;; | |
| pull_request_review_comment|pull_request_review) | |
| PR_NUMBER="${{ github.event.pull_request.number }}" | |
| IS_PR="true" | |
| ;; | |
| issues) | |
| PR_NUMBER="${{ github.event.issue.number }}" | |
| IS_PR="false" | |
| ;; | |
| workflow_dispatch) | |
| PR_NUMBER="${{ github.event.inputs.pr_number }}" | |
| IS_PR="true" | |
| ;; | |
| esac | |
| HAS_PINNED="false" | |
| if [ "$IS_PR" = "true" ] && [ -n "$PR_NUMBER" ]; then | |
| PINNED_IDS=$(bash .claude/commands/docs-review/scripts/pinned-comment.sh \ | |
| find --pr "$PR_NUMBER" --repo "${{ github.repository }}" || true) | |
| if [ -n "$PINNED_IDS" ]; then | |
| HAS_PINNED="true" | |
| fi | |
| fi | |
| { | |
| echo "pr_number=$PR_NUMBER" | |
| echo "is_pr=$IS_PR" | |
| echo "has_pinned=$HAS_PINNED" | |
| } >> "$GITHUB_OUTPUT" | |
| # Flip the PR's state label to review:in-progress for the duration of | |
| # the refresh — mirrors claude-code-review.yml's early step on the | |
| # auto-fire / #new-review path. Without this, a #update-review mention | |
| # leaves the PR's prior terminal label (outstanding-issues / | |
| # no-blockers) visible the entire 5-10 min the refresh takes — so the | |
| # author has no signal that a fresh review is in flight. The | |
| # finalize step at end-of-job transitions it back to the appropriate | |
| # terminal state. | |
| - name: Set review:in-progress label | |
| if: | | |
| steps.check-access.outputs.has_write_access == 'true' && | |
| steps.pr-context.outputs.is_pr == 'true' | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| .claude/commands/docs-review/scripts/set-review-label.sh \ | |
| --pr "${{ steps.pr-context.outputs.pr_number }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --label review:in-progress | |
| # Save the triggering comment / review / issue body to a file in | |
| # the workspace so the model can read it as MENTION_BODY without | |
| # scraping the event payload at runtime. Env vars carry the body | |
| # safely (no direct interpolation into shell -- bodies can contain | |
| # arbitrary text including shell metacharacters). | |
| # workflow_dispatch has no triggering comment; the body is a fixed | |
| # synthesized instruction scoping the run to update.md Case 1 | |
| # (fix-response) over the existing outstanding findings. | |
| - name: Save mention body | |
| id: mention | |
| if: steps.check-access.outputs.has_write_access == 'true' | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| REVIEW_BODY: ${{ github.event.review.body }} | |
| ISSUE_BODY: ${{ github.event.issue.body }} | |
| run: | | |
| case "$EVENT_NAME" in | |
| issue_comment|pull_request_review_comment) | |
| BODY="$COMMENT_BODY" | |
| ;; | |
| pull_request_review) | |
| BODY="$REVIEW_BODY" | |
| ;; | |
| issues) | |
| BODY="$ISSUE_BODY" | |
| ;; | |
| workflow_dispatch) | |
| BODY="Automated refresh (no human mention): the latest push was gated as touching only lines carried by outstanding findings. Treat this as update.md Case 1 (fix-response): re-verify each 🚨 Outstanding finding against the new diff, move resolved ones to ✅ Resolved with the commit SHA, and check the pushed lines for new problems. Do not re-extract claims or raise findings on content the push did not touch. There is no dispute to adjudicate." | |
| ;; | |
| *) | |
| BODY="" | |
| ;; | |
| esac | |
| printf '%s' "$BODY" > .claude-mention-body.txt | |
| # Run Vale on PR-changed prose files (content/docs, content/blog, | |
| # content/what-is, content/tutorials) so the | |
| # refreshed review reflects style nits in the current commit. | |
| # Skipped on issue mentions and when no in-scope prose files were | |
| # touched. The `||` fallbacks ensure both files exist even when | |
| # vale is missing or the filter crashes (mirrors claude-triage.yml). | |
| - name: Run Vale on PR-changed prose | |
| if: | | |
| steps.check-access.outputs.has_write_access == 'true' && | |
| steps.pr-context.outputs.is_pr == 'true' | |
| id: vale | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR: ${{ steps.pr-context.outputs.pr_number }} | |
| run: | | |
| CHANGED=$(gh pr diff "$PR" --name-only \ | |
| | grep -E '^content/(docs|blog|what-is|tutorials)/.*\.md$' || true) | |
| if [ -z "$CHANGED" ]; then | |
| echo '{}' > .vale-raw.json | |
| echo '[]' > .vale-findings.json | |
| echo "vale: no in-scope prose files changed; skipping" | |
| exit 0 | |
| fi | |
| vale --no-exit --output=JSON $CHANGED > .vale-raw.json 2>/dev/null \ | |
| || echo '{}' > .vale-raw.json | |
| # Vale renders markdown to HTML before applying rules, so bracket | |
| # constructions (`[here](url)`, ``) are gone before tokens | |
| # match. markdown-syntax-findings.py scans the raw markdown and emits | |
| # Vale-shaped JSON, merged in before filtering. This block mirrors | |
| # claude-code-review.yml — without it a refresh dropped the two rules | |
| # it contributes (Pulumi.EmptyAltText, Pulumi.LinkText) from a review | |
| # the initial pass had raised them on. | |
| # | |
| # This affects the ADVISORY tier only. Neither rule is blocker-tier | |
| # (see vale-deterministic-fixes.yaml), so the [style-blocker] set is | |
| # byte-identical with and without this merge, and the provenance | |
| # check downstream is unaffected. Note Pulumi.LinkText flags *vague* | |
| # link text — there is no broken-link detector here. | |
| python3 .claude/commands/docs-review/scripts/markdown-syntax-findings.py \ | |
| $CHANGED > .syntax-findings.json \ | |
| || echo '{}' > .syntax-findings.json | |
| # Concatenate per-file alert arrays (jq's `*` shallow-merges and would | |
| # *replace* Vale's array with the script's for any overlapping file). | |
| jq -s 'reduce .[] as $o ({}; reduce ($o | keys_unsorted[]) as $k (.; .[$k] = ((.[$k] // []) + $o[$k])))' \ | |
| .vale-raw.json .syntax-findings.json > .vale-raw.merged.json \ | |
| && mv .vale-raw.merged.json .vale-raw.json \ | |
| || true | |
| # No 2>/dev/null on the filter: it has no safe_main wrapper, so the | |
| # redirect discarded the only trace of a crash. (Vale's own stderr | |
| # above is still redirected — it is noisy and exits 0 regardless.) | |
| # The `||` fallback still keeps the lane green. | |
| python3 .claude/commands/docs-review/scripts/vale-findings-filter.py \ | |
| --pr "$PR" --in .vale-raw.json --out .vale-findings.json \ | |
| || echo '[]' > .vale-findings.json | |
| # Post a transient <!-- CLAUDE_PROGRESS --> comment so the author | |
| # sees something is happening while Sonnet works. The animated | |
| # spinner GIF is the action's own tracking-comment image (CDN- | |
| # stable). The post step below edits this comment to a done / | |
| # errored state when the run completes; the spinner does not | |
| # persist past terminal state. Skipped on issue mentions. | |
| - name: Post progress signal | |
| if: | | |
| steps.check-access.outputs.has_write_access == 'true' && | |
| steps.pr-context.outputs.is_pr == 'true' | |
| id: progress | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR="${{ steps.pr-context.outputs.pr_number }}" | |
| REPO="${{ github.repository }}" | |
| BODY=$(cat <<'EOF' | |
| <!-- CLAUDE_PROGRESS --> | |
| <img src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="16"> Working on it — this can take several minutes. | |
| EOF | |
| ) | |
| COMMENT_ID=$(gh api "repos/$REPO/issues/$PR/comments" \ | |
| -f body="$BODY" --jq '.id' || echo "") | |
| echo "comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT" | |
| - name: Run Claude Code | |
| if: steps.check-access.outputs.has_write_access == 'true' | |
| id: claude | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| # Use bot token so pushes trigger downstream workflows (e.g., social review) | |
| github_token: ${{ steps.esc-secrets.outputs.PULUMI_BOT_TOKEN }} | |
| # The auto-refresh gate in claude-code-review.yml dispatches this | |
| # workflow with GITHUB_TOKEN, which makes the run's actor | |
| # github-actions[bot] (type=Bot) — rejected by | |
| # claude-code-action@v1 by default. Same allowance | |
| # claude-code-review.yml makes for its bot-dispatched #new-review | |
| # path. | |
| # | |
| # workprentice is also listed so that its own `@claude | |
| # #update-review` mentions (issue_comment events whose actor is the | |
| # bot) reach the action. Without it, those mentions clear this | |
| # workflow's own check-access whitelist (the workprentice[bot] | |
| # branch above) and then get bounced by claude-code-action's | |
| # independent bot guard. The trust decision is already made for the | |
| # sibling review workflows — see the matching allowed_bots in | |
| # claude-code-review.yml and claude-social-review.yml. Both the bare | |
| # and `[bot]`-suffixed forms are listed because the action reports | |
| # the actor as bare `workprentice` while github.actor renders it as | |
| # `workprentice[bot]`. | |
| allowed_bots: 'github-actions[bot],workprentice,workprentice[bot]' | |
| # This is an optional setting that allows Claude to read CI results on PRs | |
| additional_permissions: | | |
| actions: read | |
| # Single-path prompt: the hashtag did the routing work, so | |
| # there's no in-prompt classification. The compound-mention | |
| # contract handles fix-and-refresh and dispute-and-refresh | |
| # cases by addressing embedded asks inline before re-rendering | |
| # the pinned review. | |
| prompt: | | |
| The user invoked you with `#update-review` on `${{ github.repository }}`. The hashtag means: refresh the pinned review. | |
| Context: | |
| - Pull request #${{ steps.pr-context.outputs.pr_number }} | |
| - Mention author: @${{ steps.check-access.outputs.author }} | |
| - Pinned Claude review: ${{ steps.pr-context.outputs.has_pinned == 'true' && 'EXISTS on this PR' || 'does not exist yet' }} | |
| **Read the triggering mention text from `.claude-mention-body.txt` first.** It is the body of the comment, review, or issue that invoked you. | |
| The mention may also contain: | |
| - Code changes to make ("fix the typo and then update") | |
| - Questions about specific findings ("why did you flag X?") | |
| - Disputes ("this is intentional because Y") | |
| - Combinations of the above | |
| Plan of attack: | |
| 1. Read `.claude-mention-body.txt`. | |
| 2. Address any embedded asks first: | |
| - **File edits** → Edit/Write, `gh pr checkout ${{ steps.pr-context.outputs.pr_number }}`, push. | |
| - **Questions / disputes** → fold the response into the relevant finding when you re-render the review (don't post separate `gh pr comment`s — keeps everything in the pinned sequence). | |
| 3. Refresh the pinned review against the resulting state: | |
| - If a pinned review **EXISTS**, follow `docs-review:references:update`. Pass the mention body as `MENTION_BODY` and `@${{ steps.check-access.outputs.author }}` as `MENTION_AUTHOR` (the skill's documented inputs at update.md:13–15). | |
| - If a pinned review **does not exist**, follow `.claude/commands/docs-review/ci.md` to produce an initial review. | |
| 4. Post via `bash .claude/commands/docs-review/scripts/pinned-comment.sh upsert --pr ${{ steps.pr-context.outputs.pr_number }} --body-file <path>`. | |
| **Style suggestions.** If `.vale-findings.json` exists and is non-empty, split entries on the `blocker` field. **Blocker tier** (`"blocker": true` — wrong or deprecated product names, banned terms, misspellings, agreement errors; near-zero false-positive rate): render each in 🚨 Outstanding as `- **[L<n>]** <file-in-backticks> — [style-blocker] _category_ — <message>` (standard `**[L<n>]**` anchor so the auto-refresh gate can match a fix-push; no verification-trail record needed — the validator exempts `[style-blocker]` bullets from trail-matching) and count them in the 🚨 count-table cell. Never author a `[style-blocker]` bullet yourself — it is composer-only and exempts the bullet from trail-matching; reviewer-found issues go in 🚨 as ordinary `**[L…]**` bullets with a trail record. **Advisory tier** (everything else): surface each entry under ⚠️ Low-confidence as `- **line N:** [style] _category_ — <message>` (bold the line number, italicize the category), grouped under a single `#### Style suggestions` H4 sub-heading after any regular low-confidence bullets, **expanded, never behind a `<details>`** — group them under an `##### <path>` H5 heading per file, in line order (an H5, not a bold line — a column-0 `**bold**` line is miscounted as a bucket finding). **Never write a ✏️ mark, and never write the ✏️ banner line under the count table.** This lane re-posts the inline suggestions after you exit, and a workflow step then rewrites both — plus the italic caption under the `#### Style suggestions` heading, which is reconciled to one canonical string, so don't reword it — from the set the GitHub API accepted — so any mark you carry over from the previous pinned comment, or author fresh, is stripped and recomputed. Drop every ✏️ you find in the previous body when you re-render; the step puts back the ones that are real. The mark is keyed to the **line**, not the finding: one suggestion rewrites a whole line, so if that line carries two style findings both bullets get a ✏️. Marks can therefore outnumber the banner's count — the banner counts buttons, the marks point at the lines they sit on. That is expected; do not "reconcile" it. Advisory bullets are NOT counted in the ⚠️ count-table cell. Use the `category` field; never surface the `rule` field. Delete false-positive advisory bullets silently — never list, count, or explain the ones you dropped. Full render contract: `docs-review:references:output-format`. | |
| **Inline-suggestion sidecar.** After you have published the refreshed review, Write `.style-suggestions.json` at the workspace root (a JSON array; write `[]` if none qualify) staging the ADVISORY style findings whose rewrite is a clear improvement. Schema per entry: `{"file": "<path>", "line": <n>, "original": "<exact text on that line>", "replacement": "<new text>", "category": "<category>", "note": "<≤10-word reason>"}`. A workflow step validates each entry against the diff and file content, posts them as one-click GitHub `suggestion` comments, and reconciles the pinned comment's ✏️ marks — you never post them yourself. Convert a finding ONLY when ALL hold: (a) the rewrite preserves meaning exactly — a hedge that is factually load-bearing ("usually completes in five minutes") is NOT a conversion candidate, deleting it would overpromise; (b) the fix is contained on the single flagged line; (c) you are confident the result reads better, not just shorter. Cap at 10. `original` must be the exact substring on that line in the checked-out file, or validation drops the entry. Do NOT remove the corresponding `[style]` bullet — the block stays the complete record. Blocker findings are never suggestions, and neither is anything sharing a LINE with one — a suggestion replaces the whole line, so it would re-commit the blocking text. **Stage the full qualifying set every run, not just the new ones:** the step deletes the previous run's suggestion comments before posting, so a finding you omit loses its button. **Write `[]` (not nothing) when the answer is none — this is load-bearing, not bookkeeping.** An explicit `[]` is authoritative and clears the previous run's buttons; an absent file means "unknown" and leaves them all standing, so a refresh where you simply forget the sidecar strands stale buttons on findings you no longer report. Writing the file is how you say which one you mean. | |
| Advisory style findings are **not** tracked across reviews. Each `#update-review` run generates a fresh `.vale-findings.json` against the current PR head; render those findings each time, drop them silently when they disappear, do NOT move resolved advisory nits into ✅ Resolved. Blocker-tier `[style-blocker]` bullets ARE tracked like regular findings: when a fix-push resolves one, move it to ✅ Resolved. The diff-tracking rules in `update.md` (Case 1 fix-response: move resolved to ✅) apply to human-grade catches and `[style-blocker]` bullets, not advisory `[style]` bullets. | |
| # Model and effort are set from the 2026-08-10 campaign | |
| # (`2026-08-10-update-lane-model-config` in pulumi/docs-review-benchmarks), | |
| # a synthetic replay of this lane: 4 arms x 3 reps x 2 chained refreshes, | |
| # scored on whether the re-rendered pinned body keeps its evidence spine | |
| # (the 🔍 Verification trail's line floor, 📊 Editorial balance, and the | |
| # investigation log). opus-5 at medium effort was the only arm intact in | |
| # every cell (6/6); sonnet-5 with effort unset — what ran here before — | |
| # and opus-5 unset each lost the spine in 1 of 6. It also gets there in | |
| # 0.43x the output tokens and 0.64x the turns — but Opus is 2.5x Sonnet | |
| # per token, so it costs ~1.25x, about $0.27 more per refresh at the | |
| # rate this lane fires. That premium is permanent: Sonnet 5's $2/$10 | |
| # introductory rate was made its standing price on 2026-08-10, so the | |
| # crossover the first cut of this comment predicted never happens. | |
| # It buys removing a failure that is unrecoverable (the pinned comment | |
| # is the only copy of the trail) and unvalidated on this lane. | |
| # | |
| # `--effort low` stays OFF this lane. It rewrites the pinned review | |
| # without the trail's contents — keeping the heading and replacing 40 | |
| # claim lines with a "see the full trail in the prior pass" pointer that | |
| # dangles, because the prior pass is the comment it just overwrote. One | |
| # rep of three collapsed to 11 of 40 lines and held exactly 11 through | |
| # the next refresh. The loss is permanent — later updates merge from the | |
| # prior comment — and invisible, because this workflow runs only | |
| # `validate-pinned.py count-buckets`, never the validate → splice → | |
| # validator-fix chain. Don't re-add it without wiring in full validation | |
| # first; the composer lane in claude-code-review.yml is a different job | |
| # shape and keeps its low effort. | |
| claude_args: '--model claude-opus-5 --effort medium --allowed-tools "Read,Write,Edit,Glob,Grep,Agent,WebFetch,WebSearch,Bash(gh pr:*),Bash(gh issue:*),Bash(gh api:*),Bash(gh search:*),Bash(gh release:*),Bash(gh repo view:*),Bash(gh repo list:*),Bash(git:*),Bash(bash .claude/commands/docs-review/scripts/pinned-comment.sh:*),Bash(bash ${{ github.workspace }}/.claude/commands/docs-review/scripts/pinned-comment.sh:*),Bash(cd:*),Bash(cat:*),Bash(head:*),Bash(tail:*),Bash(wc:*),Bash(file:*),Bash(stat:*),Bash(ls:*),Bash(grep:*),Bash(find:*),Bash(rg:*),Bash(awk:*),Bash(sed:*),Bash(tr:*),Bash(cut:*),Bash(paste:*),Bash(sort:*),Bash(uniq:*),Bash(diff:*),Bash(jq:*),Bash(echo:*),Bash(printf:*),Bash(tee:*),Bash(date:*),Bash(true:*),Bash(false:*),Bash(test:*),Bash(which:*),Bash(command:*),Bash(curl:*),Bash(wget:*)"' | |
| # Re-post the one-click style suggestions and reconcile the pinned | |
| # comment against what actually landed. Delete-and-repost, exactly like | |
| # the initial lane: the previous run's suggestion comments are cleared | |
| # first, so a finding the author fixed stops offering a button and a | |
| # newly-surfaced one gains one. | |
| # | |
| # --annotate-pinned instead of --annotate-draft because this lane has no | |
| # draft to intercept: the model renders the body and upserts it inside | |
| # its own step, so the published comment is the only copy. The script | |
| # PATCHes each `<!-- CLAUDE_REVIEW N/M -->` part in place rather than | |
| # fetch → concatenate → re-upsert, which would re-run the splitter over | |
| # its own continuation-<details> artifacts on every refresh. | |
| # | |
| # continue-on-error: suggestions are a convenience layer; a failure here | |
| # must never mark a successful review as errored. | |
| - name: Post inline style suggestions | |
| if: steps.claude.outcome == 'success' && steps.pr-context.outputs.is_pr == 'true' | |
| continue-on-error: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| python3 .claude/commands/docs-review/scripts/post-style-suggestions.py \ | |
| --pr "${{ steps.pr-context.outputs.pr_number }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --vale-findings .vale-findings.json \ | |
| --annotate-pinned | |
| # Surface the evidence-spine floor's verdict. The floor itself runs inside | |
| # `pinned-comment.sh upsert`, which the MODEL calls from its own Bash tool | |
| # — and this action does not echo tool output into the job log, so without | |
| # this step the check is completely invisible from CI: a run where it held | |
| # and a run where it never executed look identical. | |
| # | |
| # The ::warning:: on a restore is the point, not decoration. Every real | |
| # restore is a production instance of the model dropping the review's | |
| # evidence spine, which is exactly the rate the 2026-08-10 campaign had to | |
| # measure synthetically. Surfaced here, the lane measures it on live | |
| # traffic for free — and a run of them is the case for revisiting the | |
| # model config rather than leaning harder on the repair. | |
| - name: Report evidence-spine floor | |
| if: steps.claude.outcome == 'success' && steps.pr-context.outputs.is_pr == 'true' | |
| continue-on-error: true | |
| run: | | |
| if [ ! -f /tmp/splice-spine.json ]; then | |
| echo "::warning::No evidence-spine floor report. pinned-comment.sh upsert did not run, or ran without reaching the floor check — the refreshed review published unguarded." | |
| exit 0 | |
| fi | |
| echo "evidence-spine floor report:" | |
| cat /tmp/splice-spine.json | |
| status=$(jq -r '.status // "unknown"' /tmp/splice-spine.json) | |
| case "$status" in | |
| restored) | |
| secs=$(jq -r '[.restored[].section] | join(", ")' /tmp/splice-spine.json) | |
| echo "::warning::The refreshed review dropped evidence-spine section(s) — $secs — and they were restored from the previous pinned comment. The published review is correct; the render was not." | |
| ;; | |
| error) | |
| echo "::warning::The evidence-spine floor errored and the review published as rendered: $(jq -r '.error' /tmp/splice-spine.json)" | |
| ;; | |
| esac | |
| # Provenance spot-check. The full validate -> splice -> re-validate chain | |
| # does not run on this lane (the model renders the body freehand and | |
| # would fail most structural rules), but ONE rule earns its keep here: | |
| # `[style-blocker]` is what exempts a 🚨 bullet from trail-matching, so an | |
| # authored one routes an unverified finding into the blocking bucket | |
| # behind a check. `--only-rule` runs exactly that rule and nothing else. | |
| # | |
| # WARNING-ONLY, deliberately. There is no splice/fix chain here to repair | |
| # a violation, and failing the job after the review has already published | |
| # would put a red X on work the author can see is fine. The log line is | |
| # evidence: if it never fires, the prompt instruction is holding; if it | |
| # fires repeatedly, that is the case for either stripping the marker | |
| # automatically or bringing real validation to this lane. | |
| - name: Warn on forged [style-blocker] provenance | |
| if: steps.claude.outcome == 'success' && steps.pr-context.outputs.is_pr == 'true' | |
| continue-on-error: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR: ${{ steps.pr-context.outputs.pr_number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set +e | |
| gh api "repos/$REPO/issues/$PR/comments" --paginate \ | |
| --jq '.[] | select(.body | startswith("<!-- CLAUDE_REVIEW ")) | .body' \ | |
| > .pinned-body.md 2>/dev/null | |
| if [ ! -s .pinned-body.md ]; then | |
| echo "provenance check: no pinned review body found; nothing to check." | |
| exit 0 | |
| fi | |
| python3 .claude/commands/docs-review/scripts/validate-pinned.py check \ | |
| --body-file .pinned-body.md \ | |
| --only-rule style-blocker-provenance \ | |
| --pr "$PR" --repo "$REPO" \ | |
| --output-json /tmp/provenance.json \ | |
| --output-markdown /tmp/provenance.md | |
| rc=$? | |
| if [ $rc -eq 1 ]; then | |
| echo "::warning::A [style-blocker] bullet in the refreshed review has no matching blocker entry in .vale-findings.json. That marker exempts a 🚨 bullet from trail-matching, so an unverified finding may have entered the blocking bucket behind a check. Details below." | |
| cat /tmp/provenance.md | |
| elif [ $rc -ne 0 ]; then | |
| echo "provenance check: validator exited $rc; skipping." | |
| fi | |
| exit 0 | |
| # Runs on success or failure so the transient CLAUDE_PROGRESS | |
| # comment always reaches a terminal state. | |
| # | |
| # Outcome handling: | |
| # - success: DELETE the spinner comment, then post a fresh | |
| # `🤖 Review updated on @<author>'s request.` This is a *create* | |
| # (not an edit) so the embedded @-mention fires a GitHub | |
| # notification — that's the whole point of attribution. Editing | |
| # the existing comment to add a mention would not notify. | |
| # - failure: same delete-and-repost, with `🤖 @<author> — review | |
| # errored. …` so the requester is notified that their request | |
| # failed. | |
| # - cancelled / skipped: delete the orphan comment (newer run owns | |
| # the surface). No replacement. | |
| # | |
| # On success the review-state label transitions based on the | |
| # refreshed body's 🚨 Outstanding count: ≥1 → review:outstanding-issues, | |
| # else → review:no-blockers. On failure, set review:error if no review | |
| # was published; otherwise the existing terminal label is preserved. | |
| - name: Finalize progress signal | |
| if: always() && steps.progress.outputs.comment_id != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR="${{ steps.pr-context.outputs.pr_number }}" | |
| REPO="${{ github.repository }}" | |
| COMMENT_ID="${{ steps.progress.outputs.comment_id }}" | |
| OUTCOME="${{ steps.claude.outcome }}" | |
| AUTHOR="${{ steps.check-access.outputs.author }}" | |
| # Auto-gated runs have no human requester: "auto-refresh" is not a | |
| # GitHub user, so never render it as an @-mention (it would ping an | |
| # unrelated account of that name). Plain wording instead. | |
| if [ "$OUTCOME" = "success" ]; then | |
| gh api -X DELETE "repos/$REPO/issues/comments/$COMMENT_ID" >/dev/null 2>&1 || true | |
| if [ "$AUTHOR" = "auto-refresh" ]; then | |
| BODY=$(printf '<!-- CLAUDE_PROGRESS -->\n%s' "🤖 Review auto-refreshed — the latest push only touched lines with outstanding findings.") | |
| else | |
| BODY=$(printf '<!-- CLAUDE_PROGRESS -->\n%s' "🤖 Review updated on @${AUTHOR}'s request.") | |
| fi | |
| gh api "repos/$REPO/issues/$PR/comments" -f body="$BODY" >/dev/null || true | |
| elif [ "$OUTCOME" = "cancelled" ] || [ "$OUTCOME" = "skipped" ]; then | |
| gh api -X DELETE "repos/$REPO/issues/comments/$COMMENT_ID" >/dev/null 2>&1 || true | |
| else | |
| gh api -X DELETE "repos/$REPO/issues/comments/$COMMENT_ID" >/dev/null 2>&1 || true | |
| if [ "$AUTHOR" = "auto-refresh" ]; then | |
| BODY=$(printf '<!-- CLAUDE_PROGRESS -->\n%s' "🤖 Automatic review refresh errored; the review is still marked stale. Mention @claude #update-review to refresh it.") | |
| else | |
| BODY=$(printf '<!-- CLAUDE_PROGRESS -->\n%s' "🤖 @${AUTHOR} — review errored. Mention @claude #update-review again to retry.") | |
| fi | |
| gh api "repos/$REPO/issues/$PR/comments" -f body="$BODY" >/dev/null || true | |
| fi | |
| if [ "$OUTCOME" = "success" ]; then | |
| set +e | |
| python3 .claude/commands/docs-review/scripts/validate-pinned.py \ | |
| count-buckets --pr "$PR" --repo "$REPO" > /tmp/bucket-counts.txt | |
| rc=$? | |
| if [ $rc -eq 0 ]; then | |
| OUTSTANDING=$(grep '^outstanding=' /tmp/bucket-counts.txt | cut -d= -f2) | |
| if [ -n "$OUTSTANDING" ] && [ "$OUTSTANDING" -gt 0 ]; then | |
| LABEL="review:outstanding-issues" | |
| else | |
| LABEL="review:no-blockers" | |
| fi | |
| .claude/commands/docs-review/scripts/set-review-label.sh \ | |
| --pr "$PR" --repo "$REPO" --label "$LABEL" || \ | |
| echo "::warning::set-review-label failed (label=$LABEL); leaving labels untouched" | |
| else | |
| echo "::warning::count-buckets failed (rc=$rc); leaving label untouched" | |
| fi | |
| elif [ "$OUTCOME" != "cancelled" ] && [ "$OUTCOME" != "skipped" ]; then | |
| # Workflow errored. Only set review:error if no pinned review | |
| # exists yet — otherwise the previous run's terminal label is | |
| # still meaningful and shouldn't be overwritten by this failure. | |
| PUBLISHED=$(gh api --paginate "repos/$REPO/issues/$PR/comments" \ | |
| --jq '[.[] | select(.body | test("<!-- CLAUDE_REVIEW"))] | length' 2>/dev/null || echo "0") | |
| if [ "${PUBLISHED:-0}" -eq 0 ]; then | |
| .claude/commands/docs-review/scripts/set-review-label.sh \ | |
| --pr "$PR" --repo "$REPO" --label review:error || true | |
| fi | |
| fi | |
| env: | |
| ESC_ACTION_OIDC_AUTH: true | |
| ESC_ACTION_OIDC_ORGANIZATION: pulumi | |
| ESC_ACTION_OIDC_REQUESTED_TOKEN_TYPE: urn:pulumi:token-type:access_token:organization | |
| ESC_ACTION_ENVIRONMENT: github-secrets/pulumi-docs | |
| ESC_ACTION_EXPORT_ENVIRONMENT_VARIABLES: false |