// `script/version` prints the version `script/build` stamps into // `dist/package.json`, which is what `quak --version` reports from a build. // A `docker build .` of a clone is given no `VERSION` build arg, so the // version has to come from the `.git` in its context: the tag on a tagged // commit; the tag, the commits since it and the short commit on a later commit; // the short commit when no tag is reachable. A checkout with `.git` that still // yields no usable version must fail the build, not ship a version nobody can // trace back to its commit. // // Each test copies the script into a fresh directory, which the script then // treats as the checkout, and executes it there. import { afterEach, describe, expect, it } from "vitest"; import { execFileSync, spawnSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = fileURLToPath(new URL("../../", import.meta.url)); let checkout = ""; afterEach(() => { rmSync(checkout, { recursive: true, force: true }); }); // A checkout holding the script and a package.json that declares 0.0.0, with // no .git yet. const makeCheckout = (): void => { checkout = mkdtempSync(join(tmpdir(), "quak-version-")); mkdirSync(join(checkout, "script")); copyFileSync( join(repoRoot, "script/version"), join(checkout, "script/version"), ); chmodSync(join(checkout, "script/version"), 0o755); writeFileSync(join(checkout, "package.json"), '{ "version": "0.0.0" }\n'); }; // git in the checkout, with an identity and no commit signing, whatever the // host's own git config says. const git = (...args: string[]): string => execFileSync( "git", [ "-c", "user.name=quak", "-c", "user.email=quak@example.invalid", "-c", "commit.gpgsign=false", ...args, ], { cwd: checkout, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }, ).trim(); const makeCommittedCheckout = (): void => { makeCheckout(); git("init", "-q"); git("add", "package.json"); git("commit", "-q", "-m", "first"); }; // Runs the script with nothing in its environment but PATH and, when given, // VERSION. const runVersion = (version?: string) => spawnSync(join(checkout, "script/version"), { cwd: checkout, encoding: "utf-8", env: { PATH: process.env.PATH, VERSION: version }, }); describe("script/version", () => { it("prints the short commit of an untagged commit", () => { makeCommittedCheckout(); expect(runVersion().stdout.trim()).toBe( git("rev-parse", "--short", "HEAD"), ); }); it("prints the tag of a tagged commit", () => { makeCommittedCheckout(); git("tag", "v1.2.3"); expect(runVersion().stdout.trim()).toBe("v1.2.3"); }); // script/docker and script/cibuild pass the version they resolve on the // host as the VERSION build arg. it("prints the VERSION it is given over what git would derive", () => { makeCommittedCheckout(); expect(runVersion("x").stdout.trim()).toBe("x"); }); // `--build-arg VERSION=` must not stamp an empty version. it("treats an empty VERSION as unset", () => { makeCommittedCheckout(); expect(runVersion("").stdout.trim()).toBe( git("rev-parse", "--short", "HEAD"), ); }); // A source tarball has no .git: it keeps the version package.json // declares, and must still build. it("prints package.json's version where there is no .git", () => { makeCheckout(); const result = runVersion(); expect(result.status).toBe(0); expect(result.stdout.trim()).toBe("0.0.0"); }); // A repository with no commits stands in for any .git that git cannot // describe: git missing from the image, or refusing to read the checkout. it("fails where .git yields no version", () => { makeCheckout(); git("init", "-q"); const result = runVersion(); expect(result.status).not.toBe(0); expect(result.stdout).toBe(""); }); it.each(["dev", "unknown"])( "fails where there is .git and the version is %s", (version) => { makeCommittedCheckout(); const result = runVersion(version); expect(result.status).not.toBe(0); expect(result.stdout).toBe(""); }, ); });