Share GitHub rate limit tracking between the action and the worker (#326)
* share GitHub rate limit tracking between the action and the worker The action now counts all GitHub API requests and captures the latest `x-ratelimit-remaining`/`x-ratelimit-reset` headers via a global request hook on every Octokit instance. On exit, the usage summary is written atomically to a path specified by `PULLFROG_USAGE_SUMMARY_PATH`. The worker sets this env var before sandbox execution, reads the file afterward, and feeds the data into the Durable Object's rate limit state. This closes the visibility gap where the worker had no insight into API calls made by the sandboxed action process. * address review: refactor rate limit state, randomize usage summary path * track actual rate limit cost using x-ratelimit-remaining delta * refactor usage summary writing to use onExitSignal API Replace the monolithic registerUsageSummaryHandler with direct use of onExitSignal in main.ts and a writeGitHubUsageSummaryToFile utility in github.ts. This keeps exitHandler.ts as a pure signal handler registry (from #299) and also writes the summary on normal exit. * tweak * unify * deduplicate stuff * improve error handling --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
53970308ee
commit
cc46af0d47
@@ -18198,7 +18198,7 @@ var require_summary = __commonJS({
|
||||
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
|
||||
var os_1 = __require("os");
|
||||
var fs_1 = __require("fs");
|
||||
var { access, appendFile, writeFile } = fs_1.promises;
|
||||
var { access, appendFile, writeFile: writeFile2 } = fs_1.promises;
|
||||
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
|
||||
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
|
||||
var Summary = class {
|
||||
@@ -18256,7 +18256,7 @@ var require_summary = __commonJS({
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
|
||||
const filePath = yield this.filePath();
|
||||
const writeFunc = overwrite ? writeFile : appendFile;
|
||||
const writeFunc = overwrite ? writeFile2 : appendFile;
|
||||
yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
|
||||
return this.emptyBuffer();
|
||||
});
|
||||
@@ -26579,13 +26579,13 @@ var init_schemas = __esm({
|
||||
}
|
||||
return propValues;
|
||||
});
|
||||
const isObject4 = isObject;
|
||||
const isObject5 = isObject;
|
||||
const catchall = def.catchall;
|
||||
let value2;
|
||||
inst._zod.parse = (payload, ctx) => {
|
||||
value2 ?? (value2 = _normalized.value);
|
||||
const input = payload.value;
|
||||
if (!isObject4(input)) {
|
||||
if (!isObject5(input)) {
|
||||
payload.issues.push({
|
||||
expected: "object",
|
||||
code: "invalid_type",
|
||||
@@ -26683,7 +26683,7 @@ var init_schemas = __esm({
|
||||
return (payload, ctx) => fn2(shape, payload, ctx);
|
||||
};
|
||||
let fastpass;
|
||||
const isObject4 = isObject;
|
||||
const isObject5 = isObject;
|
||||
const jit = !globalConfig.jitless;
|
||||
const allowsEval3 = allowsEval;
|
||||
const fastEnabled = jit && allowsEval3.value;
|
||||
@@ -26692,7 +26692,7 @@ var init_schemas = __esm({
|
||||
inst._zod.parse = (payload, ctx) => {
|
||||
value2 ?? (value2 = _normalized.value);
|
||||
const input = payload.value;
|
||||
if (!isObject4(input)) {
|
||||
if (!isObject5(input)) {
|
||||
payload.issues.push({
|
||||
expected: "object",
|
||||
code: "invalid_type",
|
||||
@@ -56273,8 +56273,8 @@ var require_snapshot_utils = __commonJS({
|
||||
var require_snapshot_recorder = __commonJS({
|
||||
"node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
|
||||
"use strict";
|
||||
var { writeFile, readFile, mkdir } = __require("node:fs/promises");
|
||||
var { dirname: dirname3, resolve: resolve3 } = __require("node:path");
|
||||
var { writeFile: writeFile2, readFile, mkdir } = __require("node:fs/promises");
|
||||
var { dirname: dirname4, resolve: resolve3 } = __require("node:path");
|
||||
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
|
||||
var { InvalidArgumentError, UndiciError } = require_errors4();
|
||||
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
||||
@@ -56498,12 +56498,12 @@ var require_snapshot_recorder = __commonJS({
|
||||
throw new InvalidArgumentError("Snapshot path is required");
|
||||
}
|
||||
const resolvedPath = resolve3(path3);
|
||||
await mkdir(dirname3(resolvedPath), { recursive: true });
|
||||
await mkdir(dirname4(resolvedPath), { recursive: true });
|
||||
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
|
||||
hash: hash2,
|
||||
snapshot: snapshot2
|
||||
}));
|
||||
await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
|
||||
await writeFile2(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
|
||||
}
|
||||
/**
|
||||
* Clears all recorded snapshots
|
||||
@@ -72140,7 +72140,7 @@ var require_lodash = __commonJS({
|
||||
var symbolProto = Symbol2 ? Symbol2.prototype : void 0;
|
||||
var symbolToString = symbolProto ? symbolProto.toString : void 0;
|
||||
function baseIsRegExp(value2) {
|
||||
return isObject4(value2) && objectToString.call(value2) == regexpTag;
|
||||
return isObject5(value2) && objectToString.call(value2) == regexpTag;
|
||||
}
|
||||
function baseSlice(array3, start, end) {
|
||||
var index = -1, length = array3.length;
|
||||
@@ -72174,7 +72174,7 @@ var require_lodash = __commonJS({
|
||||
end = end === void 0 ? length : end;
|
||||
return !start && end >= length ? array3 : baseSlice(array3, start, end);
|
||||
}
|
||||
function isObject4(value2) {
|
||||
function isObject5(value2) {
|
||||
var type2 = typeof value2;
|
||||
return !!value2 && (type2 == "object" || type2 == "function");
|
||||
}
|
||||
@@ -72207,9 +72207,9 @@ var require_lodash = __commonJS({
|
||||
if (isSymbol(value2)) {
|
||||
return NAN;
|
||||
}
|
||||
if (isObject4(value2)) {
|
||||
if (isObject5(value2)) {
|
||||
var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2;
|
||||
value2 = isObject4(other) ? other + "" : other;
|
||||
value2 = isObject5(other) ? other + "" : other;
|
||||
}
|
||||
if (typeof value2 != "string") {
|
||||
return value2 === 0 ? value2 : +value2;
|
||||
@@ -72223,7 +72223,7 @@ var require_lodash = __commonJS({
|
||||
}
|
||||
function truncate(string6, options) {
|
||||
var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION;
|
||||
if (isObject4(options)) {
|
||||
if (isObject5(options)) {
|
||||
var separator2 = "separator" in options ? options.separator : separator2;
|
||||
length = "length" in options ? toInteger(options.length) : length;
|
||||
omission = "omission" in options ? baseToString2(options.omission) : omission;
|
||||
@@ -95647,14 +95647,14 @@ var require_turndown_cjs = __commonJS({
|
||||
} else if (node2.nodeType === 1) {
|
||||
replacement = replacementForNode.call(self2, node2);
|
||||
}
|
||||
return join17(output, replacement);
|
||||
return join18(output, replacement);
|
||||
}, "");
|
||||
}
|
||||
function postProcess(output) {
|
||||
var self2 = this;
|
||||
this.rules.forEach(function(rule) {
|
||||
if (typeof rule.append === "function") {
|
||||
output = join17(output, rule.append(self2.options));
|
||||
output = join18(output, rule.append(self2.options));
|
||||
}
|
||||
});
|
||||
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
||||
@@ -95666,7 +95666,7 @@ var require_turndown_cjs = __commonJS({
|
||||
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
||||
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
||||
}
|
||||
function join17(output, replacement) {
|
||||
function join18(output, replacement) {
|
||||
var s1 = trimTrailingNewlines(output);
|
||||
var s2 = trimLeadingNewlines(replacement);
|
||||
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
||||
@@ -97612,7 +97612,7 @@ var require_semver2 = __commonJS({
|
||||
|
||||
// entry.ts
|
||||
var core5 = __toESM(require_core(), 1);
|
||||
import { dirname as dirname2 } from "node:path";
|
||||
import { dirname as dirname3 } from "node:path";
|
||||
|
||||
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
||||
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
||||
@@ -136544,7 +136544,7 @@ function AskQuestionTool(ctx) {
|
||||
|
||||
// mcp/checkout.ts
|
||||
import { writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { join as join2 } from "node:path";
|
||||
import { join as join3 } from "node:path";
|
||||
|
||||
// utils/gitAuth.ts
|
||||
import { execSync as execSync2, spawnSync } from "node:child_process";
|
||||
@@ -136583,6 +136583,8 @@ function exitWithSignal(signal) {
|
||||
// utils/github.ts
|
||||
var core2 = __toESM(require_core(), 1);
|
||||
import { createSign } from "node:crypto";
|
||||
import { rename, writeFile } from "node:fs/promises";
|
||||
import { dirname, join as join2 } from "node:path";
|
||||
|
||||
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
||||
var import_light = __toESM(require_light(), 1);
|
||||
@@ -140291,6 +140293,9 @@ async function retry(fn2, options = {}) {
|
||||
}
|
||||
|
||||
// utils/github.ts
|
||||
function isObject4(value2) {
|
||||
return typeof value2 === "object" && value2 !== null;
|
||||
}
|
||||
function isOIDCAvailable() {
|
||||
return Boolean(
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
||||
@@ -140451,9 +140456,35 @@ function parseRepoContext() {
|
||||
}
|
||||
return { owner, name };
|
||||
}
|
||||
function emptyResourceUsage() {
|
||||
return {
|
||||
requestCount: 0,
|
||||
rateLimitRemaining: null,
|
||||
rateLimitResetMs: null
|
||||
};
|
||||
}
|
||||
var usageByResource = {
|
||||
core: emptyResourceUsage(),
|
||||
graphql: emptyResourceUsage()
|
||||
};
|
||||
function getGitHubUsageSummary() {
|
||||
return {
|
||||
version: 1,
|
||||
github: {
|
||||
core: usageByResource.core,
|
||||
graphql: usageByResource.graphql
|
||||
}
|
||||
};
|
||||
}
|
||||
async function writeGitHubUsageSummaryToFile(path3) {
|
||||
const summary2 = getGitHubUsageSummary();
|
||||
const tmpPath = join2(dirname(path3), `.usage-summary-${process.pid}.tmp`);
|
||||
await writeFile(tmpPath, JSON.stringify(summary2));
|
||||
await rename(tmpPath, path3);
|
||||
}
|
||||
function createOctokit(token) {
|
||||
const OctokitWithPlugins = Octokit2.plugin(throttling);
|
||||
return new OctokitWithPlugins({
|
||||
const octokit = new OctokitWithPlugins({
|
||||
auth: token,
|
||||
throttle: {
|
||||
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
|
||||
@@ -140464,6 +140495,37 @@ function createOctokit(token) {
|
||||
}
|
||||
}
|
||||
});
|
||||
const onResponse = (response) => {
|
||||
const resource = response.headers["x-ratelimit-resource"];
|
||||
if (!resource) {
|
||||
return response;
|
||||
}
|
||||
usageByResource[resource] ??= emptyResourceUsage();
|
||||
const usage = usageByResource[resource];
|
||||
usage.requestCount++;
|
||||
const remaining = response.headers["x-ratelimit-remaining"];
|
||||
const reset = response.headers["x-ratelimit-reset"];
|
||||
if (remaining !== void 0) {
|
||||
usage.rateLimitRemaining = Number(remaining);
|
||||
}
|
||||
if (reset !== void 0) {
|
||||
usage.rateLimitResetMs = Number(reset) * 1e3;
|
||||
}
|
||||
return response;
|
||||
};
|
||||
octokit.hook.wrap("request", async (request2, options) => {
|
||||
try {
|
||||
const response = await request2(options);
|
||||
onResponse(response);
|
||||
return response;
|
||||
} catch (error49) {
|
||||
if (isObject4(error49) && "response" in error49 && isObject4(error49.response) && "headers" in error49.response && isObject4(error49.response.headers)) {
|
||||
onResponse(error49.response);
|
||||
}
|
||||
throw error49;
|
||||
}
|
||||
});
|
||||
return octokit;
|
||||
}
|
||||
|
||||
// utils/token.ts
|
||||
@@ -141081,7 +141143,7 @@ ${diffPreview}`);
|
||||
"PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
|
||||
);
|
||||
}
|
||||
const diffPath = join2(tempDir, `pr-${pull_number}.diff`);
|
||||
const diffPath = join3(tempDir, `pr-${pull_number}.diff`);
|
||||
writeFileSync2(diffPath, formatResult.content);
|
||||
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
||||
return {
|
||||
@@ -141105,7 +141167,7 @@ ${diffPreview}`);
|
||||
|
||||
// mcp/checkSuite.ts
|
||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
||||
import { join as join3 } from "node:path";
|
||||
import { join as join4 } from "node:path";
|
||||
var GetCheckSuiteLogs = type({
|
||||
check_suite_id: type.number.describe("the id from check_suite.id")
|
||||
});
|
||||
@@ -141200,7 +141262,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
||||
if (!tempDir) {
|
||||
throw new Error("PULLFROG_TEMP_DIR not set");
|
||||
}
|
||||
const logsDir = join3(tempDir, "ci-logs");
|
||||
const logsDir = join4(tempDir, "ci-logs");
|
||||
mkdirSync2(logsDir, { recursive: true });
|
||||
const jobResults = [];
|
||||
for (const run2 of failedRuns) {
|
||||
@@ -141219,7 +141281,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
||||
});
|
||||
const logsUrl = logsResponse.url;
|
||||
const logsText = await fetch(logsUrl).then((r) => r.text());
|
||||
const logPath = join3(logsDir, `job-${job.id}.log`);
|
||||
const logPath = join4(logsDir, `job-${job.id}.log`);
|
||||
writeFileSync3(logPath, logsText);
|
||||
const analysis = analyzeLog(logsText, 80);
|
||||
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
|
||||
@@ -141556,7 +141618,7 @@ function ReplyToReviewCommentTool(ctx) {
|
||||
|
||||
// mcp/commitInfo.ts
|
||||
import { writeFileSync as writeFileSync4 } from "node:fs";
|
||||
import { join as join4 } from "node:path";
|
||||
import { join as join5 } from "node:path";
|
||||
var CommitInfo = type({
|
||||
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
|
||||
});
|
||||
@@ -141580,7 +141642,7 @@ function CommitInfoTool(ctx) {
|
||||
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
|
||||
);
|
||||
}
|
||||
const diffFile = join4(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
||||
const diffFile = join5(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
||||
writeFileSync4(diffFile, formatResult.content);
|
||||
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
||||
return {
|
||||
@@ -141688,7 +141750,7 @@ import { performance as performance4 } from "node:perf_hooks";
|
||||
|
||||
// prep/installNodeDependencies.ts
|
||||
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
||||
import { join as join5 } from "node:path";
|
||||
import { join as join6 } from "node:path";
|
||||
|
||||
// node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs
|
||||
function dashDashArg(agent2, agentCommand) {
|
||||
@@ -142000,7 +142062,7 @@ async function isCommandAvailable(command) {
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
function getPackageManagerFromPackageJson() {
|
||||
const packageJsonPath = join5(process.cwd(), "package.json");
|
||||
const packageJsonPath = join6(process.cwd(), "package.json");
|
||||
try {
|
||||
const content = readFileSync2(packageJsonPath, "utf-8");
|
||||
const pkg = JSON.parse(content);
|
||||
@@ -142031,7 +142093,7 @@ async function installPackageManager(name, installSpec) {
|
||||
return result.stderr || `failed to install ${name}`;
|
||||
}
|
||||
if (name === "deno") {
|
||||
const denoPath = join5(process.env.HOME || "", ".deno", "bin");
|
||||
const denoPath = join6(process.env.HOME || "", ".deno", "bin");
|
||||
process.env.PATH = `${denoPath}:${process.env.PATH}`;
|
||||
}
|
||||
log.info(`\xBB installed ${name}`);
|
||||
@@ -142040,7 +142102,7 @@ async function installPackageManager(name, installSpec) {
|
||||
var installNodeDependencies = {
|
||||
name: "installNodeDependencies",
|
||||
shouldRun: () => {
|
||||
const packageJsonPath = join5(process.cwd(), "package.json");
|
||||
const packageJsonPath = join6(process.cwd(), "package.json");
|
||||
return existsSync2(packageJsonPath);
|
||||
},
|
||||
run: async (options) => {
|
||||
@@ -142125,7 +142187,7 @@ ${errorMessage}`]
|
||||
|
||||
// prep/installPythonDependencies.ts
|
||||
import { existsSync as existsSync3 } from "node:fs";
|
||||
import { join as join6 } from "node:path";
|
||||
import { join as join7 } from "node:path";
|
||||
var PYTHON_CONFIGS = [
|
||||
{
|
||||
file: "requirements.txt",
|
||||
@@ -142197,11 +142259,11 @@ var installPythonDependencies = {
|
||||
return false;
|
||||
}
|
||||
const cwd = process.cwd();
|
||||
return PYTHON_CONFIGS.some((config3) => existsSync3(join6(cwd, config3.file)));
|
||||
return PYTHON_CONFIGS.some((config3) => existsSync3(join7(cwd, config3.file)));
|
||||
},
|
||||
run: async (options) => {
|
||||
const cwd = process.cwd();
|
||||
const config3 = PYTHON_CONFIGS.find((c) => existsSync3(join6(cwd, c.file)));
|
||||
const config3 = PYTHON_CONFIGS.find((c) => existsSync3(join7(cwd, c.file)));
|
||||
if (!config3) {
|
||||
return {
|
||||
language: "python",
|
||||
@@ -142446,7 +142508,7 @@ import {
|
||||
unlinkSync,
|
||||
writeFileSync as writeFileSync5
|
||||
} from "node:fs";
|
||||
import { dirname, join as join7, resolve } from "node:path";
|
||||
import { dirname as dirname2, join as join8, resolve } from "node:path";
|
||||
var FileReadParams = type({
|
||||
path: "string",
|
||||
"offset?": "number",
|
||||
@@ -142478,7 +142540,7 @@ function resolveReadPath(filePath) {
|
||||
}
|
||||
const home = process.env.HOME;
|
||||
if (home) {
|
||||
const cursorProjectsDir = join7(home, ".cursor", "projects");
|
||||
const cursorProjectsDir = join8(home, ".cursor", "projects");
|
||||
if (resolved.startsWith(cursorProjectsDir + "/")) {
|
||||
return resolved;
|
||||
}
|
||||
@@ -142505,9 +142567,9 @@ function resolveWritePath(filePath, shellPermission) {
|
||||
throw new Error(`path must be within the repository (symlink escape blocked): ${filePath}`);
|
||||
}
|
||||
} else {
|
||||
let ancestor = dirname(resolved);
|
||||
let ancestor = dirname2(resolved);
|
||||
while (!existsSync4(ancestor)) {
|
||||
const parent = dirname(ancestor);
|
||||
const parent = dirname2(ancestor);
|
||||
if (parent === ancestor) break;
|
||||
ancestor = parent;
|
||||
}
|
||||
@@ -142566,7 +142628,7 @@ function FileWriteTool(ctx) {
|
||||
parameters: FileWriteParams,
|
||||
execute: execute(async (params) => {
|
||||
const resolved = resolveWritePath(params.path, ctx.payload.shell);
|
||||
const dir = dirname(resolved);
|
||||
const dir = dirname2(resolved);
|
||||
mkdirSync3(dir, { recursive: true });
|
||||
writeFileSync5(resolved, params.content, "utf-8");
|
||||
return { path: params.path, written: true };
|
||||
@@ -143411,7 +143473,7 @@ async function reportReviewNodeId(ctx, reviewNodeId) {
|
||||
|
||||
// mcp/reviewComments.ts
|
||||
import { writeFileSync as writeFileSync6 } from "node:fs";
|
||||
import { join as join8 } from "node:path";
|
||||
import { join as join9 } from "node:path";
|
||||
var REVIEW_THREADS_QUERY = `
|
||||
query ($owner: String!, $name: String!, $prNumber: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
@@ -143781,7 +143843,7 @@ function GetReviewCommentsTool(ctx) {
|
||||
throw new Error("PULLFROG_TEMP_DIR not set");
|
||||
}
|
||||
const filename = `review-${params.review_id}-threads.md`;
|
||||
const commentsPath = join8(tempDir, filename);
|
||||
const commentsPath = join9(tempDir, filename);
|
||||
writeFileSync6(commentsPath, formatted.content);
|
||||
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
||||
return {
|
||||
@@ -144073,7 +144135,7 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
||||
import { randomUUID as randomUUID3 } from "node:crypto";
|
||||
import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs";
|
||||
import { userInfo } from "node:os";
|
||||
import { join as join9 } from "node:path";
|
||||
import { join as join10 } from "node:path";
|
||||
var ShellParams = type({
|
||||
command: "string",
|
||||
description: "string",
|
||||
@@ -144207,8 +144269,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
||||
if (params.background) {
|
||||
const tempDir = getTempDir();
|
||||
const handle = `bg-${randomUUID3().slice(0, 8)}`;
|
||||
const outputPath = join9(tempDir, `${handle}.log`);
|
||||
const pidPath = join9(tempDir, `${handle}.pid`);
|
||||
const outputPath = join10(tempDir, `${handle}.log`);
|
||||
const pidPath = join10(tempDir, `${handle}.pid`);
|
||||
const logFd = openSync(outputPath, "a");
|
||||
let proc2;
|
||||
try {
|
||||
@@ -144834,7 +144896,7 @@ var modes = computeModes();
|
||||
|
||||
// agents/claude.ts
|
||||
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
||||
import { join as join11 } from "node:path";
|
||||
import { join as join12 } from "node:path";
|
||||
|
||||
// package.json
|
||||
var package_default = {
|
||||
@@ -144932,13 +144994,13 @@ import { spawnSync as spawnSync4 } from "node:child_process";
|
||||
import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join as join10 } from "node:path";
|
||||
import { join as join11 } from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
async function installFromNpmTarball(params) {
|
||||
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set");
|
||||
const extractedDir = join10(tempDir, "package");
|
||||
const cliPath = join10(extractedDir, params.executablePath);
|
||||
const extractedDir = join11(tempDir, "package");
|
||||
const cliPath = join11(extractedDir, params.executablePath);
|
||||
if (existsSync5(cliPath)) {
|
||||
log.debug(`\xBB using cached binary at ${cliPath}`);
|
||||
return cliPath;
|
||||
@@ -144963,7 +145025,7 @@ async function installFromNpmTarball(params) {
|
||||
}
|
||||
}
|
||||
log.debug(`\xBB installing ${params.packageName}@${resolvedVersion}...`);
|
||||
const tarballPath = join10(tempDir, "package.tgz");
|
||||
const tarballPath = join11(tempDir, "package.tgz");
|
||||
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
|
||||
let tarballUrl;
|
||||
if (params.packageName.startsWith("@")) {
|
||||
@@ -145037,8 +145099,8 @@ async function fetchWithRetry(url4, headers, errorMessage) {
|
||||
}
|
||||
async function installFromGithub(params) {
|
||||
const pullfrogTemp = process.env.PULLFROG_TEMP_DIR;
|
||||
const installDir = pullfrogTemp ? join10(pullfrogTemp, `github-${params.owner}-${params.repo}`) : await mkdtemp(join10(tmpdir(), `${params.owner}-${params.repo}-github-`));
|
||||
const expectedCliPath = join10(installDir, params.executablePath ?? params.assetName ?? "asset");
|
||||
const installDir = pullfrogTemp ? join11(pullfrogTemp, `github-${params.owner}-${params.repo}`) : await mkdtemp(join11(tmpdir(), `${params.owner}-${params.repo}-github-`));
|
||||
const expectedCliPath = join11(installDir, params.executablePath ?? params.assetName ?? "asset");
|
||||
if (existsSync5(expectedCliPath)) {
|
||||
log.debug(`\xBB using cached binary at ${expectedCliPath}`);
|
||||
return expectedCliPath;
|
||||
@@ -145062,13 +145124,13 @@ async function installFromGithub(params) {
|
||||
mkdirSync4(installDir, { recursive: true });
|
||||
const urlPath = new URL(assetUrl).pathname;
|
||||
const fileName2 = urlPath.split("/").pop() || "asset";
|
||||
const downloadPath = join10(installDir, fileName2);
|
||||
const downloadPath = join11(installDir, fileName2);
|
||||
const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset");
|
||||
if (!assetResponse.body) throw new Error("Response body is null");
|
||||
const fileStream = createWriteStream(downloadPath);
|
||||
await pipeline(assetResponse.body, fileStream);
|
||||
log.debug(`\xBB downloaded asset to ${downloadPath}`);
|
||||
const cliPath = params.executablePath ? join10(installDir, params.executablePath) : downloadPath;
|
||||
const cliPath = params.executablePath ? join11(installDir, params.executablePath) : downloadPath;
|
||||
if (!existsSync5(cliPath)) {
|
||||
throw new Error(`Executable not found at ${cliPath}`);
|
||||
}
|
||||
@@ -145079,14 +145141,14 @@ async function installFromGithub(params) {
|
||||
async function installFromDirectTarball(params) {
|
||||
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set");
|
||||
const extractDir = join10(tempDir, "direct-package");
|
||||
const cliPath = join10(extractDir, params.executablePath);
|
||||
const extractDir = join11(tempDir, "direct-package");
|
||||
const cliPath = join11(extractDir, params.executablePath);
|
||||
if (existsSync5(cliPath)) {
|
||||
log.debug(`\xBB using cached binary at ${cliPath}`);
|
||||
return cliPath;
|
||||
}
|
||||
log.info(`\xBB downloading tarball from ${params.url}...`);
|
||||
const tarballPath = join10(tempDir, "direct-package.tgz");
|
||||
const tarballPath = join11(tempDir, "direct-package.tgz");
|
||||
const response = await fetchWithRetry(params.url, {}, "failed to download tarball");
|
||||
if (!response.body) throw new Error("response body is null");
|
||||
const fileStream = createWriteStream(tarballPath);
|
||||
@@ -145198,9 +145260,9 @@ function buildDisallowedTools(ctx) {
|
||||
return disallowed;
|
||||
}
|
||||
function writeMcpConfig(ctx) {
|
||||
const configDir = join11(ctx.tmpdir, ".claude");
|
||||
const configDir = join12(ctx.tmpdir, ".claude");
|
||||
mkdirSync5(configDir, { recursive: true });
|
||||
const configPath = join11(configDir, "mcp.json");
|
||||
const configPath = join12(configDir, "mcp.json");
|
||||
const mcpConfig = {
|
||||
mcpServers: {
|
||||
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
|
||||
@@ -145417,7 +145479,7 @@ var messageHandlers = {
|
||||
|
||||
// agents/codex.ts
|
||||
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
||||
import { join as join12 } from "node:path";
|
||||
import { join as join13 } from "node:path";
|
||||
var CODEX_CLI_VERSION = "0.101.0";
|
||||
var PREFERRED_MODEL = "gpt-5.3-codex";
|
||||
var FALLBACK_MODEL = "gpt-5.2-codex";
|
||||
@@ -145457,9 +145519,9 @@ async function resolveModel(apiKey) {
|
||||
return FALLBACK_MODEL;
|
||||
}
|
||||
function writeCodexConfig(ctx) {
|
||||
const codexDir = join12(ctx.tmpdir, ".codex");
|
||||
const codexDir = join13(ctx.tmpdir, ".codex");
|
||||
mkdirSync6(codexDir, { recursive: true });
|
||||
const configPath = join12(codexDir, "config.toml");
|
||||
const configPath = join13(codexDir, "config.toml");
|
||||
log.info(`\xBB adding MCP server '${ghPullfrogMcpName}' at ${ctx.mcpServerUrl}`);
|
||||
const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}]
|
||||
url = "${ctx.mcpServerUrl}"`];
|
||||
@@ -145711,7 +145773,7 @@ function createMessageHandlers() {
|
||||
import { spawn as spawn3 } from "node:child_process";
|
||||
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join as join13 } from "node:path";
|
||||
import { join as join14 } from "node:path";
|
||||
import { performance as performance6 } from "node:perf_hooks";
|
||||
var CURSOR_CLI_VERSION = "2026.01.28-fd13201";
|
||||
var cursorEffortModels = {
|
||||
@@ -145741,7 +145803,7 @@ var cursor = agent({
|
||||
const cliPath = await installCursor();
|
||||
configureCursorMcpServers(ctx);
|
||||
configureCursorTools(ctx);
|
||||
const projectCliConfigPath = join13(process.cwd(), ".cursor", "cli.json");
|
||||
const projectCliConfigPath = join14(process.cwd(), ".cursor", "cli.json");
|
||||
let modelOverride = null;
|
||||
if (existsSync6(projectCliConfigPath)) {
|
||||
try {
|
||||
@@ -145926,11 +145988,11 @@ var cursor = agent({
|
||||
}
|
||||
});
|
||||
function getCursorConfigDir() {
|
||||
return join13(homedir(), ".cursor");
|
||||
return join14(homedir(), ".cursor");
|
||||
}
|
||||
function configureCursorMcpServers(ctx) {
|
||||
const cursorConfigDir = getCursorConfigDir();
|
||||
const mcpConfigPath = join13(cursorConfigDir, "mcp.json");
|
||||
const mcpConfigPath = join14(cursorConfigDir, "mcp.json");
|
||||
mkdirSync7(cursorConfigDir, { recursive: true });
|
||||
const mcpServers = {
|
||||
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
|
||||
@@ -145940,7 +146002,7 @@ function configureCursorMcpServers(ctx) {
|
||||
}
|
||||
function configureCursorTools(ctx) {
|
||||
const cursorConfigDir = getCursorConfigDir();
|
||||
const cliConfigPath = join13(cursorConfigDir, "cli-config.json");
|
||||
const cliConfigPath = join14(cursorConfigDir, "cli-config.json");
|
||||
mkdirSync7(cursorConfigDir, { recursive: true });
|
||||
const shell = ctx.payload.shell;
|
||||
const deny = [];
|
||||
@@ -145969,7 +146031,7 @@ function configureCursorTools(ctx) {
|
||||
// agents/gemini.ts
|
||||
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync11 } from "node:fs";
|
||||
import { homedir as homedir2 } from "node:os";
|
||||
import { join as join14 } from "node:path";
|
||||
import { join as join15 } from "node:path";
|
||||
var geminiEffortConfig = {
|
||||
// https://ai.google.dev/gemini-api/docs/models
|
||||
// the docs mention needing to enable preview features for these models but if you
|
||||
@@ -146193,8 +146255,8 @@ function configureGeminiSettings(ctx) {
|
||||
const thinkingLevel = effortConfig.thinkingLevel;
|
||||
log.info(`\xBB model: ${model} (thinkingLevel: ${thinkingLevel})`);
|
||||
const realHome = homedir2();
|
||||
const geminiConfigDir = join14(realHome, ".gemini");
|
||||
const settingsPath = join14(geminiConfigDir, "settings.json");
|
||||
const geminiConfigDir = join15(realHome, ".gemini");
|
||||
const settingsPath = join15(geminiConfigDir, "settings.json");
|
||||
mkdirSync8(geminiConfigDir, { recursive: true });
|
||||
let existingSettings = {};
|
||||
try {
|
||||
@@ -146241,7 +146303,7 @@ function configureGeminiSettings(ctx) {
|
||||
|
||||
// agents/opencode.ts
|
||||
import { existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync12 } from "node:fs";
|
||||
import { join as join15 } from "node:path";
|
||||
import { join as join16 } from "node:path";
|
||||
import { performance as performance7 } from "node:perf_hooks";
|
||||
var OPENCODE_CLI_VERSION = "1.1.56";
|
||||
var PROVIDER_ERROR_PATTERNS = [
|
||||
@@ -146370,7 +146432,7 @@ var opencode = agent({
|
||||
run: async (ctx) => {
|
||||
const cliPath = await installOpencode();
|
||||
const tempHome = ctx.tmpdir;
|
||||
const configDir = join15(tempHome, ".config", "opencode");
|
||||
const configDir = join16(tempHome, ".config", "opencode");
|
||||
mkdirSync9(configDir, { recursive: true });
|
||||
configureOpenCode(ctx);
|
||||
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||
@@ -146388,7 +146450,7 @@ var opencode = agent({
|
||||
const env2 = {
|
||||
...process.env,
|
||||
HOME: tempHome,
|
||||
XDG_CONFIG_HOME: join15(tempHome, ".config"),
|
||||
XDG_CONFIG_HOME: join16(tempHome, ".config"),
|
||||
// set GOOGLE_GENERATIVE_AI_API_KEY alias for Google provider compatibility (if not already set)
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
||||
};
|
||||
@@ -146571,10 +146633,10 @@ ${stderrContext}`
|
||||
}
|
||||
});
|
||||
function configureOpenCode(ctx) {
|
||||
const configDir = join15(ctx.tmpdir, ".config", "opencode");
|
||||
const configDir = join16(ctx.tmpdir, ".config", "opencode");
|
||||
mkdirSync9(configDir, { recursive: true });
|
||||
const configPath = join15(configDir, "opencode.json");
|
||||
const repoConfigPath = join15(process.cwd(), "opencode.json");
|
||||
const configPath = join16(configDir, "opencode.json");
|
||||
const repoConfigPath = join16(process.cwd(), "opencode.json");
|
||||
const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath });
|
||||
if (repoConfig?.model) {
|
||||
log.info(`\xBB repo opencode model configured: ${repoConfig.model}`);
|
||||
@@ -147641,9 +147703,9 @@ async function resolveRunContextData(params) {
|
||||
import { execSync as execSync4 } from "node:child_process";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir as tmpdir2 } from "node:os";
|
||||
import { join as join16 } from "node:path";
|
||||
import { join as join17 } from "node:path";
|
||||
function createTempDirectory() {
|
||||
const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
|
||||
const sharedTempDir = mkdtempSync(join17(tmpdir2(), "pullfrog-"));
|
||||
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
||||
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
||||
return sharedTempDir;
|
||||
@@ -147768,6 +147830,10 @@ async function main() {
|
||||
var _stack2 = [];
|
||||
try {
|
||||
normalizeEnv();
|
||||
const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH;
|
||||
if (usageSummaryPath) {
|
||||
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
|
||||
}
|
||||
const timer = new Timer();
|
||||
let activityTimeout = null;
|
||||
const resolvedPromptInput = resolvePromptInput();
|
||||
@@ -147944,6 +148010,9 @@ ${instructions.user}` : null,
|
||||
};
|
||||
} finally {
|
||||
activityTimeout?.stop();
|
||||
if (usageSummaryPath) {
|
||||
await writeGitHubUsageSummaryToFile(usageSummaryPath);
|
||||
}
|
||||
}
|
||||
} catch (_2) {
|
||||
var _error2 = _2, _hasError2 = true;
|
||||
@@ -147954,7 +148023,7 @@ ${instructions.user}` : null,
|
||||
}
|
||||
|
||||
// entry.ts
|
||||
process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`;
|
||||
process.env.PATH = `${dirname3(process.execPath)}:${process.env.PATH}`;
|
||||
async function run() {
|
||||
try {
|
||||
const result = await main();
|
||||
|
||||
@@ -25699,8 +25699,8 @@ function formatJsonValue(value) {
|
||||
}
|
||||
|
||||
// utils/github.ts
|
||||
var core2 = __toESM(require_core(), 1);
|
||||
import { createSign } from "node:crypto";
|
||||
var core2 = __toESM(require_core(), 1);
|
||||
|
||||
// utils/apiUrl.ts
|
||||
function isLocalUrl(url) {
|
||||
@@ -25930,6 +25930,17 @@ function parseRepoContext() {
|
||||
}
|
||||
return { owner, name };
|
||||
}
|
||||
function emptyResourceUsage() {
|
||||
return {
|
||||
requestCount: 0,
|
||||
rateLimitRemaining: null,
|
||||
rateLimitResetMs: null
|
||||
};
|
||||
}
|
||||
var usageByResource = {
|
||||
core: emptyResourceUsage(),
|
||||
graphql: emptyResourceUsage()
|
||||
};
|
||||
|
||||
// utils/token.ts
|
||||
async function revokeGitHubInstallationToken(token) {
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export {
|
||||
PULLFROG_DIVIDER,
|
||||
stripExistingFooter,
|
||||
} from "../utils/buildPullfrogFooter.ts";
|
||||
|
||||
export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
|
||||
export {
|
||||
isValidTimeString,
|
||||
parseTimeString,
|
||||
|
||||
@@ -12,8 +12,9 @@ import { validateAgentApiKey } from "./utils/apiKeys.ts";
|
||||
import { resolveBody } from "./utils/body.ts";
|
||||
import { formatUsageSummary, log, writeSummary } from "./utils/cli.ts";
|
||||
import { reportErrorToComment } from "./utils/errorReport.ts";
|
||||
import { onExitSignal } from "./utils/exitHandler.ts";
|
||||
import { resolveGit } from "./utils/gitAuth.ts";
|
||||
import { createOctokit } from "./utils/github.ts";
|
||||
import { createOctokit, writeGitHubUsageSummaryToFile } from "./utils/github.ts";
|
||||
import { resolveInstructions } from "./utils/instructions.ts";
|
||||
import { executeLifecycleHook } from "./utils/lifecycle.ts";
|
||||
import { normalizeEnv } from "./utils/normalizeEnv.ts";
|
||||
@@ -48,6 +49,12 @@ export async function main(): Promise<MainResult> {
|
||||
// normalize env var names to uppercase (handles case-insensitive workflow files)
|
||||
normalizeEnv();
|
||||
|
||||
// write usage summary on SIGINT/SIGTERM so the worker can read it after sandbox.exec
|
||||
const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH;
|
||||
if (usageSummaryPath) {
|
||||
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
|
||||
}
|
||||
|
||||
const timer = new Timer();
|
||||
let activityTimeout: ActivityTimeout | null = null;
|
||||
|
||||
@@ -267,5 +274,8 @@ export async function main(): Promise<MainResult> {
|
||||
};
|
||||
} finally {
|
||||
activityTimeout?.stop();
|
||||
if (usageSummaryPath) {
|
||||
await writeGitHubUsageSummaryToFile(usageSummaryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24695,7 +24695,7 @@ var require_lodash = __commonJS({
|
||||
var symbolProto = Symbol2 ? Symbol2.prototype : void 0;
|
||||
var symbolToString = symbolProto ? symbolProto.toString : void 0;
|
||||
function baseIsRegExp(value2) {
|
||||
return isObject(value2) && objectToString.call(value2) == regexpTag;
|
||||
return isObject2(value2) && objectToString.call(value2) == regexpTag;
|
||||
}
|
||||
function baseSlice(array, start, end) {
|
||||
var index = -1, length = array.length;
|
||||
@@ -24729,7 +24729,7 @@ var require_lodash = __commonJS({
|
||||
end = end === void 0 ? length : end;
|
||||
return !start && end >= length ? array : baseSlice(array, start, end);
|
||||
}
|
||||
function isObject(value2) {
|
||||
function isObject2(value2) {
|
||||
var type2 = typeof value2;
|
||||
return !!value2 && (type2 == "object" || type2 == "function");
|
||||
}
|
||||
@@ -24762,9 +24762,9 @@ var require_lodash = __commonJS({
|
||||
if (isSymbol(value2)) {
|
||||
return NAN;
|
||||
}
|
||||
if (isObject(value2)) {
|
||||
if (isObject2(value2)) {
|
||||
var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2;
|
||||
value2 = isObject(other) ? other + "" : other;
|
||||
value2 = isObject2(other) ? other + "" : other;
|
||||
}
|
||||
if (typeof value2 != "string") {
|
||||
return value2 === 0 ? value2 : +value2;
|
||||
@@ -24778,7 +24778,7 @@ var require_lodash = __commonJS({
|
||||
}
|
||||
function truncate(string2, options) {
|
||||
var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION;
|
||||
if (isObject(options)) {
|
||||
if (isObject2(options)) {
|
||||
var separator2 = "separator" in options ? options.separator : separator2;
|
||||
length = "length" in options ? toInteger(options.length) : length;
|
||||
omission = "omission" in options ? baseToString(options.omission) : omission;
|
||||
@@ -41183,6 +41183,9 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
|
||||
);
|
||||
|
||||
// utils/github.ts
|
||||
function isObject(value2) {
|
||||
return typeof value2 === "object" && value2 !== null;
|
||||
}
|
||||
function parseRepoContext() {
|
||||
const githubRepo = process.env.GITHUB_REPOSITORY;
|
||||
if (!githubRepo) {
|
||||
@@ -41194,9 +41197,20 @@ function parseRepoContext() {
|
||||
}
|
||||
return { owner, name };
|
||||
}
|
||||
function emptyResourceUsage() {
|
||||
return {
|
||||
requestCount: 0,
|
||||
rateLimitRemaining: null,
|
||||
rateLimitResetMs: null
|
||||
};
|
||||
}
|
||||
var usageByResource = {
|
||||
core: emptyResourceUsage(),
|
||||
graphql: emptyResourceUsage()
|
||||
};
|
||||
function createOctokit(token) {
|
||||
const OctokitWithPlugins = Octokit2.plugin(throttling);
|
||||
return new OctokitWithPlugins({
|
||||
const octokit = new OctokitWithPlugins({
|
||||
auth: token,
|
||||
throttle: {
|
||||
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
|
||||
@@ -41207,6 +41221,37 @@ function createOctokit(token) {
|
||||
}
|
||||
}
|
||||
});
|
||||
const onResponse = (response) => {
|
||||
const resource = response.headers["x-ratelimit-resource"];
|
||||
if (!resource) {
|
||||
return response;
|
||||
}
|
||||
usageByResource[resource] ??= emptyResourceUsage();
|
||||
const usage = usageByResource[resource];
|
||||
usage.requestCount++;
|
||||
const remaining = response.headers["x-ratelimit-remaining"];
|
||||
const reset = response.headers["x-ratelimit-reset"];
|
||||
if (remaining !== void 0) {
|
||||
usage.rateLimitRemaining = Number(remaining);
|
||||
}
|
||||
if (reset !== void 0) {
|
||||
usage.rateLimitResetMs = Number(reset) * 1e3;
|
||||
}
|
||||
return response;
|
||||
};
|
||||
octokit.hook.wrap("request", async (request2, options) => {
|
||||
try {
|
||||
const response = await request2(options);
|
||||
onResponse(response);
|
||||
return response;
|
||||
} catch (error2) {
|
||||
if (isObject(error2) && "response" in error2 && isObject(error2.response) && "headers" in error2.response && isObject(error2.response.headers)) {
|
||||
onResponse(error2.response);
|
||||
}
|
||||
throw error2;
|
||||
}
|
||||
});
|
||||
return octokit;
|
||||
}
|
||||
|
||||
// mcp/comment.ts
|
||||
|
||||
+99
-1
@@ -1,10 +1,24 @@
|
||||
import { createSign } from "node:crypto";
|
||||
import { rename, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import { throttling } from "@octokit/plugin-throttling";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { apiFetch } from "./apiFetch.ts";
|
||||
import { retry } from "./retry.ts";
|
||||
|
||||
function isObject(value: unknown) {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
// we don't get access to the actual class from @octokit/rest
|
||||
// it's reachable from @octokit/request-error but we'd have to add a dependency on it
|
||||
// and it would pose a risk of accidentally pulling a different version of that class (node_modules dep graphs ❤️)
|
||||
// so it's safer to ducktype this
|
||||
interface OctokitResponseShim {
|
||||
headers: Record<string, string | number | undefined>;
|
||||
}
|
||||
|
||||
export interface InstallationToken {
|
||||
token: string;
|
||||
expires_at: string;
|
||||
@@ -339,11 +353,55 @@ export type OctokitWithPlugins = InstanceType<
|
||||
ReturnType<typeof Octokit.plugin<typeof Octokit, [typeof throttling]>>
|
||||
>;
|
||||
|
||||
export interface ResourceUsage {
|
||||
requestCount: number;
|
||||
rateLimitRemaining: number | null;
|
||||
rateLimitResetMs: number | null;
|
||||
}
|
||||
|
||||
function emptyResourceUsage(): ResourceUsage {
|
||||
return {
|
||||
requestCount: 0,
|
||||
rateLimitRemaining: null,
|
||||
rateLimitResetMs: null,
|
||||
};
|
||||
}
|
||||
|
||||
const usageByResource: Record<string, ResourceUsage> = {
|
||||
core: emptyResourceUsage(),
|
||||
graphql: emptyResourceUsage(),
|
||||
};
|
||||
|
||||
export interface UsageSummary {
|
||||
version: 1;
|
||||
github: {
|
||||
core: ResourceUsage;
|
||||
graphql: ResourceUsage;
|
||||
};
|
||||
}
|
||||
|
||||
function getGitHubUsageSummary(): UsageSummary {
|
||||
return {
|
||||
version: 1,
|
||||
github: {
|
||||
core: usageByResource.core,
|
||||
graphql: usageByResource.graphql,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeGitHubUsageSummaryToFile(path: string): Promise<void> {
|
||||
const summary = getGitHubUsageSummary();
|
||||
const tmpPath = join(dirname(path), `.usage-summary-${process.pid}.tmp`);
|
||||
await writeFile(tmpPath, JSON.stringify(summary));
|
||||
await rename(tmpPath, path);
|
||||
}
|
||||
|
||||
export function createOctokit(token: string): OctokitWithPlugins {
|
||||
// `OctokitWithPlugins` initialization based on https://github.com/actions/toolkit/blob/2506e78e82fbd2f9e94d63e75f5309118c8de1b1/packages/github/src/github.ts#L15-L22
|
||||
// we can't use it directly because it's stuck on `@octokit/core@v5` and we use the hottest `@octokit/core@v7`
|
||||
const OctokitWithPlugins = Octokit.plugin(throttling);
|
||||
return new OctokitWithPlugins({
|
||||
const octokit = new OctokitWithPlugins({
|
||||
auth: token,
|
||||
throttle: {
|
||||
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
|
||||
@@ -354,4 +412,44 @@ export function createOctokit(token: string): OctokitWithPlugins {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const onResponse = (response: OctokitResponseShim) => {
|
||||
const resource = response.headers["x-ratelimit-resource"];
|
||||
if (!resource) {
|
||||
return response;
|
||||
}
|
||||
usageByResource[resource] ??= emptyResourceUsage();
|
||||
const usage = usageByResource[resource];
|
||||
usage.requestCount++;
|
||||
const remaining = response.headers["x-ratelimit-remaining"];
|
||||
const reset = response.headers["x-ratelimit-reset"];
|
||||
if (remaining !== undefined) {
|
||||
usage.rateLimitRemaining = Number(remaining);
|
||||
}
|
||||
if (reset !== undefined) {
|
||||
usage.rateLimitResetMs = Number(reset) * 1000;
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
octokit.hook.wrap("request", async (request, options) => {
|
||||
try {
|
||||
const response = await request(options);
|
||||
onResponse(response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (
|
||||
isObject(error) &&
|
||||
"response" in error &&
|
||||
isObject(error.response) &&
|
||||
"headers" in error.response &&
|
||||
isObject(error.response.headers)
|
||||
) {
|
||||
onResponse(error.response as OctokitResponseShim);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
return octokit;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user