| 1 | import path from "node:path"; |
| 2 | import { errMsg, failure } from "../job.js"; |
| 3 | import { maxSatisfying, satisfies, truthyInput } from "../util.js"; |
| 4 | import type { Builtin } from "./index.js"; |
| 5 | import { fetchJson, installTarball, isCached, packageCache, probe, readVersionFile, rootLockHash, toolArch, toolDir } from "./tools.js"; |
| 6 | |
| 7 | interface GoRelease { |
| 8 | version: string; // "go1.22.3" |
| 9 | stable: boolean; |
| 10 | files: Array<{ filename: string; os: string; arch: string; kind: string; sha256?: string }>; |
| 11 | } |
| 12 | |
| 13 | let index: { url: string; at: number; releases: Promise<GoRelease[]> } | null = null; |
| 14 | |
| 15 | /** Go's releases index, fetched at most every 10 minutes: the job's head start (prefetch.ts) and the step share it. */ |
| 16 | function goReleases(): Promise<GoRelease[]> { |
| 17 | const url = process.env.DIFFINERY_GO_INDEX ?? "https://go.dev/dl/?mode=json&include=all"; |
| 18 | if (!index || index.url !== url || Date.now() - index.at > 10 * 60_000) { |
| 19 | const releases = fetchJson<GoRelease[]>(url); |
| 20 | index = { url, at: Date.now(), releases }; |
| 21 | releases.catch(() => { |
| 22 | if (index?.releases === releases) index = null; |
| 23 | }); |
| 24 | } |
| 25 | return index.releases; |
| 26 | } |
| 27 | |
| 28 | /** The stable Go release `spec` asks for ("1.22", "1.22.3", "stable", "oldstable", a range), as setup-go picks it. */ |
| 29 | export async function resolveGo(spec: string): Promise<{ version: string; url: string; sha256?: string } | null> { |
| 30 | const releases = await goReleases(); |
| 31 | const arch = toolArch("go"); |
| 32 | const stable = releases.filter((r) => r.stable && r.files.some((f) => f.os === "linux" && f.arch === arch && f.kind === "archive")); |
| 33 | let v: string | null; |
| 34 | if (spec === "stable") v = maxSatisfying(stable.map((r) => r.version.replace(/^go/, "")), "*"); |
| 35 | else if (spec === "oldstable") { |
| 36 | const sorted = stable.map((r) => r.version.replace(/^go/, "")); |
| 37 | const latest = maxSatisfying(sorted, "*"); |
| 38 | const latestMinor = latest ? latest.split(".").slice(0, 2).join(".") : ""; |
| 39 | v = maxSatisfying(sorted.filter((x) => !x.startsWith(latestMinor + ".") && x !== latestMinor), "*"); |
| 40 | } else v = maxSatisfying(stable.map((r) => r.version.replace(/^go/, "")), spec); |
| 41 | if (!v) return null; |
| 42 | const rel = stable.find((r) => r.version === `go${v}`)!; |
| 43 | const f = rel.files.find((x) => x.os === "linux" && x.arch === arch && x.kind === "archive")!; |
| 44 | return { version: v, url: `${process.env.DIFFINERY_GO_DL ?? "https://go.dev/dl/"}${f.filename}`, sha256: f.sha256 }; |
| 45 | } |
| 46 | |
| 47 | export const setupGo: Builtin = async (c) => { |
| 48 | let spec = c.input("go-version"); |
| 49 | const file = c.input("go-version-file"); |
| 50 | if (!spec && file) spec = await readVersionFile(c, file, "go"); |
| 51 | spec = spec.trim().replace(/^go/, ""); |
| 52 | const installedRaw = await probe(c, "go", ["version"]); |
| 53 | const installed = /go(\d+\.\d+(?:\.\d+)?)/.exec(installedRaw ?? "")?.[1] ?? null; |
| 54 | let version = installed; |
| 55 | |
| 56 | if (!spec && !installed) throw failure("No go-version specified and Go is not installed on this runner."); |
| 57 | const useInstalled = installed && (!spec || (!/^(stable|oldstable)$/.test(spec) && satisfies(installed, spec))) && !truthyInput(c.input("check-latest"), false); |
| 58 | if (useInstalled) { |
| 59 | c.log(`Using pre-installed Go ${installed}`); |
| 60 | } else { |
| 61 | let rel: Awaited<ReturnType<typeof resolveGo>>; |
| 62 | try { |
| 63 | rel = await resolveGo(spec); |
| 64 | } catch (e) { |
| 65 | throw failure(`Unable to fetch the Go versions index: ${errMsg(e)}`); |
| 66 | } |
| 67 | const arch = toolArch("go"); |
| 68 | if (!rel) throw failure(`Unable to find Go version '${spec}' for platform linux and architecture ${arch}.`); |
| 69 | const v = rel.version; |
| 70 | const dir = toolDir(c, "go", v, arch); |
| 71 | if (!isCached(dir)) await installTarball(c, rel.url, dir, 1, rel.sha256); |
| 72 | else c.log(`Found in cache @ ${dir}`); |
| 73 | c.addPath(path.join(dir, "bin")); |
| 74 | version = v; |
| 75 | } |
| 76 | const gopath = await probe(c, "go", ["env", "GOPATH"], c.toolEnv()); |
| 77 | if (gopath) c.addPath(path.join(gopath, "bin")); |
| 78 | if (version) c.setOutput("go-version", version); |
| 79 | |
| 80 | if (truthyInput(c.input("cache"), true)) { |
| 81 | let hash: string; |
| 82 | try { |
| 83 | hash = rootLockHash(c, c.input("cache-dependency-path"), ["go.sum"]); |
| 84 | if (!hash) throw failure("Some specified paths were not resolved, unable to cache dependencies."); |
| 85 | } catch (e) { |
| 86 | c.warn(`Restore cache failed: ${errMsg(e)}`); |
| 87 | return; |
| 88 | } |
| 89 | const env = c.toolEnv(); |
| 90 | const modcache = await probe(c, "go", ["env", "GOMODCACHE"], env); |
| 91 | const buildcache = await probe(c, "go", ["env", "GOCACHE"], env); |
| 92 | const imageOs = env.ImageOS ?? "ubuntu24"; |
| 93 | await packageCache(c, { |
| 94 | key: `setup-go-Linux-${process.arch}-${imageOs}-go-${version}-${hash}`, |
| 95 | paths: [modcache ?? "", buildcache ?? ""].filter(Boolean), |
| 96 | what: "go", |
| 97 | }); |
| 98 | } |
| 99 | }; |