* action: surface agent hang context in progress comment
When the activity-timeout watchdog kills a stalled opencode subprocess,
the user used to see a bare "activity timeout: no output for 30Xs" — no
provider context, no stderr trace, no clue why the run died. Investigation
of the six runs in #728 showed the same shape every time: opencode hangs
after a non-retryable provider event (auth 401, 502 stream lost, free-tier
flake), and the only useful signal was buried in stderr where the user
couldn't see it without diving into Actions logs.
Stop trying to prevent the hang. Surface it.
Add a small `AgentDiagnostic` handle on `toolState` that the harness
mutates as a run progresses (recent stderr ring buffer reference, last
provider-error label, event count). `formatAgentHangBody` renders that
into a markdown body — bold headline, one-line explanation, collapsible
`<details>` with the last ~10 stderr lines (capped to 3KB) — used by
both the agent harness's own catch path and main.ts's outer catch when
the watchdog wins the race against the harness.
Both paths converge on one formatter; the existing
"View workflow run ➔" footer affordance in `reportErrorToComment` is
unchanged, so the user still has one click from the comment to the raw
logs to develop their own thesis.
* address review: gate hang body on isHang; fix contradictory copy
- Only render `hangBody` when `isHang`. The harness sets
`agentDiagnostic` on entry, so any non-hang throw past `runOpenCode`'s
own catch (post-success `output_schema` validator, late cleanup throws)
was rendering "Pullfrog failed — N events processed…" with the real
exception message dropped — including for runs that actually succeeded
before a late throw.
- When `lastProviderError` already names the cause in the headline, the
zero-events sentence "check whether the model provider is reachable"
contradicts it (a 401 produces zero events but isn't a reachability
issue). Drop the nudge in that case; keep it for the silent-stall path
where it's still actionable.
* address copilot review: fence escape, idle parsing, secret redaction, tests
- pick a backtick fence longer than any backtick run in the rendered
stderr tail. opencode error JSON occasionally embeds triple backticks
in tool input dumps; the fixed three-tick fence let those terminate
the fence early and corrupt the rest of the comment markdown.
- parse idle seconds out of the timer reject string ("activity timeout:
no output for 301s") and use that for the hang explanation. previously
rendered total runtime, which overstated the stall by 20+ minutes for
runs that streamed for a long time before going quiet (e.g.
Rohithgilla12/data-peek#25784038918, 1230s elapsed but 304s idle).
- redact sensitive env-var values from the rendered stderr tail before
it lands in the PR comment / job summary. workflow log writes already
go through `core.setSecret` masking; PR comments and summaries bypass
that pipeline entirely. matches against `isSensitiveEnvName` (the same
*_KEY/*_TOKEN/*_SECRET/*_PASSWORD/*_CREDENTIAL surface that
`normalizeEnv` registers with the runner) and only redacts values
>= 8 chars to avoid false-positive substring hits.
- add `agentHangReport.test.ts` covering the branchy bits: idle-seconds
parsing, eventCount-zero copy with and without provider error,
fence-escape against embedded triple backticks, 3 KB tail truncation,
null-on-no-diagnostic, and secret redaction.
`startedAtMs` is dropped from `AgentDiagnostic` — total runtime was the
only consumer and idle seconds replaces it.
* strip slop: drop tests, drop redactSecrets, simplify ternary
- delete `agentHangReport.test.ts`. half the cases just pinned literal
copy ("**Pullfrog stalled**", "check whether the model provider is
reachable") which is exactly the "performative tests to every string
utility" pattern AGENTS.md flags. the other half tested 2-5 line pure
helpers (parseIdleSec / pickFence / truncation) that code review
catches. the formatter is a best-effort string output; pinning it in
tests creates churn without catching real regressions.
- remove `redactSecrets` and revert the formatter's import. theatrical
defense: opencode doesn't dump env on startup, bearer tokens aren't
in request bodies, bash is denied. the action has many other
PR-comment write paths that don't redact (comment.ts, errorReport.ts,
the progress writer) — if PR-comment secret hygiene matters, it's a
cross-cutting concern at the comment-write layer, not bolted onto
one formatter.
- factor the explanation triple-ternary into `formatExplanation` with
early returns. same logic, easier to read.
`isHang` gate, fence-length escaping, and idle-seconds parsing stay —
those are real correctness fixes.
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 }}"