ci: verify isolated Docker news journeys before merging to main
CI / check (push) Successful in 4m23s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped
CI / preflight (push) Successful in 1m36s

This commit is contained in:
Simo committed 2026-09-13 21:05:02 +02:00
1 parent 043c763484
commit 33a760ab6b
6 files changed
+274 -7

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
import { spawnSync } from "node:child_process";
import {
existsSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, dirname, join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
const bash =
process.platform === "win32"
? ((process.env.PATH ?? "")
.split(delimiter)
.flatMap((directory) => [
join(directory, "bash.exe"),
join(dirname(directory), "bin", "bash.exe"),
join(dirname(dirname(directory)), "bin", "bash.exe"),
])
.find(existsSync) ?? "bash")
: "bash";
const sha = "d".repeat(40);
function simulate(scenario: string) {
const directory = mkdtempSync(join(tmpdir(), "cms-preflight-test-"));
try {
writeFileSync(
join(directory, ".env"),
'echo unexpected-env-read > "$TEST_DIR/env-read"\n',
);
const shellDirectory = directory
.replaceAll("\\", "/")
.replace(/^([a-zA-Z]):/, (_, drive: string) => `/${drive.toLowerCase()}`);
const result = spawnSync(bash, [resolve(root, "scripts/ci-preflight.sh")], {
cwd: directory,
encoding: "utf8",
timeout: 15_000,
env: {
...process.env,
BASH_ENV: resolve(root, "src/test/ci-preflight-harness.sh"),
TEST_DIR: directory.replaceAll("\\", "/"),
TMPDIR: shellDirectory,
TEST_SHA: sha,
SCENARIO: scenario,
CMS_DEPLOY_DIR: "/must-not-read-production",
DATABASE_URL: "must-not-use-production",
},
});
if (result.error) throw result.error;
return {
status: result.status,
output: result.stdout + result.stderr,
calls: existsSync(join(directory, "calls"))
? readFileSync(join(directory, "calls"), "utf8")
: "",
remaining: readdirSync(directory).filter(
(name) => name !== "calls" && name !== ".env",
),
};
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
function imageFrom(calls: string) {
const image = calls.match(
/-t (epicnext-cms:preflight-[a-f0-9]{40}-[a-zA-Z0-9]{10}) /,
)?.[1];
expect(image).toBeDefined();
return image;
}
describe("isolated branch preflight", () => {
it("builds the production Dockerfile before testing that exact image and release", () => {
const result = simulate("success");
expect(result.status, result.output).toBe(0);
const image = imageFrom(result.calls);
expect(result.calls).toContain(`--build-arg NEXT_DEPLOYMENT_ID=${sha}`);
expect(result.calls).toContain("pnpm install --frozen-lockfile");
expect(result.calls).toContain("pnpm exec playwright install chromium");
expect(result.calls).toContain(
`news-image=${image} news-release=${sha} node --import tsx e2e/news-real/run.ts`,
);
expect(result.calls.indexOf("docker build")).toBeLessThan(
result.calls.indexOf("e2e/news-real/run.ts"),
);
expect(result.calls.indexOf("e2e/news-real/run.ts")).toBeLessThan(
result.calls.indexOf("docker image rm"),
);
expect(
result.calls.split("\n").filter((line) => line.startsWith("docker ")),
).toEqual([
expect.stringContaining("docker build --network=host"),
`docker image rm ${image}`,
]);
expect(result.calls).not.toMatch(
/UNEXPECTED|db:migrate|deploy|registry|prune|must-not/,
);
expect(result.remaining).toEqual([]);
});
it("stops after a failed build and cleans only its own attempted image", () => {
const result = simulate("build-failure");
expect(result.status).not.toBe(0);
expect(result.calls).not.toContain("e2e/news-real/run.ts");
expect(result.calls).toContain(
`docker image rm ${imageFrom(result.calls)}`,
);
expect(result.remaining).toEqual([]);
});
it.each(["news-failure", "cleanup-failure"])(
"fails and removes its private temporary directory after %s",
(scenario) => {
const result = simulate(scenario);
expect(result.status).not.toBe(0);
expect(result.calls).toContain("e2e/news-real/run.ts");
expect(result.calls).toContain(
`docker image rm ${imageFrom(result.calls)}`,
);
expect(result.calls).not.toMatch(
/prune|epicnext-cms:latest|epicnext-cms:previous/,
);
expect(result.remaining).toEqual([]);
},
);
it.each(["install-failure", "invalid-sha"])(
"does not build or remove images after %s",
(scenario) => {
const result = simulate(scenario);
expect(result.status).not.toBe(0);
expect(result.calls).toContain("git rev-parse HEAD");
if (scenario === "install-failure")
expect(result.calls).toContain("pnpm install --frozen-lockfile");
expect(result.calls).not.toContain("docker ");
expect(result.remaining).toEqual([]);
},
);
it("uses a distinct owned tag for separate runs of the same commit", () => {
const first = simulate("success");
const second = simulate("success");
expect(first.status, first.output).toBe(0);
expect(second.status, second.output).toBe(0);
expect(imageFrom(first.calls)).not.toBe(imageFrom(second.calls));
});
});
+27
View File
@@ -0,0 +1,27 @@
# Test-only command boundaries; the real preflight shell and filesystem cleanup run.
git() {
echo "git $*" >> "$TEST_DIR/calls"
[ "$*" = "rev-parse HEAD" ] || return 91
if [ "$SCENARIO" = invalid-sha ]; then echo invalid; else echo "$TEST_SHA"; fi
}
pnpm() {
echo "pnpm $*" >> "$TEST_DIR/calls"
[ "$SCENARIO" != install-failure ]
}
docker() {
echo "docker $*" >> "$TEST_DIR/calls"
case "$1 $2" in
'build --network=host') [ "$SCENARIO" != build-failure ] ;;
'image rm') [ "$SCENARIO" != cleanup-failure ] ;;
*) return 92 ;;
esac
}
node() {
echo "news-image=$NEWS_E2E_IMAGE news-release=$NEWS_E2E_RELEASE node $*" >> "$TEST_DIR/calls"
[ "$*" = "--import tsx e2e/news-real/run.ts" ] || return 93
[ "$SCENARIO" != news-failure ]
}
cp() { echo "UNEXPECTED cp $*" >> "$TEST_DIR/calls"; return 94; }
curl() { echo "UNEXPECTED curl $*" >> "$TEST_DIR/calls"; return 95; }
cat() { echo "UNEXPECTED cat $*" >> "$TEST_DIR/calls"; return 96; }
export -f git pnpm docker node cp curl cat