Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/interactive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ on:
type: string
required: false
default: "claude[bot]"
retry_on_rate_limit:
description: "When a credential attempt fails with a 429 that looks like a genuine short-lived rate limit rather than a hard quota exhaustion, wait rate_limit_retry_wait_seconds and retry that same credential once before falling through to the next configured credential. On by default; set false to fall through immediately on any failure, as before this existed."
type: boolean
required: false
default: true
rate_limit_retry_wait_seconds:
description: "Seconds to wait before retrying the same credential once, when retry_on_rate_limit judges a failure to be a genuine short-lived rate limit. Default 60. Ignored when retry_on_rate_limit is false."
type: string
required: false
default: "60"
plugins:
description: "Newline-separated Claude Code plugin names to install before responding."
type: string
Expand Down Expand Up @@ -276,6 +286,8 @@ jobs:
use_commit_signing: ${{ inputs.use_commit_signing }}
bot_id: ${{ inputs.bot_id }}
bot_name: ${{ inputs.bot_name }}
retry_on_rate_limit: ${{ inputs.retry_on_rate_limit }}
rate_limit_retry_wait_seconds: ${{ inputs.rate_limit_retry_wait_seconds }}
plugins: ${{ inputs.plugins }}
plugin_marketplaces: ${{ inputs.plugin_marketplaces }}
use_bedrock: ${{ inputs.use_bedrock }}
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ on:
type: string
required: false
default: "claude[bot]"
retry_on_rate_limit:
description: "When a credential attempt fails with a 429 that looks like a genuine short-lived rate limit rather than a hard quota exhaustion, wait rate_limit_retry_wait_seconds and retry that same credential once before falling through to the next configured credential. On by default; set false to fall through immediately on any failure, as before this existed."
type: boolean
required: false
default: true
rate_limit_retry_wait_seconds:
description: "Seconds to wait before retrying the same credential once, when retry_on_rate_limit judges a failure to be a genuine short-lived rate limit. Default 60. Ignored when retry_on_rate_limit is false."
type: string
required: false
default: "60"
plugins:
description: "Newline-separated Claude Code plugin names to install before the review."
type: string
Expand Down Expand Up @@ -299,6 +309,8 @@ jobs:
exclude_comments_by_actor: ${{ inputs.exclude_comments_by_actor }}
bot_id: ${{ inputs.bot_id }}
bot_name: ${{ inputs.bot_name }}
retry_on_rate_limit: ${{ inputs.retry_on_rate_limit }}
rate_limit_retry_wait_seconds: ${{ inputs.rate_limit_retry_wait_seconds }}
plugins: ${{ inputs.plugins }}
plugin_marketplaces: ${{ inputs.plugin_marketplaces }}
use_bedrock: ${{ inputs.use_bedrock }}
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ on:
type: string
required: false
default: "claude[bot]"
retry_on_rate_limit:
description: "When a credential attempt fails with a 429 that looks like a genuine short-lived rate limit rather than a hard quota exhaustion, wait rate_limit_retry_wait_seconds and retry that same credential once before falling through to the next configured credential. On by default; set false to fall through immediately on any failure, as before this existed."
type: boolean
required: false
default: true
rate_limit_retry_wait_seconds:
description: "Seconds to wait before retrying the same credential once, when retry_on_rate_limit judges a failure to be a genuine short-lived rate limit. Default 60. Ignored when retry_on_rate_limit is false."
type: string
required: false
default: "60"
plugins:
description: "Newline-separated Claude Code plugin names to install before triage."
type: string
Expand Down Expand Up @@ -221,6 +231,8 @@ jobs:
exclude_comments_by_actor: ${{ inputs.exclude_comments_by_actor }}
bot_id: ${{ inputs.bot_id }}
bot_name: ${{ inputs.bot_name }}
retry_on_rate_limit: ${{ inputs.retry_on_rate_limit }}
rate_limit_retry_wait_seconds: ${{ inputs.rate_limit_retry_wait_seconds }}
plugins: ${{ inputs.plugins }}
plugin_marketplaces: ${{ inputs.plugin_marketplaces }}
use_bedrock: ${{ inputs.use_bedrock }}
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ Two things worth knowing before relying on it:
- **Fallback is aimed at failures that happen before Claude does anything on GitHub.** An expired, revoked, or rate-limited credential fails at authentication, before any branch, comment, or review exists — the common case this is for. If a run instead fails mid-conversation, after already creating a branch (`branch_name_template`'s own default includes `{{timestamp}}`, so a fresh trigger gets a new branch name on every attempt) or posting a sticky comment (whose own lookup for "the comment to update" has a confirmed upstream bug when a custom `github_token` is supplied — [anthropics/claude-code-action#960](https://github.com/anthropics/claude-code-action/issues/960), which this action always does), the next credential's attempt starts fresh rather than resuming that state, and can leave a duplicate or orphaned branch or comment behind. This action does not try to reconcile that — worth checking for after any run that actually needed more than one attempt.
- **Do not enable `ACTIONS_STEP_DEBUG`/`ACTIONS_RUNNER_DEBUG` on a workflow using this.** Each parsed credential is masked (`::add-mask::`) the moment it is read, which keeps it out of every subsequent log line for the rest of the job — but the runner has a long-standing, still-open bug ([actions/runner#159](https://github.com/actions/runner/issues/159), [actions/runner#475](https://github.com/actions/runner/issues/475)) where step debug logging can print a value on the very same line that registers its own mask, before the mask takes effect. Normal logging never triggers this; debug logging specifically does.

### Retrying the same credential on a genuine rate limit

Falling through to the next credential is the right response to a quota exhaustion (an account's usage cap, which no amount of waiting inside this job will lift), but the wrong one for a genuine short-lived rate limit (too many requests in a short window, which a brief wait clears) — moving to a different account for that either burns a fallback credential unnecessarily or, if none is configured, fails a run that a short wait would have completed. `retry_on_rate_limit` (on by default) tells `action.yml` to tell the two apart: after any attempt fails, a "Rate-limit retry gate" step reads that attempt's own execution output and checks whether the failure was a 429 whose message does **not** name a multi-day-scale reset ("weekly limit", "monthly limit", and similar) — a plain 429 with no such wording is treated as a genuine rate limit worth waiting out; a 429 that does name one is a quota exhaustion, left to fall straight through to the next credential exactly as before. When the gate judges a failure retryable, this action waits `rate_limit_retry_wait_seconds` (default 60) and reruns that exact same credential once via an `attempt_N_retry` step, before falling through to the next configured credential (or giving up) only if that retry also fails.

This is a single, bounded retry per credential slot, not a configurable count: like the fallback chain itself, a composite action has no native loop, so "retry N times" would mean N more statically unrolled steps per slot rather than something one input could scale arbitrarily. One extra attempt per slot is what the static step graph actually provides. Set `retry_on_rate_limit: false` to restore the previous behaviour — fall through immediately on any failure, genuine rate limit or not.

The classification reads only the failing attempt's own `execution_file` output (the same JSON `structured_review_summary` and other steps already treat as this action's own internal state) — it is not fed anything from the triggering issue, pull request, or comment, so it carries none of the untrusted-input risk the [Security notes](#security-notes) below are about.

## Context compression (Headroom)

`headroom_enabled` (on by default, all modes) runs [Headroom](https://github.com/headroomlabs-ai/headroom) — a local HTTP proxy that compresses tool outputs, logs, and file content before they reach the model — in front of the Anthropic API for the job, and points every `anthropics/claude-code-action` call in it at that proxy via `ANTHROPIC_BASE_URL`. Review, triage, and interactive runs routinely push large diffs, CI logs, and repo-file reads through Claude Code, which is exactly the repetitive, JSON/log-shaped content Headroom compresses well.
Expand Down Expand Up @@ -338,6 +346,8 @@ Beyond the basics, every mode's reusable workflow exposes further optional input

**Bot identity** (all modes): `bot_id` / `bot_name` control the identity used for git commit authorship specifically (separate from `github_token`, which governs API calls and therefore who _comments_ appear from). Default to Claude's own upstream identity (`41898282` / `claude[bot]`) — leave them unset rather than passing an empty string, which would blank that identity out instead of falling back to it.

**Rate-limit retry** (all modes): `retry_on_rate_limit` (on by default) and `rate_limit_retry_wait_seconds` (default `60`) — see [Retrying the same credential on a genuine rate limit](#retrying-the-same-credential-on-a-genuine-rate-limit) above.

**Plugins** (all modes): `plugins` / `plugin_marketplaces` install Claude Code plugins before the run.

**Alternative model providers** (all modes): `use_bedrock`, `use_vertex`, and `use_foundry` switch off the direct Anthropic API. If your cloud credentials come from static secrets in the calling job's own environment (an AWS access key, a GCP service account JSON key), this works fine through the reusable workflows — set the relevant credentials in your own caller workflow's `env:` before it calls one of ours. **Only the OIDC-based path is restricted**: authenticating via GitHub's own OIDC token — whether that's Bedrock/Vertex/Foundry's own OIDC support, or the `anthropic_*` workload-identity-federation inputs — needs `id-token: write` reaching the composite action call itself, and (per step 3 above) none of this repository's reusable workflows declare that scope in their own hardcoded `permissions:` block — so there is nothing for even an explicit caller-side grant of `id-token: write` to flow into. If you need OIDC-based auth specifically, call `ExaDev/claude-code-action@v1` directly from your own job with your own `permissions:` block instead of going through review.yml/triage.yml/interactive.yml.
Expand Down Expand Up @@ -427,8 +437,9 @@ Worth enabling `sha_pinning_required` in the organisation's Actions settings (Se

```text
action.yml Composite action: input validation, prompt composition, stack
detection, session-cache resume, credential fallback, per-mode
tool allowlists, pinned upstream call, automatic fixes
detection, session-cache resume, credential fallback with
rate-limit retry, per-mode tool allowlists, pinned upstream
call, automatic fixes
turbo.json Turborepo task cache config (lint/typecheck/format:check)
commitlint.config.ts Conventional-commit enforcement (local hook + CI)
release.config.ts semantic-release config; scripts/move-major-tag.mjs is its
Expand Down
Loading
Loading