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.

125 lines · 5.5 KBRaw History
1import path from "node:path";
2import { errMsg, failure } from "../job.js";
3import { maxSatisfying, parseVer, satisfies, truthyInput } from "../util.js";
4import type { Builtin } from "./index.js";
5import { fetchJson, installTarball, isCached, packageCache, probe, readVersionFile, toolArch, toolDir } from "./tools.js";
6
7interface NodeDistEntry {
8 version: string; // "v22.11.0"
9 lts: string | false;
10 files: string[];
11}
12
13export function nodeMirror(): string {
14 return (process.env.DIFFINERY_NODE_MIRROR ?? "https://nodejs.org/dist").replace(/\/$/, "");
15}
16
17/** Resolves a setup-node version spec against nodejs.org's index. */
18export function resolveNodeVersion(index: NodeDistEntry[], spec: string): string | null {
19 const s = spec.trim().toLowerCase();
20 const arch = `linux-${toolArch("node")}`;
21 const avail = index.filter((e) => e.files.includes(arch) || e.files.length === 0);
22 if (s.startsWith("lts/")) {
23 const code = s.slice(4);
24 const lts = avail.filter((e) => e.lts && (code === "*" || String(e.lts).toLowerCase() === code));
25 if (code.startsWith("-")) {
26 // lts/-1: previous LTS line
27 const n = Number(code.slice(1));
28 const majors = [...new Set(avail.filter((e) => e.lts).map((e) => parseVer(e.version)![0]))].sort((a, b) => b - a);
29 const major = majors[n];
30 return major === undefined ? null : maxSatisfying(avail.filter((e) => e.lts).map((e) => e.version), String(major))?.replace(/^v/, "") ?? null;
31 }
32 return maxSatisfying(lts.map((e) => e.version), "*")?.replace(/^v/, "") ?? null;
33 }
34 if (s === "latest" || s === "current" || s === "node") return maxSatisfying(avail.map((e) => e.version), "*")?.replace(/^v/, "") ?? null;
35 return maxSatisfying(avail.map((e) => e.version), s.replace(/^v/, ""))?.replace(/^v/, "") ?? null;
36}
37
38export const setupNode: Builtin = async (c) => {
39 let spec = c.input("node-version");
40 const file = c.input("node-version-file");
41 if (!spec && file) spec = await readVersionFile(c, file, "node");
42 const installed = (await probe(c, "node", ["--version"]))?.replace(/^v/, "") ?? null;
43 let version = installed;
44
45 if (spec) {
46 const s = spec.trim().replace(/^v(?=\d)/, "");
47 let ok = false;
48 if (installed && !truthyInput(c.input("check-latest"), false)) {
49 if (/^(lts\/|latest|current|node$)/i.test(s)) {
50 try {
51 const target = resolveNodeVersion(await fetchJson<NodeDistEntry[]>(`${nodeMirror()}/index.json`), s);
52 ok = !!target && parseVer(target)![0] === parseVer(installed)![0];
53 } catch (e) {
54 c.warn(`Could not resolve '${s}' (${errMsg(e)}); using installed node ${installed}.`);
55 ok = true;
56 }
57 } else {
58 const want = parseVer(s);
59 ok = satisfies(installed, s) || (!!want && /^\d/.test(s) && want[0] === parseVer(installed)![0]);
60 }
61 }
62 if (ok) {
63 c.log(`Found in cache @ ${installed} (pre-installed node satisfies '${s}')`);
64 } else {
65 let index: NodeDistEntry[];
66 try {
67 index = await fetchJson<NodeDistEntry[]>(`${nodeMirror()}/index.json`);
68 } catch (e) {
69 throw failure(`Unable to fetch the Node.js version index: ${errMsg(e)}`);
70 }
71 const v = resolveNodeVersion(index, s);
72 if (!v) throw failure(`Unable to find Node version '${s}' for platform linux and architecture ${toolArch("node")}.`);
73 const dir = toolDir(c, "node", v, toolArch("node"));
74 if (!isCached(dir)) {
75 await installTarball(c, `${nodeMirror()}/v${v}/node-v${v}-linux-${toolArch("node")}.tar.gz`, dir);
76 } else c.log(`Found in cache @ ${dir}`);
77 c.addPath(path.join(dir, "bin"));
78 version = v;
79 }
80 } else {
81 c.log(`No node-version specified; using the pre-installed node ${installed ?? "(none)"}.`);
82 }
83 if (version) c.setOutput("node-version", `v${version}`);
84
85 const registry = c.input("registry-url");
86 if (registry) {
87 // Minimal setup-node `registry-url` support: write an .npmrc that reads NODE_AUTH_TOKEN.
88 const { writeFile, mkdir } = await import("node:fs/promises");
89 const npmrc = path.join(c.job.tempDir, ".npmrc");
90 await mkdir(c.job.tempDir, { recursive: true });
91 const url = registry.endsWith("/") ? registry : `${registry}/`;
92 const scope = c.input("scope");
93 const authLine = `${url.replace(/^\w+:/, "")}:_authToken=\${NODE_AUTH_TOKEN}`;
94 const regLine = scope ? `${scope.startsWith("@") ? scope : "@" + scope}:registry=${url}` : `registry=${url}`;
95 await writeFile(npmrc, `${authLine}\n${regLine}\n`);
96 c.exportVariable("NPM_CONFIG_USERCONFIG", npmrc);
97 c.exportVariable("NODE_AUTH_TOKEN", "XXXXX-XXXXX-XXXXX-XXXXX");
98 }
99
100 const pm = c.input("cache").toLowerCase();
101 if (pm) {
102 const env = c.toolEnv();
103 let dir: string | null = null;
104 let locks: string[];
105 if (pm === "npm") {
106 dir = await probe(c, "npm", ["config", "get", "cache"], env);
107 locks = ["**/package-lock.json", "**/npm-shrinkwrap.json"];
108 } else if (pm === "pnpm") {
109 dir = await probe(c, "pnpm", ["store", "path", "--silent"], env);
110 locks = ["**/pnpm-lock.yaml"];
111 } else if (pm === "yarn") {
112 const ver = await probe(c, "yarn", ["--version"], env);
113 dir = ver && parseVer(ver)![0] >= 2 ? await probe(c, "yarn", ["config", "get", "cacheFolder"], env) : await probe(c, "yarn", ["cache", "dir"], env);
114 locks = ["**/yarn.lock"];
115 } else {
116 throw failure(`Caching for '${pm}' is not supported`);
117 }
118 await packageCache(c, {
119 prefix: `node-cache-Linux-${toolArch("node")}-${pm}`,
120 paths: dir ? [dir] : [],
121 lockPatterns: locks,
122 what: pm,
123 });
124 }
125};