feat(docker): guide clone installation and validate paired update artifacts
This commit is contained in:
1 parent
a4266f297c
commit
8f55ff2d17
39 files changed
+473
-73
No files matched your search
@@ -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 });
|
||||
}
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
Reference in new issue
Block a user