e2e29a19fc
* accept pullfrog.yaml as well as pullfrog.yml
centralize the accepted workflow filenames in `utils/github/pullfrogWorkflow.ts`
(`PULLFROG_WORKFLOW_FILES = ["pullfrog.yml", "pullfrog.yaml"]`) and use the new
`findExistingWorkflowFile` helper at every read path: `getWorkflow` (cached),
the verify-workflow API route, and the audit/sync/download/update scripts. `.yml`
is always tried first so the common case still costs exactly one API call.
webhook handlers (push cache-bust, `workflow_run_*`) now use the shared
`isPullfrogWorkflowPath` matcher.
action runtime (`reviewCleanup.ts`) derives the running workflow's filename from
`process.env.GITHUB_WORKFLOW_REF` instead of hardcoding `.yml`, so the safety-net
follow-up dispatch targets whichever file the user actually has — strictly more
correct than today.
write paths (`createWorkflowForRepo`, `createWorkflowPR`) intentionally still
create `.yml`; existing 422 collision handling covers the rare double-install
case. UI/wiki/onboarding copy keeps saying `pullfrog.yml`; one callout in
`docs/getting-started.mdx` mentions `.yaml` works too.
also drops dead code (`utils/github/findWorkflow.ts`, parallel single-file
implementation with no importers) and the now-unused `WORKFLOW_FILENAME` export.
* rename pullfrogWorkflow.ts -> findPullfrogWorkflow.ts (verb form)
* add pre-flight check to workflow create paths
`createWorkflowForRepo` and `createWorkflowPR` now check for any existing
pullfrog workflow file (`.yml` or `.yaml`) before doing work, preventing the
degenerate state where a repo with `pullfrog.yaml` ends up with both files
dispatching on every event.
costs one `getContent` call per first-time install. existing 422 branch in
`createWorkflowForRepo` is retained as a race-condition safety net; the 409
branch now also handles the case where `createWorkflowPR` discovers an
existing file in flight.
`createWorkflowPR` return shape becomes a discriminated union; the standalone
`/api/create-workflow-pr` route returns `{ alreadyInstalled: true }` instead
of creating a redundant PR.
* promote repo to active when /api/create-workflow-pr finds existing workflow
extracts `promoteRepoToActive` from `createWorkflowForRepo`'s closure to a
shared module-level function, and wires it into the standalone PR route's
`alreadyInstalled` branch so a `needs_setup` repo with an existing `.yaml`
file doesn't go stale (was only handled by the dashboard's own create path).
addresses pullfrog review on #596.
107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
import type { WriteablePayload } from "../external.ts";
|
|
import { reportReviewNodeId } from "../mcp/review.ts";
|
|
import type { ToolContext } from "../mcp/server.ts";
|
|
import { log } from "./cli.ts";
|
|
|
|
const RE_REVIEW_PREAMBLE =
|
|
"Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
|
|
|
|
/**
|
|
* post-agent review lifecycle: runs after the agent exits (success or timeout).
|
|
*
|
|
* normally the agent handles new commits inline: create_pull_request_review
|
|
* detects HEAD movement and tells the agent to pull and review the delta.
|
|
* this dispatch is a safety net for cases where the agent couldn't handle
|
|
* it (timeout, error, etc).
|
|
*
|
|
* ordering matters: reportReviewNodeId marks this run "done" FIRST so push
|
|
* webhooks stop being suppressed by dedup. the HEAD check runs SECOND to
|
|
* catch any pushes that were suppressed while this run was in-flight.
|
|
*/
|
|
export async function postReviewCleanup(ctx: ToolContext): Promise<void> {
|
|
const review = ctx.toolState.review;
|
|
if (!review) return;
|
|
delete ctx.toolState.review;
|
|
|
|
// mark review as submitted — unlocks webhook dedup for new pushes
|
|
await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
|
|
|
|
// dispatch follow-up if PR HEAD moved past the reviewed commit
|
|
if (review.reviewedSha) {
|
|
await bestEffort(
|
|
() => dispatchFollowUpReReview(ctx, review.reviewedSha!),
|
|
"follow-up re-review dispatch"
|
|
);
|
|
}
|
|
}
|
|
|
|
async function bestEffort(fn: () => Promise<unknown>, label: string): Promise<void> {
|
|
try {
|
|
await fn();
|
|
} catch (error) {
|
|
log.debug(`${label} failed: ${error}`);
|
|
}
|
|
}
|
|
|
|
async function dispatchFollowUpReReview(ctx: ToolContext, reviewedSha: string): Promise<void> {
|
|
const issueNumber = ctx.payload.event.issue_number;
|
|
if (!issueNumber) return;
|
|
|
|
const pr = await ctx.octokit.rest.pulls.get({
|
|
owner: ctx.repo.owner,
|
|
repo: ctx.repo.name,
|
|
pull_number: issueNumber,
|
|
});
|
|
|
|
if (pr.data.head.sha === reviewedSha) return;
|
|
if (pr.data.state !== "open") return;
|
|
if (pr.data.draft) return;
|
|
|
|
log.info(
|
|
`safety net: pr HEAD moved from ${reviewedSha.slice(0, 7)} to ${pr.data.head.sha.slice(0, 7)} ` +
|
|
`and agent did not review inline — dispatching follow-up re-review`
|
|
);
|
|
|
|
const event: WriteablePayload["event"] = {
|
|
trigger: "pull_request_synchronize",
|
|
issue_number: issueNumber,
|
|
is_pr: true,
|
|
title: pr.data.title,
|
|
body: null,
|
|
branch: pr.data.head.ref,
|
|
before_sha: reviewedSha,
|
|
silent: true,
|
|
};
|
|
if (ctx.payload.event.authorPermission) {
|
|
event.authorPermission = ctx.payload.event.authorPermission;
|
|
}
|
|
|
|
const payload: WriteablePayload = {
|
|
"~pullfrog": true,
|
|
version: ctx.payload.version,
|
|
model: ctx.payload.model,
|
|
prompt: "",
|
|
eventInstructions: RE_REVIEW_PREAMBLE,
|
|
event,
|
|
};
|
|
|
|
await ctx.octokit.rest.actions.createWorkflowDispatch({
|
|
owner: ctx.repo.owner,
|
|
repo: ctx.repo.name,
|
|
workflow_id: getCurrentWorkflowFilename(),
|
|
ref: pr.data.base.repo.default_branch,
|
|
inputs: { prompt: JSON.stringify(payload) },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* derive the running workflow's filename from `GITHUB_WORKFLOW_REF`, which has the form
|
|
* `<owner>/<repo>/.github/workflows/<filename>@<ref>` (e.g. `.../pullfrog.yaml@refs/heads/main`).
|
|
* falls back to `pullfrog.yml` if the env var is missing or malformed (shouldn't happen in CI).
|
|
*/
|
|
function getCurrentWorkflowFilename(): string {
|
|
const ref = process.env.GITHUB_WORKFLOW_REF ?? "";
|
|
const match = ref.match(/\/([^/]+)@/);
|
|
return match?.[1] ?? "pullfrog.yml";
|
|
}
|