* refactor progress comments into a single bundled type + helper module
introduce ProgressComment ({ id, type: "issue" | "review" }) as the canonical handle for
the GitHub comment a run uses to report progress, and route every read/update/delete/create
through a single helper module (action/utils/progressComment.ts). previously every site that
touched the progress comment hardcoded octokit.rest.issues.*Comment, which made adding a
second comment type (review-thread replies) require duplicating the same branch in 6+ places
— the same shape that bit pullfrog/app#445.
new capability: when the address-reviews trigger fires for a one-off review comment, the
"Leaping into action" comment is now posted as a reply in that review thread instead of as
a top-level PR timeline comment. the helper handles failure (e.g. parent comment deleted)
by silently falling back to a top-level issue comment, so the run never loses its progress
surface.
changes:
- action/utils/progressComment.ts (new) — ProgressComment type + getProgressComment,
updateProgressComment, deleteProgressCommentApi, createLeapingProgressComment. uses a
structural Octokit interface to bridge the @octokit/rest version mismatch between the
action package (v22) and the root project (v21).
- action/internal/index.ts — re-export the new types and helpers for cross-boundary use.
- action/external.ts, action/utils/payload.ts — replace progressCommentId: string with
progressComment: { id: string, type: "issue" | "review" } in WriteablePayload + JsonPayload.
wire-format breaking, no legacy fallback (in-flight runs across the deploy lose their
progress comment, fine).
- action/mcp/server.ts — ToolState.progressCommentId becomes
progressComment: ProgressComment | null | undefined (same tristate semantics).
- action/main.ts, action/mcp/comment.ts, action/utils/errorReport.ts,
action/utils/postCleanup.ts — every issues.*Comment call against the progress comment
routes through the helper module. zero hardcoded API branching outside the helper.
- utils/github/triggerWorkflow.ts — drop createLeapingComment + updateCommentToLeaping;
dispatchAndTrackWorkflow gains a resolution chain (existingComment → replyToReviewComment
→ triggeringIssue → none) and an existingComment: ProgressComment param plus
replyToReviewComment: { pullNumber, commentId }.
- utils/webhooks/handleWebhook.ts — dispatch closure threads replyToReviewComment through;
the one-off review comment branch passes it and skips the now-redundant eyes reaction
on the comment we're about to reply to.
- app/trigger/[owner]/[repo]/[number]/page.tsx, utils/github/runActionLocal.ts,
app/api/cli/dispatch/route.ts, app/api/dispatch-workflow/route.ts — call sites updated to
new shape.
no schema or DB column changes. the existing WorkflowRun.progressCommentId column is still
written by id only; type lives only on the in-flight payload, which is sufficient for
runtime since it's the only thing that needs to know which API to call.
* anneal pass 1: fallback visibility + stale doc/comment updates
- progressComment.ts: when reviewReply→issue fallback fires, prepend a [!NOTE] callout
with a permalink back to the original review comment. without this, the parent comment
showed no eyes reaction (deliberately skipped) and no reply, leaving the user with no
signal that anything happened.
- wiki/post-cleanup.md: update progressCommentId references to progressComment, document
the new helper-based dispatch by type.
- wiki/main.md: update initToolState({ progressCommentId }) → ({ progressComment })
in the resolver-chain diagram.
- action/main.ts, action/mcp/review.ts: update two stale comments that referenced the
old field name.
* anneal pass 2: post-cleanup detection through fallback notice + log cleanup
- isLeapingIntoActionCommentBody: strip a leading GFM blockquote/alert before
testing the leaping prefix. without this, the [!NOTE] callout that the
reviewReply→issue fallback prepends would prevent post-cleanup from
recognizing the stuck "Leaping into action..." comment, leaving it permanently
on the PR timeline if the workflow died before any progress update.
- progressComment helper: switch from log.warning (action-flavored, emits a
::warning:: GitHub Actions annotation) to console.warn so the helper doesn't
pollute Vercel logs when invoked from the webhook context.
- triggerWorkflow.ts: drop the duplicate caller-side log on review-reply
failure — the helper already speaks loudly. Reword the catch-branch log to
reflect that it now only fires when both the reply AND the helper's internal
fallback failed.
- progressComment.ts: document that the [!NOTE] fallback notice is overwritten
on the first report_progress call, and explain the trade-off vs persisting
it through the action payload + ToolState.
* debloat: drop the [!NOTE] fallback callout
Reverting two pieces from the prior anneal pass:
- progressComment.ts: drop the [!NOTE] callout that the reviewReply→issue fallback
prepended to the leaping body. It disappeared on the agent's first report_progress
call, which made it half-committed to visibility — worse than either properly
persisting it (real engineering) or leaving the fallback silent (current choice).
The console.warn diagnostic and the workflow-run footer link in the leaping
comment itself give us enough signal for the rare case where both API endpoints
fail at once.
- isLeapingIntoActionCommentBody: revert the leading-blockquote stripping; only
needed to compensate for the [!NOTE] callout.
Keeping: the console.warn-vs-log.warning fix (real cross-runtime concern), the
duplicate-log drop in triggerWorkflow.ts, the wiki updates, and the two stale
source-comment fixes.
* fix: prevent stranded task list overwriting post-cleanup message
When a run is cancelled, the action's todoTracker may have an HTTP write in
flight to GitHub when SIGTERM lands. The action process dies, but the request
data has already left the socket — GitHub processes it and updates the comment
body to the (stale) task list. Meanwhile post-cleanup, running in a separate
process, writes the "This run was cancelled 🛑" message. If the tracker's
in-flight write happens to land *after* post-cleanup's write, the user never
sees the cancellation message.
Two-layer fix:
- Action side: cancel the tracker in the SIGTERM signal handler so no new
debounced writes get scheduled. This shrinks the race window but can't
un-send a request already on the wire.
- Post-cleanup side: after writing, verify the body landed and re-issue if
another write clobbered ours. Loops up to 3× with a 3s settle delay so
delayed in-flight writes from the dying action have time to arrive before
our read-back check decides whether to retry.
* lint: import createLeapingProgressComment from pullfrog/internal in test script
* address bot review findings: reply-target root, version bump, GET error handling
Three real findings from the bot reviews on #567 plus a small DRY pass:
1. handleWebhook reply-target: `newComments[0]` may be a reply, not a
top-level review comment. `getReviewCommentsWithReplies` returns root +
replies for any thread the review touched, and `pull_request_review_id`
filtering only narrows by *which review submitted*, not *root vs reply*.
When a user submits a single reply as their entire review (e.g. replying
to someone else's comment to ping @pullfrog), the reply ID flowed through
to `createReplyForReviewComment`, which 422s on replies-to-replies and
degraded to a top-level issue comment — exactly the polluted-PR-timeline
behavior this PR was built to remove. Walk up `in_reply_to` from the
already-fetched thread data to find the root and reply there instead.
2. action/package.json: bumped 0.0.202 → 0.0.204. main is at 0.0.203 and
our wire format changed; without a bump validateCompatibility can't
surface the mismatch on the deploy boundary, and the merge would have
gone backwards.
3. postCleanup writeAndVerify: distinguish a thrown verify-GET from a
"body got overwritten" mismatch. Treating a transient 5xx/rate-limit GET
the same as a clobber wasted PUT attempts and printed a misleading
"in-flight writes kept clobbering us" warning. We trust our PUT (which
returned 200) and exit instead of amplifying writes against a flaky API.
4. Small DRY: extracted parseProgressComment for the
`{ id: string; type } -> ProgressComment` parse that had drifted across
server.ts and postCleanup.ts.
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 }}"