131 lines
5.0 KiB
JavaScript
131 lines
5.0 KiB
JavaScript
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")));
|
|
});
|
|
});
|