diff --git a/entry b/entry index 1d9561a..4d6ad8e 100755 --- a/entry +++ b/entry @@ -140528,6 +140528,251 @@ function $git(subcommand, args3, options) { }; } +// lifecycle.ts +var LIFECYCLE_HOOK_TIMEOUT_MS = 12e4; + +// utils/subprocess.ts +import { spawn as nodeSpawn } from "node:child_process"; + +// utils/activity.ts +var DEFAULT_ACTIVITY_TIMEOUT_MS = 6e4; +var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3; +var _lastActivity = Date.now(); +function markActivity() { + _lastActivity = Date.now(); +} +function getIdleMs() { + return Date.now() - _lastActivity; +} +function wrapWrite(original, onActivity) { + const wrapped = (chunk, encodingOrCb, cb) => { + onActivity(); + if (typeof encodingOrCb === "function") { + return original(chunk, encodingOrCb); + } + return original(chunk, encodingOrCb, cb); + }; + return wrapped; +} +function startProcessOutputMonitor(ctx) { + let timedOut = false; + const originalStdoutWrite = process.stdout.write.bind(process.stdout); + const originalStderrWrite = process.stderr.write.bind(process.stderr); + process.stdout.write = wrapWrite(originalStdoutWrite, markActivity); + process.stderr.write = wrapWrite(originalStderrWrite, markActivity); + const intervalId = setInterval(() => { + const idleMs = getIdleMs(); + if (timedOut || idleMs <= ctx.timeoutMs) return; + timedOut = true; + ctx.onTimeout(idleMs); + }, ctx.checkIntervalMs); + function stop() { + clearInterval(intervalId); + process.stdout.write = originalStdoutWrite; + process.stderr.write = originalStderrWrite; + } + return { stop }; +} +function createProcessOutputActivityTimeout(ctx) { + markActivity(); + let rejectFn = null; + const promise2 = new Promise((_, reject) => { + rejectFn = reject; + }); + let monitor = null; + monitor = startProcessOutputMonitor({ + timeoutMs: ctx.timeoutMs, + checkIntervalMs: ctx.checkIntervalMs, + onTimeout: (idleMs) => { + if (!rejectFn) return; + const idleSec = Math.round(idleMs / 1e3); + if (monitor) { + monitor.stop(); + } + rejectFn(new Error(`activity timeout: no output for ${idleSec}s`)); + } + }); + return { + promise: promise2, + stop: monitor.stop + }; +} + +// utils/subprocess.ts +var activeChildren = /* @__PURE__ */ new Map(); +var externalSignalHandler = null; +function trackChild(options) { + activeChildren.set(options.child, options.killGroup ?? false); +} +function untrackChild(child) { + activeChildren.delete(child); +} +function killTrackedChildren() { + const count = activeChildren.size; + for (const entry of activeChildren) { + const child = entry[0]; + const killGroup = entry[1]; + if (killGroup && child.pid) { + try { + process.kill(-child.pid, "SIGKILL"); + continue; + } catch { + } + } + 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; +function installSignalHandlers() { + if (handlersInstalled) return; + handlersInstalled = true; + process.on("SIGINT", () => handleSignal("SIGINT")); + process.on("SIGTERM", () => handleSignal("SIGTERM")); +} +async function spawn2(options) { + const { cmd, args: args3, env: env3, input, timeout, cwd, stdio, onStdout, onStderr } = options; + const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS; + installSignalHandlers(); + const startTime = Date.now(); + let stdoutBuffer = ""; + let stderrBuffer = ""; + return new Promise((resolve2, reject) => { + const child = nodeSpawn(cmd, args3, { + env: env3 || { + PATH: process.env.PATH || "", + HOME: process.env.HOME || "" + }, + stdio: stdio || ["pipe", "pipe", "pipe"], + cwd: cwd || process.cwd() + }); + trackChild({ child }); + let timeoutId; + let activityCheckIntervalId; + let isTimedOut = false; + let isActivityTimedOut = false; + let lastActivityTime = Date.now(); + if (timeout) { + timeoutId = setTimeout(() => { + isTimedOut = true; + child.kill("SIGTERM"); + setTimeout(() => { + if (!child.killed) { + child.kill("SIGKILL"); + } + }, 5e3); + }, timeout); + } + if (activityTimeoutMs > 0) { + activityCheckIntervalId = setInterval(() => { + const idleMs = Date.now() - lastActivityTime; + if (idleMs > activityTimeoutMs) { + isActivityTimedOut = true; + const idleSec = Math.round(idleMs / 1e3); + log.error(`no output for ${idleSec}s, killing process`); + child.kill("SIGKILL"); + clearInterval(activityCheckIntervalId); + } + }, DEFAULT_ACTIVITY_CHECK_INTERVAL_MS); + } + function updateActivity() { + lastActivityTime = Date.now(); + } + if (child.stdout) { + child.stdout.on("data", (data) => { + updateActivity(); + const chunk = data.toString(); + stdoutBuffer += chunk; + onStdout?.(chunk); + }); + } + if (child.stderr) { + child.stderr.on("data", (data) => { + updateActivity(); + const chunk = data.toString(); + stderrBuffer += chunk; + onStderr?.(chunk); + }); + } + child.on("close", (exitCode) => { + const durationMs = Date.now() - startTime; + untrackChild(child); + if (timeoutId) clearTimeout(timeoutId); + if (activityCheckIntervalId) clearInterval(activityCheckIntervalId); + if (isTimedOut) { + reject(new Error(`process timed out after ${timeout}ms`)); + return; + } + if (isActivityTimedOut) { + const idleSec = Math.round((Date.now() - lastActivityTime) / 1e3); + reject(new Error(`activity timeout: no output for ${idleSec}s`)); + return; + } + resolve2({ + stdout: stdoutBuffer, + stderr: stderrBuffer, + exitCode: exitCode || 0, + durationMs + }); + }); + child.on("error", (error49) => { + const durationMs = Date.now() - startTime; + untrackChild(child); + if (timeoutId) clearTimeout(timeoutId); + if (activityCheckIntervalId) clearInterval(activityCheckIntervalId); + console.error(`[spawn] process spawn error: ${error49.message}`); + resolve2({ + stdout: stdoutBuffer, + stderr: stderrBuffer, + exitCode: 1, + durationMs + }); + }); + if (input && child.stdin && stdio?.[0] !== "ignore") { + child.stdin.write(input); + child.stdin.end(); + } + }); +} + +// utils/lifecycle.ts +async function executeLifecycleHook(params) { + if (!params.script) return; + log.info(`\xBB executing ${params.event} lifecycle hook...`); + const result = await spawn2({ + cmd: "bash", + args: ["-c", params.script], + env: process.env, + timeout: LIFECYCLE_HOOK_TIMEOUT_MS, + activityTimeout: 0, + onStdout: (chunk) => process.stdout.write(chunk), + onStderr: (chunk) => process.stderr.write(chunk) + }); + if (result.exitCode !== 0) { + const output = result.stderr || result.stdout; + throw new Error( + `lifecycle hook '${params.event}' failed with exit code ${result.exitCode}: +${output}` + ); + } + log.info(`\xBB ${params.event} lifecycle hook completed successfully`); +} + // utils/shell.ts import { spawnSync as spawnSync3 } from "node:child_process"; function $(cmd, args3, options) { @@ -140661,8 +140906,8 @@ async function fetchAndFormatPrDiff(params) { }); return formatFilesWithLineNumbers(filesResponse.data); } -async function checkoutPrBranch(params) { - const { octokit, owner, name, gitToken, pullNumber, toolState, restricted } = params; +async function checkoutPrBranch(pullNumber, params) { + const { octokit, owner, name, gitToken, toolState, restricted } = params; log.info(`\xBB checking out PR #${pullNumber}...`); const pr = await octokit.rest.pulls.get({ owner, @@ -140723,6 +140968,10 @@ async function checkoutPrBranch(params) { if (isFork) { toolState.pushUrl = `https://github.com/${headRepo.full_name}.git`; } + await executeLifecycleHook({ + event: "post-checkout", + script: params.postCheckoutScript + }); return { prNumber: pullNumber, isFork, @@ -140735,14 +140984,14 @@ function CheckoutPrTool(ctx) { description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Returns diffPath pointing to the formatted diff file.", parameters: CheckoutPr, execute: execute(async ({ pull_number }) => { - const result = await checkoutPrBranch({ + await checkoutPrBranch(pull_number, { octokit: ctx.octokit, owner: ctx.repo.owner, name: ctx.repo.name, gitToken: ctx.gitToken, - pullNumber: pull_number, toolState: ctx.toolState, - restricted: ctx.payload.bash === "restricted" + restricted: ctx.payload.bash === "restricted", + postCheckoutScript: ctx.postCheckoutScript }); const pr = await ctx.octokit.rest.pulls.get({ owner: ctx.repo.owner, @@ -141777,225 +142026,6 @@ function isMetadataYarnClassic(metadataPath) { return metadataPath.endsWith(".yarn_integrity"); } -// utils/subprocess.ts -import { spawn as nodeSpawn } from "node:child_process"; - -// utils/activity.ts -var DEFAULT_ACTIVITY_TIMEOUT_MS = 6e4; -var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3; -var _lastActivity = Date.now(); -function markActivity() { - _lastActivity = Date.now(); -} -function getIdleMs() { - return Date.now() - _lastActivity; -} -function wrapWrite(original, onActivity) { - const wrapped = (chunk, encodingOrCb, cb) => { - onActivity(); - if (typeof encodingOrCb === "function") { - return original(chunk, encodingOrCb); - } - return original(chunk, encodingOrCb, cb); - }; - return wrapped; -} -function startProcessOutputMonitor(ctx) { - let timedOut = false; - const originalStdoutWrite = process.stdout.write.bind(process.stdout); - const originalStderrWrite = process.stderr.write.bind(process.stderr); - process.stdout.write = wrapWrite(originalStdoutWrite, markActivity); - process.stderr.write = wrapWrite(originalStderrWrite, markActivity); - const intervalId = setInterval(() => { - const idleMs = getIdleMs(); - if (timedOut || idleMs <= ctx.timeoutMs) return; - timedOut = true; - ctx.onTimeout(idleMs); - }, ctx.checkIntervalMs); - function stop() { - clearInterval(intervalId); - process.stdout.write = originalStdoutWrite; - process.stderr.write = originalStderrWrite; - } - return { stop }; -} -function createProcessOutputActivityTimeout(ctx) { - markActivity(); - let rejectFn = null; - const promise2 = new Promise((_, reject) => { - rejectFn = reject; - }); - let monitor = null; - monitor = startProcessOutputMonitor({ - timeoutMs: ctx.timeoutMs, - checkIntervalMs: ctx.checkIntervalMs, - onTimeout: (idleMs) => { - if (!rejectFn) return; - const idleSec = Math.round(idleMs / 1e3); - if (monitor) { - monitor.stop(); - } - rejectFn(new Error(`activity timeout: no output for ${idleSec}s`)); - } - }); - return { - promise: promise2, - stop: monitor.stop - }; -} - -// utils/subprocess.ts -var activeChildren = /* @__PURE__ */ new Map(); -var externalSignalHandler = null; -function trackChild(options) { - activeChildren.set(options.child, options.killGroup ?? false); -} -function untrackChild(child) { - activeChildren.delete(child); -} -function killTrackedChildren() { - const count = activeChildren.size; - for (const entry of activeChildren) { - const child = entry[0]; - const killGroup = entry[1]; - if (killGroup && child.pid) { - try { - process.kill(-child.pid, "SIGKILL"); - continue; - } catch { - } - } - 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; -function installSignalHandlers() { - if (handlersInstalled) return; - handlersInstalled = true; - process.on("SIGINT", () => handleSignal("SIGINT")); - process.on("SIGTERM", () => handleSignal("SIGTERM")); -} -async function spawn2(options) { - const { cmd, args: args3, env: env3, input, timeout, cwd, stdio, onStdout, onStderr } = options; - const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS; - installSignalHandlers(); - const startTime = Date.now(); - let stdoutBuffer = ""; - let stderrBuffer = ""; - return new Promise((resolve2, reject) => { - const child = nodeSpawn(cmd, args3, { - env: env3 || { - PATH: process.env.PATH || "", - HOME: process.env.HOME || "" - }, - stdio: stdio || ["pipe", "pipe", "pipe"], - cwd: cwd || process.cwd() - }); - trackChild({ child }); - let timeoutId; - let activityCheckIntervalId; - let isTimedOut = false; - let isActivityTimedOut = false; - let lastActivityTime = Date.now(); - if (timeout) { - timeoutId = setTimeout(() => { - isTimedOut = true; - child.kill("SIGTERM"); - setTimeout(() => { - if (!child.killed) { - child.kill("SIGKILL"); - } - }, 5e3); - }, timeout); - } - if (activityTimeoutMs > 0) { - activityCheckIntervalId = setInterval(() => { - const idleMs = Date.now() - lastActivityTime; - if (idleMs > activityTimeoutMs) { - isActivityTimedOut = true; - const idleSec = Math.round(idleMs / 1e3); - log.error(`no output for ${idleSec}s, killing process`); - child.kill("SIGKILL"); - clearInterval(activityCheckIntervalId); - } - }, DEFAULT_ACTIVITY_CHECK_INTERVAL_MS); - } - function updateActivity() { - lastActivityTime = Date.now(); - } - if (child.stdout) { - child.stdout.on("data", (data) => { - updateActivity(); - const chunk = data.toString(); - stdoutBuffer += chunk; - onStdout?.(chunk); - }); - } - if (child.stderr) { - child.stderr.on("data", (data) => { - updateActivity(); - const chunk = data.toString(); - stderrBuffer += chunk; - onStderr?.(chunk); - }); - } - child.on("close", (exitCode) => { - const durationMs = Date.now() - startTime; - untrackChild(child); - if (timeoutId) clearTimeout(timeoutId); - if (activityCheckIntervalId) clearInterval(activityCheckIntervalId); - if (isTimedOut) { - reject(new Error(`process timed out after ${timeout}ms`)); - return; - } - if (isActivityTimedOut) { - const idleSec = Math.round((Date.now() - lastActivityTime) / 1e3); - reject(new Error(`activity timeout: no output for ${idleSec}s`)); - return; - } - resolve2({ - stdout: stdoutBuffer, - stderr: stderrBuffer, - exitCode: exitCode || 0, - durationMs - }); - }); - child.on("error", (error49) => { - const durationMs = Date.now() - startTime; - untrackChild(child); - if (timeoutId) clearTimeout(timeoutId); - if (activityCheckIntervalId) clearInterval(activityCheckIntervalId); - console.error(`[spawn] process spawn error: ${error49.message}`); - resolve2({ - stdout: stdoutBuffer, - stderr: stderrBuffer, - exitCode: 1, - durationMs - }); - }); - if (input && child.stdin && stdio?.[0] !== "ignore") { - child.stdin.write(input); - child.stdin.end(); - } - }); -} - // prep/installNodeDependencies.ts var nodePackageManagers = { npm: ["echo", "npm is already installed"], @@ -146102,6 +146132,8 @@ function handleAgentResult(result) { var defaultSettings = { defaultAgent: null, modes: [], + setupScript: null, + postCheckoutScript: null, repoInstructions: "", web: "enabled", search: "enabled", @@ -146138,7 +146170,14 @@ async function fetchRunContext(params) { return defaultRunContext; } return { - settings: data.settings ?? defaultSettings, + settings: { + ...defaultSettings, + ...data.settings, + // ensure arrays are never undefined (API may omit new fields for existing repos) + modes: data.settings?.modes ?? [], + setupScript: data.settings?.setupScript ?? null, + postCheckoutScript: data.settings?.postCheckoutScript ?? null + }, apiToken: data.apiToken }; } catch { @@ -146233,15 +146272,7 @@ async function setupGit(params) { return; } const prNumber = params.event.issue_number; - await checkoutPrBranch({ - octokit: params.octokit, - owner: params.owner, - name: params.name, - gitToken: params.gitToken, - pullNumber: prNumber, - toolState: params.toolState, - restricted: params.restricted - }); + await checkoutPrBranch(prNumber, params); } // utils/time.ts @@ -146357,9 +146388,15 @@ async function main() { event: payload.event, octokit, toolState, - restricted: payload.bash === "restricted" + restricted: payload.bash === "restricted", + postCheckoutScript: runContext.repoSettings.postCheckoutScript }); timer.checkpoint("git"); + await executeLifecycleHook({ + event: "setup", + script: runContext.repoSettings.setupScript + }); + timer.checkpoint("lifecycleHooks::setup"); const modes2 = [...computeModes(), ...runContext.repoSettings.modes]; const mcpHttpServer = __using(_stack, await startMcpHttpServer({ repo: runContext.repo, @@ -146370,6 +146407,7 @@ async function main() { apiToken: runContext.apiToken, agent: agent2, modes: modes2, + postCheckoutScript: runContext.repoSettings.postCheckoutScript, toolState, runId: runInfo.runId, jobId: runInfo.jobId diff --git a/lifecycle.ts b/lifecycle.ts new file mode 100644 index 0000000..318e542 --- /dev/null +++ b/lifecycle.ts @@ -0,0 +1,2 @@ +/** timeout for lifecycle hook scripts */ +export const LIFECYCLE_HOOK_TIMEOUT_MS = 12e4; // 2 minutes diff --git a/main.ts b/main.ts index b0a3ff3..d8c97bc 100644 --- a/main.ts +++ b/main.ts @@ -16,6 +16,7 @@ import { setupExitHandler } from "./utils/exitHandler.ts"; import { resolveGit } from "./utils/gitAuth.ts"; import { createOctokit } from "./utils/github.ts"; import { resolveInstructions } from "./utils/instructions.ts"; +import { executeLifecycleHook } from "./utils/lifecycle.ts"; import { normalizeEnv } from "./utils/normalizeEnv.ts"; import { resolvePayload, resolvePromptInput } from "./utils/payload.ts"; import { handleAgentResult } from "./utils/run.ts"; @@ -126,9 +127,17 @@ export async function main(): Promise { octokit, toolState, restricted: payload.bash === "restricted", + postCheckoutScript: runContext.repoSettings.postCheckoutScript, }); timer.checkpoint("git"); + // execute setup lifecycle hook (runs once at initialization) + await executeLifecycleHook({ + event: "setup", + script: runContext.repoSettings.setupScript, + }); + timer.checkpoint("lifecycleHooks::setup"); + const modes = [...computeModes(), ...runContext.repoSettings.modes]; await using mcpHttpServer = await startMcpHttpServer({ @@ -140,6 +149,7 @@ export async function main(): Promise { apiToken: runContext.apiToken, agent, modes, + postCheckoutScript: runContext.repoSettings.postCheckoutScript, toolState, runId: runInfo.runId, jobId: runInfo.jobId, diff --git a/mcp/checkout.ts b/mcp/checkout.ts index 3ec55b5..4999065 100644 --- a/mcp/checkout.ts +++ b/mcp/checkout.ts @@ -4,8 +4,9 @@ import type { Octokit, RestEndpointMethodTypes } from "@octokit/rest"; import { type } from "arktype"; import { log } from "../utils/cli.ts"; import { $git } from "../utils/gitAuth.ts"; +import { executeLifecycleHook } from "../utils/lifecycle.ts"; import { $ } from "../utils/shell.ts"; -import type { ToolContext, ToolState } from "./server.ts"; +import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; type PullFile = RestEndpointMethodTypes["pulls"]["listFiles"]["response"]["data"][number]; @@ -162,16 +163,9 @@ export async function fetchAndFormatPrDiff(params: FetchPrDiffParams): Promise { - const { octokit, owner, name, gitToken, pullNumber, toolState, restricted } = params; + const { octokit, owner, name, gitToken, toolState, restricted } = params; log.info(`» checking out PR #${pullNumber}...`); // fetch PR metadata @@ -288,6 +283,12 @@ export async function checkoutPrBranch( toolState.pushUrl = `https://github.com/${headRepo.full_name}.git`; } + // execute post-checkout lifecycle hook + await executeLifecycleHook({ + event: "post-checkout", + script: params.postCheckoutScript, + }); + return { prNumber: pullNumber, isFork, @@ -303,14 +304,14 @@ export function CheckoutPrTool(ctx: ToolContext) { "Returns diffPath pointing to the formatted diff file.", parameters: CheckoutPr, execute: execute(async ({ pull_number }) => { - const result = await checkoutPrBranch({ + await checkoutPrBranch(pull_number, { octokit: ctx.octokit, owner: ctx.repo.owner, name: ctx.repo.name, gitToken: ctx.gitToken, - pullNumber: pull_number, toolState: ctx.toolState, restricted: ctx.payload.bash === "restricted", + postCheckoutScript: ctx.postCheckoutScript, }); // fetch PR metadata to return result diff --git a/mcp/server.ts b/mcp/server.ts index a5bbdf4..188383d 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -67,6 +67,7 @@ export interface ToolContext { apiToken: string; agent: Agent; modes: Mode[]; + postCheckoutScript: string | null; toolState: ToolState; runId: string; jobId: string | undefined; diff --git a/utils/lifecycle.ts b/utils/lifecycle.ts new file mode 100644 index 0000000..07f333b --- /dev/null +++ b/utils/lifecycle.ts @@ -0,0 +1,37 @@ +import { LIFECYCLE_HOOK_TIMEOUT_MS } from "../lifecycle.ts"; +import { log } from "./cli.ts"; +import { spawn } from "./subprocess.ts"; + +export interface ExecuteLifecycleHookParams { + event: string; + script: string | null; +} + +/** + * execute a lifecycle hook script if one is configured. + * runs the script in a bash shell with a timeout. + */ +export async function executeLifecycleHook(params: ExecuteLifecycleHookParams): Promise { + if (!params.script) return; + + log.info(`» executing ${params.event} lifecycle hook...`); + + const result = await spawn({ + cmd: "bash", + args: ["-c", params.script], + env: process.env, + timeout: LIFECYCLE_HOOK_TIMEOUT_MS, + activityTimeout: 0, + onStdout: (chunk) => process.stdout.write(chunk), + onStderr: (chunk) => process.stderr.write(chunk), + }); + + if (result.exitCode !== 0) { + const output = result.stderr || result.stdout; + throw new Error( + `lifecycle hook '${params.event}' failed with exit code ${result.exitCode}:\n${output}` + ); + } + + log.info(`» ${params.event} lifecycle hook completed successfully`); +} diff --git a/utils/runContext.ts b/utils/runContext.ts index baf80a8..232faa7 100644 --- a/utils/runContext.ts +++ b/utils/runContext.ts @@ -11,6 +11,8 @@ export interface Mode { export interface RepoSettings { defaultAgent: AgentName | null; modes: Mode[]; + setupScript: string | null; + postCheckoutScript: string | null; repoInstructions: string; web: ToolPermission; search: ToolPermission; @@ -26,6 +28,8 @@ export interface RunContext { const defaultSettings: RepoSettings = { defaultAgent: null, modes: [], + setupScript: null, + postCheckoutScript: null, repoInstructions: "", web: "enabled", search: "enabled", @@ -81,7 +85,14 @@ export async function fetchRunContext(params: { } return { - settings: data.settings ?? defaultSettings, + settings: { + ...defaultSettings, + ...data.settings, + // ensure arrays are never undefined (API may omit new fields for existing repos) + modes: data.settings?.modes ?? [], + setupScript: data.settings?.setupScript ?? null, + postCheckoutScript: data.settings?.postCheckoutScript ?? null, + }, apiToken: data.apiToken, }; } catch { diff --git a/utils/setup.ts b/utils/setup.ts index ee8b42c..069914c 100644 --- a/utils/setup.ts +++ b/utils/setup.ts @@ -45,15 +45,19 @@ export function setupTestRepo(options: SetupOptions): void { } } -interface SetupGitParams { +export interface GitContext { gitToken: string; owner: string; name: string; - event: PayloadEvent; octokit: OctokitWithPlugins; toolState: ToolState; // restricted bash mode: disables git hooks to prevent token exfiltration restricted: boolean; + postCheckoutScript: string | null; +} + +export interface SetupGitParams extends GitContext { + event: PayloadEvent; } /** @@ -149,15 +153,7 @@ export async function setupGit(params: SetupGitParams): Promise { // PR event: checkout PR branch using shared helper const prNumber = params.event.issue_number; - // use shared checkout helper (handles fork remotes, push config, etc.) + // use shared checkout helper (handles fork remotes, push config, post-checkout hook) // this updates toolState.pushUrl for fork PRs and sets toolState.issueNumber - await checkoutPrBranch({ - octokit: params.octokit, - owner: params.owner, - name: params.name, - gitToken: params.gitToken, - pullNumber: prNumber, - toolState: params.toolState, - restricted: params.restricted, - }); + await checkoutPrBranch(prNumber, params); }