Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

114 lines · 4.6 KBRaw History
1import { failure, type JobRunner, type Scope, type StepIO } from "../job.js";
2import type { StepExec } from "../steps/run.js";
3import type { ExecResult } from "../util.js";
4import { artifactDownload, artifactUpload } from "./artifacts.js";
5import { cacheAction, cacheRestoreAction, cacheSaveAction } from "./cache.js";
6import { checkout } from "./checkout.js";
7import { setupGo } from "./setup-go.js";
8import { setupNode } from "./setup-node.js";
9import { setupPython } from "./setup-python.js";
10
11export interface BuiltinContext {
12 job: JobRunner;
13 io: StepIO;
14 scope: Scope;
15 ctx: Record<string, unknown>;
16 envCtx: Record<string, string>;
17 uses: string;
18 name: string;
19 /** Raw `with:` values (already interpolated); lookups are case-insensitive. */
20 inputs: Record<string, string>;
21 input(name: string, dflt?: string): string;
22 log(msg: string): void;
23 warn(msg: string): void;
24 setOutput(name: string, value: string): void;
25 exportVariable(name: string, value: string): void;
26 addPath(dir: string): void;
27 /** Env for child tools (job env + GITHUB_ENV + PATH additions). */
28 toolEnv(extra?: Record<string, string>): NodeJS.ProcessEnv;
29 exec(cmd: string, args: string[], o?: { cwd?: string; env?: NodeJS.ProcessEnv; capture?: boolean; silent?: boolean; input?: string }): Promise<ExecResult>;
30 /** Like exec but throws on non-zero exit. */
31 run(cmd: string, args: string[], o?: { cwd?: string; env?: NodeJS.ProcessEnv; capture?: boolean; silent?: boolean; input?: string }): Promise<ExecResult>;
32 registerPost(name: string, condition: string, fn: (c: BuiltinContext) => Promise<void>): void;
33}
34
35export type Builtin = (c: BuiltinContext) => Promise<void>;
36
37const githubScript: Builtin = async () => {
38 throw failure("actions/github-script is not supported on Diffinery runners yet. Use a `run:` step with `node` or `curl` against $GITHUB_API_URL instead.");
39};
40
41const BUILTINS: Record<string, Builtin> = {
42 "actions/checkout": checkout,
43 "actions/cache": cacheAction,
44 "actions/cache/restore": cacheRestoreAction,
45 "actions/cache/save": cacheSaveAction,
46 "actions/upload-artifact": artifactUpload,
47 "actions/download-artifact": artifactDownload,
48 "actions/setup-node": setupNode,
49 "actions/setup-python": setupPython,
50 "actions/setup-go": setupGo,
51 "actions/github-script": githubScript,
52};
53
54export function getBuiltin(name: string, _ref: string): Builtin | null {
55 if (process.env.DIFFINERY_DISABLE_BUILTINS === "1") return null;
56 return BUILTINS[name.toLowerCase()] ?? null;
57}
58
59export function makeBuiltinContext(job: JobRunner, x: StepExec, inputs: Record<string, string>, uses: string): BuiltinContext {
60 const lower = new Map(Object.entries(inputs).map(([k, v]) => [k.toLowerCase(), v]));
61 const make = (io: StepIO, envCtx: Record<string, string>, name: string): BuiltinContext => {
62 const toolEnv = (extra: Record<string, string> = {}) => job.processEnv({ ...envCtx, ...job.envAccum }, extra);
63 const c: BuiltinContext = {
64 job,
65 io,
66 scope: x.scope,
67 ctx: x.ctx,
68 envCtx,
69 uses,
70 name,
71 inputs,
72 input: (n, d = "") => {
73 const v = lower.get(n.toLowerCase());
74 return v === undefined || v === "" ? d : v;
75 },
76 log: (m) => job.logger.line(io.logIndex, m),
77 warn: (m) => {
78 job.logger.line(io.logIndex, `##[warning]${m}`);
79 job.addAnnotation(io.logIndex, { level: "warning", message: m });
80 },
81 setOutput: (k, v) => {
82 io.outputs[k] = v;
83 },
84 exportVariable: (k, v) => {
85 job.envAccum[k] = v;
86 envCtx[k] = v;
87 },
88 addPath: (d) => job.addPath(d),
89 toolEnv,
90 exec: (cmd, args, o = {}) => job.tool(cmd, args, { io, cwd: o.cwd, env: o.env ?? toolEnv(), capture: o.capture, silent: o.silent, input: o.input }),
91 run: async (cmd, args, o = {}) => {
92 const r = await c.exec(cmd, args, { ...o, capture: o.capture ?? true });
93 if (job.isStopping) throw failure("cancelled");
94 if (r.exitCode !== 0) {
95 const detail = o.silent ? ` ${r.stderr.trim().split("\n").slice(-5).join("\n")}` : "";
96 throw failure(`The process '${cmd}' failed with exit code ${r.exitCode}${detail ? ":" + detail : ""}`);
97 }
98 return r;
99 },
100 registerPost: (pname, condition, fn) => {
101 job.registerPost({
102 name: pname,
103 condition,
104 run: async (logIndex) => {
105 const pio: StepIO = { logIndex, outputs: {}, state: {} };
106 await fn(make(pio, { ...envCtx, ...job.envAccum }, pname));
107 },
108 });
109 },
110 };
111 return c;
112 };
113 return make(x.io, x.envCtx, x.name);
114}