import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, it } from "vitest"; import { inspectWorkflowNodeRuntime, verifyWorkflowNodeRuntime } from "./verify-workflow-node-runtime.mjs"; const temporaryDirectories = []; const setupNodeStep = [ " - name: Set up Node.js", " uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4", " with:", " node-version-file: .nvmrc", ].join("\n"); const persistNodeStep = [" - name: Persist Node.js toolchain", " run: bash scripts/ci/persist-node-toolchain.sh"].join("\n"); const corepackSteps = [ " - name: Enable Corepack", " run: corepack enable --install-directory /usr/local/bin", " - name: Prepare pnpm 11.12.0", " run: corepack prepare pnpm@11.12.0 --activate", ].join("\n"); afterEach(() => { for (const directory of temporaryDirectories.splice(0)) { fs.rmSync(directory, { recursive: true, force: true }); } }); function validWorkflow(extraSteps = "") { return [ "name: CI", "jobs:", " verify:", " runs-on: ubuntu-latest", " steps:", setupNodeStep, persistNodeStep, corepackSteps, extraSteps, " - name: Install dependencies", " run: pnpm install --frozen-lockfile", ] .filter(Boolean) .join("\n"); } function createRepository(workflow = validWorkflow()) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "hivegrid-workflow-node-")); temporaryDirectories.push(root); fs.mkdirSync(path.join(root, ".gitea/workflows"), { recursive: true }); fs.mkdirSync(path.join(root, "apps/hocuspocus"), { recursive: true }); fs.mkdirSync(path.join(root, "scripts/ci"), { recursive: true }); fs.writeFileSync(path.join(root, ".nvmrc"), "v24.18.0\n"); fs.writeFileSync(path.join(root, "Dockerfile"), "FROM node:24.18.0-bookworm-slim@sha256:abc\n"); fs.writeFileSync(path.join(root, "apps/hocuspocus/Dockerfile"), "FROM node:24.18.0-bookworm-slim@sha256:def\n"); fs.writeFileSync(path.join(root, ".gitea/workflows/ci.yml"), workflow); fs.writeFileSync( path.join(root, "scripts/ci/persist-node-toolchain.sh"), [ ".nvmrc", "RUNNER_TOOL_CACHE", 'ln -sfn "$node_binary" "$install_directory/node"', 'ln -sfn "$corepack_binary" "$install_directory/corepack"', 'actual_version="$("$install_directory/node" --version)"', ].join("\n") ); return root; } describe("workflow Node.js runtime verification", () => { it("accepts a persistent pinned setup-node runtime before pnpm", () => { const result = inspectWorkflowNodeRuntime(validWorkflow(), ".gitea/workflows/ci.yml"); assert.deepEqual(result.failures, []); assert.equal(result.passes.length, 2); }); it("rejects a pnpm job without setup-node", () => { const source = validWorkflow().replace(`${setupNodeStep}\n`, ""); const result = inspectWorkflowNodeRuntime(source, ".gitea/workflows/ci.yml"); assert.equal(result.failures.length, 1); assert.match(result.failures[0], /must run before/); }); it("rejects setup-node after the first pnpm command", () => { const source = validWorkflow().replace(`${setupNodeStep}\n`, "") + `\n${setupNodeStep}`; const result = inspectWorkflowNodeRuntime(source, ".gitea/workflows/ci.yml"); assert.ok(result.failures.some((failure) => failure.includes("must run before"))); }); it("rejects setup-node without the shared version file", () => { const source = validWorkflow().replace("node-version-file: .nvmrc", 'node-version: "24"'); const result = inspectWorkflowNodeRuntime(source, ".gitea/workflows/ci.yml"); assert.equal(result.failures.length, 1); assert.match(result.failures[0], /node-version-file/); }); it("rejects a Gitea job that does not persist setup-node's toolchain", () => { const source = validWorkflow().replace(`${persistNodeStep}\n`, ""); const result = inspectWorkflowNodeRuntime(source, ".gitea/workflows/ci.yml"); assert.ok(result.failures.some((failure) => failure.includes("persist its runtime"))); }); it("rejects a Gitea Corepack shim outside the persistent tool directory", () => { const source = validWorkflow().replace("corepack enable --install-directory /usr/local/bin", "corepack enable"); const result = inspectWorkflowNodeRuntime(source, ".gitea/workflows/ci.yml"); assert.ok(result.failures.some((failure) => failure.includes("persistent /usr/local/bin"))); }); it("keeps local, CI, and production Node.js runtimes aligned", () => { const root = createRepository(); const result = verifyWorkflowNodeRuntime(root); assert.deepEqual(result.failures, []); assert.equal(result.passes.length, 6); }); it("rejects a runtime below pnpm's minimum", () => { const root = createRepository(); fs.writeFileSync(path.join(root, ".nvmrc"), "v22.12.0\n"); const result = verifyWorkflowNodeRuntime(root); assert.ok(result.failures.some((failure) => failure.includes("minimum 22.13.0"))); }); });