From 20b08b53214c58291796130c7bae930f7b1c0887 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:32:06 +0000 Subject: [PATCH] =?UTF-8?q?Add=20"Rerun=20failed=20job=20=E2=9E=94"=20link?= =?UTF-8?q?=20to=20error=20comment=20footer=20(#355)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add "Rerun failed job" link to error comment footer * Remove issueNumber guard from rerun link The rerun action only needs run_id โ€” the issue number in the trigger URL path is just a route segment requirement. Use 0 as a fallback so the link is always shown when a run ID is available. * Overload `[number]` path segment as `runId` for the rerun action For the rerun trigger, the `[number]` path param now carries the workflow run ID instead of an issue number. The rerun link changes from `/trigger/o/r/ISSUE?action=rerun&run_id=RID` to `/trigger/o/r/RID?action=rerun`. - Remove `run_id` query param from page.tsx and searchParams type - Add error handling around `reRunWorkflow` for invalid run IDs - Drop `issueNumber` from `BuildErrorCommentBodyParams` and all rerun link builders (errorReport, exitHandler, postCleanup) * Reorder validation: action first, then rerun, then issueNumber * address review: remove early toolState assignment, restructure rerun into dedicated block * revert self-contained rerun block, share auth logic via issueOrRunId * improve error handling * normalize runid early --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> Co-authored-by: Cursor Co-authored-by: Mateusz Burzyล„ski --- entry | 24 ++++++++++++------- mcp/comment.ts | 6 +++-- mcp/review.ts | 14 ++++++----- mcp/server.ts | 2 +- post | 46 ++++++++++++++++++++++++++++-------- utils/buildPullfrogFooter.ts | 2 +- utils/errorReport.ts | 15 ++++++++++-- utils/postCleanup.ts | 39 ++++++++++++++++++++---------- utils/workflow.ts | 8 ++++--- 9 files changed, 110 insertions(+), 46 deletions(-) diff --git a/entry b/entry index 904d9af..f2acde1 100755 --- a/entry +++ b/entry @@ -141312,14 +141312,14 @@ async function buildCommentFooter({ customParts }) { const repoContext = parseRepoContext(); - const runId = process.env.GITHUB_RUN_ID; + const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0; let jobId; if (runId && octokit) { try { const { data: jobs } = await octokit.rest.actions.listJobsForWorkflowRun({ owner: repoContext.owner, repo: repoContext.name, - run_id: parseInt(runId, 10) + run_id: runId }); jobId = jobs.jobs[0]?.id.toString(); } catch { @@ -143345,12 +143345,12 @@ function CreatePullRequestReviewTool(ctx) { } } const footer = buildPullfrogFooter({ - workflowRun: { + workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId - }, + } : void 0, customParts }); const updatedBody = (body || "") + footer; @@ -146893,10 +146893,18 @@ ${ctx.error}` : ctx.error; } const repoContext = parseRepoContext(); const octokit = createOctokit(getGitHubInstallationToken()); - const runId = process.env.GITHUB_RUN_ID; + const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0; + const customParts = []; + if (runId) { + const apiUrl = getApiUrl(); + customParts.push( + `[Rerun failed job \u2794](${apiUrl}/trigger/${repoContext.owner}/${repoContext.name}/${runId}?action=rerun)` + ); + } const footer = buildPullfrogFooter({ triggeredBy: true, - workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0 + workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0, + customParts }); await octokit.rest.issues.updateComment({ owner: repoContext.owner, @@ -147626,7 +147634,7 @@ function parseTimeString(input) { // utils/workflow.ts async function resolveRun(params) { - const runId = process.env.GITHUB_RUN_ID || ""; + const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0; const githubRepo = process.env.GITHUB_REPOSITORY; if (!githubRepo || !githubRepo.includes("/")) { throw new Error(`GITHUB_REPOSITORY env var must be set to "owner/repo", got: ${githubRepo}`); @@ -147638,7 +147646,7 @@ async function resolveRun(params) { const jobs = await params.octokit.rest.actions.listJobsForWorkflowRun({ owner, repo, - run_id: parseInt(runId, 10) + run_id: runId }); const matchingJob = jobs.data.jobs.find((job) => job.name === jobName); if (matchingJob) { diff --git a/mcp/comment.ts b/mcp/comment.ts index 055988f..1956645 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -25,7 +25,9 @@ async function buildCommentFooter({ customParts, }: BuildCommentFooterParams): Promise { const repoContext = parseRepoContext(); - const runId = process.env.GITHUB_RUN_ID; + const runId = process.env.GITHUB_RUN_ID + ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) + : undefined; let jobId: string | undefined; if (runId && octokit) { @@ -34,7 +36,7 @@ async function buildCommentFooter({ const { data: jobs } = await octokit.rest.actions.listJobsForWorkflowRun({ owner: repoContext.owner, repo: repoContext.name, - run_id: parseInt(runId, 10), + run_id: runId, }); // use the first job's ID available jobId = jobs.jobs[0]?.id.toString(); diff --git a/mcp/review.ts b/mcp/review.ts index b30bb4e..4b0aa13 100644 --- a/mcp/review.ts +++ b/mcp/review.ts @@ -139,12 +139,14 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) { } const footer = buildPullfrogFooter({ - workflowRun: { - owner: ctx.repo.owner, - repo: ctx.repo.name, - runId: ctx.runId, - jobId: ctx.jobId, - }, + workflowRun: ctx.runId + ? { + owner: ctx.repo.owner, + repo: ctx.repo.name, + runId: ctx.runId, + jobId: ctx.jobId, + } + : undefined, customParts, }); diff --git a/mcp/server.ts b/mcp/server.ts index fe83841..532c8a5 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -100,7 +100,7 @@ export interface ToolContext { modes: Mode[]; postCheckoutScript: string | null; toolState: ToolState; - runId: string; + runId: number | undefined; jobId: string | undefined; // set after MCP server starts โ€” used by delegate tool to pass URL to subagents mcpServerUrl: string; diff --git a/post b/post index e5edd39..72147cc 100755 --- a/post +++ b/post @@ -37493,6 +37493,22 @@ var schema = ark.schema; var define2 = ark.define; var declare = ark.declare; +// utils/apiUrl.ts +function isLocalUrl(url2) { + return url2.hostname === "localhost" || url2.hostname === "127.0.0.1"; +} +function getApiUrl() { + const raw = process.env.API_URL || "https://pullfrog.com"; + const parsed2 = new URL(raw); + if (parsed2.protocol !== "https:" && !isLocalUrl(parsed2)) { + throw new Error( + `API_URL must use https:// (got ${parsed2.protocol}). only localhost is exempt.` + ); + } + log.debug(`resolved API_URL: ${raw}`); + return raw; +} + // utils/buildPullfrogFooter.ts var PULLFROG_DIVIDER = ""; var FROG_LOGO = `Pullfrog`; @@ -41419,15 +41435,25 @@ function getJobToken() { // utils/postCleanup.ts var SHOULD_CHECK_REASON = true; function buildErrorCommentBody(params) { - const workflowRunLink = params.runId ? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})` : "workflow run logs"; - const errorMessage = params.isCancellation ? `This run was cancelled \u{1F6D1} + let errorMessage = params.isCancellation ? `This run was cancelled \u{1F6D1} -The workflow was cancelled before completion. Please check the ${workflowRunLink} for details.` : `This run croaked \u{1F635} +The workflow was cancelled before completion.` : `This run croaked \u{1F635} -The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`; +The workflow encountered an error before any progress could be reported.`; + if (params.runId) { + errorMessage += " Please check the link below for details."; + } + const customParts = []; + if (!params.isCancellation && params.runId) { + const apiUrl = getApiUrl(); + customParts.push( + `[Rerun failed job \u2794](${apiUrl}/trigger/${params.owner}/${params.repo}/${params.runId}?action=rerun)` + ); + } const footer = buildPullfrogFooter({ triggeredBy: true, - workflowRun: params.runId ? { owner: params.owner, repo: params.repo, runId: params.runId } : void 0 + workflowRun: params.runId ? { owner: params.owner, repo: params.repo, runId: params.runId } : void 0, + customParts }); return `${errorMessage}${footer}`; } @@ -41457,12 +41483,12 @@ async function validateStuckProgressComment(params) { } } async function getIsCancelled(params) { - if (!params.runIdStr) return false; + if (!params.runId) return false; try { const jobsResult = await params.octokit.rest.actions.listJobsForWorkflowRun({ owner: params.repoContext.owner, repo: params.repoContext.name, - run_id: Number.parseInt(params.runIdStr, 10) + run_id: params.runId }); const currentJobName = process.env.GITHUB_JOB; const currentJob = currentJobName ? jobsResult.data.jobs.find( @@ -41489,7 +41515,7 @@ async function getIsCancelled(params) { } async function runPostCleanup() { log.info("\xBB [post] starting post cleanup"); - const runIdStr = process.env.GITHUB_RUN_ID; + const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0; let promptInput = null; try { const resolved = resolvePromptInput(); @@ -41514,8 +41540,8 @@ async function runPostCleanup() { const body = buildErrorCommentBody({ owner: repoContext.owner, repo: repoContext.name, - runId: runIdStr, - isCancellation: SHOULD_CHECK_REASON ? await getIsCancelled({ octokit, repoContext, runIdStr }) : false + runId, + isCancellation: SHOULD_CHECK_REASON ? await getIsCancelled({ octokit, repoContext, runId }) : false }); await octokit.rest.issues.updateComment({ owner: repoContext.owner, diff --git a/utils/buildPullfrogFooter.ts b/utils/buildPullfrogFooter.ts index abaf82e..5f60ee8 100644 --- a/utils/buildPullfrogFooter.ts +++ b/utils/buildPullfrogFooter.ts @@ -10,7 +10,7 @@ export interface AgentInfo { export interface WorkflowRunFooterInfo { owner: string; repo: string; - runId: string; + runId: number; /** optional job ID - if provided, will append /job/{jobId} to the workflow run URL */ jobId?: string | undefined; } diff --git a/utils/errorReport.ts b/utils/errorReport.ts index a3a86ba..a16636d 100644 --- a/utils/errorReport.ts +++ b/utils/errorReport.ts @@ -1,4 +1,5 @@ import type { ToolState } from "../mcp/server.ts"; +import { getApiUrl } from "./apiUrl.ts"; import { buildPullfrogFooter } from "./buildPullfrogFooter.ts"; import { createOctokit, parseRepoContext } from "./github.ts"; import { getGitHubInstallationToken } from "./token.ts"; @@ -19,12 +20,22 @@ export async function reportErrorToComment(ctx: ReportErrorParams): Promise; octokit: ReturnType; - runIdStr: string | undefined; + runId: number | undefined; }; async function getIsCancelled(params: GetIsCancelledParams): Promise { - if (!params.runIdStr) return false; // can't check without a run ID โ€” assume failure + if (!params.runId) return false; // can't check without a run ID โ€” assume failure try { const jobsResult = await params.octokit.rest.actions.listJobsForWorkflowRun({ owner: params.repoContext.owner, repo: params.repoContext.name, - run_id: Number.parseInt(params.runIdStr, 10), + run_id: params.runId, }); // find current job by matching GITHUB_JOB env var. @@ -125,7 +136,9 @@ async function getIsCancelled(params: GetIsCancelledParams): Promise { export async function runPostCleanup(): Promise { log.info("ยป [post] starting post cleanup"); - const runIdStr = process.env.GITHUB_RUN_ID; + const runId = process.env.GITHUB_RUN_ID + ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) + : undefined; // resolve prompt input once and use it for both issue number and comment ID extraction // only use the object form (JSON payload), not plain string prompts @@ -159,9 +172,9 @@ export async function runPostCleanup(): Promise { const body = buildErrorCommentBody({ owner: repoContext.owner, repo: repoContext.name, - runId: runIdStr, + runId, isCancellation: SHOULD_CHECK_REASON - ? await getIsCancelled({ octokit, repoContext, runIdStr }) + ? await getIsCancelled({ octokit, repoContext, runId }) : false, }); diff --git a/utils/workflow.ts b/utils/workflow.ts index 798ed3a..ae62eff 100644 --- a/utils/workflow.ts +++ b/utils/workflow.ts @@ -6,7 +6,7 @@ interface ResolveRunParams { } export interface ResolveRunResult { - runId: string; + runId: number | undefined; jobId: string | undefined; } @@ -15,7 +15,9 @@ export interface ResolveRunResult { * Uses GITHUB_REPOSITORY and GITHUB_RUN_ID env vars. */ export async function resolveRun(params: ResolveRunParams): Promise { - const runId = process.env.GITHUB_RUN_ID || ""; + const runId = process.env.GITHUB_RUN_ID + ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) + : undefined; const githubRepo = process.env.GITHUB_REPOSITORY; if (!githubRepo || !githubRepo.includes("/")) { throw new Error(`GITHUB_REPOSITORY env var must be set to "owner/repo", got: ${githubRepo}`); @@ -28,7 +30,7 @@ export async function resolveRun(params: ResolveRunParams): Promise job.name === jobName); if (matchingJob) {