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.

160 lines · 7.9 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
37interface BuiltinDef {
38 run: Builtin;
39 /** Major versions (`@vN`) the built-in mirrors; other refs (SHAs, branches, newer majors) run the real action. */
40 majors: [number, number];
41 /** The `with:` inputs it understands; anything else runs the real action. */
42 inputs: string[];
43 /** Further input values it cannot handle (the real action runs instead). */
44 supports?: (inputs: Map<string, string>) => boolean;
45}
46
47const hostArch = (v: string | undefined) => !v || v.toLowerCase() === (process.arch === "arm64" ? "arm64" : "x64");
48const CACHE_INPUTS = ["key", "path", "restore-keys", "upload-chunk-size", "enablecrossosarchive", "fail-on-cache-miss", "lookup-only", "save-always"];
49
50/**
51 * Actions the runner implements itself because that is much faster than
52 * downloading and starting them. Each mirrors the official action's inputs,
53 * outputs and storage (the same cache and artifact services), and steps
54 * aside for the real action when it sees a ref or input it does not mirror.
55 */
56const BUILTINS: Record<string, BuiltinDef> = {
57 "actions/checkout": {
58 run: checkout,
59 majors: [2, 6],
60 inputs: ["repository", "ref", "token", "ssh-key", "ssh-known-hosts", "ssh-strict", "ssh-user", "persist-credentials", "path", "clean", "filter", "sparse-checkout", "sparse-checkout-cone-mode", "fetch-depth", "fetch-tags", "show-progress", "lfs", "submodules", "set-safe-directory", "github-server-url"],
61 supports: (i) => !i.get("ssh-key"),
62 },
63 "actions/cache": { run: cacheAction, majors: [3, 6], inputs: CACHE_INPUTS },
64 "actions/cache/restore": { run: cacheRestoreAction, majors: [3, 6], inputs: CACHE_INPUTS },
65 "actions/cache/save": { run: cacheSaveAction, majors: [3, 6], inputs: CACHE_INPUTS },
66 "actions/upload-artifact": { run: artifactUpload, majors: [4, 7], inputs: ["name", "path", "if-no-files-found", "retention-days", "compression-level", "overwrite", "include-hidden-files", "archive"] },
67 "actions/download-artifact": { run: artifactDownload, majors: [4, 8], inputs: ["name", "path", "pattern", "merge-multiple", "github-token", "repository", "run-id", "artifact-ids", "skip-decompress", "digest-mismatch"] },
68 "actions/setup-node": {
69 run: setupNode,
70 majors: [3, 7],
71 inputs: ["node-version", "node-version-file", "architecture", "check-latest", "registry-url", "scope", "token", "cache", "cache-dependency-path", "always-auth", "package-manager-cache"],
72 supports: (i) => hostArch(i.get("architecture")) && ["", "npm", "pnpm", "yarn"].includes((i.get("cache") ?? "").toLowerCase()),
73 },
74 "actions/setup-python": {
75 run: setupPython,
76 majors: [4, 6],
77 inputs: ["python-version", "python-version-file", "cache", "architecture", "check-latest", "token", "cache-dependency-path", "update-environment", "allow-prereleases"],
78 supports: (i) =>
79 hostArch(i.get("architecture")) &&
80 !/^(pypy|graalpy)/i.test(i.get("python-version") ?? "") &&
81 ["", "pip", "pipenv"].includes((i.get("cache") ?? "").toLowerCase()) &&
82 (i.get("update-environment") ?? "true").toLowerCase() !== "false",
83 },
84 "actions/setup-go": { run: setupGo, majors: [4, 6], inputs: ["go-version", "go-version-file", "check-latest", "token", "cache", "cache-dependency-path", "architecture"], supports: (i) => hostArch(i.get("architecture")) },
85};
86
87const OFF = /^(0|false|no|off)$/i;
88
89/**
90 * The built-in for `name@ref` with these `with:` inputs, or null to run the
91 * real action. `DIFFINERY_BUILTINS: false` in a workflow, job or step `env:`
92 * (or DIFFINERY_DISABLE_BUILTINS=1 on the runner) always runs the real action.
93 */
94export function getBuiltin(name: string, ref: string, inputs: Record<string, string> = {}, env: Record<string, string> = {}): Builtin | null {
95 if (process.env.DIFFINERY_DISABLE_BUILTINS === "1" || OFF.test(env.DIFFINERY_BUILTINS ?? "")) return null;
96 const def = BUILTINS[name.toLowerCase()];
97 const m = /^v(\d+)(\.\d+){0,2}$/.exec(ref);
98 if (!def || !m || Number(m[1]) < def.majors[0] || Number(m[1]) > def.majors[1]) return null;
99 const given = new Map(Object.entries(inputs).map(([k, v]) => [k.toLowerCase(), v]));
100 if ([...given.keys()].some((k) => !def.inputs.includes(k))) return null;
101 if (def.supports && !def.supports(given)) return null;
102 return def.run;
103}
104
105export function makeBuiltinContext(job: JobRunner, x: StepExec, inputs: Record<string, string>, uses: string): BuiltinContext {
106 const lower = new Map(Object.entries(inputs).map(([k, v]) => [k.toLowerCase(), v]));
107 const make = (io: StepIO, envCtx: Record<string, string>, name: string): BuiltinContext => {
108 const toolEnv = (extra: Record<string, string> = {}) => job.processEnv({ ...envCtx, ...job.envAccum }, extra);
109 const c: BuiltinContext = {
110 job,
111 io,
112 scope: x.scope,
113 ctx: x.ctx,
114 envCtx,
115 uses,
116 name,
117 inputs,
118 input: (n, d = "") => {
119 const v = lower.get(n.toLowerCase());
120 return v === undefined || v === "" ? d : v;
121 },
122 log: (m) => job.logger.line(io.logIndex, m),
123 warn: (m) => {
124 job.logger.line(io.logIndex, `##[warning]${m}`);
125 job.addAnnotation(io.logIndex, { level: "warning", message: m });
126 },
127 setOutput: (k, v) => {
128 io.outputs[k] = v;
129 },
130 exportVariable: (k, v) => {
131 job.envAccum[k] = v;
132 envCtx[k] = v;
133 },
134 addPath: (d) => job.addPath(d),
135 toolEnv,
136 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 }),
137 run: async (cmd, args, o = {}) => {
138 const r = await c.exec(cmd, args, { ...o, capture: o.capture ?? true });
139 if (job.isStopping) throw failure("cancelled");
140 if (r.exitCode !== 0) {
141 const detail = o.silent ? ` ${r.stderr.trim().split("\n").slice(-5).join("\n")}` : "";
142 throw failure(`The process '${cmd}' failed with exit code ${r.exitCode}${detail ? ":" + detail : ""}`);
143 }
144 return r;
145 },
146 registerPost: (pname, condition, fn) => {
147 job.registerPost({
148 name: pname,
149 condition,
150 run: async (logIndex) => {
151 const pio: StepIO = { logIndex, outputs: {}, state: {} };
152 await fn(make(pio, { ...envCtx, ...job.envAccum }, pname));
153 },
154 });
155 },
156 };
157 return c;
158 };
159 return make(x.io, x.envCtx, x.name);
160}