fix(ci): harden website deploy rollback
This commit is contained in:
parent
96fe4d26c7
commit
7e4d13a85c
3 changed files with 193 additions and 5 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
|
|
@ -292,6 +294,59 @@ describe("workspace boundaries", () => {
|
|||
expect(deployScript).toContain("--dry-run");
|
||||
expect(deployScript).toContain("DEPLOY_RESTART_STRATEGY");
|
||||
});
|
||||
|
||||
test("rolls back the latest image when production smoke checks fail", () => {
|
||||
const result = runDeployScriptWithFakes(
|
||||
{
|
||||
curl: failingCurlCommand,
|
||||
git: fakeGitCommand,
|
||||
podman: fakePodmanCommand("dimensionlab-website.service"),
|
||||
},
|
||||
{
|
||||
DEPLOY_CONTAINER_CLI: "podman",
|
||||
DEPLOY_EVENT_NAME: "push",
|
||||
DEPLOY_REF: "refs/heads/main",
|
||||
DEPLOY_RESTART_STRATEGY: "quadlet-container",
|
||||
DEPLOY_SHA: "1234567890abcdef",
|
||||
DEPLOY_SMOKE_TIMEOUT_SECONDS: "0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("smoke checks failed");
|
||||
expect(result.stderr).toContain("rolling back to localhost/dimensionlab-website:rollback-");
|
||||
expect(result.log).toMatch(
|
||||
/tag localhost\/dimensionlab-website:latest localhost\/dimensionlab-website:rollback-\d{14}/,
|
||||
);
|
||||
expect(result.log).toMatch(
|
||||
/tag localhost\/dimensionlab-website:rollback-\d{14} localhost\/dimensionlab-website:latest/,
|
||||
);
|
||||
expect(result.log.match(/^stop dimensionlab-website$/gm)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("refuses stop-based deploys unless the container belongs to the expected unit", () => {
|
||||
const result = runDeployScriptWithFakes(
|
||||
{
|
||||
curl: passingCurlCommand,
|
||||
git: fakeGitCommand,
|
||||
podman: fakePodmanCommand("other.service"),
|
||||
},
|
||||
{
|
||||
DEPLOY_CONTAINER_CLI: "podman",
|
||||
DEPLOY_EVENT_NAME: "push",
|
||||
DEPLOY_REF: "refs/heads/main",
|
||||
DEPLOY_RESTART_STRATEGY: "quadlet-container",
|
||||
DEPLOY_SHA: "1234567890abcdef",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain(
|
||||
"refusing to stop dimensionlab-website; expected PODMAN_SYSTEMD_UNIT=dimensionlab-website.service",
|
||||
);
|
||||
expect(result.log).not.toContain("build ");
|
||||
expect(result.log).not.toContain("stop dimensionlab-website");
|
||||
});
|
||||
});
|
||||
|
||||
type WorkspacePackageExport =
|
||||
|
|
@ -308,3 +363,86 @@ function expectPackageExport(
|
|||
): void {
|
||||
expect(actual).toMatchObject(expected);
|
||||
}
|
||||
|
||||
function runDeployScriptWithFakes(
|
||||
commands: Record<string, string>,
|
||||
env: Record<string, string>,
|
||||
): { log: string; status: number | null; stderr: string; stdout: string } {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "dimensionlab-deploy-test-"));
|
||||
const logPath = join(tempDir, "commands.log");
|
||||
|
||||
for (const [name, source] of Object.entries(commands)) {
|
||||
const commandPath = join(tempDir, name);
|
||||
writeFileSync(commandPath, source);
|
||||
chmodSync(commandPath, 0o755);
|
||||
}
|
||||
|
||||
const result = spawnSync("bash", [join(root, "scripts/deploy-dimensionlab-website.sh")], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
DEPLOY_TEST_LOG: logPath,
|
||||
PATH: `${tempDir}:${process.env.PATH ?? ""}`,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
log: existsSync(logPath) ? readFileSync(logPath, "utf8") : "",
|
||||
status: result.status,
|
||||
stderr: result.stderr,
|
||||
stdout: result.stdout,
|
||||
};
|
||||
}
|
||||
|
||||
const fakeGitCommand = `#!/usr/bin/env bash
|
||||
case "$1" in
|
||||
branch)
|
||||
echo main
|
||||
;;
|
||||
rev-parse)
|
||||
echo 1234567890abcdef
|
||||
;;
|
||||
config|submodule)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
`;
|
||||
|
||||
function fakePodmanCommand(systemdUnit: string): string {
|
||||
return `#!/usr/bin/env bash
|
||||
printf '%s\\n' "$*" >> "$DEPLOY_TEST_LOG"
|
||||
if [ "$1" = "image" ] && [ "$2" = "inspect" ]; then
|
||||
if [ "$4" = "--format" ]; then
|
||||
echo sha256:new
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = "inspect" ]; then
|
||||
case "$*" in
|
||||
*PODMAN_SYSTEMD_UNIT*)
|
||||
echo ${systemdUnit}
|
||||
;;
|
||||
*State.Running*)
|
||||
echo true
|
||||
;;
|
||||
*'.Image'*|*'{{.Image}}'*)
|
||||
echo sha256:new
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
`;
|
||||
}
|
||||
|
||||
const failingCurlCommand = `#!/usr/bin/env bash
|
||||
printf 'curl %s\\n' "$*" >> "$DEPLOY_TEST_LOG"
|
||||
exit 22
|
||||
`;
|
||||
|
||||
const passingCurlCommand = `#!/usr/bin/env bash
|
||||
printf 'curl %s\\n' "$*" >> "$DEPLOY_TEST_LOG"
|
||||
if [ "$*" = *'/api/dashboard/tiles'* ]; then
|
||||
printf '{"state":"ready","tiles":[]}'
|
||||
fi
|
||||
`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue