Add "Rerun failed job ➔" link to error comment footer (#355)
* 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 <cursoragent@cursor.com> Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
c0fd69560f
commit
20b08b5321
@@ -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) {
|
||||
|
||||
+4
-2
@@ -25,7 +25,9 @@ async function buildCommentFooter({
|
||||
customParts,
|
||||
}: BuildCommentFooterParams): Promise<string> {
|
||||
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();
|
||||
|
||||
+8
-6
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -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 = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
||||
var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+13
-2
@@ -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<void
|
||||
|
||||
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)
|
||||
: undefined;
|
||||
|
||||
const customParts: string[] = [];
|
||||
if (runId) {
|
||||
const apiUrl = getApiUrl();
|
||||
customParts.push(
|
||||
`[Rerun failed job ➔](${apiUrl}/trigger/${repoContext.owner}/${repoContext.name}/${runId}?action=rerun)`
|
||||
);
|
||||
}
|
||||
|
||||
// build footer with workflow run link
|
||||
const footer = buildPullfrogFooter({
|
||||
triggeredBy: true,
|
||||
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : undefined,
|
||||
customParts,
|
||||
});
|
||||
|
||||
await octokit.rest.issues.updateComment({
|
||||
|
||||
+26
-13
@@ -1,4 +1,5 @@
|
||||
import { LEAPING_INTO_ACTION_PREFIX } from "../mcp/comment.ts";
|
||||
import { getApiUrl } from "./apiUrl.ts";
|
||||
import { buildPullfrogFooter } from "./buildPullfrogFooter.ts";
|
||||
import { log } from "./cli.ts";
|
||||
import { createOctokit, parseRepoContext } from "./github.ts";
|
||||
@@ -15,22 +16,32 @@ const SHOULD_CHECK_REASON = true;
|
||||
type BuildErrorCommentBodyParams = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
runId: string | undefined;
|
||||
runId: number | undefined;
|
||||
isCancellation: boolean;
|
||||
};
|
||||
|
||||
function buildErrorCommentBody(params: BuildErrorCommentBodyParams): string {
|
||||
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 🛑\n\nThe workflow was cancelled before completion. Please check the ${workflowRunLink} for details.`
|
||||
: `This run croaked 😵\n\nThe workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
||||
let errorMessage = params.isCancellation
|
||||
? `This run was cancelled 🛑\n\nThe workflow was cancelled before completion.`
|
||||
: `This run croaked 😵\n\nThe workflow encountered an error before any progress could be reported.`;
|
||||
|
||||
if (params.runId) {
|
||||
errorMessage += " Please check the link below for details.";
|
||||
}
|
||||
|
||||
const customParts: string[] = [];
|
||||
if (!params.isCancellation && params.runId) {
|
||||
const apiUrl = getApiUrl();
|
||||
customParts.push(
|
||||
`[Rerun failed job ➔](${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 }
|
||||
: undefined,
|
||||
customParts,
|
||||
});
|
||||
return `${errorMessage}${footer}`;
|
||||
}
|
||||
@@ -76,16 +87,16 @@ async function validateStuckProgressComment(
|
||||
type GetIsCancelledParams = {
|
||||
repoContext: ReturnType<typeof parseRepoContext>;
|
||||
octokit: ReturnType<typeof createOctokit>;
|
||||
runIdStr: string | undefined;
|
||||
runId: number | undefined;
|
||||
};
|
||||
|
||||
async function getIsCancelled(params: GetIsCancelledParams): Promise<boolean> {
|
||||
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<boolean> {
|
||||
export async function runPostCleanup(): Promise<void> {
|
||||
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<void> {
|
||||
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,
|
||||
});
|
||||
|
||||
|
||||
+5
-3
@@ -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<ResolveRunResult> {
|
||||
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<ResolveRunRe
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user