Nothing verified this repository until now. The workflow runs on every pull request (no branches filter, so intermediate stack links get a status) and on pushes to main, in node:22-alpine, the Dockerfile's own base: npm ci, npm test, the socket-level pass, then scripts/boot-from-copy.sh. The script stands in for a docker build, which would need the Docker socket in the instance-scoped runner and so root on the production VPS for every workflow. It starts the CMD from the COPY sources alone and fails on the MODULE_NOT_FOUND that #11 had to fix by hand. __tests__/ci.test.js pins the workflow and what the script assumes of the Dockerfile. Refs #24 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
3.3 KiB
JavaScript
93 lines
3.3 KiB
JavaScript
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
// Pins on the CI workflow and on what the boot check assumes of the Dockerfile.
|
|
// The runtime stays 0-dependency and vitest ships no YAML parser, so these read
|
|
// the files with narrow regexes: each one looks at a single key, never at the
|
|
// YAML structure as a whole.
|
|
|
|
const ROOT = path.join(__dirname, "..");
|
|
const CI_PATH = path.join(ROOT, ".forgejo", "workflows", "ci.yml");
|
|
const ci = fs.readFileSync(CI_PATH, "utf-8");
|
|
const dockerfile = fs.readFileSync(path.join(ROOT, "Dockerfile"), "utf-8");
|
|
|
|
// Comment lines stripped, so an explanation that quotes a key cannot satisfy
|
|
// (or break) a pin.
|
|
const ciCode = ci
|
|
.split("\n")
|
|
.filter((line) => !/^\s*#/.test(line))
|
|
.join("\n");
|
|
|
|
// Lines of a top-level-indented block (` pull_request:`), up to the next key at
|
|
// the same or a lower indentation.
|
|
function block(text, key) {
|
|
const lines = text.split("\n");
|
|
const start = lines.findIndex((l) => new RegExp(`^(\\s*)${key}:`).test(l));
|
|
if (start === -1) return null;
|
|
const indent = lines[start].match(/^(\s*)/)[1].length;
|
|
const out = [lines[start]];
|
|
for (const line of lines.slice(start + 1)) {
|
|
if (line.trim() === "") continue;
|
|
if (line.match(/^(\s*)/)[1].length <= indent) break;
|
|
out.push(line);
|
|
}
|
|
return out.join("\n");
|
|
}
|
|
|
|
describe("CI workflow", () => {
|
|
test("runs in the Dockerfile's own base image", () => {
|
|
const from = dockerfile.match(/^\s*FROM\s+(\S+)/m);
|
|
const container = ciCode.match(/^\s*container:\s*["']?([^"'\s]+)["']?\s*$/m);
|
|
|
|
expect(from).not.toBeNull();
|
|
expect(container).not.toBeNull();
|
|
expect(container[1]).toBe(from[1]);
|
|
});
|
|
|
|
test("runs the unit tests, the socket pass and the boot check", () => {
|
|
expect(ciCode).toMatch(/^\s*run:\s*npm ci\s*$/m);
|
|
expect(ciCode).toMatch(/^\s*run:\s*npm test\s*$/m);
|
|
expect(ciCode).toMatch(/^\s*run:\s*bash test-curl\.sh\s*$/m);
|
|
expect(ciCode).toMatch(/^\s*run:\s*sh scripts\/boot-from-copy\.sh\s*$/m);
|
|
});
|
|
|
|
test("builds every pull request, whatever branch it targets", () => {
|
|
// A `branches: [main]` here would leave the intermediate links of a stack
|
|
// without any status.
|
|
const pr = block(ciCode, "pull_request");
|
|
|
|
expect(pr).not.toBeNull();
|
|
expect(pr).not.toMatch(/branches(-ignore)?:/);
|
|
});
|
|
|
|
test("builds pushes to main", () => {
|
|
const push = block(ciCode, "push");
|
|
|
|
expect(push).not.toBeNull();
|
|
expect(push).toMatch(/branches:\s*\[\s*main\s*\]/);
|
|
});
|
|
|
|
test("avoids the two forms the runner rejects before the first command", () => {
|
|
// `defaults:` anywhere, and `working-directory` on a `uses:` step.
|
|
expect(ciCode).not.toMatch(/^\s*defaults:/m);
|
|
const steps = ciCode.split(/^\s*- (?=name:|uses:|run:)/m);
|
|
for (const step of steps) {
|
|
if (/^\s*uses:|\n\s*uses:/.test(step)) expect(step).not.toMatch(/working-directory:/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("what the boot check assumes of the Dockerfile", () => {
|
|
test("the image still drops to the node user, as the script does", () => {
|
|
expect(dockerfile).toMatch(/^\s*USER\s+node\s*$/m);
|
|
});
|
|
|
|
test("CMD is in exec form, the only one the script parses", () => {
|
|
const cmd = dockerfile.match(/^\s*CMD\s+(.+)$/m);
|
|
|
|
expect(cmd).not.toBeNull();
|
|
const argv = JSON.parse(cmd[1]);
|
|
expect(Array.isArray(argv)).toBe(true);
|
|
expect(argv.length).toBeGreaterThan(0);
|
|
});
|
|
});
|