1a88ab3199
The v0.8 milestone: installable on any Debian-based server via docker (Gitea registry image, honest sandbox degrade), deb (amd64+arm64, advisory postinst), and arch-aware binary — with release consistency enforced by pre-commit hooks + Gitea Actions (3-runner fleet on CT 113, machine-made releases with the full 8-asset matrix + registry image), the CLI carrying the full suite (guide, channel-aware doctor), and the admin runbook (deploy/README.md, evidence-backed recipes). ---ci--- phase: 6 milestone: v0.8 status: complete requirements: covered: [REQ-8-001, REQ-8-002, REQ-8-003, REQ-8-004, REQ-8-005] partial: [] ---/ci---
136 lines
6.0 KiB
TypeScript
136 lines
6.0 KiB
TypeScript
// Per-channel E2Es (v0.8 P4, REQ-8-004, MH-4d): docker + deb channels,
|
|
// env-gated like the v0.4 fresh-clone E2E (D-055 pattern — visible skip,
|
|
// never a silent green). Both need docker + the released artifacts:
|
|
// CLI_E2E_DOCKER=1 (pnpm cli:test:e2e:docker) — pulls the latest
|
|
// registry image, runs it, in-container doctor/verify,
|
|
// honest refusals.
|
|
// CLI_E2E_DEB=1 (pnpm cli:test:e2e:deb) — downloads the latest
|
|
// release's amd64 deb, installs in a fresh debian
|
|
// container, advisory doctor, guide.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const FORGE = "https://git.coreci.dev";
|
|
|
|
function sh(cmd: string, args: string[], opts: { cwd?: string; timeout?: number } = {}) {
|
|
return spawnSync(cmd, args, {
|
|
cwd: opts.cwd,
|
|
encoding: "utf8",
|
|
timeout: opts.timeout ?? 300_000,
|
|
env: { ...process.env },
|
|
});
|
|
}
|
|
|
|
function latestRelease(): { tag: string; assets: Array<{ name: string; url: string }> } {
|
|
const api = `${FORGE}/api/v1/repos/coreci/nextcraft/releases/latest`;
|
|
const res = spawnSync("curl", ["-fsSL", "--max-time", "30", api], { encoding: "utf8" });
|
|
assert.ok(res.status === 0, `release API reachable: ${res.stderr}`);
|
|
const raw = JSON.parse(res.stdout);
|
|
return {
|
|
tag: raw.tag_name,
|
|
assets: (raw.assets ?? []).map((a: { name: string; browser_download_url: string }) => ({
|
|
name: a.name,
|
|
url: a.browser_download_url,
|
|
})),
|
|
};
|
|
}
|
|
|
|
test(
|
|
"docker-channel E2E: pull image -> run -> in-container doctor/verify -> refusals",
|
|
{
|
|
skip: process.env.CLI_E2E_DOCKER
|
|
? false
|
|
: "env-gated: set CLI_E2E_DOCKER=1 (pnpm cli:test:e2e:docker) — needs docker + the registry image",
|
|
},
|
|
async () => {
|
|
assert.ok(sh("docker", ["info"]).status === 0, "docker daemon reachable");
|
|
const rel = latestRelease();
|
|
|
|
// Pull the release image
|
|
const pull = sh("docker", ["pull", `git.coreci.dev/coreci/nextcraft:${rel.tag}`]);
|
|
assert.ok(pull.status === 0, `image pull: ${pull.stderr}`);
|
|
|
|
// Run + probe + refusals in one pass (shell script via docker)
|
|
const script = `set -e
|
|
docker rm -f nc-e2e >/dev/null 2>&1 || true
|
|
docker run -d --name nc-e2e git.coreci.dev/coreci/nextcraft:${rel.tag} >/dev/null
|
|
ok=""
|
|
for i in $(seq 1 30); do
|
|
if docker exec nc-e2e python3 -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8420/health', timeout=3).status == 200 else 1)" 2>/dev/null; then ok=1; break; fi
|
|
sleep 2
|
|
done
|
|
[ -n "$ok" ] || { echo "HEALTH-FAIL"; docker logs nc-e2e 2>&1 | tail -20; exit 1; }
|
|
echo HEALTH-OK
|
|
docker exec nc-e2e nextcraft --version
|
|
docker exec nc-e2e nextcraft guide docker | grep -q "docker run" && echo GUIDE-OK
|
|
docker exec nc-e2e nextcraft deploy; rc=$?
|
|
[ "$rc" = "1" ] && echo DEPLOY-REFUSED-OK || { echo "DEPLOY-NOT-REFUSED rc=$rc"; exit 1; }
|
|
docker rm -f nc-e2e >/dev/null
|
|
echo E2E-DONE
|
|
`;
|
|
const run = sh("bash", ["-c", script]);
|
|
assert.ok(run.status === 0, `docker E2E failed:\n${run.stdout}\n${run.stderr}`);
|
|
assert.ok(run.stdout.includes("HEALTH-OK"));
|
|
assert.ok(run.stdout.includes(rel.tag), "--version reports the release tag");
|
|
assert.ok(run.stdout.includes("GUIDE-OK"), "in-container guide works");
|
|
assert.ok(run.stdout.includes("DEPLOY-REFUSED-OK"), "in-container deploy refuses honestly");
|
|
assert.ok(run.stdout.includes("E2E-DONE"), "clean teardown");
|
|
},
|
|
);
|
|
|
|
test(
|
|
"deb-channel E2E: latest amd64 deb -> fresh debian container -> advisory doctor + guide",
|
|
{
|
|
skip: process.env.CLI_E2E_DEB
|
|
? false
|
|
: "env-gated: set CLI_E2E_DEB=1 (pnpm cli:test:e2e:deb) — needs docker + the released deb",
|
|
},
|
|
async () => {
|
|
assert.ok(sh("docker", ["info"]).status === 0, "docker daemon reachable");
|
|
const rel = latestRelease();
|
|
const ver = rel.tag.replace(/^v/, "");
|
|
const debAsset = rel.assets.find((a) => a.name === `nextcraft_${ver}_amd64.deb`);
|
|
assert.ok(debAsset, `amd64 deb on release ${rel.tag}`);
|
|
|
|
const work = mkdtempSync(join(tmpdir(), "nc-deb-e2e-"));
|
|
const debPath = join(work, debAsset.name);
|
|
const dl = sh("curl", ["-fsSL", "--max-time", "120", "-o", debPath, debAsset.url]);
|
|
assert.ok(dl.status === 0, `deb download: ${dl.stderr}`);
|
|
|
|
// Verify the checksum sidecar before install (the admin's real flow)
|
|
const sumAsset = rel.assets.find((a) => a.name === `${debAsset.name}.sha256`);
|
|
if (sumAsset) {
|
|
const sumDl = sh("curl", ["-fsSL", "--max-time", "30", sumAsset.url]);
|
|
const expected = sumDl.stdout.trim().split(/\s+/)[0];
|
|
const actual = sh("sha256sum", [debPath]).stdout.trim().split(/\s+/)[0];
|
|
assert.equal(actual, expected, "deb checksum verifies before install");
|
|
}
|
|
|
|
// Stream the deb into a fresh debian container (stdin tar — sibling
|
|
// bind mounts resolve on the host, the same lesson as the release lane)
|
|
const script = `set -e
|
|
mkdir -p /deb && tar -xf - -C /deb
|
|
dpkg -i /deb/*.deb >/tmp/dpkg.log 2>&1 || true
|
|
/usr/bin/nextcraft --version
|
|
/usr/bin/nextcraft guide deb | grep -q "dpkg -i" && echo GUIDE-OK
|
|
dpkg -s nextcraft | grep -q "Status: install ok installed" && echo DPKG-STATUS-OK
|
|
`;
|
|
const tarRun = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`tar -cf - -C "${work}" "${debAsset.name}" | docker run --rm -i debian:bookworm bash -c '${script}'`,
|
|
],
|
|
{ encoding: "utf8", timeout: 300_000 },
|
|
);
|
|
assert.ok(tarRun.status === 0, `deb E2E failed:\n${tarRun.stdout}\n${tarRun.stderr}`);
|
|
assert.ok(tarRun.stdout.includes(rel.tag), "--version reports the release tag");
|
|
assert.ok(tarRun.stdout.includes("GUIDE-OK"), "guide deb topic works");
|
|
assert.ok(tarRun.stdout.includes("DPKG-STATUS-OK"), "dpkg reports installed-ok (advisory postinst honored)");
|
|
},
|
|
);
|