Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50f2678f55 | |||
| b748355cbe | |||
| c86752cf1d | |||
| a4c7c0fc15 | |||
| abdbdc7245 | |||
| 5393d3dab4 | |||
| 3c2f3722ff | |||
| 6541bdc4f4 | |||
| 1393ffb7b8 | |||
| f663d5e34d |
@@ -0,0 +1,35 @@
|
|||||||
|
name: Test get-installation-token
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-token:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get installation token
|
||||||
|
id: token
|
||||||
|
uses: pullfrog/pullfrog/get-installation-token@main
|
||||||
|
|
||||||
|
- name: Verify token with Node.js
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
node -e '
|
||||||
|
const res = await fetch("https://api.github.com/installation/repositories?per_page=1", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "token " + process.env.GITHUB_TOKEN,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("GET installation/repositories failed: " + res.status + " " + (await res.text()));
|
||||||
|
const data = await res.json();
|
||||||
|
console.log("authenticated — installation has access to", data.total_count, "repo(s)");
|
||||||
|
console.log("first repo:", data.repositories[0].full_name);
|
||||||
|
'
|
||||||
+19
-1
@@ -1,10 +1,13 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import { build } from "esbuild";
|
import { build } from "esbuild";
|
||||||
import { readFileSync, writeFileSync } from "fs";
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
|
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
|
||||||
|
|
||||||
|
rmSync("./dist", { recursive: true, force: true });
|
||||||
|
mkdirSync("./dist", { recursive: true });
|
||||||
|
|
||||||
// Plugin to strip shebangs from output files
|
// Plugin to strip shebangs from output files
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
@@ -73,6 +76,21 @@ await build({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build ESM library entrypoints for programmatic imports
|
||||||
|
await build({
|
||||||
|
...sharedConfig,
|
||||||
|
entryPoints: ["./index.ts"],
|
||||||
|
outfile: "./dist/index.js",
|
||||||
|
target: "node20",
|
||||||
|
});
|
||||||
|
|
||||||
|
await build({
|
||||||
|
...sharedConfig,
|
||||||
|
entryPoints: ["./internal/index.ts"],
|
||||||
|
outfile: "./dist/internal.js",
|
||||||
|
target: "node20",
|
||||||
|
});
|
||||||
|
|
||||||
// prepend shebang after strip (esbuild banner can't guarantee line 1 placement)
|
// prepend shebang after strip (esbuild banner can't guarantee line 1 placement)
|
||||||
const cliPath = "./dist/cli.mjs";
|
const cliPath = "./dist/cli.mjs";
|
||||||
const cliContent = readFileSync(cliPath, "utf8");
|
const cliContent = readFileSync(cliPath, "utf8");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { deleteProgressComment, reportProgress } from "./mcp/comment.ts";
|
import { deleteProgressComment, reportProgress } from "./mcp/comment.ts";
|
||||||
|
import { startInstallation } from "./mcp/dependencies.ts";
|
||||||
import {
|
import {
|
||||||
initToolState,
|
initToolState,
|
||||||
startMcpHttpServer,
|
startMcpHttpServer,
|
||||||
@@ -307,6 +308,8 @@ export async function main(): Promise<MainResult> {
|
|||||||
log.info(`» MCP server started at ${mcpHttpServer.url}`);
|
log.info(`» MCP server started at ${mcpHttpServer.url}`);
|
||||||
timer.checkpoint("mcpServer");
|
timer.checkpoint("mcpServer");
|
||||||
|
|
||||||
|
startInstallation(toolContext);
|
||||||
|
|
||||||
if (payload.model) log.info(`» model: ${payload.model}`);
|
if (payload.model) log.info(`» model: ${payload.model}`);
|
||||||
if (payload.timeout) log.info(`» timeout: ${payload.timeout}`);
|
if (payload.timeout) log.info(`» timeout: ${payload.timeout}`);
|
||||||
log.info(`» push: ${payload.push}`);
|
log.info(`» push: ${payload.push}`);
|
||||||
|
|||||||
+7
-44
@@ -1,49 +1,12 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { apiFetch } from "../utils/apiFetch.ts";
|
|
||||||
import { getApiUrl } from "../utils/apiUrl.ts";
|
import { getApiUrl } from "../utils/apiUrl.ts";
|
||||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
||||||
import { retry } from "../utils/retry.ts";
|
import { patchWorkflowRunFields } from "../utils/patchWorkflowRunFields.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";
|
||||||
|
|
||||||
type CommentNodeIdField = "planCommentNodeId" | "summaryCommentNodeId";
|
|
||||||
|
|
||||||
// IMPORTANT: this route authenticates via Pullfrog API JWT (verifyApiToken),
|
|
||||||
// NOT a GitHub token. use ctx.apiToken here. see wiki/api-auth.md.
|
|
||||||
export async function updateCommentNodeId(
|
|
||||||
ctx: ToolContext,
|
|
||||||
field: CommentNodeIdField,
|
|
||||||
nodeId: string
|
|
||||||
): Promise<void> {
|
|
||||||
if (ctx.runId === undefined || !ctx.apiToken) return;
|
|
||||||
try {
|
|
||||||
await retry(
|
|
||||||
async () => {
|
|
||||||
const response = await apiFetch({
|
|
||||||
path: `/api/workflow-run/${ctx.runId}`,
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${ctx.apiToken}`,
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ [field]: nodeId }),
|
|
||||||
signal: AbortSignal.timeout(10_000),
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
maxAttempts: 3,
|
|
||||||
delayMs: 2000,
|
|
||||||
label: `updateCommentNodeId(${field})`,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
log.warning(`updateCommentNodeId(${field}) exhausted retries: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The prefix text for the initial "leaping into action" comment.
|
* The prefix text for the initial "leaping into action" comment.
|
||||||
* This is used to identify if a comment is still in its initial state
|
* This is used to identify if a comment is still in its initial state
|
||||||
@@ -118,7 +81,7 @@ export function CreateCommentTool(ctx: ToolContext) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.data.node_id) {
|
if (result.data.node_id) {
|
||||||
await updateCommentNodeId(ctx, "summaryCommentNodeId", result.data.node_id);
|
await patchWorkflowRunFields(ctx, { summaryCommentNodeId: result.data.node_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -138,7 +101,7 @@ export function CreateCommentTool(ctx: ToolContext) {
|
|||||||
|
|
||||||
if (commentType === "Plan") {
|
if (commentType === "Plan") {
|
||||||
if (result.data.node_id) {
|
if (result.data.node_id) {
|
||||||
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
||||||
}
|
}
|
||||||
// add "Implement plan" link (needs comment ID, so create-then-update)
|
// add "Implement plan" link (needs comment ID, so create-then-update)
|
||||||
const customParts = [buildImplementPlanLink(ctx, issueNumber, result.data.id)];
|
const customParts = [buildImplementPlanLink(ctx, issueNumber, result.data.id)];
|
||||||
@@ -161,7 +124,7 @@ export function CreateCommentTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (commentType === "Summary" && result.data.node_id) {
|
if (commentType === "Summary" && result.data.node_id) {
|
||||||
await updateCommentNodeId(ctx, "summaryCommentNodeId", result.data.node_id);
|
await patchWorkflowRunFields(ctx, { summaryCommentNodeId: result.data.node_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -266,7 +229,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 updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -300,7 +263,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 updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -353,7 +316,7 @@ export async function reportProgress(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (updateResult.data.node_id) {
|
if (updateResult.data.node_id) {
|
||||||
await updateCommentNodeId(ctx, "planCommentNodeId", updateResult.data.node_id);
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: updateResult.data.node_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+3
-2
@@ -67,9 +67,10 @@ Inspect the repository structure to determine how dependencies should be install
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* start dependency installation in the background (non-blocking, idempotent)
|
* start dependency installation in the background (non-blocking, idempotent).
|
||||||
|
* called eagerly from main.ts at startup and also available via MCP tools.
|
||||||
*/
|
*/
|
||||||
function startInstallation(ctx: ToolContext): void {
|
export function startInstallation(ctx: ToolContext): void {
|
||||||
// already started or completed - do nothing
|
// already started or completed - do nothing
|
||||||
if (ctx.toolState.dependencyInstallation) {
|
if (ctx.toolState.dependencyInstallation) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+13
-5
@@ -1,5 +1,6 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
||||||
|
import { patchWorkflowRunFields } from "../utils/patchWorkflowRunFields.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";
|
||||||
|
|
||||||
@@ -21,16 +22,23 @@ export function IssueTool(ctx: ToolContext) {
|
|||||||
name: "create_issue",
|
name: "create_issue",
|
||||||
description: "Create a new GitHub issue",
|
description: "Create a new GitHub issue",
|
||||||
parameters: Issue,
|
parameters: Issue,
|
||||||
execute: execute(async ({ title, body, labels, assignees }) => {
|
execute: execute(async (params) => {
|
||||||
const result = await ctx.octokit.rest.issues.create({
|
const result = await ctx.octokit.rest.issues.create({
|
||||||
owner: ctx.repo.owner,
|
owner: ctx.repo.owner,
|
||||||
repo: ctx.repo.name,
|
repo: ctx.repo.name,
|
||||||
title: title,
|
title: params.title,
|
||||||
body: fixDoubleEscapedString(body),
|
body: fixDoubleEscapedString(params.body),
|
||||||
labels: labels ?? [],
|
labels: params.labels ?? [],
|
||||||
assignees: assignees ?? [],
|
assignees: params.assignees ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeId = result.data.node_id;
|
||||||
|
if (typeof nodeId === "string" && nodeId.length > 0) {
|
||||||
|
await patchWorkflowRunFields(ctx, {
|
||||||
|
issueNodeId: nodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
issueId: result.data.id,
|
issueId: result.data.id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type } from "arktype";
|
|||||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
||||||
|
import { patchWorkflowRunFields } from "../utils/patchWorkflowRunFields.ts";
|
||||||
import { $ } from "../utils/shell.ts";
|
import { $ } from "../utils/shell.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";
|
||||||
@@ -94,6 +95,12 @@ export function CreatePullRequestTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof result.data.node_id === "string" && result.data.node_id.length > 0) {
|
||||||
|
await patchWorkflowRunFields(ctx, {
|
||||||
|
prNodeId: result.data.node_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
pullRequestId: result.data.id,
|
pullRequestId: result.data.id,
|
||||||
|
|||||||
+7
-29
@@ -1,11 +1,11 @@
|
|||||||
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { formatMcpToolRef } from "../external.ts";
|
import { formatMcpToolRef } from "../external.ts";
|
||||||
import { apiFetch } from "../utils/apiFetch.ts";
|
|
||||||
import { getApiUrl } from "../utils/apiUrl.ts";
|
import { getApiUrl } from "../utils/apiUrl.ts";
|
||||||
import { buildPullfrogFooter } from "../utils/buildPullfrogFooter.ts";
|
import { buildPullfrogFooter } from "../utils/buildPullfrogFooter.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
||||||
|
import { patchWorkflowRunFields } from "../utils/patchWorkflowRunFields.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";
|
||||||
|
|
||||||
@@ -305,34 +305,12 @@ async function createAndSubmitWithFooter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* report the review node ID to the server so the WorkflowRun is marked as "review submitted".
|
* report the review node ID so the WorkflowRun is marked as "review submitted".
|
||||||
* exported for use in main.ts post-agent cleanup.
|
* exported for use in main.ts post-agent cleanup.
|
||||||
*/
|
*/
|
||||||
export async function reportReviewNodeId(ctx: ToolContext, reviewNodeId: string): Promise<void> {
|
export async function reportReviewNodeId(
|
||||||
for (let remaining = 2; remaining >= 0; remaining--) {
|
ctx: ToolContext,
|
||||||
try {
|
params: { nodeId: string }
|
||||||
const response = await apiFetch({
|
): Promise<void> {
|
||||||
path: `/api/workflow-run/${ctx.runId}`,
|
await patchWorkflowRunFields(ctx, { reviewNodeId: params.nodeId });
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${ctx.apiToken}`,
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ reviewNodeId }),
|
|
||||||
signal: AbortSignal.timeout(10_000),
|
|
||||||
});
|
|
||||||
if (response.ok) return;
|
|
||||||
if (remaining > 0) {
|
|
||||||
log.debug(`reportReviewNodeId got ${response.status}, retrying (${remaining} left)`);
|
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (remaining > 0) {
|
|
||||||
log.debug(`reportReviewNodeId failed, retrying (${remaining} left): ${error}`);
|
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
|
||||||
} else {
|
|
||||||
log.debug(`reportReviewNodeId exhausted retries: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ export function UploadFileTool(ctx: ToolContext) {
|
|||||||
return tool({
|
return tool({
|
||||||
name: "upload_file",
|
name: "upload_file",
|
||||||
description:
|
description:
|
||||||
"upload a file to get a permanent public URL. use for screenshots, artifacts, or any files you want to reference in PRs/comments. max 10MB, images/text/archives allowed.",
|
"upload a file to get a permanent public URL. use for screenshots, artifacts, or any files you want to reference in PRs/comments. max 10MB, images/text/archives allowed. when embedding uploaded images in comments or PR bodies, always use markdown image syntax: ",
|
||||||
parameters: UploadFileParams,
|
parameters: UploadFileParams,
|
||||||
execute: execute(async (params) => {
|
execute: execute(async (params) => {
|
||||||
// read file from disk eagerly on purpose to avoid its content being changed by the time it's uploaded
|
// read file from disk eagerly on purpose to avoid its content being changed by the time it's uploaded
|
||||||
|
|||||||
+5
-8
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pullfrog",
|
"name": "pullfrog",
|
||||||
"version": "0.0.197",
|
"version": "0.0.199",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"pullfrog": "dist/cli.mjs",
|
"pullfrog": "dist/cli.mjs",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "node esbuild.config.js",
|
"build": "node esbuild.config.js && tsc -p tsconfig.exports.json",
|
||||||
"check:entrypoints": "node scripts/check-entrypoint-imports.ts",
|
"check:entrypoints": "node scripts/check-entrypoint-imports.ts",
|
||||||
"play": "node play.ts",
|
"play": "node play.ts",
|
||||||
"runtest": "node test/run.ts",
|
"runtest": "node test/run.ts",
|
||||||
@@ -74,22 +74,19 @@
|
|||||||
"url": "https://github.com/pullfrog/pullfrog/issues"
|
"url": "https://github.com/pullfrog/pullfrog/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/pullfrog/pullfrog#readme",
|
"homepage": "https://github.com/pullfrog/pullfrog#readme",
|
||||||
"zshy": {
|
|
||||||
"exports": "./index.ts"
|
|
||||||
},
|
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"@pullfrog/source": "./index.ts",
|
"@pullfrog/source": "./index.ts",
|
||||||
"types": "./dist/index.d.cts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.cjs"
|
"default": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./internal": {
|
"./internal": {
|
||||||
"@pullfrog/source": "./internal/index.ts",
|
"@pullfrog/source": "./internal/index.ts",
|
||||||
"types": "./dist/internal.d.cts",
|
"types": "./dist/internal/index.d.ts",
|
||||||
"import": "./dist/internal.js",
|
"import": "./dist/internal.js",
|
||||||
"default": "./dist/internal.js"
|
"default": "./dist/internal.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ exports[`latest model per provider snapshot > matches snapshot 1`] = `
|
|||||||
"releaseDate": "2026-04-07",
|
"releaseDate": "2026-04-07",
|
||||||
},
|
},
|
||||||
"openrouter": {
|
"openrouter": {
|
||||||
"modelId": "z-ai/glm-5.1",
|
"modelId": "openrouter/elephant-alpha",
|
||||||
"releaseDate": "2026-04-07",
|
"releaseDate": "2026-04-13",
|
||||||
},
|
},
|
||||||
"xai": {
|
"xai": {
|
||||||
"modelId": "grok-4.20-multi-agent-0309",
|
"modelId": "grok-4.20-multi-agent-0309",
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declarationMap": false,
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["index.ts", "internal/index.ts"]
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export interface WorkflowRunFooterInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BuildPullfrogFooterParams {
|
export interface BuildPullfrogFooterParams {
|
||||||
/** add "Triggered by Pullfrog" link */
|
/** add "via Pullfrog" link */
|
||||||
triggeredBy?: boolean;
|
triggeredBy?: boolean;
|
||||||
/** add "View workflow run" link */
|
/** add "View workflow run" link */
|
||||||
workflowRun?: WorkflowRunFooterInfo | undefined;
|
workflowRun?: WorkflowRunFooterInfo | undefined;
|
||||||
@@ -52,7 +52,7 @@ export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.triggeredBy) {
|
if (params.triggeredBy) {
|
||||||
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
|
parts.push("via [Pullfrog](https://pullfrog.com)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.model) {
|
if (params.model) {
|
||||||
|
|||||||
@@ -153,9 +153,10 @@ ${ctx.userQuoted}`;
|
|||||||
|
|
||||||
const eventInstructions = ctx.payload.eventInstructions ?? "";
|
const eventInstructions = ctx.payload.eventInstructions ?? "";
|
||||||
if (eventInstructions) {
|
if (eventInstructions) {
|
||||||
|
const parts = [ctx.eventTitle, eventInstructions].filter(Boolean);
|
||||||
return `************* YOUR TASK *************
|
return `************* YOUR TASK *************
|
||||||
|
|
||||||
${eventInstructions}`;
|
${parts.join("\n\n")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { ToolContext } from "../mcp/server.ts";
|
||||||
|
import { apiFetch } from "./apiFetch.ts";
|
||||||
|
import { log } from "./cli.ts";
|
||||||
|
import { retry } from "./retry.ts";
|
||||||
|
|
||||||
|
/** Keys accepted by PATCH /api/workflow-run/[runId] — keep in sync with `ALLOWED_FIELDS` in `app/api/workflow-run/[runId]/route.ts`. */
|
||||||
|
export type WorkflowRunArtifactPatchKey =
|
||||||
|
| "prNodeId"
|
||||||
|
| "issueNodeId"
|
||||||
|
| "reviewNodeId"
|
||||||
|
| "planCommentNodeId"
|
||||||
|
| "summaryCommentNodeId";
|
||||||
|
|
||||||
|
export type WorkflowRunArtifactPatch = Partial<Record<WorkflowRunArtifactPatchKey, string>>;
|
||||||
|
|
||||||
|
const ARTIFACT_PATCH_KEYS: WorkflowRunArtifactPatchKey[] = [
|
||||||
|
"prNodeId",
|
||||||
|
"issueNodeId",
|
||||||
|
"reviewNodeId",
|
||||||
|
"planCommentNodeId",
|
||||||
|
"summaryCommentNodeId",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** PATCH workflow-run artifact fields (Pullfrog JWT, not GitHub). */
|
||||||
|
export async function patchWorkflowRunFields(
|
||||||
|
ctx: ToolContext,
|
||||||
|
fields: WorkflowRunArtifactPatch
|
||||||
|
): Promise<void> {
|
||||||
|
if (ctx.runId === undefined || !ctx.apiToken) return;
|
||||||
|
const body: Record<string, string> = {};
|
||||||
|
for (const key of ARTIFACT_PATCH_KEYS) {
|
||||||
|
const value = fields[key];
|
||||||
|
if (typeof value === "string" && value.length > 0) {
|
||||||
|
body[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(body).length === 0) return;
|
||||||
|
try {
|
||||||
|
await retry(
|
||||||
|
async () => {
|
||||||
|
const response = await apiFetch({
|
||||||
|
path: `/api/workflow-run/${ctx.runId}`,
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${ctx.apiToken}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
delayMs: 2000,
|
||||||
|
label: "patchWorkflowRunFields",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log.warning(`patchWorkflowRunFields exhausted retries: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function postReviewCleanup(ctx: ToolContext): Promise<void> {
|
|||||||
delete ctx.toolState.review;
|
delete ctx.toolState.review;
|
||||||
|
|
||||||
// mark review as submitted — unlocks webhook dedup for new pushes
|
// mark review as submitted — unlocks webhook dedup for new pushes
|
||||||
await bestEffort(() => reportReviewNodeId(ctx, review.nodeId), "reportReviewNodeId");
|
await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
|
||||||
|
|
||||||
// dispatch follow-up if PR HEAD moved past the reviewed commit
|
// dispatch follow-up if PR HEAD moved past the reviewed commit
|
||||||
if (review.reviewedSha) {
|
if (review.reviewedSha) {
|
||||||
|
|||||||
+10
-4
@@ -57,18 +57,24 @@ const defaultRunContext: RunContext = {
|
|||||||
export async function fetchRunContext(params: {
|
export async function fetchRunContext(params: {
|
||||||
token: string;
|
token: string;
|
||||||
repoContext: RepoContext;
|
repoContext: RepoContext;
|
||||||
|
oidcToken?: string | undefined;
|
||||||
}): Promise<RunContext> {
|
}): Promise<RunContext> {
|
||||||
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);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Authorization: `Bearer ${params.token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
if (params.oidcToken) {
|
||||||
|
headers["X-GitHub-OIDC-Token"] = params.oidcToken;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await apiFetch({
|
const response = await apiFetch({
|
||||||
path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
||||||
headers: {
|
headers,
|
||||||
Authorization: `Bearer ${params.token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
import type { Octokit } from "@octokit/rest";
|
import type { Octokit } from "@octokit/rest";
|
||||||
import packageJson from "../package.json" with { type: "json" };
|
import packageJson from "../package.json" with { type: "json" };
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
@@ -32,9 +33,16 @@ export async function resolveRunContextData(
|
|||||||
|
|
||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
|
let oidcToken: string | undefined;
|
||||||
|
try {
|
||||||
|
oidcToken = await core.getIDToken("pullfrog-api");
|
||||||
|
} catch {
|
||||||
|
// OIDC not available (local dev, non-actions environment, fork PRs)
|
||||||
|
}
|
||||||
|
|
||||||
const [repoResponse, runContext] = await Promise.all([
|
const [repoResponse, runContext] = await Promise.all([
|
||||||
params.octokit.repos.get({ owner: repoContext.owner, repo: repoContext.name }),
|
params.octokit.repos.get({ owner: repoContext.owner, repo: repoContext.name }),
|
||||||
fetchRunContext({ token: params.token, repoContext }),
|
fetchRunContext({ token: params.token, repoContext, oidcToken }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-39
@@ -1,10 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Secret detection and redaction utilities
|
* Secret detection and env filtering utilities
|
||||||
* Redacts actual secret values rather than using pattern matching
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getGitHubInstallationToken } from "./token.ts";
|
|
||||||
|
|
||||||
// patterns for sensitive env var names
|
// patterns for sensitive env var names
|
||||||
export const SENSITIVE_PATTERNS = [
|
export const SENSITIVE_PATTERNS = [
|
||||||
/_KEY$/i,
|
/_KEY$/i,
|
||||||
@@ -47,38 +44,3 @@ export function resolveEnv(mode: EnvMode | undefined): Record<string, string | u
|
|||||||
// custom env object - merge with restricted base
|
// custom env object - merge with restricted base
|
||||||
return { ...filterEnv(), ...mode };
|
return { ...filterEnv(), ...mode };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllSecrets(): string[] {
|
|
||||||
const secrets: string[] = [];
|
|
||||||
|
|
||||||
// collect all env var values matching SENSITIVE_PATTERNS
|
|
||||||
for (const [key, value] of Object.entries(process.env)) {
|
|
||||||
if (value && isSensitiveEnvName(key)) {
|
|
||||||
secrets.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add GitHub installation token (stored in memory, not in env)
|
|
||||||
try {
|
|
||||||
const token = getGitHubInstallationToken();
|
|
||||||
if (token) {
|
|
||||||
secrets.push(token);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// token not set yet, ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
return secrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function redactSecrets(content: string, secrets?: string[]): string {
|
|
||||||
const secretsToRedact = [...(secrets ?? []), ...getAllSecrets()];
|
|
||||||
let redacted = content;
|
|
||||||
for (const secret of secretsToRedact) {
|
|
||||||
if (secret) {
|
|
||||||
const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
redacted = redacted.replaceAll(new RegExp(escaped, "g"), "[REDACTED_SECRET]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return redacted;
|
|
||||||
}
|
|
||||||
|
|||||||
+29
-5
@@ -1,9 +1,19 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { resolve } from "node:path";
|
import { tmpdir } from "node:os";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
import { getDevDependencyVersion } from "./version.ts";
|
||||||
|
|
||||||
const skillsBin = resolve(import.meta.dirname, "../node_modules/.bin/skills");
|
const skillsVersion = getDevDependencyVersion("skills");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* install a skill globally via the `skills` CLI.
|
||||||
|
*
|
||||||
|
* runs `npx skills add <ref> --skill <name> -g` with `cwd` set to os tmpdir
|
||||||
|
* so npm doesn't walk up and find a project-level `.npmrc` with pnpm-specific
|
||||||
|
* settings (e.g. `public-hoist-pattern`) that break npx binary resolution.
|
||||||
|
* the `-g` flag writes to `$HOME/.agents/skills/` which is controlled by
|
||||||
|
* `params.env.HOME` (the fake HOME), so cwd has no effect on install location.
|
||||||
|
*/
|
||||||
export function addSkill(params: {
|
export function addSkill(params: {
|
||||||
ref: string;
|
ref: string;
|
||||||
skill: string;
|
skill: string;
|
||||||
@@ -11,9 +21,21 @@ export function addSkill(params: {
|
|||||||
agent: string;
|
agent: string;
|
||||||
}): void {
|
}): void {
|
||||||
const result = spawnSync(
|
const result = spawnSync(
|
||||||
skillsBin,
|
"npx",
|
||||||
["add", params.ref, "--skill", params.skill, "-g", "-a", params.agent, "-y"],
|
[
|
||||||
|
"-y",
|
||||||
|
`skills@${skillsVersion}`,
|
||||||
|
"add",
|
||||||
|
params.ref,
|
||||||
|
"--skill",
|
||||||
|
params.skill,
|
||||||
|
"-g",
|
||||||
|
"-a",
|
||||||
|
params.agent,
|
||||||
|
"-y",
|
||||||
|
],
|
||||||
{
|
{
|
||||||
|
cwd: tmpdir(),
|
||||||
env: { ...process.env, ...params.env },
|
env: { ...process.env, ...params.env },
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
@@ -22,6 +44,8 @@ export function addSkill(params: {
|
|||||||
if (result.status === 0) {
|
if (result.status === 0) {
|
||||||
log.info(`installed ${params.skill} skill (${params.agent})`);
|
log.info(`installed ${params.skill} skill (${params.agent})`);
|
||||||
} else {
|
} else {
|
||||||
log.info(`${params.skill} skill install failed: ${(result.stderr?.toString() || "").trim()}`);
|
const stderr = (result.stderr?.toString() || "").trim();
|
||||||
|
const errorMsg = result.error ? result.error.message : stderr;
|
||||||
|
log.info(`${params.skill} skill install failed: ${errorMsg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user