From f31a0580da3f4ed3d7977e94253039225c28c966 Mon Sep 17 00:00:00 2001 From: Claude Auto-Fix Date: Sat, 9 May 2026 09:21:09 -0400 Subject: [PATCH 1/3] feat(pr-review): switch to App-token identity (#250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch `pr-review/action.yml` from `github.token` to a resolved GitHub App token for all `claude-code-action` invocations. Review comments now post under the App bot identity (e.g. `my-app[bot]`) for consistency with `tag-claude`, `lint-failure`, `ci-failure`, and `apply-fix`. Changes: - `pr-review/action.yml`: add `app_id` / `app_private_key` inputs; insert `Generate App token` (actions/create-github-app-token@1b10c78 # v3.1.1) and `Resolve review token` steps before `claude-code-action`; replace `github_token: ${{ github.token }}` with `steps.token.outputs.value`. - Bot-comment selectors updated at three sites (quality gate, synthesis, shadow gate fallback) plus the incomplete-review warning step — all four change `.user.login == "github-actions[bot]"` / `.author.login == ...` to `endswith("[bot]")` so they match any bot identity, not just `github-actions[bot]`. - `.github/workflows/claude-pr-review.yml`: add `app_id` and `app_private_key` to `workflow_call` secrets block; wire both through to the composite-action step via `secrets.app_id || secrets.APP_ID` and `secrets.app_private_key || secrets.APP_PRIVATE_KEY` for compatibility with both dogfood and consumer naming conventions. - `CLAUDE.md`: reverse the `github.token` carve-out in the token-selection convention; add `pr-review` to the Required secrets table. - `README.md`: update Quick Start PR Review example, migration notes, and prerequisite text to reflect that `APP_ID` + `APP_PRIVATE_KEY` are now required for `pr-review`. --- .github/workflows/claude-pr-review.yml | 6 ++++ CLAUDE.md | 4 +-- README.md | 12 ++++---- pr-review/action.yml | 39 ++++++++++++++++++++++---- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml index 57e00c4..56f5713 100644 --- a/.github/workflows/claude-pr-review.yml +++ b/.github/workflows/claude-pr-review.yml @@ -12,6 +12,10 @@ on: secrets: claude_code_oauth_token: required: true + app_id: + required: true + app_private_key: + required: true inputs: model: description: 'Claude model to use' @@ -54,5 +58,7 @@ jobs: - uses: glitchwerks/github-actions/pr-review@v2 with: claude_code_oauth_token: ${{ secrets.claude_code_oauth_token || secrets.CLAUDE_CODE_OAUTH_TOKEN }} + app_id: ${{ secrets.app_id || secrets.APP_ID }} + app_private_key: ${{ secrets.app_private_key || secrets.APP_PRIVATE_KEY }} model: ${{ inputs.model || 'claude-sonnet-4-5' }} max_turns: ${{ inputs.max_turns || '30' }} diff --git a/CLAUDE.md b/CLAUDE.md index 5ce4b17..1084648 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ The `claude-*.yml` reusable workflows (everything except `ci-failure.yaml` and ` **Workflows with `container: ghcr.io/...` must declare `packages: read`** in their `permissions:` block when the image is in an org-owned private GHCR package. GitHub Actions issues an implicit `docker login + docker pull` before the job container starts, authenticated with `GITHUB_TOKEN`; without `packages: read` the registry returns `manifest unknown` (an authorization-masked error that looks like the image is missing). All five Phase 5 (#188) container-pinned reusable workflows hit this trap because their `permissions:` blocks were carried over from the pre-container form — see #192 for the diagnosis and the hotfix that added `packages: read` to each. Add this scope alongside `contents:`, `pull-requests:`, etc. for any future workflow with a `container:` directive pointing at GHCR. -**Token selection for `claude-code-action`:** Use `github_token: ${{ github.token }}` for read-only operations (PR review, which does not push commits). Use the resolved App token (`${{ steps.token.outputs.value }}`) when `claude-code-action` must push commits — tag responses, lint-diagnose, lint-failure, ci-failure, and apply-fix all pass the App token. GitHub suppresses `synchronize` events for pushes authenticated with `GITHUB_TOKEN`, so an App token is required to re-trigger downstream workflows like `pr-review`. App tokens are short-lived and show a distinct bot identity (e.g., `my-app[bot]`). +**Token selection for `claude-code-action`:** All workflows now use the resolved App token (`${{ steps.token.outputs.value }}`) for `github_token` — including `pr-review`, which does not push commits but benefits from a consistent App bot identity. The prior carve-out that used `github.token` (surfacing as `github-actions[bot]`) for read-only PR review was reversed under issue #250 for identity consistency across all Claude-powered workflows. GitHub suppresses `synchronize` events for pushes authenticated with `GITHUB_TOKEN`, so an App token is required to re-trigger downstream workflows like `pr-review` on those paths as well. App tokens are short-lived and show a distinct bot identity (e.g., `my-app[bot]`). **Composite action inputs are always strings** — there is no `type` field. Boolean inputs like `require_association` arrive as the string `'true'`/`'false'` and must be compared with `[ "$VAR" = "true" ]`. @@ -103,5 +103,5 @@ When changes are released: move both `v2` and the new `v2.x.x` tag to the latest | Secret | Used by | |---|---| | `CLAUDE_CODE_OAUTH_TOKEN` | All `claude-code-action` invocations | -| `APP_ID` | `ci-failure.yaml`, `apply-fix`, `lint-failure` — GitHub App ID for generating short-lived tokens for git push and API calls | +| `APP_ID` | `ci-failure.yaml`, `apply-fix`, `lint-failure`, `pr-review` — GitHub App ID for generating short-lived tokens for git push and API calls | | `APP_PRIVATE_KEY` | Same as above — GitHub App private key (PEM format) | diff --git a/README.md b/README.md index b25baf4..f7a8192 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Reusable GitHub Actions and workflows for Claude-powered automation — PR review, `@claude` mention responses, lint-failure diagnosis, CI-failure diagnosis, and manual fix application — built on top of [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action). -`CLAUDE_CODE_OAUTH_TOKEN` is required for all actions. Write-capable actions (those that push commits or trigger downstream workflows) additionally require `APP_ID` and `APP_PRIVATE_KEY` — credentials for a GitHub App that issues short-lived tokens at job time. +`CLAUDE_CODE_OAUTH_TOKEN` is required for all actions. All actions also require `APP_ID` and `APP_PRIVATE_KEY` — credentials for a GitHub App that issues short-lived tokens at job time, ensuring a consistent bot identity (e.g., `my-app[bot]`) across all Claude-powered comments and commits. **New consumer?** Start with [docs/consumer-onboarding.md](docs/consumer-onboarding.md) — an end-to-end walkthrough for wiring this library into a `glitchwerks` org repo for the first time. The reference docs below assume you have already completed the basic setup it covers. @@ -61,6 +61,8 @@ jobs: uses: glitchwerks/github-actions/.github/workflows/claude-pr-review.yml@v2 secrets: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + app_id: ${{ secrets.APP_ID }} + app_private_key: ${{ secrets.APP_PRIVATE_KEY }} ``` Optional inputs: @@ -157,15 +159,15 @@ If you need more control (e.g., embed the review step inside a larger job), use ### Breaking changes - **All `uses:` references must change from `@v1` to `@v2`.** Every workflow or composite action call that pins to `@v1` must be updated. -- **`gh_pat` input has been removed.** The deprecated `gh_pat` input is no longer accepted by any action. `app_id` + `app_private_key` are now required for all write-capable actions (`tag-claude`, `lint-failure`, `ci-failure`, `apply-fix`). +- **`gh_pat` input has been removed.** The deprecated `gh_pat` input is no longer accepted by any action. `app_id` + `app_private_key` are now required for all actions (`tag-claude`, `lint-failure`, `ci-failure`, `apply-fix`, `pr-review`). - **`tag-claude` no longer falls back to `github.token`.** An App token is mandatory — omitting `app_id` and `app_private_key` will cause the action to fail at the token resolution step. -- **All write-capable actions now post under the GitHub App's bot identity** (e.g., `my-app[bot]`) rather than under `github-actions[bot]` or a PAT's user identity. +- **All actions now post under the GitHub App's bot identity** (e.g., `my-app[bot]`) rather than under `github-actions[bot]` or a PAT's user identity. This includes `pr-review`, which previously used `github.token` (issue #250). ### Migration steps 1. Update every `uses: glitchwerks/github-actions/...@v1` line (and `uses: .github/workflows/...@v1`) to `@v2`. 2. If you were passing `gh_pat`, create a GitHub App and add `APP_ID` + `APP_PRIVATE_KEY` as repository secrets. See the GitHub App setup section for instructions. -3. For `tag-claude` consumers: ensure your caller workflow passes `app_id` and `app_private_key` secrets. +3. For `tag-claude` and `pr-review` consumers: ensure your caller workflow passes `app_id` and `app_private_key` secrets. ### Before and after @@ -477,4 +479,4 @@ actionlint # from repo root ## Prerequisites - A `CLAUDE_CODE_OAUTH_TOKEN` secret must be set on the consuming repository (or organization). Obtain this token from [claude.ai](https://claude.ai). -- A GitHub App (`APP_ID` + `APP_PRIVATE_KEY`) is required for write operations (git push, triggering downstream workflows). See the GitHub App setup section under CI Failure Diagnosis for instructions. +- A GitHub App (`APP_ID` + `APP_PRIVATE_KEY`) is required for all actions — for identity consistency (`pr-review`) and write operations (git push, triggering downstream workflows) in the others. See the GitHub App setup section under CI Failure Diagnosis for instructions. diff --git a/pr-review/action.yml b/pr-review/action.yml index 2b3affe..ded08ac 100644 --- a/pr-review/action.yml +++ b/pr-review/action.yml @@ -5,6 +5,12 @@ inputs: claude_code_oauth_token: description: 'Claude Code OAuth token' required: true + app_id: + description: 'GitHub App ID for generating a short-lived token (required for App-identity comments)' + required: false + app_private_key: + description: 'GitHub App private key for generating a short-lived token (required for App-identity comments)' + required: false model: description: 'Claude model to use' required: false @@ -194,12 +200,35 @@ runs: echo "::warning::No baked persona at /opt/claude/.claude/CLAUDE.md — running without overlay persona" fi + - name: Generate App token + id: app-token + if: inputs.app_id != '' && inputs.app_private_key != '' + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ inputs.app_id }} + private-key: ${{ inputs.app_private_key }} + + - name: Resolve review token + id: token + shell: bash + # shellcheck disable=SC2016 + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + if [ -n "$APP_TOKEN" ]; then + echo "::debug::Using GitHub App token for review identity" + echo "value=$APP_TOKEN" >> "$GITHUB_OUTPUT" + else + echo "::error::No authentication token provided. Set app_id + app_private_key inputs. See README for GitHub App setup instructions." + exit 1 + fi + - uses: anthropics/claude-code-action@v1 id: claude-review if: steps.authz.outputs.skip != 'true' && steps.size-check.outputs.skip != 'true' with: claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} - github_token: ${{ github.token }} + github_token: ${{ steps.token.outputs.value }} use_sticky_comment: true track_progress: true claude_args: --max-turns ${{ env.EFFECTIVE_MAX_TURNS }} --model ${{ inputs.model }} --allowedTools "Bash(gh pr diff:*),Bash(gh pr review:*),Bash(gh pr view:*)" @@ -267,7 +296,7 @@ runs: # default ordering. Pagination: 100 comments is enough for any # realistic PR; if a PR ever exceeds, the gate misses newer pages. BODY=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \ - --jq 'map(select(.user.login == "github-actions[bot]")) | sort_by(.updated_at) | last | .body // ""' \ + --jq 'map(select(.user.login | endswith("[bot]"))) | sort_by(.updated_at) | last | .body // ""' \ 2>/dev/null || echo "") # If no bot comment exists, the review has not completed — skip the status @@ -338,7 +367,7 @@ runs: # In practice each new run produces a separate sticky comment (multiple bot # comments accumulate on a PR with multiple pushes — see action.yml line 254). COMMENT_JSON=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \ - --jq "map(select(.user.login == \"github-actions[bot]\") | select(.updated_at >= \"$REVIEW_START_TIME\")) | sort_by(.updated_at) | last // empty" \ + --jq "map(select((.user.login | endswith(\"[bot]\")) and .updated_at >= \"$REVIEW_START_TIME\")) | sort_by(.updated_at) | last // empty" \ 2>/dev/null || echo "") if [ -z "$COMMENT_JSON" ]; then @@ -448,7 +477,7 @@ runs: # edited bot comment; the API returns created_at ASC by default and does not # honour sort/direction params. BODY=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \ - --jq 'map(select(.user.login == "github-actions[bot]")) | sort_by(.updated_at) | last | .body // ""' \ + --jq 'map(select(.user.login | endswith("[bot]"))) | sort_by(.updated_at) | last | .body // ""' \ 2>/dev/null || echo "") fi @@ -557,7 +586,7 @@ runs: # from human comments or comments posted by a prior run on the same PR. COMMENT_COUNT=$(gh pr view "$PR_NUMBER" --json comments \ | jq --arg since "$REVIEW_START_TIME" \ - '[.comments[] | select(.author.login == "github-actions[bot]") | select(.createdAt > $since)] | length' \ + '[.comments[] | select(.author.login | endswith("[bot]")) | select(.createdAt > $since)] | length' \ 2>/dev/null || echo "0") if [ "$COMMENT_COUNT" -gt "0" ]; then From 675bf64a14b8b69b1fb6d9cda266e22cd36e5ee1 Mon Sep 17 00:00:00 2001 From: Claude Auto-Fix Date: Sat, 9 May 2026 09:28:14 -0400 Subject: [PATCH 2/3] chore(pr-review): update stale 'github-actions[bot]' references in comments and log strings Selector logic was already updated to endswith("[bot]") in the previous commit; this is a follow-up to make explanatory comments and echo log lines reflect the broader matcher. No logic change. Co-Authored-By: Claude Opus 4.7 --- pr-review/action.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pr-review/action.yml b/pr-review/action.yml index ded08ac..2e666b1 100644 --- a/pr-review/action.yml +++ b/pr-review/action.yml @@ -283,7 +283,7 @@ runs: # shellcheck disable=SC1090 source "$SEVERITY_REGEX_SH" - # Fetch the most recent comment authored by github-actions[bot]. Claude's + # Fetch the most recent comment authored by any [bot] account. Claude's # review uses use_sticky_comment: true, but in practice each new run # produces a separate sticky comment (multiple bot comments accumulate # on a PR with multiple pushes). We grep the genuinely-latest one. @@ -304,7 +304,7 @@ runs: # Branch protection will see no status and treat it as a missing required # check, which correctly blocks merge until the review actually runs. if [ -z "$BODY" ]; then - echo "Quality gate: no github-actions[bot] sticky comment found — skipping status post" + echo "Quality gate: no [bot] sticky comment found — skipping status post" exit 0 fi @@ -483,7 +483,7 @@ runs: # Skip entirely when there is no bot comment (review did not complete). if [ -z "$BODY" ]; then - echo "Shadow gate: no github-actions[bot] sticky comment found — skipping" + echo "Shadow gate: no [bot] sticky comment found — skipping" exit 0 fi @@ -582,7 +582,7 @@ runs: # Skip the warning if Claude already posted a review comment during this run. # Claude posts its summary as a regular PR comment (via use_sticky_comment), not a # formal review submission, so we check comments rather than reviews. - # Filter by author (github-actions[bot]) and timestamp to avoid false positives + # Filter by author (any [bot]) and timestamp to avoid false positives # from human comments or comments posted by a prior run on the same PR. COMMENT_COUNT=$(gh pr view "$PR_NUMBER" --json comments \ | jq --arg since "$REVIEW_START_TIME" \ From dd2cdf9d232d2d8a87a52c71f9c202258cfd6f96 Mon Sep 17 00:00:00 2001 From: Claude Auto-Fix Date: Sat, 9 May 2026 09:33:50 -0400 Subject: [PATCH 3/3] fix(pr-review): gate quality + shadow selectors by REVIEW_START_TIME (#250 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review surfaced a regression: the authoritative quality-gate selector (line 299) and the shadow-gate fallback selector (line 480) used `sort_by(.updated_at) | last` without a timestamp filter. Under the previous literal `github-actions[bot]` matcher this was safe — only the review job's runner posted as that user. Under the new `endswith("[bot]")` matcher introduced by this PR, any other bot's more-recent comment (Dependabot, Renovate, CodeQL, etc.) could win the sort and the gate would read the wrong body — silent skip-fail of exactly the kind #250 was scoped to avoid. Mirror the synthesis step's timestamp gate (line 370) at both authoritative and shadow sites: filter to comments updated at or after $REVIEW_START_TIME before sorting. Also explicitly forward REVIEW_START_TIME into each step's env: block to make the dependency visible. Co-Authored-By: Claude Opus 4.7 --- pr-review/action.yml | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/pr-review/action.yml b/pr-review/action.yml index 2e666b1..a451d5d 100644 --- a/pr-review/action.yml +++ b/pr-review/action.yml @@ -275,6 +275,7 @@ runs: # status checks per the HEAD commit of the PR's head branch. HEAD_SHA: ${{ github.event.pull_request.head.sha }} SEVERITY_REGEX_SH: ${{ github.action_path }}/lib/severity-regex.sh + REVIEW_START_TIME: ${{ env.REVIEW_START_TIME }} run: | set -euo pipefail REPO="$GH_REPOSITORY" @@ -283,20 +284,25 @@ runs: # shellcheck disable=SC1090 source "$SEVERITY_REGEX_SH" - # Fetch the most recent comment authored by any [bot] account. Claude's - # review uses use_sticky_comment: true, but in practice each new run - # produces a separate sticky comment (multiple bot comments accumulate - # on a PR with multiple pushes). We grep the genuinely-latest one. + # Fetch the most recent comment authored by any [bot] account *that was + # updated during this review run*. The endswith("[bot]") matcher picks + # up Claude's App-identity comment regardless of bot login (#250 made + # the matcher login-agnostic), but it would also pick up stale comments + # from other bots (Dependabot, Renovate, CodeQL, etc.) — so we MUST + # gate by REVIEW_START_TIME, otherwise a foreign bot updating its own + # comment after Claude finishes would silently win the sort and the + # gate would read the wrong body. Mirrors the synthesis step at + # ~line 370 and the incomplete-review check at ~line 589. # # IMPORTANT (#184 fix): the GitHub REST API's /issues/{n}/comments # endpoint does NOT support sort/direction query params — those are # silently ignored and results come back in created_at ASC order. # We must sort manually in jq. Using sort_by(.updated_at) | last - # picks the most-recently-edited bot comment regardless of API - # default ordering. Pagination: 100 comments is enough for any + # picks the most-recently-edited matching bot comment regardless of + # API default ordering. Pagination: 100 comments is enough for any # realistic PR; if a PR ever exceeds, the gate misses newer pages. BODY=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \ - --jq 'map(select(.user.login | endswith("[bot]"))) | sort_by(.updated_at) | last | .body // ""' \ + --jq "map(select((.user.login | endswith(\"[bot]\")) and .updated_at >= \"$REVIEW_START_TIME\")) | sort_by(.updated_at) | last | .body // \"\"" \ 2>/dev/null || echo "") # If no bot comment exists, the review has not completed — skip the status @@ -457,6 +463,7 @@ runs: # potentially hitting API replication lag on the just-patched body. # shellcheck disable=SC2016 SYNTHESIS_COMMENT_ID: ${{ env.SYNTHESIS_COMMENT_ID }} + REVIEW_START_TIME: ${{ env.REVIEW_START_TIME }} run: | set -euo pipefail REPO="$GH_REPOSITORY" @@ -467,7 +474,9 @@ runs: # --- Fetch the review comment body. ------------------------------------------ # If the synthesis step pinned a comment ID to $GITHUB_ENV, use it directly # (avoids replication-lag race on the just-PATCHed body). Otherwise fall back - # to the sort_by(updated_at)|last selector. + # to the sort_by(updated_at)|last selector, gated by REVIEW_START_TIME so a + # foreign bot's stale comment can't win the sort under endswith("[bot]") + # (#250) — mirrors the authoritative gate's selector. if [ -n "${SYNTHESIS_COMMENT_ID:-}" ]; then BODY=$(gh api "repos/$REPO/issues/comments/$SYNTHESIS_COMMENT_ID" \ --jq '.body // ""' 2>/dev/null || echo "") @@ -477,7 +486,7 @@ runs: # edited bot comment; the API returns created_at ASC by default and does not # honour sort/direction params. BODY=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \ - --jq 'map(select(.user.login | endswith("[bot]"))) | sort_by(.updated_at) | last | .body // ""' \ + --jq "map(select((.user.login | endswith(\"[bot]\")) and .updated_at >= \"$REVIEW_START_TIME\")) | sort_by(.updated_at) | last | .body // \"\"" \ 2>/dev/null || echo "") fi