From 7766cd003be14cac1f49c77b59ef9ab22582f33a Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Wed, 7 Oct 2026 23:26:42 +0200 Subject: [PATCH 1/4] ci(pullfrog): retry the review with the fallback subscription token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PULLFROG_OAUTH_FALLBACK can hold a second subscription token, but the workflow only ever passed the standard names — all unset here — so the agent's single credential was the one Pullfrog holds at account scope. That limit is per account and shared with interactive local use of the same subscription, and an afternoon of local work exhausts it: in the Pinakes repository every review from 16:08 onward failed with `429 ... would exceed your account's rate limit` followed by `no other model or provider was selected`, after two that had succeeded earlier. A per-account limit is not fixed by a second expression — the first credential has to be tried and seen to fail — so this is a second attempt, not an `||`. Written as `secrets.A || secrets.B` it would be worse than nothing: with no repository-level token set, that resolves to the spare one on every run and drains both quotas at once. Three details that are not obvious from the diff: - Only the presence flag is hoisted to job level, not the token. A step-level `if:` cannot read the `secrets` context at all — GitHub refuses to parse the whole workflow with `Unrecognized named-value: 'secrets'`, which I confirmed by dispatching a throwaway workflow rather than assuming. `env` is readable there, hence the flag. - The provider keys stay on the agent steps. Moving them to job level would put every token in the environment of the checkout and of the final `run:` too, which cuts against `persist-credentials: false` and `shell: restricted`. - The fallback step passes only the fallback token. If another provider were configured and usable, the first step would already have reached it, since Pullfrog walks its credentials in order. Degrades safely: with the secret unset, HAS_OAUTH_FALLBACK is false, the second step is skipped and behaviour is exactly as before. The final step restores the failure the first step's continue-on-error held back, so a review that genuinely cannot run still reports red instead of passing silently. This repository still needs the secret itself to be set for the fallback to do anything. --- .github/workflows/pullfrog.yml | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index dbfda92..113f899 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -29,6 +29,14 @@ jobs: pullfrog: name: Pullfrog agent runs-on: ubuntu-latest + env: + # Whether a second subscription token is configured. Only the presence is + # hoisted to job level, not the token: a step-level `if:` cannot read the + # `secrets` context at all — GitHub refuses to parse the whole workflow + # with `Unrecognized named-value: 'secrets'` — while it does read `env`. + # The provider keys deliberately stay on the agent steps, so a token is + # never in the environment of the checkout or of the final `run:`. + HAS_OAUTH_FALLBACK: ${{ secrets.PULLFROG_OAUTH_FALLBACK != '' }} permissions: # The action mints a short-lived OIDC token to prove this run's identity # to Pullfrog's own token service, which is how a Router or subscription @@ -54,6 +62,11 @@ jobs: # unauthenticated fetch works. persist-credentials: false - name: Run agent + id: agent + # Held back rather than fatal: a failure here is the cue for the + # fallback step below. The final step restores the failure when no + # attempt succeeded, so a genuinely broken review still reports red. + continue-on-error: true # Pinned to a commit SHA rather than the documented `@v0`, which is a # tag that MOVES — it has already advanced through ninety v0.1.x # releases. This is the one action here that runs an agent with access @@ -127,3 +140,26 @@ jobs: # both limits are required — set them to the real limits of that model # OPENAI_COMPATIBLE_CONTEXT: "128000" # OPENAI_COMPATIBLE_MAX_OUTPUT: "16384" + + # A per-account rate limit is not fixed by a second expression, only by a + # second attempt: the first credential has to be tried and seen to fail. + # Only the fallback token is passed here. If another provider were + # configured and usable, the step above would already have reached it — + # Pullfrog walks its credentials in order — so duplicating the whole set + # would widen the token surface without changing any outcome. + - name: Run agent with the fallback subscription token + id: agent_fallback + if: steps.agent.outcome == 'failure' && env.HAS_OAUTH_FALLBACK == 'true' + uses: pullfrog/pullfrog@0d318bef8c7cf7ae3f193ef32b2bc74e1d94b4d1 # v0.1.90 + with: + prompt: ${{ inputs.prompt }} + push: disabled + shell: restricted + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.PULLFROG_OAUTH_FALLBACK }} + + - name: Fail when no attempt succeeded + if: steps.agent.outcome == 'failure' && steps.agent_fallback.outcome != 'success' + run: | + echo "::error::The agent failed with the primary credential, and the fallback failed too or is not configured." + exit 1 From e9e7449dff95bb39e5e5cb0dfcf4630a81249db4 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 8 Oct 2026 21:46:40 +0200 Subject: [PATCH 2/4] Bound Pullfrog retries and document credential priority and failure scope --- .github/workflows/pullfrog.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index 113f899..7e37f6b 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -29,6 +29,7 @@ jobs: pullfrog: name: Pullfrog agent runs-on: ubuntu-latest + timeout-minutes: 45 # Two bounded attempts plus checkout and cleanup. env: # Whether a second subscription token is configured. Only the presence is # hoisted to job level, not the token: a step-level `if:` cannot read the @@ -78,6 +79,7 @@ jobs: uses: pullfrog/pullfrog@0d318bef8c7cf7ae3f193ef32b2bc74e1d94b4d1 # v0.1.90 with: prompt: ${{ inputs.prompt }} + timeout: 20m # REVIEW ONLY. `push` defaults to `enabled`, which lets the agent # push branches and open pull requests of its own. Every commit and # pull request in this repository is authored by its maintainer, so @@ -141,6 +143,14 @@ jobs: # OPENAI_COMPATIBLE_CONTEXT: "128000" # OPENAI_COMPATIBLE_MAX_OUTPUT: "16384" + # The action exposes result, not a structured failure code. One retry is + # therefore allowed after ANY failure, not only a 429. Each attempt is + # capped at 20m: deterministic errors can spend the fallback quota, and + # a late failure after posting can repeat a review. This bounded trade-off + # is deliberate; a failed review must never turn into a green no-op. + # Env credentials are tried before account credentials in the upstream + # utils/credentialPool.ts selectConfiguredCredential workflow loop: + # https://github.com/pullfrog/pullfrog/blob/0d318bef8c7cf7ae3f193ef32b2bc74e1d94b4d1/utils/credentialPool.ts#L140-L150 # A per-account rate limit is not fixed by a second expression, only by a # second attempt: the first credential has to be tried and seen to fail. # Only the fallback token is passed here. If another provider were @@ -153,6 +163,7 @@ jobs: uses: pullfrog/pullfrog@0d318bef8c7cf7ae3f193ef32b2bc74e1d94b4d1 # v0.1.90 with: prompt: ${{ inputs.prompt }} + timeout: 20m push: disabled shell: restricted env: From 8da49fe96086c7ba25cb88a73387ca433b5918a5 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 8 Oct 2026 21:51:11 +0200 Subject: [PATCH 3/4] Cite verified runtime OAuth ordering for the fallback review --- .github/workflows/pullfrog.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index 7e37f6b..90d7bd4 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -148,9 +148,10 @@ jobs: # capped at 20m: deterministic errors can spend the fallback quota, and # a late failure after posting can repeat a review. This bounded trade-off # is deliberate; a failed review must never turn into a green no-op. - # Env credentials are tried before account credentials in the upstream - # utils/credentialPool.ts selectConfiguredCredential workflow loop: - # https://github.com/pullfrog/pullfrog/blob/0d318bef8c7cf7ae3f193ef32b2bc74e1d94b4d1/utils/credentialPool.ts#L140-L150 + # The workflow OAuth token leads account OAuth tokens of the same kind. + # Verified in runtime 0.1.97: stable subscription sorting keeps workflow + # entries first. This does not claim API keys outrank subscriptions. + # https://github.com/pullfrog/pullfrog/blob/f0684f2c9286f321085978685f6cbe91d51d4233/utils/credentialPool.ts#L175-L187 # A per-account rate limit is not fixed by a second expression, only by a # second attempt: the first credential has to be tried and seen to fail. # Only the fallback token is passed here. If another provider were From 16d0afc7102f8f69f9de1386144814048681c214 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 8 Oct 2026 21:55:51 +0200 Subject: [PATCH 4/4] Preserve provider configuration in the fallback review attempt --- .github/workflows/pullfrog.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index 90d7bd4..1dfa89c 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -154,10 +154,9 @@ jobs: # https://github.com/pullfrog/pullfrog/blob/f0684f2c9286f321085978685f6cbe91d51d4233/utils/credentialPool.ts#L175-L187 # A per-account rate limit is not fixed by a second expression, only by a # second attempt: the first credential has to be tried and seen to fail. - # Only the fallback token is passed here. If another provider were - # configured and usable, the step above would already have reached it — - # Pullfrog walks its credentials in order — so duplicating the whole set - # would widen the token surface without changing any outcome. + # Preserve the primary step's provider configuration for non-Claude + # models and replace only its OAuth token. Keys stay scoped to the two + # agent steps, never checkout or final failure reporting. - name: Run agent with the fallback subscription token id: agent_fallback if: steps.agent.outcome == 'failure' && env.HAS_OAUTH_FALLBACK == 'true' @@ -168,7 +167,20 @@ jobs: push: disabled shell: restricted env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.PULLFROG_OAUTH_FALLBACK }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GOOGLE_GENERATIVE_AI_API_KEY: + ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + XAI_API_KEY: ${{ secrets.XAI_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} + KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }} + META_MODEL_API_KEY: ${{ secrets.META_MODEL_API_KEY }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - name: Fail when no attempt succeeded if: steps.agent.outcome == 'failure' && steps.agent_fallback.outcome != 'success'