fix: replace domain-specific exit handler with generic signal handler registry (#299)
* fix: replace domain-specific exit handler with generic signal handler registry Rewrite exitHandler.ts as a generic, domain-agnostic exit signal module that exports onExitSignal(handler) returning a dispose function. - subprocess.ts now registers via onExitSignal instead of direct process.on(SIGINT/SIGTERM) calls - resolveTokens registers a signal handler that captures tokens by closure, fixing the race condition where the exit handler would read the wrong token after disposal - Remove setupExitHandler and runCleanup — domain cleanup is handled by post.ts + await using Co-authored-by: Cursor <cursoragent@cursor.com> * tweaks * simplify handler installation * extract to util * fix race in dispose * wrap dispose body in try/finally to ensure disposingRef always settles --------- Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
70f1c47a28
commit
ee100354da
@@ -148,11 +148,11 @@ var require_command = __commonJS({
|
|||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.issue = exports.issueCommand = void 0;
|
exports.issue = exports.issueCommand = void 0;
|
||||||
var os = __importStar(__require("os"));
|
var os2 = __importStar(__require("os"));
|
||||||
var utils_1 = require_utils();
|
var utils_1 = require_utils();
|
||||||
function issueCommand(command, properties, message) {
|
function issueCommand(command, properties, message) {
|
||||||
const cmd = new Command(command, properties, message);
|
const cmd = new Command(command, properties, message);
|
||||||
process.stdout.write(cmd.toString() + os.EOL);
|
process.stdout.write(cmd.toString() + os2.EOL);
|
||||||
}
|
}
|
||||||
exports.issueCommand = issueCommand;
|
exports.issueCommand = issueCommand;
|
||||||
function issue3(name, message = "") {
|
function issue3(name, message = "") {
|
||||||
@@ -236,7 +236,7 @@ var require_file_command = __commonJS({
|
|||||||
exports.prepareKeyValueMessage = exports.issueFileCommand = void 0;
|
exports.prepareKeyValueMessage = exports.issueFileCommand = void 0;
|
||||||
var crypto2 = __importStar(__require("crypto"));
|
var crypto2 = __importStar(__require("crypto"));
|
||||||
var fs3 = __importStar(__require("fs"));
|
var fs3 = __importStar(__require("fs"));
|
||||||
var os = __importStar(__require("os"));
|
var os2 = __importStar(__require("os"));
|
||||||
var utils_1 = require_utils();
|
var utils_1 = require_utils();
|
||||||
function issueFileCommand(command, message) {
|
function issueFileCommand(command, message) {
|
||||||
const filePath = process.env[`GITHUB_${command}`];
|
const filePath = process.env[`GITHUB_${command}`];
|
||||||
@@ -246,7 +246,7 @@ var require_file_command = __commonJS({
|
|||||||
if (!fs3.existsSync(filePath)) {
|
if (!fs3.existsSync(filePath)) {
|
||||||
throw new Error(`Missing file at path: ${filePath}`);
|
throw new Error(`Missing file at path: ${filePath}`);
|
||||||
}
|
}
|
||||||
fs3.appendFileSync(filePath, `${(0, utils_1.toCommandValue)(message)}${os.EOL}`, {
|
fs3.appendFileSync(filePath, `${(0, utils_1.toCommandValue)(message)}${os2.EOL}`, {
|
||||||
encoding: "utf8"
|
encoding: "utf8"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ var require_file_command = __commonJS({
|
|||||||
if (convertedValue.includes(delimiter)) {
|
if (convertedValue.includes(delimiter)) {
|
||||||
throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`);
|
throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`);
|
||||||
}
|
}
|
||||||
return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`;
|
return `${key}<<${delimiter}${os2.EOL}${convertedValue}${os2.EOL}${delimiter}`;
|
||||||
}
|
}
|
||||||
exports.prepareKeyValueMessage = prepareKeyValueMessage;
|
exports.prepareKeyValueMessage = prepareKeyValueMessage;
|
||||||
}
|
}
|
||||||
@@ -11280,7 +11280,7 @@ var require_RetryHandler = __commonJS({
|
|||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
var RetryHandler = class _RetryHandler {
|
var RetryHandler = class _RetryHandler {
|
||||||
constructor(opts, handlers) {
|
constructor(opts, handlers2) {
|
||||||
const { retryOptions, ...dispatchOpts } = opts;
|
const { retryOptions, ...dispatchOpts } = opts;
|
||||||
const {
|
const {
|
||||||
// Retry scoped
|
// Retry scoped
|
||||||
@@ -11295,8 +11295,8 @@ var require_RetryHandler = __commonJS({
|
|||||||
retryAfter,
|
retryAfter,
|
||||||
statusCodes
|
statusCodes
|
||||||
} = retryOptions ?? {};
|
} = retryOptions ?? {};
|
||||||
this.dispatch = handlers.dispatch;
|
this.dispatch = handlers2.dispatch;
|
||||||
this.handler = handlers.handler;
|
this.handler = handlers2.handler;
|
||||||
this.opts = dispatchOpts;
|
this.opts = dispatchOpts;
|
||||||
this.abort = null;
|
this.abort = null;
|
||||||
this.aborted = false;
|
this.aborted = false;
|
||||||
@@ -17513,7 +17513,7 @@ var require_lib = __commonJS({
|
|||||||
}
|
}
|
||||||
exports.isHttps = isHttps;
|
exports.isHttps = isHttps;
|
||||||
var HttpClient = class {
|
var HttpClient = class {
|
||||||
constructor(userAgent2, handlers, requestOptions) {
|
constructor(userAgent2, handlers2, requestOptions) {
|
||||||
this._ignoreSslError = false;
|
this._ignoreSslError = false;
|
||||||
this._allowRedirects = true;
|
this._allowRedirects = true;
|
||||||
this._allowRedirectDowngrade = false;
|
this._allowRedirectDowngrade = false;
|
||||||
@@ -17523,7 +17523,7 @@ var require_lib = __commonJS({
|
|||||||
this._keepAlive = false;
|
this._keepAlive = false;
|
||||||
this._disposed = false;
|
this._disposed = false;
|
||||||
this.userAgent = userAgent2;
|
this.userAgent = userAgent2;
|
||||||
this.handlers = handlers || [];
|
this.handlers = handlers2 || [];
|
||||||
this.requestOptions = requestOptions;
|
this.requestOptions = requestOptions;
|
||||||
if (requestOptions) {
|
if (requestOptions) {
|
||||||
if (requestOptions.ignoreSslError != null) {
|
if (requestOptions.ignoreSslError != null) {
|
||||||
@@ -18983,7 +18983,7 @@ var require_toolrunner = __commonJS({
|
|||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.argStringToArray = exports.ToolRunner = void 0;
|
exports.argStringToArray = exports.ToolRunner = void 0;
|
||||||
var os = __importStar(__require("os"));
|
var os2 = __importStar(__require("os"));
|
||||||
var events = __importStar(__require("events"));
|
var events = __importStar(__require("events"));
|
||||||
var child = __importStar(__require("child_process"));
|
var child = __importStar(__require("child_process"));
|
||||||
var path3 = __importStar(__require("path"));
|
var path3 = __importStar(__require("path"));
|
||||||
@@ -19038,12 +19038,12 @@ var require_toolrunner = __commonJS({
|
|||||||
_processLineBuffer(data, strBuffer, onLine) {
|
_processLineBuffer(data, strBuffer, onLine) {
|
||||||
try {
|
try {
|
||||||
let s = strBuffer + data.toString();
|
let s = strBuffer + data.toString();
|
||||||
let n = s.indexOf(os.EOL);
|
let n = s.indexOf(os2.EOL);
|
||||||
while (n > -1) {
|
while (n > -1) {
|
||||||
const line = s.substring(0, n);
|
const line = s.substring(0, n);
|
||||||
onLine(line);
|
onLine(line);
|
||||||
s = s.substring(n + os.EOL.length);
|
s = s.substring(n + os2.EOL.length);
|
||||||
n = s.indexOf(os.EOL);
|
n = s.indexOf(os2.EOL);
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -19212,7 +19212,7 @@ var require_toolrunner = __commonJS({
|
|||||||
}
|
}
|
||||||
const optionsNonNull = this._cloneExecOptions(this.options);
|
const optionsNonNull = this._cloneExecOptions(this.options);
|
||||||
if (!optionsNonNull.silent && optionsNonNull.outStream) {
|
if (!optionsNonNull.silent && optionsNonNull.outStream) {
|
||||||
optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os.EOL);
|
optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os2.EOL);
|
||||||
}
|
}
|
||||||
const state = new ExecState(optionsNonNull, this.toolPath);
|
const state = new ExecState(optionsNonNull, this.toolPath);
|
||||||
state.on("debug", (message) => {
|
state.on("debug", (message) => {
|
||||||
@@ -19700,7 +19700,7 @@ var require_core = __commonJS({
|
|||||||
var command_1 = require_command();
|
var command_1 = require_command();
|
||||||
var file_command_1 = require_file_command();
|
var file_command_1 = require_file_command();
|
||||||
var utils_1 = require_utils();
|
var utils_1 = require_utils();
|
||||||
var os = __importStar(__require("os"));
|
var os2 = __importStar(__require("os"));
|
||||||
var path3 = __importStar(__require("path"));
|
var path3 = __importStar(__require("path"));
|
||||||
var oidc_utils_1 = require_oidc_utils();
|
var oidc_utils_1 = require_oidc_utils();
|
||||||
var ExitCode;
|
var ExitCode;
|
||||||
@@ -19768,7 +19768,7 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
|||||||
if (filePath) {
|
if (filePath) {
|
||||||
return (0, file_command_1.issueFileCommand)("OUTPUT", (0, file_command_1.prepareKeyValueMessage)(name, value2));
|
return (0, file_command_1.issueFileCommand)("OUTPUT", (0, file_command_1.prepareKeyValueMessage)(name, value2));
|
||||||
}
|
}
|
||||||
process.stdout.write(os.EOL);
|
process.stdout.write(os2.EOL);
|
||||||
(0, command_1.issueCommand)("set-output", { name }, (0, utils_1.toCommandValue)(value2));
|
(0, command_1.issueCommand)("set-output", { name }, (0, utils_1.toCommandValue)(value2));
|
||||||
}
|
}
|
||||||
exports.setOutput = setOutput2;
|
exports.setOutput = setOutput2;
|
||||||
@@ -19802,7 +19802,7 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
|||||||
}
|
}
|
||||||
exports.notice = notice;
|
exports.notice = notice;
|
||||||
function info2(message) {
|
function info2(message) {
|
||||||
process.stdout.write(message + os.EOL);
|
process.stdout.write(message + os2.EOL);
|
||||||
}
|
}
|
||||||
exports.info = info2;
|
exports.info = info2;
|
||||||
function startGroup3(name) {
|
function startGroup3(name) {
|
||||||
@@ -135779,6 +135779,31 @@ import { join } from "node:path";
|
|||||||
var core3 = __toESM(require_core(), 1);
|
var core3 = __toESM(require_core(), 1);
|
||||||
import assert2 from "node:assert/strict";
|
import assert2 from "node:assert/strict";
|
||||||
|
|
||||||
|
// utils/exitHandler.ts
|
||||||
|
import os from "node:os";
|
||||||
|
var handlers = /* @__PURE__ */ new Set();
|
||||||
|
var installed = false;
|
||||||
|
function onExitSignal(handler2) {
|
||||||
|
installSignalHandlers();
|
||||||
|
handlers.add(handler2);
|
||||||
|
return () => {
|
||||||
|
handlers.delete(handler2);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function installSignalHandlers() {
|
||||||
|
if (installed) return;
|
||||||
|
installed = true;
|
||||||
|
async function handleSignal(signal) {
|
||||||
|
await Promise.allSettled([...handlers].map((h) => Promise.try(h, signal)));
|
||||||
|
exitWithSignal(signal);
|
||||||
|
}
|
||||||
|
process.on("SIGINT", handleSignal);
|
||||||
|
process.on("SIGTERM", handleSignal);
|
||||||
|
}
|
||||||
|
function exitWithSignal(signal) {
|
||||||
|
process.exit(128 + os.constants.signals[signal]);
|
||||||
|
}
|
||||||
|
|
||||||
// utils/github.ts
|
// utils/github.ts
|
||||||
var core2 = __toESM(require_core(), 1);
|
var core2 = __toESM(require_core(), 1);
|
||||||
import { createSign } from "node:crypto";
|
import { createSign } from "node:crypto";
|
||||||
@@ -139728,18 +139753,31 @@ async function resolveTokens(params) {
|
|||||||
log.info("\xBB acquired full MCP token");
|
log.info("\xBB acquired full MCP token");
|
||||||
const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", mcpToken);
|
const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", mcpToken);
|
||||||
mcpTokenValue = mcpToken;
|
mcpTokenValue = mcpToken;
|
||||||
return {
|
let disposingRef;
|
||||||
gitToken,
|
const dispose = async () => {
|
||||||
mcpToken,
|
if (disposingRef) {
|
||||||
async [Symbol.asyncDispose]() {
|
return disposingRef.promise;
|
||||||
|
}
|
||||||
|
disposingRef = Promise.withResolvers();
|
||||||
|
try {
|
||||||
mcpTokenValue = void 0;
|
mcpTokenValue = void 0;
|
||||||
revertGithubToken();
|
revertGithubToken();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
revokeGitHubInstallationToken(gitToken),
|
revokeGitHubInstallationToken(gitToken),
|
||||||
revokeGitHubInstallationToken(mcpToken)
|
revokeGitHubInstallationToken(mcpToken)
|
||||||
]);
|
]);
|
||||||
|
} finally {
|
||||||
|
removeSignalHandler();
|
||||||
|
disposingRef.resolve();
|
||||||
|
disposingRef = void 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const removeSignalHandler = onExitSignal(dispose);
|
||||||
|
return {
|
||||||
|
gitToken,
|
||||||
|
mcpToken,
|
||||||
|
[Symbol.asyncDispose]: dispose
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function getGitHubInstallationToken() {
|
function getGitHubInstallationToken() {
|
||||||
assert2(mcpTokenValue, "tokens not set. call resolveTokens first.");
|
assert2(mcpTokenValue, "tokens not set. call resolveTokens first.");
|
||||||
@@ -140647,13 +140685,13 @@ function createProcessOutputActivityTimeout(ctx) {
|
|||||||
var activeChildren = /* @__PURE__ */ new Map();
|
var activeChildren = /* @__PURE__ */ new Map();
|
||||||
var externalSignalHandler = null;
|
var externalSignalHandler = null;
|
||||||
function trackChild(options) {
|
function trackChild(options) {
|
||||||
|
installSignalHandler();
|
||||||
activeChildren.set(options.child, options.killGroup ?? false);
|
activeChildren.set(options.child, options.killGroup ?? false);
|
||||||
}
|
}
|
||||||
function untrackChild(child) {
|
function untrackChild(child) {
|
||||||
activeChildren.delete(child);
|
activeChildren.delete(child);
|
||||||
}
|
}
|
||||||
function killTrackedChildren() {
|
function killTrackedChildren() {
|
||||||
const count = activeChildren.size;
|
|
||||||
for (const entry of activeChildren) {
|
for (const entry of activeChildren) {
|
||||||
const child = entry[0];
|
const child = entry[0];
|
||||||
const killGroup = entry[1];
|
const killGroup = entry[1];
|
||||||
@@ -140666,34 +140704,27 @@ function killTrackedChildren() {
|
|||||||
}
|
}
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
}
|
}
|
||||||
return count;
|
|
||||||
}
|
|
||||||
function cleanupAndExit(signal) {
|
|
||||||
const count = killTrackedChildren();
|
|
||||||
if (count > 0) {
|
|
||||||
log.info(`\xBB received ${signal}, killing ${count} subprocess(es)...`);
|
|
||||||
}
|
|
||||||
setTimeout(() => process.exit(1), 500).unref();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
function handleSignal(signal) {
|
|
||||||
if (externalSignalHandler) {
|
|
||||||
externalSignalHandler(signal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cleanupAndExit(signal);
|
|
||||||
}
|
}
|
||||||
var handlersInstalled = false;
|
var handlersInstalled = false;
|
||||||
function installSignalHandlers() {
|
function installSignalHandler() {
|
||||||
if (handlersInstalled) return;
|
if (handlersInstalled) return;
|
||||||
handlersInstalled = true;
|
handlersInstalled = true;
|
||||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
onExitSignal((signal) => {
|
||||||
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
if (externalSignalHandler) {
|
||||||
|
externalSignalHandler(signal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const count = activeChildren.size;
|
||||||
|
if (count > 0) {
|
||||||
|
log.info(`\xBB received ${signal}, killing ${count} subprocess(es)...`);
|
||||||
|
}
|
||||||
|
killTrackedChildren();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
async function spawn2(options) {
|
async function spawn2(options) {
|
||||||
const { cmd, args: args2, env: env2, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
const { cmd, args: args2, env: env2, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
||||||
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
||||||
installSignalHandlers();
|
installSignalHandler();
|
||||||
const startTime = performance3.now();
|
const startTime = performance3.now();
|
||||||
let stdoutBuffer = "";
|
let stdoutBuffer = "";
|
||||||
let stderrBuffer = "";
|
let stderrBuffer = "";
|
||||||
@@ -141302,7 +141333,6 @@ function stripExistingFooter(body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mcp/comment.ts
|
// mcp/comment.ts
|
||||||
var LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
|
|
||||||
async function buildCommentFooter({
|
async function buildCommentFooter({
|
||||||
agent: agent2,
|
agent: agent2,
|
||||||
octokit,
|
octokit,
|
||||||
@@ -145378,10 +145408,10 @@ var cursorEffortModels = {
|
|||||||
max: "opus-4.5-thinking"
|
max: "opus-4.5-thinking"
|
||||||
};
|
};
|
||||||
async function installCursor() {
|
async function installCursor() {
|
||||||
const os = process.platform === "darwin" ? "darwin" : "linux";
|
const os2 = process.platform === "darwin" ? "darwin" : "linux";
|
||||||
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
||||||
return await installFromDirectTarball({
|
return await installFromDirectTarball({
|
||||||
url: `https://downloads.cursor.com/lab/${CURSOR_CLI_VERSION}/${os}/${arch}/agent-cli-package.tar.gz`,
|
url: `https://downloads.cursor.com/lab/${CURSOR_CLI_VERSION}/${os2}/${arch}/agent-cli-package.tar.gz`,
|
||||||
executablePath: "cursor-agent",
|
executablePath: "cursor-agent",
|
||||||
stripComponents: 1
|
stripComponents: 1
|
||||||
});
|
});
|
||||||
@@ -146495,83 +146525,6 @@ ${ctx.error}` : ctx.error;
|
|||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils/exitHandler.ts
|
|
||||||
function buildErrorCommentBody(params) {
|
|
||||||
const workflowRunLink = params.runId ? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})` : "workflow run logs";
|
|
||||||
const errorMessage = params.isCancellation ? `This run was cancelled \u{1F6D1}
|
|
||||||
|
|
||||||
The workflow was cancelled before completion. Please check the ${workflowRunLink} for details.` : `This run croaked \u{1F635}
|
|
||||||
|
|
||||||
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
|
||||||
const footer = buildPullfrogFooter({
|
|
||||||
triggeredBy: true,
|
|
||||||
workflowRun: params.runId ? { owner: params.owner, repo: params.repo, runId: params.runId } : void 0
|
|
||||||
});
|
|
||||||
return `${errorMessage}${footer}`;
|
|
||||||
}
|
|
||||||
var cleanupFn;
|
|
||||||
function setupExitHandler(toolState) {
|
|
||||||
let hasCleanedUp = false;
|
|
||||||
async function cleanup(isCancellation) {
|
|
||||||
if (hasCleanedUp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hasCleanedUp = true;
|
|
||||||
const token = process.env.GITHUB_TOKEN;
|
|
||||||
const commentId = toolState.progressCommentId;
|
|
||||||
const wasUpdated = toolState.wasUpdated === true;
|
|
||||||
if (token && commentId && !wasUpdated) {
|
|
||||||
try {
|
|
||||||
const repoContext = parseRepoContext();
|
|
||||||
const octokit = createOctokit(token);
|
|
||||||
const existingComment = await octokit.rest.issues.getComment({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
comment_id: commentId
|
|
||||||
});
|
|
||||||
const commentBody = existingComment.data.body || "";
|
|
||||||
if (commentBody.startsWith(LEAPING_INTO_ACTION_PREFIX)) {
|
|
||||||
const runId = process.env.GITHUB_RUN_ID;
|
|
||||||
const body = buildErrorCommentBody({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
runId,
|
|
||||||
isCancellation
|
|
||||||
});
|
|
||||||
await octokit.rest.issues.updateComment({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
comment_id: commentId,
|
|
||||||
body
|
|
||||||
});
|
|
||||||
log.info("\xBB updated progress comment with error message");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (token) {
|
|
||||||
try {
|
|
||||||
await revokeGitHubInstallationToken(token);
|
|
||||||
log.debug("\xBB installation token revoked");
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cleanupFn = cleanup;
|
|
||||||
function handleSignal2() {
|
|
||||||
log.info("\xBB workflow cancelled, cleaning up...");
|
|
||||||
cleanup(true).finally(() => process.exit(1));
|
|
||||||
}
|
|
||||||
process.on("SIGINT", handleSignal2);
|
|
||||||
process.on("SIGTERM", handleSignal2);
|
|
||||||
}
|
|
||||||
async function runCleanup() {
|
|
||||||
try {
|
|
||||||
await cleanupFn?.(false);
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// utils/normalizeEnv.ts
|
// utils/normalizeEnv.ts
|
||||||
function maskValue(value2) {
|
function maskValue(value2) {
|
||||||
if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
|
if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
|
||||||
@@ -146981,7 +146934,6 @@ async function main() {
|
|||||||
const toolState = initToolState({
|
const toolState = initToolState({
|
||||||
progressCommentId: typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : void 0
|
progressCommentId: typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : void 0
|
||||||
});
|
});
|
||||||
setupExitHandler(toolState);
|
|
||||||
resolveGit();
|
resolveGit();
|
||||||
const jobToken = getJobToken();
|
const jobToken = getJobToken();
|
||||||
const initialOctokit = createOctokit(jobToken);
|
const initialOctokit = createOctokit(jobToken);
|
||||||
@@ -147141,9 +147093,7 @@ ${instructions.user}` : null,
|
|||||||
error: errorMessage
|
error: errorMessage
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
if (activityTimeout) {
|
activityTimeout?.stop();
|
||||||
activityTimeout.stop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (_2) {
|
} catch (_2) {
|
||||||
var _error2 = _2, _hasError2 = true;
|
var _error2 = _2, _hasError2 = true;
|
||||||
@@ -147166,8 +147116,6 @@ async function run() {
|
|||||||
} catch (error49) {
|
} catch (error49) {
|
||||||
const errorMessage = error49 instanceof Error ? error49.message : "Unknown error occurred";
|
const errorMessage = error49 instanceof Error ? error49.message : "Unknown error occurred";
|
||||||
core5.setFailed(`Action failed: ${errorMessage}`);
|
core5.setFailed(`Action failed: ${errorMessage}`);
|
||||||
} finally {
|
|
||||||
await runCleanup();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await run();
|
await run();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { main } from "./main.ts";
|
import { main } from "./main.ts";
|
||||||
import { runCleanup } from "./utils/exitHandler.ts";
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -22,8 +21,6 @@ async function run(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
core.setFailed(`Action failed: ${errorMessage}`);
|
core.setFailed(`Action failed: ${errorMessage}`);
|
||||||
} finally {
|
|
||||||
await runCleanup();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { validateAgentApiKey } from "./utils/apiKeys.ts";
|
|||||||
import { resolveBody } from "./utils/body.ts";
|
import { resolveBody } from "./utils/body.ts";
|
||||||
import { log, writeSummary } from "./utils/cli.ts";
|
import { log, writeSummary } from "./utils/cli.ts";
|
||||||
import { reportErrorToComment } from "./utils/errorReport.ts";
|
import { reportErrorToComment } from "./utils/errorReport.ts";
|
||||||
import { setupExitHandler } from "./utils/exitHandler.ts";
|
|
||||||
import { resolveGit } from "./utils/gitAuth.ts";
|
import { resolveGit } from "./utils/gitAuth.ts";
|
||||||
import { createOctokit } from "./utils/github.ts";
|
import { createOctokit } from "./utils/github.ts";
|
||||||
import { resolveInstructions } from "./utils/instructions.ts";
|
import { resolveInstructions } from "./utils/instructions.ts";
|
||||||
@@ -52,8 +51,6 @@ export async function main(): Promise<MainResult> {
|
|||||||
typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : undefined,
|
typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
setupExitHandler(toolState);
|
|
||||||
|
|
||||||
// resolve and fingerprint git binary before any agent code runs
|
// resolve and fingerprint git binary before any agent code runs
|
||||||
resolveGit();
|
resolveGit();
|
||||||
|
|
||||||
@@ -247,8 +244,6 @@ export async function main(): Promise<MainResult> {
|
|||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
if (activityTimeout) {
|
activityTimeout?.stop();
|
||||||
activityTimeout.stop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41198,37 +41198,8 @@ var ReplyToReviewComment = type({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
// utils/token.ts
|
|
||||||
var core3 = __toESM(require_core(), 1);
|
|
||||||
function getJobToken() {
|
|
||||||
const inputToken = core3.getInput("token");
|
|
||||||
if (inputToken) {
|
|
||||||
return inputToken;
|
|
||||||
}
|
|
||||||
const fallbackToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
||||||
if (fallbackToken) {
|
|
||||||
return fallbackToken;
|
|
||||||
}
|
|
||||||
throw new Error("token input is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// utils/exitHandler.ts
|
|
||||||
function buildErrorCommentBody(params) {
|
|
||||||
const workflowRunLink = params.runId ? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})` : "workflow run logs";
|
|
||||||
const errorMessage = params.isCancellation ? `This run was cancelled \u{1F6D1}
|
|
||||||
|
|
||||||
The workflow was cancelled before completion. Please check the ${workflowRunLink} for details.` : `This run croaked \u{1F635}
|
|
||||||
|
|
||||||
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
|
||||||
const footer = buildPullfrogFooter({
|
|
||||||
triggeredBy: true,
|
|
||||||
workflowRun: params.runId ? { owner: params.owner, repo: params.repo, runId: params.runId } : void 0
|
|
||||||
});
|
|
||||||
return `${errorMessage}${footer}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// utils/payload.ts
|
// utils/payload.ts
|
||||||
var core4 = __toESM(require_core(), 1);
|
var core3 = __toESM(require_core(), 1);
|
||||||
|
|
||||||
// external.ts
|
// external.ts
|
||||||
var agentsManifest = {
|
var agentsManifest = {
|
||||||
@@ -41399,7 +41370,7 @@ var Inputs = type({
|
|||||||
"cwd?": type.string.or("undefined")
|
"cwd?": type.string.or("undefined")
|
||||||
});
|
});
|
||||||
function resolvePromptInput() {
|
function resolvePromptInput() {
|
||||||
const prompt = core4.getInput("prompt", { required: true });
|
const prompt = core3.getInput("prompt", { required: true });
|
||||||
let parsed2;
|
let parsed2;
|
||||||
try {
|
try {
|
||||||
parsed2 = JSON.parse(prompt);
|
parsed2 = JSON.parse(prompt);
|
||||||
@@ -41414,8 +41385,35 @@ function resolvePromptInput() {
|
|||||||
return jsonPayload;
|
return jsonPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// utils/token.ts
|
||||||
|
var core4 = __toESM(require_core(), 1);
|
||||||
|
function getJobToken() {
|
||||||
|
const inputToken = core4.getInput("token");
|
||||||
|
if (inputToken) {
|
||||||
|
return inputToken;
|
||||||
|
}
|
||||||
|
const fallbackToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
||||||
|
if (fallbackToken) {
|
||||||
|
return fallbackToken;
|
||||||
|
}
|
||||||
|
throw new Error("token input is required");
|
||||||
|
}
|
||||||
|
|
||||||
// utils/postCleanup.ts
|
// utils/postCleanup.ts
|
||||||
var SHOULD_CHECK_REASON = true;
|
var SHOULD_CHECK_REASON = true;
|
||||||
|
function buildErrorCommentBody(params) {
|
||||||
|
const workflowRunLink = params.runId ? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})` : "workflow run logs";
|
||||||
|
const errorMessage = params.isCancellation ? `This run was cancelled \u{1F6D1}
|
||||||
|
|
||||||
|
The workflow was cancelled before completion. Please check the ${workflowRunLink} for details.` : `This run croaked \u{1F635}
|
||||||
|
|
||||||
|
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
||||||
|
const footer = buildPullfrogFooter({
|
||||||
|
triggeredBy: true,
|
||||||
|
workflowRun: params.runId ? { owner: params.owner, repo: params.repo, runId: params.runId } : void 0
|
||||||
|
});
|
||||||
|
return `${errorMessage}${footer}`;
|
||||||
|
}
|
||||||
async function validateStuckProgressComment(promptInput, octokit, owner, repo) {
|
async function validateStuckProgressComment(promptInput, octokit, owner, repo) {
|
||||||
if (!promptInput?.progressCommentId) {
|
if (!promptInput?.progressCommentId) {
|
||||||
log.info("[post] no progressCommentId in prompt input, skipping cleanup");
|
log.info("[post] no progressCommentId in prompt input, skipping cleanup");
|
||||||
|
|||||||
+1
-6
@@ -5,11 +5,7 @@ import { config } from "dotenv";
|
|||||||
import { runInDocker } from "../utils/docker.ts";
|
import { runInDocker } from "../utils/docker.ts";
|
||||||
import { ensureGitHubToken } from "../utils/github.ts";
|
import { ensureGitHubToken } from "../utils/github.ts";
|
||||||
import { isInsideDocker } from "../utils/globals.ts";
|
import { isInsideDocker } from "../utils/globals.ts";
|
||||||
import {
|
import { killTrackedChildren, setSignalHandler } from "../utils/subprocess.ts";
|
||||||
installSignalHandlers,
|
|
||||||
killTrackedChildren,
|
|
||||||
setSignalHandler,
|
|
||||||
} from "../utils/subprocess.ts";
|
|
||||||
import {
|
import {
|
||||||
type AgentResult,
|
type AgentResult,
|
||||||
agents,
|
agents,
|
||||||
@@ -482,7 +478,6 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSignalHandler(handleCancel);
|
setSignalHandler(handleCancel);
|
||||||
installSignalHandlers();
|
|
||||||
|
|
||||||
// run tests with limited concurrency to avoid overwhelming agent APIs
|
// run tests with limited concurrency to avoid overwhelming agent APIs
|
||||||
const maxConcurrency = 5;
|
const maxConcurrency = 5;
|
||||||
|
|||||||
+1
-3
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { agentsManifest } from "../external.ts";
|
import { agentsManifest } from "../external.ts";
|
||||||
import type { Inputs } from "../main.ts";
|
import type { Inputs } from "../main.ts";
|
||||||
import { installSignalHandlers, trackChild, untrackChild } from "../utils/subprocess.ts";
|
import { trackChild, untrackChild } from "../utils/subprocess.ts";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -165,8 +165,6 @@ const DEFAULT_TEST_TIMEOUT = "10m";
|
|||||||
// run agent and stream output with prefix labels
|
// run agent and stream output with prefix labels
|
||||||
// note: activity timeout is enforced in action main and subprocess utils
|
// note: activity timeout is enforced in action main and subprocess utils
|
||||||
export async function runAgentStreaming(options: RunStreamingOptions): Promise<AgentResult> {
|
export async function runAgentStreaming(options: RunStreamingOptions): Promise<AgentResult> {
|
||||||
installSignalHandlers();
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
const prefix = getPrefix({ test: options.test, agent: options.agent });
|
const prefix = getPrefix({ test: options.test, agent: options.agent });
|
||||||
|
|||||||
+22
-106
@@ -1,119 +1,35 @@
|
|||||||
import { LEAPING_INTO_ACTION_PREFIX } from "../mcp/comment.ts";
|
import os from "node:os";
|
||||||
import type { ToolState } from "../mcp/server.ts";
|
|
||||||
import { buildPullfrogFooter } from "./buildPullfrogFooter.ts";
|
type ExitSignalHandler = (signal: "SIGINT" | "SIGTERM") => void | Promise<void>;
|
||||||
import { log } from "./cli.ts";
|
|
||||||
import { createOctokit, parseRepoContext } from "./github.ts";
|
const handlers = new Set<ExitSignalHandler>();
|
||||||
import { revokeGitHubInstallationToken } from "./token.ts";
|
let installed = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build error comment body with error message and footer
|
* Register a handler to run when the process receives SIGINT or SIGTERM.
|
||||||
|
* Returns a dispose function that removes the handler.
|
||||||
*/
|
*/
|
||||||
export function buildErrorCommentBody(params: {
|
export function onExitSignal(handler: ExitSignalHandler): () => void {
|
||||||
owner: string;
|
installSignalHandlers();
|
||||||
repo: string;
|
handlers.add(handler);
|
||||||
runId: string | undefined;
|
return () => {
|
||||||
isCancellation: boolean;
|
handlers.delete(handler);
|
||||||
}): string {
|
};
|
||||||
const workflowRunLink = params.runId
|
|
||||||
? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})`
|
|
||||||
: "workflow run logs";
|
|
||||||
const errorMessage = params.isCancellation
|
|
||||||
? `This run was cancelled 🛑\n\nThe workflow was cancelled before completion. Please check the ${workflowRunLink} for details.`
|
|
||||||
: `This run croaked 😵\n\nThe workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
|
||||||
const footer = buildPullfrogFooter({
|
|
||||||
triggeredBy: true,
|
|
||||||
workflowRun: params.runId
|
|
||||||
? { owner: params.owner, repo: params.repo, runId: params.runId }
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
return `${errorMessage}${footer}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleanupFn: ((isCancellation: boolean) => Promise<void>) | undefined;
|
function installSignalHandlers(): void {
|
||||||
|
if (installed) return;
|
||||||
|
installed = true;
|
||||||
|
|
||||||
export function setupExitHandler(toolState: ToolState): void {
|
async function handleSignal(signal: "SIGINT" | "SIGTERM") {
|
||||||
let hasCleanedUp = false;
|
await Promise.allSettled([...handlers].map((h) => Promise.try(h, signal)));
|
||||||
|
exitWithSignal(signal);
|
||||||
async function cleanup(isCancellation: boolean): Promise<void> {
|
|
||||||
if (hasCleanedUp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hasCleanedUp = true;
|
|
||||||
|
|
||||||
const token = process.env.GITHUB_TOKEN;
|
|
||||||
const commentId = toolState.progressCommentId;
|
|
||||||
const wasUpdated = toolState.wasUpdated === true;
|
|
||||||
|
|
||||||
// update progress comment if it was never updated (still shows "leaping into action")
|
|
||||||
if (token && commentId && !wasUpdated) {
|
|
||||||
try {
|
|
||||||
const repoContext = parseRepoContext();
|
|
||||||
const octokit = createOctokit(token);
|
|
||||||
|
|
||||||
const existingComment = await octokit.rest.issues.getComment({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
comment_id: commentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const commentBody = existingComment.data.body || "";
|
|
||||||
|
|
||||||
// only update if comment still shows the initial "leaping into action" message
|
|
||||||
if (commentBody.startsWith(LEAPING_INTO_ACTION_PREFIX)) {
|
|
||||||
const runId = process.env.GITHUB_RUN_ID;
|
|
||||||
|
|
||||||
const body = buildErrorCommentBody({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
runId,
|
|
||||||
isCancellation,
|
|
||||||
});
|
|
||||||
|
|
||||||
await octokit.rest.issues.updateComment({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
comment_id: commentId,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info("» updated progress comment with error message");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore errors during cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// revoke token
|
|
||||||
if (token) {
|
|
||||||
try {
|
|
||||||
await revokeGitHubInstallationToken(token);
|
|
||||||
log.debug("» installation token revoked");
|
|
||||||
} catch {
|
|
||||||
// ignore errors during cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// store cleanup function for runCleanup()
|
|
||||||
cleanupFn = cleanup;
|
|
||||||
|
|
||||||
// handle cancellation signals
|
|
||||||
function handleSignal(): void {
|
|
||||||
log.info("» workflow cancelled, cleaning up...");
|
|
||||||
cleanup(true).finally(() => process.exit(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("SIGINT", handleSignal);
|
process.on("SIGINT", handleSignal);
|
||||||
process.on("SIGTERM", handleSignal);
|
process.on("SIGTERM", handleSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function exitWithSignal(signal: "SIGINT" | "SIGTERM") {
|
||||||
* Run cleanup explicitly. Called from entry.ts in finally block.
|
process.exit(128 + os.constants.signals[signal]);
|
||||||
*/
|
|
||||||
export async function runCleanup(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await cleanupFn?.(false);
|
|
||||||
} catch {
|
|
||||||
// ignore errors during cleanup
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -1,6 +1,6 @@
|
|||||||
import { LEAPING_INTO_ACTION_PREFIX } from "../mcp/comment.ts";
|
import { LEAPING_INTO_ACTION_PREFIX } from "../mcp/comment.ts";
|
||||||
|
import { buildPullfrogFooter } from "./buildPullfrogFooter.ts";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
import { buildErrorCommentBody } from "./exitHandler.ts";
|
|
||||||
import { createOctokit, parseRepoContext } from "./github.ts";
|
import { createOctokit, parseRepoContext } from "./github.ts";
|
||||||
import { type ResolvedPromptInput, resolvePromptInput } from "./payload.ts";
|
import { type ResolvedPromptInput, resolvePromptInput } from "./payload.ts";
|
||||||
import { getJobToken } from "./token.ts";
|
import { getJobToken } from "./token.ts";
|
||||||
@@ -14,6 +14,30 @@ type JsonPromptInput = Extract<ResolvedPromptInput, object>; // not string
|
|||||||
* */
|
* */
|
||||||
const SHOULD_CHECK_REASON = true;
|
const SHOULD_CHECK_REASON = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build error comment body with error message and footer
|
||||||
|
*/
|
||||||
|
function buildErrorCommentBody(params: {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
runId: string | undefined;
|
||||||
|
isCancellation: boolean;
|
||||||
|
}): string {
|
||||||
|
const workflowRunLink = params.runId
|
||||||
|
? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})`
|
||||||
|
: "workflow run logs";
|
||||||
|
const errorMessage = params.isCancellation
|
||||||
|
? `This run was cancelled 🛑\n\nThe workflow was cancelled before completion. Please check the ${workflowRunLink} for details.`
|
||||||
|
: `This run croaked 😵\n\nThe workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
||||||
|
const footer = buildPullfrogFooter({
|
||||||
|
triggeredBy: true,
|
||||||
|
workflowRun: params.runId
|
||||||
|
? { owner: params.owner, repo: params.repo, runId: params.runId }
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
return `${errorMessage}${footer}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that the progress comment is stuck on "Leaping into action"
|
* Validate that the progress comment is stuck on "Leaping into action"
|
||||||
* Fetches the comment by ID and checks if it starts with LEAPING_INTO_ACTION_PREFIX
|
* Fetches the comment by ID and checks if it starts with LEAPING_INTO_ACTION_PREFIX
|
||||||
|
|||||||
+18
-26
@@ -2,6 +2,7 @@ import { type ChildProcess, spawn as nodeSpawn } from "node:child_process";
|
|||||||
import { performance } from "node:perf_hooks";
|
import { performance } from "node:perf_hooks";
|
||||||
import { DEFAULT_ACTIVITY_CHECK_INTERVAL_MS, DEFAULT_ACTIVITY_TIMEOUT_MS } from "./activity.ts";
|
import { DEFAULT_ACTIVITY_CHECK_INTERVAL_MS, DEFAULT_ACTIVITY_TIMEOUT_MS } from "./activity.ts";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
import { onExitSignal } from "./exitHandler.ts";
|
||||||
|
|
||||||
export type TrackChildOptions = {
|
export type TrackChildOptions = {
|
||||||
child: ChildProcess;
|
child: ChildProcess;
|
||||||
@@ -18,6 +19,9 @@ let externalSignalHandler: SignalHandler | null = null;
|
|||||||
|
|
||||||
// track a child process for cleanup on Ctrl+C
|
// track a child process for cleanup on Ctrl+C
|
||||||
export function trackChild(options: TrackChildOptions): void {
|
export function trackChild(options: TrackChildOptions): void {
|
||||||
|
// the signal handler cleans up all tracked children
|
||||||
|
// so we only have to install it once some child gets tracked
|
||||||
|
installSignalHandler();
|
||||||
activeChildren.set(options.child, options.killGroup ?? false);
|
activeChildren.set(options.child, options.killGroup ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +36,7 @@ export function setSignalHandler(handler: SignalHandler | null): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// kill all tracked children without exiting
|
// kill all tracked children without exiting
|
||||||
export function killTrackedChildren(): number {
|
export function killTrackedChildren() {
|
||||||
const count = activeChildren.size;
|
|
||||||
for (const entry of activeChildren) {
|
for (const entry of activeChildren) {
|
||||||
const child = entry[0];
|
const child = entry[0];
|
||||||
const killGroup = entry[1];
|
const killGroup = entry[1];
|
||||||
@@ -47,35 +50,24 @@ export function killTrackedChildren(): number {
|
|||||||
}
|
}
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
}
|
}
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup handler for SIGINT/SIGTERM - kills all tracked children
|
|
||||||
function cleanupAndExit(signal: string): void {
|
|
||||||
const count = killTrackedChildren();
|
|
||||||
if (count > 0) {
|
|
||||||
log.info(`» received ${signal}, killing ${count} subprocess(es)...`);
|
|
||||||
}
|
|
||||||
// force exit after a short delay if process is stuck
|
|
||||||
setTimeout(() => process.exit(1), 500).unref();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSignal(signal: NodeJS.Signals): void {
|
|
||||||
if (externalSignalHandler) {
|
|
||||||
externalSignalHandler(signal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cleanupAndExit(signal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// install signal handlers once (call early in process lifecycle)
|
// install signal handlers once (call early in process lifecycle)
|
||||||
let handlersInstalled = false;
|
let handlersInstalled = false;
|
||||||
export function installSignalHandlers(): void {
|
function installSignalHandler(): void {
|
||||||
if (handlersInstalled) return;
|
if (handlersInstalled) return;
|
||||||
handlersInstalled = true;
|
handlersInstalled = true;
|
||||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
onExitSignal((signal) => {
|
||||||
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
if (externalSignalHandler) {
|
||||||
|
externalSignalHandler(signal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const count = activeChildren.size;
|
||||||
|
if (count > 0) {
|
||||||
|
log.info(`» received ${signal}, killing ${count} subprocess(es)...`);
|
||||||
|
}
|
||||||
|
killTrackedChildren();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpawnOptions {
|
export interface SpawnOptions {
|
||||||
@@ -106,7 +98,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
const { cmd, args, env, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
const { cmd, args, env, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
||||||
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
||||||
|
|
||||||
installSignalHandlers();
|
installSignalHandler();
|
||||||
|
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
let stdoutBuffer = "";
|
let stdoutBuffer = "";
|
||||||
|
|||||||
+24
-6
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import type { PushPermission } from "../external.ts";
|
import type { PushPermission } from "../external.ts";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
import { onExitSignal } from "./exitHandler.ts";
|
||||||
import { acquireNewToken } from "./github.ts";
|
import { acquireNewToken } from "./github.ts";
|
||||||
import { isGitHubActions } from "./globals.ts";
|
import { isGitHubActions } from "./globals.ts";
|
||||||
|
|
||||||
@@ -131,18 +132,35 @@ export async function resolveTokens(params: ResolveTokensParams): Promise<TokenR
|
|||||||
const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", mcpToken);
|
const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", mcpToken);
|
||||||
mcpTokenValue = mcpToken;
|
mcpTokenValue = mcpToken;
|
||||||
|
|
||||||
return {
|
let disposingRef: PromiseWithResolvers<void> | undefined;
|
||||||
gitToken,
|
|
||||||
mcpToken,
|
const dispose = async () => {
|
||||||
async [Symbol.asyncDispose]() {
|
if (disposingRef) {
|
||||||
|
// this can happen if the signal arrives when disposing tokens
|
||||||
|
// we make sure to wait for the current dispose to complete
|
||||||
|
return disposingRef.promise;
|
||||||
|
}
|
||||||
|
disposingRef = Promise.withResolvers();
|
||||||
|
try {
|
||||||
mcpTokenValue = undefined;
|
mcpTokenValue = undefined;
|
||||||
revertGithubToken();
|
revertGithubToken();
|
||||||
// revoke both tokens
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
revokeGitHubInstallationToken(gitToken),
|
revokeGitHubInstallationToken(gitToken),
|
||||||
revokeGitHubInstallationToken(mcpToken),
|
revokeGitHubInstallationToken(mcpToken),
|
||||||
]);
|
]);
|
||||||
},
|
} finally {
|
||||||
|
removeSignalHandler();
|
||||||
|
disposingRef.resolve();
|
||||||
|
disposingRef = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSignalHandler = onExitSignal(dispose);
|
||||||
|
|
||||||
|
return {
|
||||||
|
gitToken,
|
||||||
|
mcpToken,
|
||||||
|
[Symbol.asyncDispose]: dispose,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user