* fix: 6 unaddressed log-audit / run-audit findings
- #836 + #818 (clerk middleware SyntaxError on action-runtime endpoints):
narrow proxy.ts matcher to exclude /api/repo/<owner>/<repo>/run-context,
/api/runtime/*, /api/proxy-token. these are server-to-server with their
own auth and have no Clerk session to evaluate, so clerkMiddleware's
decodeJwt throws turn into 500s on every request.
- #837 (npx EBADDEVENGINES on customer's package.json): change runCli's
bootstrap cwd from $GITHUB_WORKSPACE to os.tmpdir() so npm v11+ doesn't
enforce devEngines.packageManager from the customer's tree before our
bootstrap can install pullfrog. CLI process.chdir's to payload.cwd
internally, so the runtime work still happens in $GITHUB_WORKSPACE.
- #838 (createWorkflowDispatch silently dropped 39 user runs on a 5xx
spike): add bounded retry (3 attempts, ~750ms total) on Octokit 5xx and
network errors inside dispatchReservedRun. preserves the existing 422
"Unexpected inputs" path. retry budget stays well under GitHub's 10s
webhook redelivery window.
- #833 (bail() redirect("/signout") propagated NEXT_REDIRECT into webhook
handlers, 40+ 500s/24h): drop the redirect side-effect; bail now just
classifies bad-credentials as non-retryable and propagates. UI flows
that wanted auto-signout on revoked tokens can detect it themselves;
the side-effect was wrong for any non-page caller.
- #835 (BYOK provider billing-exhausted fell through to raw error
renderer, 37 review-mode runs/24h with no PR-side signal):
- extend providerErrors patterns with Anthropic "credit balance is too
low" + extract isProviderBillingExhausted / extractProviderId helpers
- add a renderer branch in runErrorRenderer.ts that names the provider
(parsed from providerID=) and links to its billing dashboard
- route handleAgentResult's !result.success path through the renderer
+ reportErrorToComment with createIfMissing, so review-mode and
silent triggers get an actionable PR comment regardless of mode
- #834 (post-hook ERR_MODULE_NOT_FOUND already fixed on main, hardening):
- add a vitest invariant that walks the entryPost.ts import graph and
refuses any non-relative / non-node: specifier — catches the next
`@actions/core` slip-up before publish
- add an analyze-logs classifier so future entryPost crashes surface as
failure:post-hook-module-not-found instead of hiding inside
failure:unknown / failure:git-lock-file
Co-authored-by: Cursor <cursoragent@cursor.com>
* anneal: fix dead-code matcher, 404 url, silent-trigger gate, and grammar regression
Round-1 anneal pass on the audit-fixes PR surfaced two critical issues +
several majors that the original fixes shipped with:
- proxy.ts matcher 1 (`(?!_next|[^?]*\.(...))`) still caught every
/api/... route because matcher arrays are OR'd. The narrowing in
matcher 2 was dead code → middleware still 500'd on /api/runtime/*,
/api/proxy-token, /api/repo/.../run-context. Carve-out now lives in
BOTH matchers.
- opencode.ai/billing returns 404; canonical top-up surface is /zen.
deepseek /usage is the consumption page, not the top-up flow (/top_up
is correct). google /apikey is the keys list, not billing (/usage is
the spend dashboard). All three URL strings updated.
- handleAgentResult gated reportErrorToComment behind `if (!ctx.silent)`,
contradicting the createIfMissing intent — silent IncrementalReview /
pull_request_synchronize / auto-label still got zero PR signal on BYOK
billing exhaustion, the exact failure mode #835 was meant to fix.
Moved createIfMissing into finalizeSuccessRun's existing render-and-
post block (single source of truth), reverted handleAgentResult to its
prior shape. Side benefit: drops the double-PATCH that fired on every
non-silent !success path with an existing progress comment.
- Anthropic-direct error rendered "**Your your provider account is out
of credit.**" because Anthropic SDK has no providerID= tag, so
extractProviderId returned null and the headline composed
"Your " + "your provider". Added detectProviderId Anthropic fallback
(matches "Anthropic API" / "credit balance is too low") so the link
is reachable AND made the headline conditional on whether a provider
id was detected.
- Reordered renderRunError classifier: BYOK billing-exhausted now runs
BEFORE api-key auth detection. Providers commonly return 401 for
billing exhaustion (DeepSeek, Gemini), and the OpenCode harness logs
often include "API Error: 401" in the raw error body, which
isApiKeyAuthError would otherwise match — surfacing "rotate your key"
when the actual fix is "top up credits".
- isTransientUpstreamError missed ENOTFOUND / ENETUNREACH / EHOSTUNREACH
(undici DNS-class failures Octokit doesn't wrap with a status). Added
to the prefix alternation.
- Tightened "10s webhook redelivery budget" / "GitHub redelivers"
wording in triggerWorkflow.ts and bail.ts JSDoc — GitHub's 10s is the
response timeout (it doesn't auto-redeliver); upstream webhook proxy
retries are what multiplied the failure.
Co-authored-by: Cursor <cursoragent@cursor.com>
* anneal r2: unbreak proxy.ts matcher 2, extend carve-out, mkdtemp bootstrap
Round-2 anneal (security + cross-cutting lenses) on top of the round-1
audit-fixes commit caught a critical regression + several majors:
- proxy.ts matcher 2 from r1 (`/(api|trpc)(?!...)(.*)`) does NOT compile.
path-to-regexp rejects a top-level `(?!` after `)` as "Pattern cannot
start with '?'", and Next.js's SourceSchema runs the same validator at
build time and aborts via `process.exit(1)`. PR #840's Vercel + preview
deployments have been failing since 978eca26 for exactly this reason.
Fixed by nesting the lookahead inside an outer parameter group:
`/(api|trpc)((?!...).*)`. Same shape matcher 1 already uses, which is
why m1 always compiled. Verified `pnpm next build` succeeds end-to-end.
- proxy.ts carve-out was incomplete relative to its own justification.
The "no Clerk session, decodeJwt 500" failure mode applies to ALL
server-to-server action-runtime endpoints — five more share the exact
shape: /api/repo/.../learnings, /api/repo/.../pr/.../summary-comment,
/api/repo/.../issue/.../plan-comment, /api/workflow-run/, and
/api/github/installation-token. Extended both matchers. /api/upload/
signed-url stays in (dual auth: Clerk session OR bearer JWT — needs
middleware for the user path).
- proxy.ts carve-outs were unanchored: a future /api/proxy-token-info,
/api/proxy-tokens, or /api/repo/X/Y/run-context-foo would silently
bypass Clerk. Added `(?:$|/)` for path-prefix carve-outs (allow exact
match or sub-path), `$` for routes with browser-callable siblings
(e.g. `learnings/history` is browser-Clerk, `learnings$` is action-
bearer-JWT). Verified 30/31 routes via path-to-regexp test harness.
- runCli.ts cwd flipped from $GITHUB_WORKSPACE to os.tmpdir() in r1
(#837 fix for npm v11 devEngines.packageManager EBADDEVENGINES). But
$TMPDIR is overridable from a prior $GITHUB_ENV step — a customer-
authored or compromised prior step can plant /atk/node_modules/
pullfrog/ and `echo "TMPDIR=/atk" >> $GITHUB_ENV`, and our npx
--yes pullfrog@<v> bootstrap resolves the local install first,
executing attacker code with full action env (provider keys, OIDC,
installation token, CODEX_AUTH_JSON). Switched to mkdtempSync(join
(tmpdir(), "pullfrog-bootstrap-")) — fresh per-invocation 0700 dir,
not pre-writable by anything earlier in the job.
- runLifecycle.ts: writeRunErrorOutputs (catch-path) didn't pass
createIfMissing: true, contradicting the symmetric intent of the
r1 finalizeSuccessRun fix. Silent triggers (IncrementalReview /
pull_request_synchronize / auto-label) that throw past the success
path still got zero PR signal — exact failure mode #835 was meant
to close. Now both paths pass createIfMissing: true.
- runErrorRenderer.ts detectProviderId regex `/Anthropic API|credit
balance is too low/i` could mis-tag a non-Anthropic billing-exhausted
error that mentioned "Anthropic API" in passing (fallback-chain agent
prompt text, OpenCode harness logs). Tightened to /credit balance is
too low/i — Anthropic-specific phrasing, sufficient for the direct-
Anthropic SDK case the fallback exists to handle.
- runErrorRenderer.ts JSDoc: r1's classifier reorder put hang at #6 in
code but the JSDoc still listed it at #2. Reordered the doc to match
dispatch order, with explicit note that hang is a sub-source for the
api-key check (which is why hangBody is precomputed early).
- analyze-logs.ts: ERR_MODULE_NOT_FOUND.*entryPost regex needs `s`
flag so it survives Node v23+ stack-trace reformatting onto multiple
lines. One-char fix to defend the #834 classification bucket.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Pullfrog is a GitHub bot that brings the full power of your favorite coding agents into GitHub. It's open source and powered by GitHub Actions.
Tag @pullfrog — Tag @pullfrog in a comment anywhere in your repo. It will pull in any relevant context using the action's internal MCP server and perform the appropriate task.
Prompt from the web — Trigger arbitrary tasks from the Pullfrog dashboard
Automated triggers — Configure Pullfrog to trigger agent runs in response to specific events. Each of these triggers can be associated with custom prompt instructions.
issue created
issue labeled
PR created
PR review created
PR review requested
and more...
Pullfrog is the bridge between your preferred coding agents and GitHub. Use it for:
🤖 Coding tasks — Tell @pullfrog to implement something and it'll spin up a PR. If CI fails, it'll read the logs and attempt a fix automatically. It'll automatically address any PR reviews too.
🔍 PR review — Coding agents are great at reviewing PRs. Using the "PR created" trigger, you can configure Pullfrog to auto-review new PRs.
🤙 Issue management — Via the "issue created" trigger, Pullfrog can automatically respond to common questions, create implementation plans, and link to related issues/PRs. Or (if you're feeling lucky) you can prompt it to immediately attempt a PR addressing new issues.
Literally whatever — Want to have the agent automatically add docs to all new PRs? Cut a new release with agent-written notes on every commit to main? Pullfrog lets you do it.
Standalone Usage
You can also use pullfrog/pullfrog as a step in your own workflows. The action exposes a result output that can be consumed by subsequent steps.
Example: Auto-generate release notes on new tags
name:Releaseon:push:tags:['v*']permissions:contents:writejobs:release:runs-on:ubuntu-lateststeps:- name:Checkoutuses:actions/checkout@v4with:fetch-depth:0- name:Generate release notesid:notesuses:pullfrog/pullfrog@v0with:prompt:| Generate release notes for ${{ github.ref_name }}.
Compare commits between this tag and the previous tag.
Format as markdown: summary paragraph, then ### Features, ### Fixes, ### Breaking Changes sections.
Omit empty sections. Be concise.env:ANTHROPIC_API_KEY:${{ secrets.ANTHROPIC_API_KEY }}# write to file to avoid shell escaping issues with special characters- name:Create GitHub releaserun:| notesfile="$RUNNER_TEMP/release-notes-$GITHUB_RUN_ID.md"
printf '%s' "$NOTES" > "$notesfile"
gh release create ${{ github.ref_name }} --title "${{ github.ref_name }}" --notes-file "$notesfile"env:GH_TOKEN:${{ github.token }}NOTES:${{ steps.notes.outputs.result }}
Example: Structured Output with Zod Schema
You can force the agent to return structured JSON output by providing a JSON schema. This allows you to reliably parse and use the agent's response in subsequent workflow steps.
You can define your JSON schema directly or uou can use any validation library that converts to JSON Schema. Here's an example using Zod:
name:Release Checkon:pull_request:types:[closed]jobs:check-release:if:github.event.pull_request.merged == trueruns-on:ubuntu-lateststeps:- uses:actions/checkout@v4- name:Install dependenciesrun:npm install --no-save --no-package-lock zod @actions/core- name:Generate Schemaid:schemarun:| node -e '
import { z } from "zod";
import { setOutput } from "@actions/core";
const schema = z.object({
version: z.string().describe("Semantic version number (e.g. 1.0.0)"),
isBreaking: z.boolean().describe("Whether this release contains breaking changes"),
changelog: z.array(z.string()).describe("List of changes in this release"),
});
setOutput("schema", JSON.stringify(z.toJSONSchema(schema)));
'- name:Analyze PRid:analysisuses:pullfrog/pullfrog@v0with:prompt:| Analyze this PR and determine semantic versioning impact.
Return a JSON object matching the provided schema.output_schema:${{ steps.schema.outputs.schema }}env:ANTHROPIC_API_KEY:${{ secrets.ANTHROPIC_API_KEY }}- name:Process Resultrun:| # Parse the JSON result using fromJSON()
echo "Version: ${{ fromJSON(steps.analysis.outputs.result).version }}"
echo "Breaking: ${{ fromJSON(steps.analysis.outputs.result).isBreaking }}"