Files
shockbot/utils/payload.ts
T

161 lines
5.3 KiB
TypeScript

import { isAbsolute, resolve } from "node:path";
import * as core from "@actions/core";
import { type } from "arktype";
import type { AuthorPermission, PayloadEvent } from "../external.ts";
import { log } from "./cli.ts";
import type { RepoSettings } from "./runContext.ts";
const ShellPermissionInput = type.enumerated("disabled", "restricted", "enabled");
const PushPermissionInput = type.enumerated("disabled", "restricted", "enabled");
export const Inputs = type({
prompt: "string",
"model?": type.string.or("undefined"),
"context_window?": type.number.or("undefined"),
"timeout?": type.string.or("undefined"),
"push?": PushPermissionInput.or("undefined"),
"shell?": ShellPermissionInput.or("undefined"),
"cwd?": type.string.or("undefined"),
"output_schema?": type.string.or("undefined"),
});
export type Inputs = typeof Inputs.infer;
function isPayloadEvent(value: unknown): value is PayloadEvent {
return typeof value === "object" && value !== null && "trigger" in value;
}
function resolveCwd(cwd: string | undefined): string | undefined {
const workspace = process.env.GITHUB_WORKSPACE;
if (!cwd) return workspace;
if (isAbsolute(cwd)) return cwd;
return workspace ? resolve(workspace, cwd) : cwd;
}
const COLLABORATOR_PERMISSIONS: AuthorPermission[] = ["admin", "maintain", "write"];
function isCollaborator(event: PayloadEvent): boolean {
const perm = event.authorPermission;
return perm !== undefined && COLLABORATOR_PERMISSIONS.includes(perm);
}
export type ResolvedPromptInput = string | Record<string, unknown>;
export function resolvePromptInput(): ResolvedPromptInput {
const prompt = core.getInput("prompt", { required: true });
let parsed: unknown;
try {
parsed = JSON.parse(prompt);
} catch {
return prompt;
}
if (!parsed || typeof parsed !== "object") {
return prompt;
}
return parsed as Record<string, unknown>;
}
function resolveNonPromptInputs() {
const contextWindowRaw = core.getInput("context_window");
return Inputs.omit("prompt").assert({
model: core.getInput("model") || undefined,
context_window: contextWindowRaw ? parseInt(contextWindowRaw, 10) : undefined,
timeout: core.getInput("timeout") || undefined,
cwd: core.getInput("cwd") || undefined,
push: (core.getInput("push") as "disabled" | "restricted" | "enabled") || undefined,
shell: (core.getInput("shell") as "disabled" | "restricted" | "enabled") || undefined,
});
}
export function resolvePayload(
resolvedPromptInput: ResolvedPromptInput,
repoSettings: RepoSettings
) {
// Helper: extract a string field from an unknown JSON payload
function str(obj: Record<string, unknown> | undefined, key: string): string | undefined {
const v = obj?.[key];
return typeof v === "string" ? v : undefined;
}
const [prompt, jsonPayload] =
typeof resolvedPromptInput !== "string"
? [
str(resolvedPromptInput, "prompt") ?? JSON.stringify(resolvedPromptInput),
resolvedPromptInput,
]
: [resolvedPromptInput, undefined];
const inputs = resolveNonPromptInputs();
const rawEvent = jsonPayload?.["event"];
const event: PayloadEvent = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" };
const model =
str(jsonPayload, "model") ?? inputs.model ?? repoSettings.model ?? process.env.OLLAMA_MODEL ?? undefined;
const contextWindowStr = str(jsonPayload, "context_window");
const contextWindow =
(contextWindowStr ? parseInt(contextWindowStr, 10) : undefined) ??
inputs.context_window ??
undefined;
const isNonCollaborator = !isCollaborator(event);
const repoShell = repoSettings.shell ?? "restricted";
const inputShell = inputs.shell;
let resolvedShell = repoShell;
if (inputShell === "disabled") {
resolvedShell = "disabled";
} else if (inputShell === "restricted" && resolvedShell === "enabled") {
resolvedShell = "restricted";
}
if (isNonCollaborator && resolvedShell === "enabled") {
resolvedShell = "restricted";
}
return {
"~shockbot": true as const,
model,
contextWindow,
prompt,
triggerer:
str(jsonPayload, "triggerer") ?? process.env.GITHUB_ACTOR ?? undefined,
eventInstructions: str(jsonPayload, "eventInstructions"),
event,
timeout: inputs.timeout ?? str(jsonPayload, "timeout"),
cwd: resolveCwd(inputs.cwd),
progressComment: (() => {
const pc = jsonPayload?.["progressComment"];
if (!pc || typeof pc !== "object") return undefined;
const p = pc as Record<string, unknown>;
if (typeof p.id !== "string" || p.type !== "issue") return undefined;
return { id: p.id, type: "issue" as const };
})(),
push: inputs.push ?? repoSettings.push ?? "restricted",
shell: resolvedShell,
};
}
export type ResolvedPayload = ReturnType<typeof resolvePayload>;
export function resolveOutputSchema(): Record<string, unknown> | undefined {
const raw = core.getInput("output_schema");
if (!raw) return undefined;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
throw new Error(`invalid output_schema: not valid JSON`);
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`invalid output_schema: must be a JSON object`);
}
log.info("» structured output schema provided — output will be required");
return parsed as Record<string, unknown>;
}