Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 250fe7eaa1 | |||
| 4a8c432a48 |
@@ -143506,10 +143506,15 @@ var findInstallationId = async (jwt2, repoOwner, repoName) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
async function acquireTokenViaGitHubApp(opts) {
|
async function acquireTokenViaGitHubApp(opts) {
|
||||||
|
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
|
||||||
|
);
|
||||||
|
}
|
||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
const config3 = {
|
const config3 = {
|
||||||
appId: process.env.GITHUB_APP_ID,
|
appId: process.env.GITHUB_APP_ID,
|
||||||
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
||||||
repoOwner: repoContext.owner,
|
repoOwner: repoContext.owner,
|
||||||
repoName: repoContext.name
|
repoName: repoContext.name
|
||||||
};
|
};
|
||||||
@@ -144705,6 +144710,33 @@ async function checkoutPrBranch(pullNumber, params) {
|
|||||||
forkUrl: isFork ? `https://github.com/${headRepo.full_name}.git` : void 0
|
forkUrl: isFork ? `https://github.com/${headRepo.full_name}.git` : void 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function deepenForBeforeSha(params) {
|
||||||
|
const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
||||||
|
if (!isShallow) return;
|
||||||
|
const maxIterations = 10;
|
||||||
|
for (let i = 0; i < maxIterations; i++) {
|
||||||
|
try {
|
||||||
|
$("git", ["cat-file", "-t", params.beforeSha], { log: false });
|
||||||
|
log.debug(`\xBB before_sha ${params.beforeSha.slice(0, 7)} is now reachable`);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
log.debug(
|
||||||
|
`\xBB deepening by 50 to reach before_sha ${params.beforeSha.slice(0, 7)} (attempt ${i + 1}/${maxIterations})`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
$git("fetch", ["--deepen=50", "--no-tags", "origin"], {
|
||||||
|
token: params.gitToken
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
log.debug(`\xBB deepen for before_sha failed (force-push may have rewritten history)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug(
|
||||||
|
`\xBB before_sha ${params.beforeSha.slice(0, 7)} not reachable after ${maxIterations * 50} commits`
|
||||||
|
);
|
||||||
|
}
|
||||||
function CheckoutPrTool(ctx) {
|
function CheckoutPrTool(ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "checkout_pr",
|
name: "checkout_pr",
|
||||||
@@ -144720,6 +144752,13 @@ function CheckoutPrTool(ctx) {
|
|||||||
shell: ctx.payload.shell,
|
shell: ctx.payload.shell,
|
||||||
postCheckoutScript: ctx.postCheckoutScript
|
postCheckoutScript: ctx.postCheckoutScript
|
||||||
});
|
});
|
||||||
|
const event = ctx.payload.event;
|
||||||
|
if ("before_sha" in event && event.before_sha) {
|
||||||
|
deepenForBeforeSha({
|
||||||
|
gitToken: ctx.gitToken,
|
||||||
|
beforeSha: event.before_sha
|
||||||
|
});
|
||||||
|
}
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
const pr = await ctx.octokit.rest.pulls.get({
|
||||||
owner: ctx.repo.owner,
|
owner: ctx.repo.owner,
|
||||||
repo: ctx.repo.name,
|
repo: ctx.repo.name,
|
||||||
@@ -144975,7 +145014,7 @@ function fixDoubleEscapedString(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mcp/comment.ts
|
// mcp/comment.ts
|
||||||
async function updatePlanCommentId(ctx, planCommentNodeId) {
|
async function updateCommentNodeId(ctx, field, nodeId) {
|
||||||
if (ctx.runId === void 0 || !ctx.apiToken) return;
|
if (ctx.runId === void 0 || !ctx.apiToken) return;
|
||||||
try {
|
try {
|
||||||
await retry(
|
await retry(
|
||||||
@@ -144987,7 +145026,7 @@ async function updatePlanCommentId(ctx, planCommentNodeId) {
|
|||||||
authorization: `Bearer ${ctx.apiToken}`,
|
authorization: `Bearer ${ctx.apiToken}`,
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ planCommentNodeId }),
|
body: JSON.stringify({ [field]: nodeId }),
|
||||||
signal: AbortSignal.timeout(1e4)
|
signal: AbortSignal.timeout(1e4)
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
||||||
@@ -144995,11 +145034,11 @@ async function updatePlanCommentId(ctx, planCommentNodeId) {
|
|||||||
{
|
{
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
delayMs: 2e3,
|
delayMs: 2e3,
|
||||||
label: "updatePlanCommentId"
|
label: `updateCommentNodeId(${field})`
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error49) {
|
} catch (error49) {
|
||||||
log.warning(`updatePlanCommentId exhausted retries: ${error49}`);
|
log.warning(`updateCommentNodeId(${field}) exhausted retries: ${error49}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function buildCommentFooter(params) {
|
async function buildCommentFooter(params) {
|
||||||
@@ -145038,14 +145077,14 @@ async function addFooter(ctx, body) {
|
|||||||
var Comment = type({
|
var Comment = type({
|
||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
body: type.string.describe("the comment body content"),
|
body: type.string.describe("the comment body content"),
|
||||||
type: type.enumerated("Plan", "Comment").describe(
|
type: type.enumerated("Plan", "Summary", "Comment").describe(
|
||||||
"Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)."
|
"Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)."
|
||||||
).optional()
|
).optional()
|
||||||
});
|
});
|
||||||
function CreateCommentTool(ctx) {
|
function CreateCommentTool(ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "create_issue_comment",
|
name: "create_issue_comment",
|
||||||
description: "Create a comment on a GitHub issue. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' only when creating a standalone plan comment to record as this run's plan.",
|
description: "Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments, type: 'Summary' for PR summary comments.",
|
||||||
parameters: Comment,
|
parameters: Comment,
|
||||||
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
||||||
const bodyWithFooter = await addFooter(ctx, body);
|
const bodyWithFooter = await addFooter(ctx, body);
|
||||||
@@ -145056,7 +145095,10 @@ function CreateCommentTool(ctx) {
|
|||||||
body: bodyWithFooter
|
body: bodyWithFooter
|
||||||
});
|
});
|
||||||
if (commentType === "Plan" && result.data.node_id) {
|
if (commentType === "Plan" && result.data.node_id) {
|
||||||
await updatePlanCommentId(ctx, result.data.node_id);
|
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
||||||
|
}
|
||||||
|
if (commentType === "Summary" && result.data.node_id) {
|
||||||
|
await updateCommentNodeId(ctx, "summaryCommentNodeId", result.data.node_id);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -145128,7 +145170,7 @@ async function reportProgress(ctx, params) {
|
|||||||
});
|
});
|
||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
if (isPlanMode && result2.data.node_id) {
|
if (isPlanMode && result2.data.node_id) {
|
||||||
await updatePlanCommentId(ctx, result2.data.node_id);
|
await updateCommentNodeId(ctx, "planCommentNodeId", result2.data.node_id);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
commentId: result2.data.id,
|
commentId: result2.data.id,
|
||||||
@@ -145154,7 +145196,7 @@ async function reportProgress(ctx, params) {
|
|||||||
});
|
});
|
||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
if (isPlanMode && result2.data.node_id) {
|
if (isPlanMode && result2.data.node_id) {
|
||||||
await updatePlanCommentId(ctx, result2.data.node_id);
|
await updateCommentNodeId(ctx, "planCommentNodeId", result2.data.node_id);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
commentId: result2.data.id,
|
commentId: result2.data.id,
|
||||||
@@ -145195,7 +145237,7 @@ async function reportProgress(ctx, params) {
|
|||||||
body: bodyWithPlanLink
|
body: bodyWithPlanLink
|
||||||
});
|
});
|
||||||
if (updateResult.data.node_id) {
|
if (updateResult.data.node_id) {
|
||||||
await updatePlanCommentId(ctx, updateResult.data.node_id);
|
await updateCommentNodeId(ctx, "planCommentNodeId", updateResult.data.node_id);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
commentId: updateResult.data.id,
|
commentId: updateResult.data.id,
|
||||||
@@ -146219,6 +146261,7 @@ var NOSHELL_BLOCKED_SUBCOMMANDS = {
|
|||||||
bisect: "Blocked: git bisect run can execute arbitrary shell commands."
|
bisect: "Blocked: git bisect run can execute arbitrary shell commands."
|
||||||
};
|
};
|
||||||
var NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"];
|
var NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"];
|
||||||
|
var COLLAPSE_THRESHOLD = 200;
|
||||||
var subcommandPattern = regex("^[a-z][a-z0-9-]*$");
|
var subcommandPattern = regex("^[a-z][a-z0-9-]*$");
|
||||||
var Git = type({
|
var Git = type({
|
||||||
subcommand: type(subcommandPattern).describe("Git subcommand (e.g., 'status', 'log', 'diff')"),
|
subcommand: type(subcommandPattern).describe("Git subcommand (e.g., 'status', 'log', 'diff')"),
|
||||||
@@ -146253,6 +146296,14 @@ function GitTool(ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const output = $("git", [subcommand, ...args2], { log: false });
|
const output = $("git", [subcommand, ...args2], { log: false });
|
||||||
|
const lineCount = output.split("\n").length;
|
||||||
|
if (lineCount > COLLAPSE_THRESHOLD) {
|
||||||
|
log.group(`git ${subcommand} output (${lineCount} lines)`, () => {
|
||||||
|
log.info(output);
|
||||||
|
});
|
||||||
|
} else if (output) {
|
||||||
|
log.info(output);
|
||||||
|
}
|
||||||
return { success: true, output };
|
return { success: true, output };
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -147410,7 +147461,7 @@ function ResolveReviewThreadTool(ctx) {
|
|||||||
// mcp/selectMode.ts
|
// mcp/selectMode.ts
|
||||||
var SelectModeParams = type({
|
var SelectModeParams = type({
|
||||||
mode: type.string.describe(
|
mode: type.string.describe(
|
||||||
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts')"
|
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts', 'Summarize')"
|
||||||
),
|
),
|
||||||
"issue_number?": type("number").describe(
|
"issue_number?": type("number").describe(
|
||||||
"optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)"
|
"optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)"
|
||||||
@@ -147577,7 +147628,37 @@ An existing plan comment was found for this issue. Update that comment with the
|
|||||||
3. Finalize:
|
3. Finalize:
|
||||||
- call \`${ghPullfrogMcpName}/report_progress\` with results
|
- call \`${ghPullfrogMcpName}/report_progress\` with results
|
||||||
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
|
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
|
||||||
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`
|
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`,
|
||||||
|
Summarize: `### Checklist
|
||||||
|
|
||||||
|
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`.
|
||||||
|
2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt:
|
||||||
|
- the diff file path
|
||||||
|
- PR metadata (title, file count, commit count, base/head branches)
|
||||||
|
- format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with before/after framing
|
||||||
|
- instruct it to use the TOC to selectively read relevant diff sections, not the entire file
|
||||||
|
- instruct it to return the full summary markdown via \`${ghPullfrogMcpName}/set_output\`
|
||||||
|
3. After the subagent completes, call \`${ghPullfrogMcpName}/create_issue_comment\` with \`type: "Summary"\` and the summary body.
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
|
Use mini or auto effort.`,
|
||||||
|
SummaryUpdate: `### Checklist (updating existing summary)
|
||||||
|
|
||||||
|
An existing summary comment was found for this PR. Update it rather than creating a new one.
|
||||||
|
|
||||||
|
1. Use \`previousSummaryBody\` from this response as the current summary to revise.
|
||||||
|
2. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`.
|
||||||
|
3. Delegate a subagent with:
|
||||||
|
- the diff file path and PR metadata
|
||||||
|
- the existing summary body (\`previousSummaryBody\`) so it can update rather than rewrite from scratch
|
||||||
|
- format instructions from EVENT INSTRUCTIONS (if any)
|
||||||
|
- instruct it to produce an updated summary reflecting the current state of the PR and return via \`${ghPullfrogMcpName}/set_output\`
|
||||||
|
4. After the subagent completes, call \`${ghPullfrogMcpName}/edit_issue_comment\` with \`commentId: existingSummaryCommentId\` (from this response) and the updated summary body.
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
|
Use mini or auto effort.`
|
||||||
};
|
};
|
||||||
var modeInstructionParent = {
|
var modeInstructionParent = {
|
||||||
IncrementalReview: "Review",
|
IncrementalReview: "Review",
|
||||||
@@ -147609,6 +147690,21 @@ async function fetchExistingPlanComment(ctx, issueNumber) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function fetchExistingSummaryComment(ctx, prNumber) {
|
||||||
|
if (!ctx.apiToken) return null;
|
||||||
|
try {
|
||||||
|
const response = await apiFetch({
|
||||||
|
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`,
|
||||||
|
method: "GET",
|
||||||
|
headers: { authorization: `Bearer ${ctx.apiToken}` },
|
||||||
|
signal: AbortSignal.timeout(1e4)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
return response.ok && "commentId" in data ? data : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
function SelectModeTool(ctx) {
|
function SelectModeTool(ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "select_mode",
|
name: "select_mode",
|
||||||
@@ -147651,6 +147747,23 @@ function SelectModeTool(ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (selectedMode.name === "Summarize") {
|
||||||
|
const prNumber = ctx.payload.event.issue_number;
|
||||||
|
if (prNumber !== void 0) {
|
||||||
|
const existing = await fetchExistingSummaryComment(ctx, prNumber);
|
||||||
|
if (existing !== null) {
|
||||||
|
ctx.toolState.existingSummaryCommentId = existing.commentId;
|
||||||
|
return {
|
||||||
|
...buildOrchestratorGuidance(selectedMode, {
|
||||||
|
...guidanceOpts,
|
||||||
|
overrideGuidance: modeGuidance.SummaryUpdate
|
||||||
|
}),
|
||||||
|
existingSummaryCommentId: existing.commentId,
|
||||||
|
previousSummaryBody: existing.body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return buildOrchestratorGuidance(selectedMode, guidanceOpts);
|
return buildOrchestratorGuidance(selectedMode, guidanceOpts);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -148426,6 +148539,21 @@ Your job is to fix issues THIS PR introduced, not to fix all CI failures. If in
|
|||||||
5. **PROGRESS** - ${reportProgressInstruction}
|
5. **PROGRESS** - ${reportProgressInstruction}
|
||||||
|
|
||||||
Do NOT overwrite a good comment with links/details with a generic message like "I have completed the task." If your previous report_progress call already contains all the necessary information and links, skip the final call entirely.`
|
Do NOT overwrite a good comment with links/details with a generic message like "I have completed the task." If your previous report_progress call already contains all the necessary information and links, skip the final call entirely.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Summarize",
|
||||||
|
description: "Summarize a PR with a structured comment that is updated in place on subsequent pushes",
|
||||||
|
prompt: `Follow these steps.
|
||||||
|
|
||||||
|
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number to get PR metadata and diffPath.
|
||||||
|
|
||||||
|
2. **ANALYZE** - Read the diff file. Use the TOC to selectively read relevant sections \u2014 do not read the entire file unless the PR is small.
|
||||||
|
|
||||||
|
3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with before/after framing.
|
||||||
|
|
||||||
|
4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body.
|
||||||
|
|
||||||
|
${permalinkTip}`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -149609,7 +149737,7 @@ import { isAbsolute, resolve } from "node:path";
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.179",
|
version: "0.0.180",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -25898,10 +25898,15 @@ var findInstallationId = async (jwt, repoOwner, repoName) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
async function acquireTokenViaGitHubApp(opts) {
|
async function acquireTokenViaGitHubApp(opts) {
|
||||||
|
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
|
||||||
|
);
|
||||||
|
}
|
||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
const config = {
|
const config = {
|
||||||
appId: process.env.GITHUB_APP_ID,
|
appId: process.env.GITHUB_APP_ID,
|
||||||
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
||||||
repoOwner: repoContext.owner,
|
repoOwner: repoContext.owner,
|
||||||
repoName: repoContext.name
|
repoName: repoContext.name
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -334,6 +334,44 @@ export async function checkoutPrBranch(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeepenForBeforeShaParams = {
|
||||||
|
gitToken: string;
|
||||||
|
beforeSha: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function deepenForBeforeSha(params: DeepenForBeforeShaParams): void {
|
||||||
|
const isShallow =
|
||||||
|
$("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
||||||
|
if (!isShallow) return;
|
||||||
|
|
||||||
|
const maxIterations = 10;
|
||||||
|
for (let i = 0; i < maxIterations; i++) {
|
||||||
|
try {
|
||||||
|
$("git", ["cat-file", "-t", params.beforeSha], { log: false });
|
||||||
|
log.debug(`» before_sha ${params.beforeSha.slice(0, 7)} is now reachable`);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// not reachable yet, deepen
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
`» deepening by 50 to reach before_sha ${params.beforeSha.slice(0, 7)} (attempt ${i + 1}/${maxIterations})`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
$git("fetch", ["--deepen=50", "--no-tags", "origin"], {
|
||||||
|
token: params.gitToken,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
log.debug(`» deepen for before_sha failed (force-push may have rewritten history)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
`» before_sha ${params.beforeSha.slice(0, 7)} not reachable after ${maxIterations * 50} commits`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function CheckoutPrTool(ctx: ToolContext) {
|
export function CheckoutPrTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "checkout_pr",
|
name: "checkout_pr",
|
||||||
@@ -352,6 +390,16 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
|||||||
postCheckoutScript: ctx.postCheckoutScript,
|
postCheckoutScript: ctx.postCheckoutScript,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// for incremental review/rereview: deepen the clone to include before_sha
|
||||||
|
// so `git diff before_sha...HEAD` works without the agent needing to fetch manually
|
||||||
|
const event = ctx.payload.event;
|
||||||
|
if ("before_sha" in event && event.before_sha) {
|
||||||
|
deepenForBeforeSha({
|
||||||
|
gitToken: ctx.gitToken,
|
||||||
|
beforeSha: event.before_sha,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
const pr = await ctx.octokit.rest.pulls.get({
|
||||||
owner: ctx.repo.owner,
|
owner: ctx.repo.owner,
|
||||||
repo: ctx.repo.name,
|
repo: ctx.repo.name,
|
||||||
|
|||||||
+21
-12
@@ -9,8 +9,14 @@ import { retry } from "../utils/retry.ts";
|
|||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
/** PATCH workflow-run with plan comment node_id so plan revisions can update that comment in place. */
|
type CommentNodeIdField = "planCommentNodeId" | "summaryCommentNodeId";
|
||||||
async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string): Promise<void> {
|
|
||||||
|
/** PATCH workflow-run with a comment node_id so future runs can update that comment in place. */
|
||||||
|
export async function updateCommentNodeId(
|
||||||
|
ctx: ToolContext,
|
||||||
|
field: CommentNodeIdField,
|
||||||
|
nodeId: string
|
||||||
|
): Promise<void> {
|
||||||
if (ctx.runId === undefined || !ctx.apiToken) return;
|
if (ctx.runId === undefined || !ctx.apiToken) return;
|
||||||
try {
|
try {
|
||||||
await retry(
|
await retry(
|
||||||
@@ -22,7 +28,7 @@ async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string):
|
|||||||
authorization: `Bearer ${ctx.apiToken}`,
|
authorization: `Bearer ${ctx.apiToken}`,
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ planCommentNodeId }),
|
body: JSON.stringify({ [field]: nodeId }),
|
||||||
signal: AbortSignal.timeout(10_000),
|
signal: AbortSignal.timeout(10_000),
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
||||||
@@ -30,11 +36,11 @@ async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string):
|
|||||||
{
|
{
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
delayMs: 2000,
|
delayMs: 2000,
|
||||||
label: "updatePlanCommentId",
|
label: `updateCommentNodeId(${field})`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warning(`updatePlanCommentId exhausted retries: ${error}`);
|
log.warning(`updateCommentNodeId(${field}) exhausted retries: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,9 +113,9 @@ export const Comment = type({
|
|||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
body: type.string.describe("the comment body content"),
|
body: type.string.describe("the comment body content"),
|
||||||
type: type
|
type: type
|
||||||
.enumerated("Plan", "Comment")
|
.enumerated("Plan", "Summary", "Comment")
|
||||||
.describe(
|
.describe(
|
||||||
"Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)."
|
"Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)."
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
@@ -118,7 +124,7 @@ export function CreateCommentTool(ctx: ToolContext) {
|
|||||||
return tool({
|
return tool({
|
||||||
name: "create_issue_comment",
|
name: "create_issue_comment",
|
||||||
description:
|
description:
|
||||||
"Create a comment on a GitHub issue. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' only when creating a standalone plan comment to record as this run's plan.",
|
"Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments, type: 'Summary' for PR summary comments.",
|
||||||
parameters: Comment,
|
parameters: Comment,
|
||||||
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
||||||
const bodyWithFooter = await addFooter(ctx, body);
|
const bodyWithFooter = await addFooter(ctx, body);
|
||||||
@@ -131,7 +137,10 @@ export function CreateCommentTool(ctx: ToolContext) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (commentType === "Plan" && result.data.node_id) {
|
if (commentType === "Plan" && result.data.node_id) {
|
||||||
await updatePlanCommentId(ctx, result.data.node_id);
|
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
||||||
|
}
|
||||||
|
if (commentType === "Summary" && result.data.node_id) {
|
||||||
|
await updateCommentNodeId(ctx, "summaryCommentNodeId", result.data.node_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -241,7 +250,7 @@ export async function reportProgress(
|
|||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
|
|
||||||
if (isPlanMode && result.data.node_id) {
|
if (isPlanMode && result.data.node_id) {
|
||||||
await updatePlanCommentId(ctx, result.data.node_id);
|
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -278,7 +287,7 @@ export async function reportProgress(
|
|||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
|
|
||||||
if (isPlanMode && result.data.node_id) {
|
if (isPlanMode && result.data.node_id) {
|
||||||
await updatePlanCommentId(ctx, result.data.node_id);
|
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -336,7 +345,7 @@ export async function reportProgress(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (updateResult.data.node_id) {
|
if (updateResult.data.node_id) {
|
||||||
await updatePlanCommentId(ctx, updateResult.data.node_id);
|
await updateCommentNodeId(ctx, "planCommentNodeId", updateResult.data.node_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+11
@@ -218,6 +218,8 @@ const NOSHELL_BLOCKED_SUBCOMMANDS: Record<string, string> = {
|
|||||||
// (avoids false positives like --exclude matching --exec)
|
// (avoids false positives like --exclude matching --exec)
|
||||||
const NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"];
|
const NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"];
|
||||||
|
|
||||||
|
const COLLAPSE_THRESHOLD = 200;
|
||||||
|
|
||||||
// SECURITY: subcommand must match [a-z][a-z0-9-]* to reject flags passed as the subcommand.
|
// SECURITY: subcommand must match [a-z][a-z0-9-]* to reject flags passed as the subcommand.
|
||||||
// this blocks injection of global git options like -c, -C, --exec-path, --config-env, etc.
|
// this blocks injection of global git options like -c, -C, --exec-path, --config-env, etc.
|
||||||
//
|
//
|
||||||
@@ -269,6 +271,15 @@ export function GitTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const output = $("git", [subcommand, ...args], { log: false });
|
const output = $("git", [subcommand, ...args], { log: false });
|
||||||
|
const lineCount = output.split("\n").length;
|
||||||
|
if (lineCount > COLLAPSE_THRESHOLD) {
|
||||||
|
log.group(`git ${subcommand} output (${lineCount} lines)`, () => {
|
||||||
|
log.info(output);
|
||||||
|
});
|
||||||
|
} else if (output) {
|
||||||
|
log.info(output);
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, output };
|
return { success: true, output };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+73
-1
@@ -7,7 +7,7 @@ import { execute, tool } from "./shared.ts";
|
|||||||
|
|
||||||
export const SelectModeParams = type({
|
export const SelectModeParams = type({
|
||||||
mode: type.string.describe(
|
mode: type.string.describe(
|
||||||
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts')"
|
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts', 'Summarize')"
|
||||||
),
|
),
|
||||||
"issue_number?": type("number").describe(
|
"issue_number?": type("number").describe(
|
||||||
"optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)"
|
"optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)"
|
||||||
@@ -185,6 +185,38 @@ An existing plan comment was found for this issue. Update that comment with the
|
|||||||
- call \`${ghPullfrogMcpName}/report_progress\` with results
|
- call \`${ghPullfrogMcpName}/report_progress\` with results
|
||||||
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
|
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
|
||||||
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`,
|
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`,
|
||||||
|
|
||||||
|
Summarize: `### Checklist
|
||||||
|
|
||||||
|
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` — this returns PR metadata and a \`diffPath\`.
|
||||||
|
2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt:
|
||||||
|
- the diff file path
|
||||||
|
- PR metadata (title, file count, commit count, base/head branches)
|
||||||
|
- format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with before/after framing
|
||||||
|
- instruct it to use the TOC to selectively read relevant diff sections, not the entire file
|
||||||
|
- instruct it to return the full summary markdown via \`${ghPullfrogMcpName}/set_output\`
|
||||||
|
3. After the subagent completes, call \`${ghPullfrogMcpName}/create_issue_comment\` with \`type: "Summary"\` and the summary body.
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
|
Use mini or auto effort.`,
|
||||||
|
|
||||||
|
SummaryUpdate: `### Checklist (updating existing summary)
|
||||||
|
|
||||||
|
An existing summary comment was found for this PR. Update it rather than creating a new one.
|
||||||
|
|
||||||
|
1. Use \`previousSummaryBody\` from this response as the current summary to revise.
|
||||||
|
2. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` — this returns PR metadata and a \`diffPath\`.
|
||||||
|
3. Delegate a subagent with:
|
||||||
|
- the diff file path and PR metadata
|
||||||
|
- the existing summary body (\`previousSummaryBody\`) so it can update rather than rewrite from scratch
|
||||||
|
- format instructions from EVENT INSTRUCTIONS (if any)
|
||||||
|
- instruct it to produce an updated summary reflecting the current state of the PR and return via \`${ghPullfrogMcpName}/set_output\`
|
||||||
|
4. After the subagent completes, call \`${ghPullfrogMcpName}/edit_issue_comment\` with \`commentId: existingSummaryCommentId\` (from this response) and the updated summary body.
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
|
Use mini or auto effort.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
type OrchestratorGuidance = {
|
type OrchestratorGuidance = {
|
||||||
@@ -218,6 +250,9 @@ function buildOrchestratorGuidance(mode: Mode, opts: BuildGuidanceOpts = {}): Or
|
|||||||
// matches the API response for /repo/[owner]/[repo]/issue/[issueNumber]/plan-comment
|
// matches the API response for /repo/[owner]/[repo]/issue/[issueNumber]/plan-comment
|
||||||
export type PlanCommentResponsePayload = { error: string } | { commentId: number; body: string };
|
export type PlanCommentResponsePayload = { error: string } | { commentId: number; body: string };
|
||||||
|
|
||||||
|
// matches the API response for /repo/[owner]/[repo]/pr/[prNumber]/summary-comment
|
||||||
|
export type SummaryCommentResponsePayload = { error: string } | { commentId: number; body: string };
|
||||||
|
|
||||||
async function fetchExistingPlanComment(
|
async function fetchExistingPlanComment(
|
||||||
ctx: ToolContext,
|
ctx: ToolContext,
|
||||||
issueNumber: number
|
issueNumber: number
|
||||||
@@ -237,6 +272,25 @@ async function fetchExistingPlanComment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchExistingSummaryComment(
|
||||||
|
ctx: ToolContext,
|
||||||
|
prNumber: number
|
||||||
|
): Promise<Extract<SummaryCommentResponsePayload, { commentId: number }> | null> {
|
||||||
|
if (!ctx.apiToken) return null;
|
||||||
|
try {
|
||||||
|
const response = await apiFetch({
|
||||||
|
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`,
|
||||||
|
method: "GET",
|
||||||
|
headers: { authorization: `Bearer ${ctx.apiToken}` },
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
const data = (await response.json()) as SummaryCommentResponsePayload;
|
||||||
|
return response.ok && "commentId" in data ? data : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function SelectModeTool(ctx: ToolContext) {
|
export function SelectModeTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "select_mode",
|
name: "select_mode",
|
||||||
@@ -287,6 +341,24 @@ export function SelectModeTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedMode.name === "Summarize") {
|
||||||
|
const prNumber = ctx.payload.event.issue_number;
|
||||||
|
if (prNumber !== undefined) {
|
||||||
|
const existing = await fetchExistingSummaryComment(ctx, prNumber);
|
||||||
|
if (existing !== null) {
|
||||||
|
ctx.toolState.existingSummaryCommentId = existing.commentId;
|
||||||
|
return {
|
||||||
|
...buildOrchestratorGuidance(selectedMode, {
|
||||||
|
...guidanceOpts,
|
||||||
|
overrideGuidance: modeGuidance.SummaryUpdate,
|
||||||
|
}),
|
||||||
|
existingSummaryCommentId: existing.commentId,
|
||||||
|
previousSummaryBody: existing.body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return buildOrchestratorGuidance(selectedMode, guidanceOpts);
|
return buildOrchestratorGuidance(selectedMode, guidanceOpts);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export interface ToolState {
|
|||||||
// set by select_mode when Plan + issue_number and plan-comment API returns existing plan (for report_progress target_plan_comment)
|
// set by select_mode when Plan + issue_number and plan-comment API returns existing plan (for report_progress target_plan_comment)
|
||||||
existingPlanCommentId?: number;
|
existingPlanCommentId?: number;
|
||||||
previousPlanBody?: string;
|
previousPlanBody?: string;
|
||||||
|
// set by select_mode when Summarize mode and summary-comment API returns existing summary
|
||||||
|
existingSummaryCommentId?: number;
|
||||||
output?: string;
|
output?: string;
|
||||||
usageEntries: AgentUsage[];
|
usageEntries: AgentUsage[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,6 +312,22 @@ Your job is to fix issues THIS PR introduced, not to fix all CI failures. If in
|
|||||||
|
|
||||||
Do NOT overwrite a good comment with links/details with a generic message like "I have completed the task." If your previous report_progress call already contains all the necessary information and links, skip the final call entirely.`,
|
Do NOT overwrite a good comment with links/details with a generic message like "I have completed the task." If your previous report_progress call already contains all the necessary information and links, skip the final call entirely.`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Summarize",
|
||||||
|
description:
|
||||||
|
"Summarize a PR with a structured comment that is updated in place on subsequent pushes",
|
||||||
|
prompt: `Follow these steps.
|
||||||
|
|
||||||
|
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number to get PR metadata and diffPath.
|
||||||
|
|
||||||
|
2. **ANALYZE** - Read the diff file. Use the TOC to selectively read relevant sections — do not read the entire file unless the PR is small.
|
||||||
|
|
||||||
|
3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with before/after framing.
|
||||||
|
|
||||||
|
4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body.
|
||||||
|
|
||||||
|
${permalinkTip}`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/pullfrog",
|
"name": "@pullfrog/pullfrog",
|
||||||
"version": "0.0.179",
|
"version": "0.0.180",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -41256,8 +41256,8 @@ var LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
|
|||||||
var Comment = type({
|
var Comment = type({
|
||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
body: type.string.describe("the comment body content"),
|
body: type.string.describe("the comment body content"),
|
||||||
type: type.enumerated("Plan", "Comment").describe(
|
type: type.enumerated("Plan", "Summary", "Comment").describe(
|
||||||
"Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)."
|
"Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)."
|
||||||
).optional()
|
).optional()
|
||||||
});
|
});
|
||||||
var EditComment = type({
|
var EditComment = type({
|
||||||
@@ -41284,7 +41284,7 @@ var core3 = __toESM(require_core(), 1);
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.179",
|
version: "0.0.180",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -216,10 +216,6 @@ export async function runAgentStreaming(options: RunStreamingOptions): Promise<A
|
|||||||
GITHUB_OUTPUT: githubOutputFile,
|
GITHUB_OUTPUT: githubOutputFile,
|
||||||
};
|
};
|
||||||
|
|
||||||
// clear CI runner's GITHUB_TOKEN so ensureGitHubToken() mints a
|
|
||||||
// properly scoped token for the target GITHUB_REPOSITORY via OIDC
|
|
||||||
delete subEnv.GITHUB_TOKEN;
|
|
||||||
|
|
||||||
const child = spawn("node", ["play.ts", "--raw", JSON.stringify(fixture)], {
|
const child = spawn("node", ["play.ts", "--raw", JSON.stringify(fixture)], {
|
||||||
cwd: actionDir,
|
cwd: actionDir,
|
||||||
env: subEnv as Record<string, string>,
|
env: subEnv as Record<string, string>,
|
||||||
|
|||||||
+28
-11
@@ -277,11 +277,17 @@ const findInstallationId = async (
|
|||||||
|
|
||||||
// for local development only
|
// for local development only
|
||||||
async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<string> {
|
async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<string> {
|
||||||
|
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
const config: GitHubAppConfig = {
|
const config: GitHubAppConfig = {
|
||||||
appId: process.env.GITHUB_APP_ID!,
|
appId: process.env.GITHUB_APP_ID,
|
||||||
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n")!,
|
privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
||||||
repoOwner: repoContext.owner,
|
repoOwner: repoContext.owner,
|
||||||
repoName: repoContext.name,
|
repoName: repoContext.name,
|
||||||
};
|
};
|
||||||
@@ -292,20 +298,31 @@ async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<str
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure a GitHub token is available in the environment.
|
* ensure a GitHub token is available in the environment.
|
||||||
*
|
*
|
||||||
* If neither `GITHUB_TOKEN` nor `GH_TOKEN` is set, attempts to acquire an
|
* when OIDC is available (CI), always mints a fresh token scoped to
|
||||||
* installation token using `GITHUB_APP_ID` / `GITHUB_PRIVATE_KEY`.
|
* GITHUB_REPOSITORY — overriding any inherited GITHUB_TOKEN that may
|
||||||
|
* be scoped to the wrong repo.
|
||||||
*
|
*
|
||||||
* **Not intended for production use** — this is a convenience for local
|
* otherwise falls back to GitHub App credentials for local development.
|
||||||
* development and test harnesses where tokens aren't pre-provisioned.
|
*
|
||||||
|
* only called from play.ts (test/dev path) — the live action calls
|
||||||
|
* main() directly and never calls this.
|
||||||
*/
|
*/
|
||||||
export async function ensureGitHubToken(): Promise<void> {
|
export async function ensureGitHubToken(): Promise<void> {
|
||||||
|
// when OIDC is available, always mint a fresh token scoped to
|
||||||
|
// GITHUB_REPOSITORY. the inherited GITHUB_TOKEN may be scoped to a
|
||||||
|
// different repo (e.g., runner token for pullfrog/app when tests
|
||||||
|
// target pullfrog/test-repo).
|
||||||
|
if (isOIDCAvailable()) {
|
||||||
|
const token = await acquireNewToken();
|
||||||
|
process.env.GITHUB_TOKEN = token;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
|
if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
|
||||||
if (isOIDCAvailable() || (process.env.GITHUB_APP_ID && process.env.GITHUB_PRIVATE_KEY)) {
|
const token = await acquireNewToken();
|
||||||
const token = await acquireNewToken();
|
process.env.GITHUB_TOKEN = token;
|
||||||
process.env.GITHUB_TOKEN = token;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user