feat: adapt pullfrog for gitea + ollama
This commit is contained in:
+36
-87
@@ -2,50 +2,12 @@ import { isAbsolute, resolve } from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import { type } from "arktype";
|
||||
import type { AuthorPermission, PayloadEvent } from "../external.ts";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { log } from "./cli.ts";
|
||||
import type { RepoSettings } from "./runContext.ts";
|
||||
import { validateCompatibility } from "./versioning.ts";
|
||||
|
||||
// tool permission enum types for inputs
|
||||
const ShellPermissionInput = type.enumerated("disabled", "restricted", "enabled");
|
||||
const PushPermissionInput = type.enumerated("disabled", "restricted", "enabled");
|
||||
|
||||
// schema for JSON payload passed via prompt (internal dispatch invocation)
|
||||
// note: permissions are intentionally NOT included here to prevent injection attacks
|
||||
// permissions are derived from event.authorPermission instead
|
||||
export const JsonPayload = type({
|
||||
"~pullfrog": "true",
|
||||
version: "string",
|
||||
"model?": "string | undefined",
|
||||
prompt: "string",
|
||||
"triggerer?": "string | undefined",
|
||||
|
||||
"eventInstructions?": "string",
|
||||
"previousRunsNote?": "string",
|
||||
"event?": "object",
|
||||
"timeout?": "string | undefined",
|
||||
"progressComment?": type({
|
||||
id: "string",
|
||||
type: "'issue' | 'review'",
|
||||
}).or("undefined"),
|
||||
"generateSummary?": "boolean | undefined",
|
||||
});
|
||||
|
||||
// permission levels that indicate collaborator status (have push access)
|
||||
const COLLABORATOR_PERMISSIONS: AuthorPermission[] = ["admin", "maintain", "write"];
|
||||
|
||||
// check if the event author has collaborator-level permissions
|
||||
function isCollaborator(event: PayloadEvent): boolean {
|
||||
const perm = event.authorPermission;
|
||||
return perm !== undefined && COLLABORATOR_PERMISSIONS.includes(perm);
|
||||
}
|
||||
|
||||
// inputs schema - action inputs from core.getInput()
|
||||
// note: tool permissions use .or("undefined") because getInput() || undefined
|
||||
// explicitly sets the property to undefined when empty, which is different from
|
||||
// the property being absent. arktype's "prop?" means "optional to include" but
|
||||
// if included, must match the type - so we need to explicitly allow undefined.
|
||||
export const Inputs = type({
|
||||
prompt: "string",
|
||||
"model?": type.string.or("undefined"),
|
||||
@@ -69,7 +31,14 @@ function resolveCwd(cwd: string | undefined): string | undefined {
|
||||
return workspace ? resolve(workspace, cwd) : cwd;
|
||||
}
|
||||
|
||||
export type ResolvedPromptInput = string | typeof JsonPayload.infer;
|
||||
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 });
|
||||
@@ -78,19 +47,14 @@ export function resolvePromptInput(): ResolvedPromptInput {
|
||||
try {
|
||||
parsed = JSON.parse(prompt);
|
||||
} catch {
|
||||
// JSON parse error is fine (plain text prompt)
|
||||
return prompt;
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== "object" || !("~pullfrog" in parsed)) {
|
||||
// if it doesn't look like a pullfrog payload, return the plain text prompt
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
// validation errors should propagate
|
||||
const jsonPayload = JsonPayload.assert(parsed);
|
||||
validateCompatibility(jsonPayload.version, packageJson.version);
|
||||
return jsonPayload;
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function resolveNonPromptInputs() {
|
||||
@@ -98,91 +62,76 @@ function resolveNonPromptInputs() {
|
||||
model: core.getInput("model") || undefined,
|
||||
timeout: core.getInput("timeout") || undefined,
|
||||
cwd: core.getInput("cwd") || undefined,
|
||||
push: core.getInput("push") || undefined,
|
||||
shell: core.getInput("shell") || undefined,
|
||||
push: (core.getInput("push") as "disabled" | "restricted" | "enabled") || undefined,
|
||||
shell: (core.getInput("shell") as "disabled" | "restricted" | "enabled") || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const isPullfrog = (actor: string | null | undefined): boolean => {
|
||||
actor = actor?.replace("[bot]", "");
|
||||
return !!actor && (actor === "pullfrog" || actor === "pullfrogdev");
|
||||
};
|
||||
|
||||
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"
|
||||
? [resolvedPromptInput.prompt, resolvedPromptInput]
|
||||
? [
|
||||
str(resolvedPromptInput, "prompt") ?? JSON.stringify(resolvedPromptInput),
|
||||
resolvedPromptInput,
|
||||
]
|
||||
: [resolvedPromptInput, undefined];
|
||||
|
||||
const inputs = resolveNonPromptInputs();
|
||||
|
||||
// resolve event - use type guard for jsonPayload.event, fallback to unknown trigger
|
||||
const rawEvent = jsonPayload?.event;
|
||||
const rawEvent = jsonPayload?.["event"];
|
||||
const event: PayloadEvent = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" };
|
||||
|
||||
const model = jsonPayload?.model ?? inputs.model ?? repoSettings.model ?? undefined;
|
||||
const model =
|
||||
str(jsonPayload, "model") ?? inputs.model ?? repoSettings.model ?? process.env.OLLAMA_MODEL ?? undefined;
|
||||
|
||||
// determine shell permission - strictest setting wins
|
||||
// precedence: disabled > restricted > enabled
|
||||
// non-collaborators always get at least "restricted"
|
||||
const isNonCollaborator = !isCollaborator(event);
|
||||
const repoShell = repoSettings.shell ?? "restricted";
|
||||
const inputShell = inputs.shell;
|
||||
|
||||
// resolve shell: start with repo setting, then apply restrictions
|
||||
let resolvedShell = repoShell;
|
||||
|
||||
// input can only make it stricter (disabled > restricted > enabled)
|
||||
if (inputShell === "disabled") {
|
||||
resolvedShell = "disabled";
|
||||
} else if (inputShell === "restricted" && resolvedShell === "enabled") {
|
||||
resolvedShell = "restricted";
|
||||
}
|
||||
|
||||
// non-collaborators get at least "restricted" (can't have "enabled")
|
||||
if (isNonCollaborator && resolvedShell === "enabled") {
|
||||
resolvedShell = "restricted";
|
||||
}
|
||||
|
||||
// build payload - precedence: inputs > repoSettings > fallbacks
|
||||
// note: modes are NOT in payload - they come from repoSettings in main()
|
||||
return {
|
||||
"~pullfrog": true as const,
|
||||
version: jsonPayload?.version ?? packageJson.version,
|
||||
"~shockbot": true as const,
|
||||
model,
|
||||
prompt,
|
||||
triggerer:
|
||||
jsonPayload?.triggerer ??
|
||||
// it's not a common use case but GITHUB_ACTOR can be a user when the workflow is manually triggered by a user through GitHub Actions UI
|
||||
(!isPullfrog(process.env.GITHUB_ACTOR) ? process.env.GITHUB_ACTOR : undefined),
|
||||
eventInstructions: jsonPayload?.eventInstructions,
|
||||
previousRunsNote: jsonPayload?.previousRunsNote,
|
||||
str(jsonPayload, "triggerer") ?? process.env.GITHUB_ACTOR ?? undefined,
|
||||
eventInstructions: str(jsonPayload, "eventInstructions"),
|
||||
event,
|
||||
timeout: inputs.timeout ?? jsonPayload?.timeout,
|
||||
timeout: inputs.timeout ?? str(jsonPayload, "timeout"),
|
||||
cwd: resolveCwd(inputs.cwd),
|
||||
progressComment: jsonPayload?.progressComment,
|
||||
generateSummary: jsonPayload?.generateSummary,
|
||||
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 };
|
||||
})(),
|
||||
|
||||
// permissions: inputs > repoSettings > fallbacks
|
||||
push: inputs.push ?? repoSettings.push ?? "restricted",
|
||||
shell: resolvedShell,
|
||||
|
||||
// set by proxy logic in main.ts when routing through OpenRouter
|
||||
proxyModel: undefined as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export type ResolvedPayload = ReturnType<typeof resolvePayload>;
|
||||
|
||||
/**
|
||||
* Parse and validate the optional `output_schema` action input. Returns the
|
||||
* parsed object when present, or `undefined` when absent. Throws on invalid
|
||||
* JSON or non-object payloads — these are workflow-author errors that should
|
||||
* surface immediately, not silently degrade to "no schema".
|
||||
*/
|
||||
export function resolveOutputSchema(): Record<string, unknown> | undefined {
|
||||
const raw = core.getInput("output_schema");
|
||||
if (!raw) return undefined;
|
||||
|
||||
Reference in New Issue
Block a user