Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5353d80388 | |||
| 2dea842981 |
@@ -45360,14 +45360,14 @@ var require_retry_agent = __commonJS({
|
|||||||
this.#options = options;
|
this.#options = options;
|
||||||
}
|
}
|
||||||
dispatch(opts, handler2) {
|
dispatch(opts, handler2) {
|
||||||
const retry = new RetryHandler({
|
const retry2 = new RetryHandler({
|
||||||
...opts,
|
...opts,
|
||||||
retryOptions: this.#options
|
retryOptions: this.#options
|
||||||
}, {
|
}, {
|
||||||
dispatch: this.#agent.dispatch.bind(this.#agent),
|
dispatch: this.#agent.dispatch.bind(this.#agent),
|
||||||
handler: handler2
|
handler: handler2
|
||||||
});
|
});
|
||||||
return this.#agent.dispatch(opts, retry);
|
return this.#agent.dispatch(opts, retry2);
|
||||||
}
|
}
|
||||||
close() {
|
close() {
|
||||||
return this.#agent.close();
|
return this.#agent.close();
|
||||||
@@ -83328,7 +83328,7 @@ function query({
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/action",
|
name: "@pullfrog/action",
|
||||||
version: "0.0.151",
|
version: "0.0.152",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -92866,6 +92866,35 @@ import { pipeline } from "node:stream/promises";
|
|||||||
// utils/github.ts
|
// utils/github.ts
|
||||||
var core2 = __toESM(require_core(), 1);
|
var core2 = __toESM(require_core(), 1);
|
||||||
import { createSign } from "node:crypto";
|
import { createSign } from "node:crypto";
|
||||||
|
|
||||||
|
// utils/retry.ts
|
||||||
|
var defaultShouldRetry = (error41) => {
|
||||||
|
if (!(error41 instanceof Error)) return false;
|
||||||
|
return error41.name === "AbortError" || error41.message.includes("fetch failed") || error41.message.includes("ECONNRESET") || error41.message.includes("ETIMEDOUT");
|
||||||
|
};
|
||||||
|
async function retry(fn2, options = {}) {
|
||||||
|
const maxAttempts = options.maxAttempts ?? 3;
|
||||||
|
const delayMs = options.delayMs ?? 1e3;
|
||||||
|
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
||||||
|
const label = options.label ?? "operation";
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn2();
|
||||||
|
} catch (error41) {
|
||||||
|
lastError = error41;
|
||||||
|
if (attempt === maxAttempts || !shouldRetry(error41)) {
|
||||||
|
throw error41;
|
||||||
|
}
|
||||||
|
const delay2 = delayMs * attempt;
|
||||||
|
log.warning(`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay2}ms...`);
|
||||||
|
await new Promise((resolve2) => setTimeout(resolve2, delay2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// utils/github.ts
|
||||||
function isGitHubActionsEnvironment() {
|
function isGitHubActionsEnvironment() {
|
||||||
return Boolean(process.env.GITHUB_ACTIONS);
|
return Boolean(process.env.GITHUB_ACTIONS);
|
||||||
}
|
}
|
||||||
@@ -92998,7 +93027,7 @@ async function acquireTokenViaGitHubApp() {
|
|||||||
}
|
}
|
||||||
async function acquireNewToken() {
|
async function acquireNewToken() {
|
||||||
if (isGitHubActionsEnvironment()) {
|
if (isGitHubActionsEnvironment()) {
|
||||||
return await acquireTokenViaOIDC();
|
return await retry(() => acquireTokenViaOIDC(), { label: "token exchange" });
|
||||||
} else {
|
} else {
|
||||||
return await acquireTokenViaGitHubApp();
|
return await acquireTokenViaGitHubApp();
|
||||||
}
|
}
|
||||||
@@ -94721,7 +94750,7 @@ var agents = {
|
|||||||
// main.ts
|
// main.ts
|
||||||
import { mkdtemp as mkdtemp2 } from "node:fs/promises";
|
import { mkdtemp as mkdtemp2 } from "node:fs/promises";
|
||||||
import { tmpdir as tmpdir2 } from "node:os";
|
import { tmpdir as tmpdir2 } from "node:os";
|
||||||
import { join as join10 } from "node:path";
|
import { join as join11 } from "node:path";
|
||||||
|
|
||||||
// node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
|
// node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
|
||||||
function getUserAgent() {
|
function getUserAgent() {
|
||||||
@@ -122940,6 +122969,10 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// mcp/checkout.ts
|
||||||
|
import { writeFileSync as writeFileSync4 } from "node:fs";
|
||||||
|
import { join as join8 } from "node:path";
|
||||||
|
|
||||||
// utils/shell.ts
|
// utils/shell.ts
|
||||||
import { spawnSync as spawnSync4 } from "node:child_process";
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
||||||
function $(cmd, args3, options) {
|
function $(cmd, args3, options) {
|
||||||
@@ -123023,10 +123056,10 @@ async function checkoutPrBranch(params) {
|
|||||||
const remoteName = `pr-${pullNumber}`;
|
const remoteName = `pr-${pullNumber}`;
|
||||||
const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
|
const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
|
||||||
try {
|
try {
|
||||||
$("git", ["remote", "add", remoteName, forkUrl]);
|
$("git", ["remote", "add", remoteName, forkUrl], { log: false });
|
||||||
log.debug(`\u{1F4CC} added remote '${remoteName}' for fork ${headRepo.full_name}`);
|
log.debug(`\u{1F4CC} added remote '${remoteName}' for fork ${headRepo.full_name}`);
|
||||||
} catch {
|
} catch {
|
||||||
$("git", ["remote", "set-url", remoteName, forkUrl]);
|
$("git", ["remote", "set-url", remoteName, forkUrl], { log: false });
|
||||||
log.debug(`\u{1F4CC} updated remote '${remoteName}' for fork ${headRepo.full_name}`);
|
log.debug(`\u{1F4CC} updated remote '${remoteName}' for fork ${headRepo.full_name}`);
|
||||||
}
|
}
|
||||||
$("git", ["config", `branch.${localBranch}.pushRemote`, remoteName]);
|
$("git", ["config", `branch.${localBranch}.pushRemote`, remoteName]);
|
||||||
@@ -123046,7 +123079,7 @@ async function checkoutPrBranch(params) {
|
|||||||
function CheckoutPrTool(ctx) {
|
function CheckoutPrTool(ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "checkout_pr",
|
name: "checkout_pr",
|
||||||
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Use this when you need to work on an existing PR.",
|
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. The PR diff is written to a file (diffPath) for grep access. For small diffs, it's also returned inline.",
|
||||||
parameters: CheckoutPr,
|
parameters: CheckoutPr,
|
||||||
execute: execute(async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
const result = await checkoutPrBranch({
|
const result = await checkoutPrBranch({
|
||||||
@@ -123072,6 +123105,12 @@ function CheckoutPrTool(ctx) {
|
|||||||
pull_number,
|
pull_number,
|
||||||
mediaType: { format: "diff" }
|
mediaType: { format: "diff" }
|
||||||
});
|
});
|
||||||
|
const diffContent = diffResponse.data;
|
||||||
|
const diffPath = join8(process.env.PULLFROG_TEMP_DIR, `pr-${pull_number}.diff`);
|
||||||
|
writeFileSync4(diffPath, diffContent);
|
||||||
|
log.debug(`wrote diff to ${diffPath} (${diffContent.length} bytes)`);
|
||||||
|
const MAX_INLINE_DIFF_SIZE = 50 * 1024;
|
||||||
|
const diff = diffContent.length <= MAX_INLINE_DIFF_SIZE ? diffContent : `Diff is ${(diffContent.length / 1024).toFixed(0)}KB - use grep or read from ${diffPath}`;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
number: pr.data.number,
|
number: pr.data.number,
|
||||||
@@ -123082,7 +123121,8 @@ function CheckoutPrTool(ctx) {
|
|||||||
maintainerCanModify: pr.data.maintainer_can_modify,
|
maintainerCanModify: pr.data.maintainer_can_modify,
|
||||||
url: pr.data.html_url,
|
url: pr.data.html_url,
|
||||||
headRepo: headRepo.full_name,
|
headRepo: headRepo.full_name,
|
||||||
diff: diffResponse.data
|
diff,
|
||||||
|
diffPath
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -123191,7 +123231,7 @@ function DebugShellCommandTool(_ctx) {
|
|||||||
|
|
||||||
// prep/installNodeDependencies.ts
|
// prep/installNodeDependencies.ts
|
||||||
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
||||||
import { join as join8 } from "node:path";
|
import { join as join9 } from "node:path";
|
||||||
|
|
||||||
// node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs
|
// node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs
|
||||||
function dashDashArg(agent2, agentCommand) {
|
function dashDashArg(agent2, agentCommand) {
|
||||||
@@ -123503,7 +123543,7 @@ async function isCommandAvailable(command) {
|
|||||||
return result.exitCode === 0;
|
return result.exitCode === 0;
|
||||||
}
|
}
|
||||||
function getPackageManagerFromPackageJson() {
|
function getPackageManagerFromPackageJson() {
|
||||||
const packageJsonPath = join8(process.cwd(), "package.json");
|
const packageJsonPath = join9(process.cwd(), "package.json");
|
||||||
try {
|
try {
|
||||||
const content = readFileSync2(packageJsonPath, "utf-8");
|
const content = readFileSync2(packageJsonPath, "utf-8");
|
||||||
const pkg = JSON.parse(content);
|
const pkg = JSON.parse(content);
|
||||||
@@ -123534,7 +123574,7 @@ async function installPackageManager(name, installSpec) {
|
|||||||
return result.stderr || `failed to install ${name}`;
|
return result.stderr || `failed to install ${name}`;
|
||||||
}
|
}
|
||||||
if (name === "deno") {
|
if (name === "deno") {
|
||||||
const denoPath = join8(process.env.HOME || "", ".deno", "bin");
|
const denoPath = join9(process.env.HOME || "", ".deno", "bin");
|
||||||
process.env.PATH = `${denoPath}:${process.env.PATH}`;
|
process.env.PATH = `${denoPath}:${process.env.PATH}`;
|
||||||
}
|
}
|
||||||
log.info(`\u2705 installed ${name}`);
|
log.info(`\u2705 installed ${name}`);
|
||||||
@@ -123543,7 +123583,7 @@ async function installPackageManager(name, installSpec) {
|
|||||||
var installNodeDependencies = {
|
var installNodeDependencies = {
|
||||||
name: "installNodeDependencies",
|
name: "installNodeDependencies",
|
||||||
shouldRun: () => {
|
shouldRun: () => {
|
||||||
const packageJsonPath = join8(process.cwd(), "package.json");
|
const packageJsonPath = join9(process.cwd(), "package.json");
|
||||||
return existsSync3(packageJsonPath);
|
return existsSync3(packageJsonPath);
|
||||||
},
|
},
|
||||||
run: async () => {
|
run: async () => {
|
||||||
@@ -123606,7 +123646,7 @@ var installNodeDependencies = {
|
|||||||
|
|
||||||
// prep/installPythonDependencies.ts
|
// prep/installPythonDependencies.ts
|
||||||
import { existsSync as existsSync4 } from "node:fs";
|
import { existsSync as existsSync4 } from "node:fs";
|
||||||
import { join as join9 } from "node:path";
|
import { join as join10 } from "node:path";
|
||||||
var PYTHON_CONFIGS = [
|
var PYTHON_CONFIGS = [
|
||||||
{
|
{
|
||||||
file: "requirements.txt",
|
file: "requirements.txt",
|
||||||
@@ -123678,11 +123718,11 @@ var installPythonDependencies = {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const cwd2 = process.cwd();
|
const cwd2 = process.cwd();
|
||||||
return PYTHON_CONFIGS.some((config2) => existsSync4(join9(cwd2, config2.file)));
|
return PYTHON_CONFIGS.some((config2) => existsSync4(join10(cwd2, config2.file)));
|
||||||
},
|
},
|
||||||
run: async () => {
|
run: async () => {
|
||||||
const cwd2 = process.cwd();
|
const cwd2 = process.cwd();
|
||||||
const config2 = PYTHON_CONFIGS.find((c) => existsSync4(join9(cwd2, c.file)));
|
const config2 = PYTHON_CONFIGS.find((c) => existsSync4(join10(cwd2, c.file)));
|
||||||
if (!config2) {
|
if (!config2) {
|
||||||
return {
|
return {
|
||||||
language: "python",
|
language: "python",
|
||||||
@@ -124484,6 +124524,13 @@ function StartReviewTool(ctx) {
|
|||||||
commit_id: pr.data.head.sha
|
commit_id: pr.data.head.sha
|
||||||
// no 'event' = PENDING review
|
// no 'event' = PENDING review
|
||||||
});
|
});
|
||||||
|
log.debug(`createReview response: ${JSON.stringify(result.data)}`);
|
||||||
|
if (!result.data.id || !result.data.node_id) {
|
||||||
|
log.debug(result);
|
||||||
|
throw new Error(
|
||||||
|
`createReview returned invalid data: id=${result.data.id}, node_id=${result.data.node_id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
reviewId = result.data.id;
|
reviewId = result.data.id;
|
||||||
reviewNodeId = result.data.node_id;
|
reviewNodeId = result.data.node_id;
|
||||||
log.debug(`created new pending review: id=${reviewId}`);
|
log.debug(`created new pending review: id=${reviewId}`);
|
||||||
@@ -124512,8 +124559,7 @@ function StartReviewTool(ctx) {
|
|||||||
};
|
};
|
||||||
log.debug(`review session started: id=${reviewId}, nodeId=${reviewNodeId}`);
|
log.debug(`review session started: id=${reviewId}, nodeId=${reviewNodeId}`);
|
||||||
return {
|
return {
|
||||||
message: `Review session started for PR #${pull_number}.`,
|
message: `Review session started for PR #${pull_number}. Add comments with add_review_comment, then submit with submit_review.`
|
||||||
instructions: "Analyze: What does this PR change? Is the approach sound? What bugs, edge cases, or security issues exist? Before commenting: Skip nitpicks unless requested. Only comment if the codebase maintainer would care."
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -124535,24 +124581,51 @@ function AddReviewCommentTool(ctx) {
|
|||||||
if (!ctx.toolState.review) {
|
if (!ctx.toolState.review) {
|
||||||
throw new Error("No review session started. Call start_review first.");
|
throw new Error("No review session started. Call start_review first.");
|
||||||
}
|
}
|
||||||
|
const reviewNodeId = ctx.toolState.review.nodeId;
|
||||||
log.debug(
|
log.debug(
|
||||||
`adding review comment: reviewNodeId=${ctx.toolState.review.nodeId}, path=${path4}, line=${line}, side=${side || "RIGHT"}`
|
`adding review comment: reviewNodeId=${reviewNodeId}, path=${path4}, line=${line}, side=${side || "RIGHT"}`
|
||||||
);
|
);
|
||||||
const result = await ctx.octokit.graphql(
|
let result;
|
||||||
ADD_PULL_REQUEST_REVIEW_THREAD,
|
try {
|
||||||
{
|
result = await ctx.octokit.graphql(
|
||||||
pullRequestReviewId: ctx.toolState.review.nodeId,
|
ADD_PULL_REQUEST_REVIEW_THREAD,
|
||||||
path: path4,
|
{
|
||||||
line,
|
pullRequestReviewId: reviewNodeId,
|
||||||
body,
|
path: path4,
|
||||||
side: side || "RIGHT"
|
line,
|
||||||
}
|
body,
|
||||||
);
|
side: side || "RIGHT"
|
||||||
log.debug(`review comment added: threadId=${result.addPullRequestReviewThread.thread.id}`);
|
}
|
||||||
|
);
|
||||||
|
log.debug(`addPullRequestReviewThread response: ${JSON.stringify(result)}`);
|
||||||
|
} catch (error41) {
|
||||||
|
const errorMsg = error41 instanceof Error ? error41.message : String(error41);
|
||||||
|
log.debug(`addPullRequestReviewThread error: ${errorMsg}`);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add comment to ${path4}:${line}. GraphQL error: ${errorMsg}. Ensure the line is part of the diff and the path is correct.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add comment to ${path4}:${line}. GraphQL returned null response.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!result.addPullRequestReviewThread) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add comment to ${path4}:${line}. addPullRequestReviewThread is null. Response: ${JSON.stringify(result)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!result.addPullRequestReviewThread.thread) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add comment to ${path4}:${line}. thread is null. The line must be part of the diff. Response: ${JSON.stringify(result)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const threadId = result.addPullRequestReviewThread.thread.id;
|
||||||
|
log.debug(`review comment added: threadId=${threadId}`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Comment added to ${path4}:${line}`,
|
message: `Comment added to ${path4}:${line}`,
|
||||||
threadId: result.addPullRequestReviewThread.thread.id
|
threadId
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -124594,6 +124667,10 @@ function SubmitReviewTool(ctx) {
|
|||||||
event: "COMMENT",
|
event: "COMMENT",
|
||||||
body: bodyWithFooter
|
body: bodyWithFooter
|
||||||
});
|
});
|
||||||
|
log.debug(`submitReview response: ${JSON.stringify(result.data)}`);
|
||||||
|
if (!result.data.id) {
|
||||||
|
throw new Error(`submitReview returned invalid data: ${JSON.stringify(result.data)}`);
|
||||||
|
}
|
||||||
log.debug(`review submitted: reviewId=${result.data.id}, state=${result.data.state}`);
|
log.debug(`review submitted: reviewId=${result.data.id}, state=${result.data.state}`);
|
||||||
delete ctx.toolState.review;
|
delete ctx.toolState.review;
|
||||||
await deleteProgressComment(ctx);
|
await deleteProgressComment(ctx);
|
||||||
@@ -125248,7 +125325,7 @@ function resolveAgent({
|
|||||||
return agent2;
|
return agent2;
|
||||||
}
|
}
|
||||||
async function createTempDirectory() {
|
async function createTempDirectory() {
|
||||||
const sharedTempDir = await mkdtemp2(join10(tmpdir2(), "pullfrog-"));
|
const sharedTempDir = await mkdtemp2(join11(tmpdir2(), "pullfrog-"));
|
||||||
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
||||||
log.info(`\u{1F4C2} PULLFROG_TEMP_DIR has been created at ${sharedTempDir}`);
|
log.info(`\u{1F4C2} PULLFROG_TEMP_DIR has been created at ${sharedTempDir}`);
|
||||||
return sharedTempDir;
|
return sharedTempDir;
|
||||||
|
|||||||
+23
-5
@@ -1,3 +1,5 @@
|
|||||||
|
import { writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
import type { Octokit } from "@octokit/rest";
|
import type { Octokit } from "@octokit/rest";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { ToolContext } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
@@ -20,6 +22,7 @@ export type CheckoutPrResult = {
|
|||||||
url: string;
|
url: string;
|
||||||
headRepo: string;
|
headRepo: string;
|
||||||
diff: string;
|
diff: string;
|
||||||
|
diffPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CheckoutPrBranchParams {
|
interface CheckoutPrBranchParams {
|
||||||
@@ -104,13 +107,13 @@ export async function checkoutPrBranch(
|
|||||||
const remoteName = `pr-${pullNumber}`;
|
const remoteName = `pr-${pullNumber}`;
|
||||||
const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
|
const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
|
||||||
|
|
||||||
// add fork as a named remote (ignore error if already exists)
|
// add fork as a named remote (suppress logging to avoid "error: remote already exists" spam)
|
||||||
try {
|
try {
|
||||||
$("git", ["remote", "add", remoteName, forkUrl]);
|
$("git", ["remote", "add", remoteName, forkUrl], { log: false });
|
||||||
log.debug(`📌 added remote '${remoteName}' for fork ${headRepo.full_name}`);
|
log.debug(`📌 added remote '${remoteName}' for fork ${headRepo.full_name}`);
|
||||||
} catch {
|
} catch {
|
||||||
// remote already exists, update its URL
|
// remote already exists, update its URL
|
||||||
$("git", ["remote", "set-url", remoteName, forkUrl]);
|
$("git", ["remote", "set-url", remoteName, forkUrl], { log: false });
|
||||||
log.debug(`📌 updated remote '${remoteName}' for fork ${headRepo.full_name}`);
|
log.debug(`📌 updated remote '${remoteName}' for fork ${headRepo.full_name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +143,8 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
|||||||
return tool({
|
return tool({
|
||||||
name: "checkout_pr",
|
name: "checkout_pr",
|
||||||
description:
|
description:
|
||||||
"Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Use this when you need to work on an existing PR.",
|
"Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. " +
|
||||||
|
"The PR diff is written to a file (diffPath) for grep access. For small diffs, it's also returned inline.",
|
||||||
parameters: CheckoutPr,
|
parameters: CheckoutPr,
|
||||||
execute: execute(async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
const result = await checkoutPrBranch({
|
const result = await checkoutPrBranch({
|
||||||
@@ -174,6 +178,19 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
|||||||
mediaType: { format: "diff" },
|
mediaType: { format: "diff" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// write diff to file for grep access
|
||||||
|
const diffContent = diffResponse.data as unknown as string;
|
||||||
|
const diffPath = join(process.env.PULLFROG_TEMP_DIR!, `pr-${pull_number}.diff`);
|
||||||
|
writeFileSync(diffPath, diffContent);
|
||||||
|
log.debug(`wrote diff to ${diffPath} (${diffContent.length} bytes)`);
|
||||||
|
|
||||||
|
// return diff inline only if small enough
|
||||||
|
const MAX_INLINE_DIFF_SIZE = 50 * 1024; // 50KB
|
||||||
|
const diff =
|
||||||
|
diffContent.length <= MAX_INLINE_DIFF_SIZE
|
||||||
|
? diffContent
|
||||||
|
: `Diff is ${(diffContent.length / 1024).toFixed(0)}KB - use grep or read from ${diffPath}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
number: pr.data.number,
|
number: pr.data.number,
|
||||||
@@ -184,7 +201,8 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
|||||||
maintainerCanModify: pr.data.maintainer_can_modify,
|
maintainerCanModify: pr.data.maintainer_can_modify,
|
||||||
url: pr.data.html_url,
|
url: pr.data.html_url,
|
||||||
headRepo: headRepo.full_name,
|
headRepo: headRepo.full_name,
|
||||||
diff: diffResponse.data as unknown as string,
|
diff,
|
||||||
|
diffPath,
|
||||||
} satisfies CheckoutPrResult;
|
} satisfies CheckoutPrResult;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+59
-17
@@ -92,6 +92,13 @@ export function StartReviewTool(ctx: ToolContext) {
|
|||||||
commit_id: pr.data.head.sha,
|
commit_id: pr.data.head.sha,
|
||||||
// no 'event' = PENDING review
|
// no 'event' = PENDING review
|
||||||
});
|
});
|
||||||
|
log.debug(`createReview response: ${JSON.stringify(result.data)}`);
|
||||||
|
if (!result.data.id || !result.data.node_id) {
|
||||||
|
log.debug(result);
|
||||||
|
throw new Error(
|
||||||
|
`createReview returned invalid data: id=${result.data.id}, node_id=${result.data.node_id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
reviewId = result.data.id;
|
reviewId = result.data.id;
|
||||||
reviewNodeId = result.data.node_id;
|
reviewNodeId = result.data.node_id;
|
||||||
log.debug(`created new pending review: id=${reviewId}`);
|
log.debug(`created new pending review: id=${reviewId}`);
|
||||||
@@ -126,10 +133,7 @@ export function StartReviewTool(ctx: ToolContext) {
|
|||||||
log.debug(`review session started: id=${reviewId}, nodeId=${reviewNodeId}`);
|
log.debug(`review session started: id=${reviewId}, nodeId=${reviewNodeId}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: `Review session started for PR #${pull_number}.`,
|
message: `Review session started for PR #${pull_number}. Add comments with add_review_comment, then submit with submit_review.`,
|
||||||
instructions:
|
|
||||||
"Analyze: What does this PR change? Is the approach sound? What bugs, edge cases, or security issues exist? " +
|
|
||||||
"Before commenting: Skip nitpicks unless requested. Only comment if the codebase maintainer would care.",
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -160,28 +164,58 @@ export function AddReviewCommentTool(ctx: ToolContext) {
|
|||||||
throw new Error("No review session started. Call start_review first.");
|
throw new Error("No review session started. Call start_review first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reviewNodeId = ctx.toolState.review.nodeId;
|
||||||
log.debug(
|
log.debug(
|
||||||
`adding review comment: reviewNodeId=${ctx.toolState.review.nodeId}, path=${path}, line=${line}, side=${side || "RIGHT"}`
|
`adding review comment: reviewNodeId=${reviewNodeId}, path=${path}, line=${line}, side=${side || "RIGHT"}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// add comment thread via GraphQL (REST doesn't support adding to existing pending review)
|
// add comment thread via GraphQL (REST doesn't support adding to existing pending review)
|
||||||
const result = await ctx.octokit.graphql<AddPullRequestReviewThreadResponse>(
|
let result: AddPullRequestReviewThreadResponse;
|
||||||
ADD_PULL_REQUEST_REVIEW_THREAD,
|
try {
|
||||||
{
|
result = await ctx.octokit.graphql<AddPullRequestReviewThreadResponse>(
|
||||||
pullRequestReviewId: ctx.toolState.review.nodeId,
|
ADD_PULL_REQUEST_REVIEW_THREAD,
|
||||||
path,
|
{
|
||||||
line,
|
pullRequestReviewId: reviewNodeId,
|
||||||
body,
|
path,
|
||||||
side: side || "RIGHT",
|
line,
|
||||||
}
|
body,
|
||||||
);
|
side: side || "RIGHT",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
log.debug(`addPullRequestReviewThread response: ${JSON.stringify(result)}`);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
log.debug(`addPullRequestReviewThread error: ${errorMsg}`);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add comment to ${path}:${line}. GraphQL error: ${errorMsg}. ` +
|
||||||
|
`Ensure the line is part of the diff and the path is correct.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
log.debug(`review comment added: threadId=${result.addPullRequestReviewThread.thread.id}`);
|
// check if the mutation succeeded - null means the line is not in the diff
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add comment to ${path}:${line}. GraphQL returned null response.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!result.addPullRequestReviewThread) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add comment to ${path}:${line}. addPullRequestReviewThread is null. Response: ${JSON.stringify(result)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!result.addPullRequestReviewThread.thread) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add comment to ${path}:${line}. thread is null. The line must be part of the diff. Response: ${JSON.stringify(result)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const threadId = result.addPullRequestReviewThread.thread.id;
|
||||||
|
log.debug(`review comment added: threadId=${threadId}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Comment added to ${path}:${line}`,
|
message: `Comment added to ${path}:${line}`,
|
||||||
threadId: result.addPullRequestReviewThread.thread.id,
|
threadId,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -238,6 +272,10 @@ export function SubmitReviewTool(ctx: ToolContext) {
|
|||||||
body: bodyWithFooter,
|
body: bodyWithFooter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.debug(`submitReview response: ${JSON.stringify(result.data)}`);
|
||||||
|
if (!result.data.id) {
|
||||||
|
throw new Error(`submitReview returned invalid data: ${JSON.stringify(result.data)}`);
|
||||||
|
}
|
||||||
log.debug(`review submitted: reviewId=${result.data.id}, state=${result.data.state}`);
|
log.debug(`review submitted: reviewId=${result.data.id}, state=${result.data.state}`);
|
||||||
|
|
||||||
// clear review state
|
// clear review state
|
||||||
@@ -342,6 +380,10 @@ export function ReviewTool(ctx: ToolContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const result = await ctx.octokit.rest.pulls.createReview(params);
|
const result = await ctx.octokit.rest.pulls.createReview(params);
|
||||||
|
log.debug(`createReview (legacy) response: ${JSON.stringify(result.data)}`);
|
||||||
|
if (!result.data.id) {
|
||||||
|
throw new Error(`createReview returned invalid data: ${JSON.stringify(result.data)}`);
|
||||||
|
}
|
||||||
const reviewId = result.data.id;
|
const reviewId = result.data.id;
|
||||||
|
|
||||||
// build quick links footer and update the review body
|
// build quick links footer and update the review body
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.151",
|
"version": "0.0.152",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
+1
-1
@@ -278,7 +278,7 @@ export const log = {
|
|||||||
/**
|
/**
|
||||||
* Print debug message (only if LOG_LEVEL=debug)
|
* Print debug message (only if LOG_LEVEL=debug)
|
||||||
*/
|
*/
|
||||||
debug: (message: string): void => {
|
debug: (message: string | unknown): void => {
|
||||||
if (isDebugEnabled()) {
|
if (isDebugEnabled()) {
|
||||||
if (isGitHubActions) {
|
if (isGitHubActions) {
|
||||||
// using this instead of core.debug
|
// using this instead of core.debug
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,7 @@
|
|||||||
import { createSign } from "node:crypto";
|
import { createSign } from "node:crypto";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
import { retry } from "./retry.ts";
|
||||||
|
|
||||||
export interface InstallationToken {
|
export interface InstallationToken {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -56,7 +57,6 @@ async function acquireTokenViaOIDC(): Promise<string> {
|
|||||||
|
|
||||||
log.info("» exchanging OIDC token for installation token...");
|
log.info("» exchanging OIDC token for installation token...");
|
||||||
|
|
||||||
// Add timeout to prevent long waits (30 seconds)
|
|
||||||
const timeoutMs = 30000;
|
const timeoutMs = 30000;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
@@ -234,7 +234,7 @@ async function acquireTokenViaGitHubApp(): Promise<string> {
|
|||||||
|
|
||||||
async function acquireNewToken(): Promise<string> {
|
async function acquireNewToken(): Promise<string> {
|
||||||
if (isGitHubActionsEnvironment()) {
|
if (isGitHubActionsEnvironment()) {
|
||||||
return await acquireTokenViaOIDC();
|
return await retry(() => acquireTokenViaOIDC(), { label: "token exchange" });
|
||||||
} else {
|
} else {
|
||||||
return await acquireTokenViaGitHubApp();
|
return await acquireTokenViaGitHubApp();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user