Files
shockbot/main.ts
T
2026-01-24 18:56:51 +00:00

147 lines
4.8 KiB
TypeScript

// changes to tool permissions should be reflected in wiki/granular-tools.md
import { ensureProgressCommentUpdated } from "./mcp/comment.ts";
import { initToolState, startMcpHttpServer } from "./mcp/server.ts";
import { computeModes } from "./modes.ts";
import { resolveAgent } from "./utils/agent.ts";
import { validateApiKey } from "./utils/apiKeys.ts";
import { resolveBody } from "./utils/body.ts";
import { log, writeSummary } from "./utils/cli.ts";
import { reportErrorToComment } from "./utils/errorReport.ts";
import { createOctokit } from "./utils/github.ts";
import { resolveInstructions } from "./utils/instructions.ts";
import { normalizeEnv } from "./utils/normalizeEnv.ts";
import { resolvePayload } from "./utils/payload.ts";
import { resolveRepoData } from "./utils/repoData.ts";
import { handleAgentResult } from "./utils/run.ts";
import { createTempDirectory, setupGit } from "./utils/setup.ts";
import { Timer } from "./utils/timer.ts";
import { resolveInstallationToken } from "./utils/token.ts";
import { resolveRun } from "./utils/workflow.ts";
export { Inputs } from "./utils/payload.ts";
export interface MainResult {
success: boolean;
output?: string | undefined;
error?: string | undefined;
}
export async function main(): Promise<MainResult> {
// normalize env var names to uppercase (handles case-insensitive workflow files)
normalizeEnv();
// store original GITHUB_TOKEN
process.env.ORIGINAL_GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const timer = new Timer();
await using tokenRef = await resolveInstallationToken();
process.env.GITHUB_TOKEN = tokenRef.token;
const octokit = createOctokit(tokenRef.token);
const runInfo = await resolveRun({ octokit });
const toolState = initToolState({ runInfo });
try {
const repo = await resolveRepoData({ octokit, token: tokenRef.token });
timer.checkpoint("repoData");
// resolve payload after repoData so permissions can use DB settings
// precedence: action inputs > json payload > repoSettings > fallbacks
const payload = resolvePayload(repo.repoSettings);
if (payload.cwd && process.cwd() !== payload.cwd) {
process.chdir(payload.cwd);
}
// resolve body - fetches body_html and converts to markdown if images present
// this ensures agents receive markdown with working signed image URLs
const originalBody = payload.event.body;
const resolvedBody = await resolveBody({ event: payload.event, octokit, repo });
if (resolvedBody !== originalBody) {
payload.event.body = resolvedBody;
// also update prompt if original body was included there
if (originalBody && payload.prompt.includes(originalBody)) {
payload.prompt = payload.prompt.replace(originalBody, resolvedBody ?? "");
}
}
const tmpdir = createTempDirectory();
const agent = resolveAgent({ payload, repoSettings: repo.repoSettings });
validateApiKey({
agent,
owner: repo.owner,
name: repo.name,
});
await setupGit({
token: tokenRef.token,
originalToken: process.env.ORIGINAL_GITHUB_TOKEN,
bashPermission: payload.bash,
owner: repo.owner,
name: repo.name,
event: payload.event,
octokit,
toolState,
});
timer.checkpoint("git");
const modes = [...computeModes(), ...repo.repoSettings.modes];
await using mcpHttpServer = await startMcpHttpServer({
repo,
payload,
octokit,
githubInstallationToken: tokenRef.token,
agent,
modes,
toolState,
runId: runInfo.runId,
jobId: runInfo.jobId,
});
log.info(`» MCP server started at ${mcpHttpServer.url}`);
timer.checkpoint("mcpServer");
const instructions = resolveInstructions({
payload,
repoData: repo,
modes,
});
const result = await agent.run({
payload,
mcpServerUrl: mcpHttpServer.url,
tmpdir,
instructions,
});
// write last progress body to job summary
if (toolState.lastProgressBody) {
await writeSummary(toolState.lastProgressBody);
}
const mainResult = await handleAgentResult(result);
return mainResult;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
log.error(errorMessage);
try {
await reportErrorToComment({ toolState, error: errorMessage });
} catch {
// error reporting failed, but don't let it mask the original error
}
return {
success: false,
error: errorMessage,
};
} finally {
// ensure progress comment is updated if it was never updated during execution
// do this before revoking the token so we can still make API calls
try {
await ensureProgressCommentUpdated(toolState);
} catch {
// error updating comment, but don't let it mask the original error
}
}
}