Skip to content

feat(pr-review): switch to App-token identity (#250)#252

Merged
cbeaulieu-gt merged 3 commits intomainfrom
issue-250-pr-review-app-token
May 9, 2026
Merged

feat(pr-review): switch to App-token identity (#250)#252
cbeaulieu-gt merged 3 commits intomainfrom
issue-250-pr-review-app-token

Conversation

@cbeaulieu-gt
Copy link
Copy Markdown
Member

Closes #250

Summary

Reverses the deliberate github.token carve-out documented in #114 for the pr-review action. Under a single-operator personal-repo constraint, the cost of the carve-out (review comments posting as github-actions[bot] while every other Claude-powered workflow posts as the App's [bot] identity) outweighs the benefit (avoiding consumer onboarding regression, which doesn't bind here).

Changes

  • pr-review/action.yml
    • Adds app_id and app_private_key composite-action inputs.
    • Inserts actions/create-github-app-token@1b10c78… # v3.1.1 (SHA-pinned, matching apply-fix/lint-failure/tag-claude pattern) and a Resolve review token step that emits a clear error if both inputs are missing.
    • claude-code-action@v1 step now consumes ${{ steps.token.outputs.value }} for github_token (was ${{ github.token }}).
    • Four bot-comment selectors updated atomically from literal github-actions[bot] match to endswith("[bot]"):
      • Line 299 — quality-gate authoritative selector
      • Line 370 — synthesis step (already had timestamp guard)
      • Line 480 — shadow-gate fallback selector
      • Line 589 — incomplete-review check (uses gh pr view --json commentsauthor.login not user.login)
    • Selector hardening (review feedback): the authoritative gate (line 299) and shadow-gate fallback (line 480) now also gate by \$REVIEW_START_TIME, mirroring the synthesis step. Without this, a foreign bot (Dependabot, Renovate, CodeQL) updating its comment after Claude's review finishes could silently win the sort under the widened endswith("[bot]") matcher.
  • .github/workflows/claude-pr-review.yml — adds app_id/app_private_key to the workflow_call secrets: block (required: true), wires through to the composite step with secrets.app_id || secrets.APP_ID fallback.
  • CLAUDE.md## Key conventions § Token selection updated to reflect that all workflows now use the App token; carve-out reversed under feat(pr-review): switch to App-token identity (revisit #114 carve-out) #250. Required-secrets table lists pr-review under APP_ID/APP_PRIVATE_KEY.
  • README.md — Quick Start example, prerequisites, and migration breaking-changes section updated.

Test plan

  • CI: actionlint passes (verified locally).
  • Dogfood: this PR's own claude-pr-review job runs under the new App identity and posts a sticky comment that endswith("[bot]").
  • Quality-gate status check posts on this PR using the new selector + timestamp filter; agrees with shadow gate.
  • Floated v2 against an external consumer (siege-web or similar) — review run posts under App identity, no consumer regression.
  • Verify no foreign-bot interference: this repo currently has no Dependabot/Renovate/CodeQL configured, but the REVIEW_START_TIME guard makes the selector resilient if any are added later.

Notes

  • Breaking for consumers of glitchwerks/github-actions/.github/workflows/claude-pr-review.yml@v2: callers must now pass app_id and app_private_key secrets. Migration guidance is in the updated README.
  • The fourth selector (line 589) uses gh pr view --json comments which exposes author.login rather than the REST API's user.login — both code paths updated.

🤖 Generated by Claude Code on behalf of @cbeaulieu-gt

Claude Auto-Fix and others added 3 commits May 9, 2026 09:21
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`.
…mments 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 <noreply@anthropic.com>
…250 review)

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 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

Claude finished @cbeaulieu-gt's task in 2m 59s —— View job


Claude PR Review — Complete

Reviewed PR #252: feat(pr-review): switch to App-token identity (#250)

This PR reverses the github.token carve-out for pr-review and switches to App-token identity for consistency across all Claude-powered workflows. The implementation follows established patterns from other actions and includes defensive hardening via REVIEW_START_TIME gating to prevent foreign bot interference.


Findings

🟢 Medium — Input validation mismatch creates confusing failure mode

pr-review/action.yml:9-13, .github/workflows/claude-pr-review.yml:15-18

The composite action declares app_id and app_private_key as required: false, but the reusable workflow marks these secrets as required: true. This creates a state where:

  • External consumers calling the workflow must provide the secrets (enforced at workflow level)
  • Internal calls to the composite action accept missing inputs but fail at runtime (line 222-223)

The runtime error message is clear, but this inconsistency is confusing:

# pr-review/action.yml
app_id:
  description: 'GitHub App ID for generating a short-lived token (required for App-identity comments)'
  required: false  # ← Should this be true?

Recommendation: Consider marking these inputs as required: true in the composite action to match the workflow contract and fail fast with a clear validation error. Alternatively, document why they're marked optional if there's a future path that doesn't require them.

Impact: Low — the error is caught at runtime with a clear message, but the mismatch creates unnecessary confusion during integration.


🟢 Medium — Timestamp comparison might pick up foreign bot in edge case

pr-review/action.yml:305, 376, 489

All three bot-comment selectors use >= "$REVIEW_START_TIME" to filter comments:

map(select((.user.login | endswith("[bot]")) and .updated_at >= "$REVIEW_START_TIME"))

Using >= creates a theoretical race: if Dependabot (or another bot) updates its comment with updated_at exactly equal to REVIEW_START_TIME, it would pass the filter and could be selected instead of Claude's comment.

Recommendation: Change the timestamp comparison from >= to > to ensure only comments updated after the review started are selected. This makes the filter strictly exclusive of any pre-existing bot activity.

-map(select((.user.login | endswith("[bot]")) and .updated_at >= "$REVIEW_START_TIME"))
+map(select((.user.login | endswith("[bot]")) and .updated_at > "$REVIEW_START_TIME"))

Impact: Very low probability in practice (requires exact timestamp collision to the second in ISO 8601), but the fix is trivial and eliminates the edge case entirely.


Nit — Error message assumes missing inputs rather than token generation failure

pr-review/action.yml:222-223

The "Resolve review token" step's error message assumes the inputs are missing:

echo "::error::No authentication token provided. Set app_id + app_private_key inputs. See README for GitHub App setup instructions."

However, the step also runs when app_id and app_private_key are provided but the App token generation fails (e.g., invalid private key format, expired credentials, network error). In that case, the error message is misleading.

Recommendation: Differentiate between missing inputs and token generation failure:

if [ -n "${{ inputs.app_id }}" ] && [ -n "${{ inputs.app_private_key }}" ]; then
  echo "::error::GitHub App token generation failed. Check APP_ID and APP_PRIVATE_KEY validity."
else
  echo "::error::No authentication token provided. Set app_id + app_private_key inputs. See README for GitHub App setup instructions."
fi
exit 1

This provides clearer diagnostics for troubleshooting.

Impact: Developer experience — clearer error messages reduce time to resolution when integration fails.


Nit — Selector synchronization relies on manual discipline

pr-review/action.yml:305, 376, 489 (and comments at lines 294, 370, 479)

Three separate code locations use nearly-identical jq selectors for bot comments:

  1. Authoritative quality gate (line 305)
  2. Synthesis step (line 376)
  3. Shadow gate fallback (line 489)

The comments explicitly note these must stay synchronized (~line 294: "Mirrors the synthesis step at ~line 370 and the incomplete-review check at ~line 589"). If a future edit updates one selector but not the others, the gates could diverge and produce inconsistent results.

Recommendation: Extract the common jq filter expression into a shell variable or sourced function (similar to how SEVERITY_REGEX_SH is sourced) to eliminate duplication:

# In a new file lib/bot-comment-selector.sh:
BOT_COMMENT_JQ_FILTER='map(select((.user.login | endswith("[bot]")) and .updated_at > $review_start)) | sort_by(.updated_at) | last'

# In action.yml:
source "${{ github.action_path }}/lib/bot-comment-selector.sh"
BODY=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \
  --jq "$BOT_COMMENT_JQ_FILTER | .body // \"\"" \
  --arg review_start "$REVIEW_START_TIME")

This ensures all three callsites use the exact same logic.

Impact: Maintainability — prevents future drift between selectors that must agree.


Strengths

Consistent identity across workflows — App-token usage aligns pr-review with tag-claude, lint-failure, ci-failure, and apply-fix. This eliminates the confusing mixed-identity state where most actions posted as my-app[bot] but pr-review posted as github-actions[bot].

Defensive selector hardening — Adding REVIEW_START_TIME gating to all three bot-comment selectors (authoritative gate line 305, synthesis line 376, shadow gate line 489) prevents foreign bots (Dependabot, Renovate, CodeQL) from winning the sort_by(.updated_at) | last selection. Without this guard, a Renovate comment updated after Claude finishes could silently hijack the quality gate. This addresses the core risk identified in #250.

SHA-pinned action dependencyactions/create-github-app-token@1b10c78... is digest-pinned following security best practices. Matches the pattern used in apply-fix, lint-failure, and tag-claude (consistency across the library).

Clear breaking-change documentation — README.md and CLAUDE.md both updated to reflect the new requirement. Migration steps are explicit, and the "Required secrets" table correctly lists pr-review under APP_ID / APP_PRIVATE_KEY.

Dogfooding validation — This PR's own claude-pr-review job runs under the new App identity, providing immediate smoke-test coverage for the changes.


Code Quality

The implementation is clean and follows established patterns. Variable naming is clear (APP_TOKEN, REVIEW_START_TIME), and inline comments explain the reasoning well (e.g., the detailed comment at lines 287-295 explaining why REVIEW_START_TIME gating is necessary). The refactoring from literal github-actions[bot] to endswith("[bot]") is applied atomically across all four selectors.


Security

No security regressions identified. The switch to App tokens is a lateral move (both github.token and App tokens are short-lived and scoped). SHA-pinning of the create-github-app-token action prevents supply-chain attacks. The app_private_key is passed through GitHub Actions secrets, which is the correct mechanism.

One minor note: there's no validation that the app_private_key is well-formed PEM before attempting token generation, but this is consistent with how other actions (apply-fix, lint-failure, tag-claude) handle it — the create-github-app-token action itself will reject invalid keys.


Performance

No performance impact. The API calls remain the same; only the filtering logic changes. The endswith("[bot]") jq predicate is efficient (string suffix check), and the timestamp comparison adds negligible overhead.


Test Coverage

This repository has no automated test suite (per CLAUDE.md: "There are no other build steps, tests, or package managers"). The test plan in the PR description is manual but comprehensive:

  • actionlint passes
  • ✅ Dogfood: this PR's own review job exercises the new code path
  • ✅ Quality-gate status check posts using new selector + timestamp filter
  • ✅ External consumer validation planned (siege-web float)

The lack of automated tests means the selector synchronization issue (Nit finding above) relies entirely on code review discipline to catch drift.


Documentation

Documentation is thorough and accurately reflects the changes:

  • CLAUDE.md — "Token selection for claude-code-action" section updated to explain that all workflows now use App token (line 63). The carve-out rationale is preserved for historical context but marked as reversed under feat(pr-review): switch to App-token identity (revisit #114 carve-out) #250.
  • README.md — Prerequisites section updated (line 5), breaking changes section expanded (lines 162, 165), and Quick Start example updated (lines 64-65).
  • Inline comments — The new comments at lines 287-295 (authoritative gate), 370 (synthesis), and 474-479 (shadow gate fallback) explain the REVIEW_START_TIME gating rationale and cross-reference the other callsites that must stay in sync.

The migration guidance could be slightly more explicit about the failure mode (i.e., "If you don't update, your workflow will fail at the 'Resolve review token' step with an explicit error message"), but the current documentation is sufficient.


Verdict: APPROVE

@cbeaulieu-gt
Copy link
Copy Markdown
Member Author

v2.6.0 release withdrawn. This PR was shipped as part of v2.6.0, but the release was rolled back: v2 floating tag is now back at e92a334 (v2.5.1).

Reason: the App-token requirement introduced here is breaking for any consumer pinned to an older v2.x.x reusable workflow, because those older wrappers do not pass app_id / app_private_key through. The combination with #254's API-fallback bug (#255) made the regression visible on the first real consumer run (glitchwerks/mom-bot#21).

Tracking the rollout-process problem under #256. The code change here is correct on its own; the failure was in the release/distribution path. A successor release will ship once the consumer-impact gate is in place.

🤖 Generated by Claude Code on behalf of @cbeaulieu-gt

cbeaulieu-gt added a commit to glitchwerks/mom-bot that referenced this pull request May 9, 2026
…ber assoc check

Without `secrets: inherit`, the v2.6.1 reusable workflows fail with
"No authentication token provided" because they require `app_id` and
`app_private_key` (added in glitchwerks/github-actions#252). The auth
fallback then misreads cbeaulieu-gt as CONTRIBUTOR (their glitchwerks
org membership is private), causing a misleading auth-denial.

`authorized_users: cbeaulieu-gt` short-circuits the association check
per glitchwerks/github-actions#253.

Refs #22
cbeaulieu-gt added a commit to glitchwerks/rsl-siege-manager that referenced this pull request May 9, 2026
…ber assoc check

Without `secrets: inherit`, the v2.6.1 reusable workflows fail with
"No authentication token provided" because they require `app_id` and
`app_private_key` (added in glitchwerks/github-actions#252). The auth
fallback then misreads cbeaulieu-gt as CONTRIBUTOR (their glitchwerks
org membership is private), causing a misleading auth-denial.

`authorized_users: cbeaulieu-gt` short-circuits the association check
per glitchwerks/github-actions#253.

Refs #325
cbeaulieu-gt added a commit to glitchwerks/mom-bot that referenced this pull request May 9, 2026
* chore(deps): bump glitchwerks/github-actions pins to v2.6.1 (#22)

v2.6.1 ships a fix for a pr-review quality-gate false-positive
(glitchwerks/github-actions#257). Bump the three reusable-workflow
pins from their stale versions:

- ci-failure.yaml: v2.3.0 → v2.6.1
- claude-pr-review.yml: v2.5.1 → v2.6.1
- claude-tag-respond.yml: v2.3.0 → v2.6.1

Closes #22

* fix(workflows): pass App secrets via inherit + bypass private-org-member assoc check

Without `secrets: inherit`, the v2.6.1 reusable workflows fail with
"No authentication token provided" because they require `app_id` and
`app_private_key` (added in glitchwerks/github-actions#252). The auth
fallback then misreads cbeaulieu-gt as CONTRIBUTOR (their glitchwerks
org membership is private), causing a misleading auth-denial.

`authorized_users: cbeaulieu-gt` short-circuits the association check
per glitchwerks/github-actions#253.

Refs #22
cbeaulieu-gt added a commit to glitchwerks/rsl-siege-manager that referenced this pull request May 9, 2026
* chore(deps): bump glitchwerks/github-actions pins to v2.6.1 (#325)

v2.6.1 ships a fix for a pr-review quality-gate false-positive
(glitchwerks/github-actions#257). Bump the four reusable-workflow
pins from their stale versions:

- claude-lint-fix.yml: v2.3.0 → v2.6.1
- ci-failure.yaml: v2.3.0 → v2.6.1
- claude-pr-review.yml: v2.5.1 → v2.6.1
- claude-tag-respond.yml: v2.3.0 → v2.6.1

Closes #325

* fix(workflows): pass App secrets via inherit + bypass private-org-member assoc check

Without `secrets: inherit`, the v2.6.1 reusable workflows fail with
"No authentication token provided" because they require `app_id` and
`app_private_key` (added in glitchwerks/github-actions#252). The auth
fallback then misreads cbeaulieu-gt as CONTRIBUTOR (their glitchwerks
org membership is private), causing a misleading auth-denial.

`authorized_users: cbeaulieu-gt` short-circuits the association check
per glitchwerks/github-actions#253.

Refs #325
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(pr-review): switch to App-token identity (revisit #114 carve-out)

1 participant