feat(docker): guide clone installation and validate paired update artifacts
CI / check (push) Failing after 1m18s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped

This commit is contained in:
Simo committed 2026-09-13 17:57:14 +02:00
1 parent a4266f297c
commit 8f55ff2d17
39 files changed
+473 -73

No files matched your search

+48
View File
@@ -146,3 +146,51 @@ it("hands a completed installation to the existing updater", () => {
expect(result.output).toContain("Update invoked");
expect(result.profile).toContain("MODE=prebuilt");
});
it("defaults a new clone to source without requiring registry access", () => {
const result = configure(
"https://hotel.example\n\n",
"AUTH_SECRET=existing\n",
);
expect(result.status, result.output).toBe(0);
expect(result.profile).toContain("MODE=source");
});
it("keeps a saved prebuilt choice when the operator accepts defaults", () => {
const result = configure(
"\n\n",
"AUTH_SECRET=existing\n",
"MODE=prebuilt\nPUBLIC_URL=https://hotel.example\n",
);
expect(result.status, result.output).toBe(0);
expect(result.profile).toContain("MODE=prebuilt");
});
it("preflight treats dotenv as configuration data instead of executing host commands", () => {
const dir = mkdtempSync(join(tmpdir(), "cms-preflight-test-"));
try {
mkdirSync(join(dir, "scripts"));
copyFileSync(
resolve(root, "scripts/docker-preflight.sh"),
join(dir, "scripts/docker-preflight.sh"),
);
writeFileSync(join(dir, ".env"), "touch injected\n");
const result = spawnSync(bash, [join(dir, "scripts/docker-preflight.sh")], {
cwd: dir,
encoding: "utf8",
timeout: 15000,
env: {
...process.env,
ENV_FILE: join(dir, ".env"),
MYSQL_PORT: "1",
REDIS_PORT: "1",
CMS_PORT: "1",
BASH_ENV: resolve(root, "src/test/docker-install-harness.sh"),
},
});
if (result.error) throw result.error;
expect(existsSync(join(dir, "injected"))).toBe(false);
expect(result.stdout).toContain("Dotenv is not executed");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
+93 -21
View File
@@ -25,11 +25,15 @@ const bash =
.find((path) => existsSync(path)) ?? "bash")
: "bash";
const sha = "a".repeat(40);
function simulate(scenario: string) {
function simulate(scenario: string, args: string[] = []) {
const dir = mkdtempSync(join(tmpdir(), "cms-compose-test-"));
try {
mkdirSync(join(dir, "scripts"));
mkdirSync(join(dir, "logs"));
copyFileSync(
resolve(root, "scripts/docker-update-options.sh"),
join(dir, "scripts/docker-update-options.sh"),
);
copyFileSync(
resolve(root, "scripts/docker-config.sh"),
join(dir, "scripts/docker-config.sh"),
@@ -56,26 +60,30 @@ function simulate(scenario: string) {
join(dir, ".docker-install"),
`MODE=${scenario === "saved-source" ? "source" : "prebuilt"}\nPUBLIC_URL=https://saved.test\n`,
);
const result = spawnSync(bash, [join(dir, "scripts/docker-update.sh")], {
cwd: dir,
encoding: "utf8",
timeout: 25000,
env: {
...process.env,
BASH_ENV: resolve(root, "src/test/docker-update-harness.sh"),
TEST_DIR: dir.replaceAll("\\", "/"),
TEST_SHA: sha,
SCENARIO: scenario,
CMS_PUBLIC_URL: scenario.startsWith("saved-")
? undefined
: "https://example.test",
CMS_IMAGE_REPOSITORY: scenario.startsWith("saved-")
? undefined
: scenario.startsWith("registry")
? "registry.test/team/cms"
: "",
const result = spawnSync(
bash,
[join(dir, "scripts/docker-update.sh"), ...args],
{
cwd: dir,
encoding: "utf8",
timeout: 25000,
env: {
...process.env,
BASH_ENV: resolve(root, "src/test/docker-update-harness.sh"),
TEST_DIR: dir.replaceAll("\\", "/"),
TEST_SHA: sha,
SCENARIO: scenario,
CMS_PUBLIC_URL: scenario.startsWith("saved-")
? undefined
: "https://example.test",
CMS_IMAGE_REPOSITORY: scenario.startsWith("saved-")
? undefined
: scenario.startsWith("registry")
? "registry.test/team/cms"
: "",
},
},
});
);
if (result.error) throw result.error;
return {
status: result.status,
@@ -86,7 +94,12 @@ function simulate(scenario: string) {
: "",
};
} finally {
rmSync(dir, { recursive: true, force: true });
rmSync(dir, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 100,
});
}
}
describe("Docker clone updates", () => {
@@ -212,3 +225,62 @@ it("keeps source builds available for a saved source installation", () => {
expect(r.calls).toContain("docker compose build");
expect(r.calls).not.toContain("docker pull");
});
it("checks candidate storage before any database migration", () => {
const r = simulate("storage-failure");
expect(r.status).not.toBe(0);
expect(r.calls).toContain(
"docker compose run --rm --no-deps --entrypoint node",
);
expect(r.calls).not.toContain("--entrypoint pnpm");
expect(r.restored).toBe(false);
});
it("rejects a migration artifact from another revision before database changes", () => {
const r = simulate("migration-revision-mismatch");
expect(r.status).not.toBe(0);
expect(r.calls).not.toContain("--entrypoint pnpm");
expect(r.output).toContain("no matching release label");
});
it("uses both immutable artifacts without pulling Git when a checked-out release is requested", () => {
const digest = `sha256:${"1".repeat(64)}`;
const migrations = `sha256:${"2".repeat(64)}`;
const r = simulate("registry", [
"--skip-pull",
"--app-digest",
digest,
"--migrations-digest",
migrations,
]);
expect(r.status, r.output).toBe(0);
expect(r.calls).not.toContain("git pull");
expect(r.calls).toContain(`docker pull registry.test/team/cms@${digest}`);
expect(r.calls).toContain(`docker pull registry.test/team/cms@${migrations}`);
});
it("rejects incomplete artifact selection before contacting Git or Docker", () => {
const r = simulate("registry", ["--app-digest", `sha256:${"1".repeat(64)}`]);
expect(r.status).not.toBe(0);
expect(r.calls).toBe("");
});
it("rejects a legacy migrations image without release metadata before database changes", () => {
const r = simulate("migration-revision-missing");
expect(r.status).not.toBe(0);
expect(r.calls).not.toContain("--entrypoint pnpm");
expect(r.calls).not.toContain("compose up");
expect(r.output).toContain("Rebuild/publish both artifacts");
});
it("requires prebuilt mode when selecting immutable artifacts", () => {
const digest = `sha256:${"1".repeat(64)}`;
const r = simulate("saved-source", [
"--app-digest",
digest,
"--migrations-digest",
digest,
]);
expect(r.status).not.toBe(0);
expect(r.output).toContain("Digest selection requires prebuilt mode");
expect(r.calls).not.toContain("docker pull");
expect(r.calls).not.toContain("--entrypoint pnpm");
expect(r.calls).not.toContain("compose up");
});