A static nginx image (unprivileged, read-only) of the production build, published to Gitea's registry for amd64 and arm64: version tags go to prod, manual dev-<commit> builds to dev, each smoke-tested first and optionally announced to Dockhand. The README describes running it with Docker Compose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { createHmac } from "node:crypto";
|
|
import { imagePublication } from "../scripts/image-publication.mjs";
|
|
import { notifyDockhand } from "../scripts/notify-dockhand.mjs";
|
|
|
|
const revision = "abcdef01".repeat(5);
|
|
const publish = (event: string, ref: string, inputTag = "") =>
|
|
imagePublication({ event, ref, inputTag, revision });
|
|
|
|
describe("image publication channels", () => {
|
|
it.each(["v1.2.3", "1.2.3", "v0.0.0", "v10.20.300"])(
|
|
"publishes stable version %s to prod",
|
|
(tag) =>
|
|
expect(publish("push", `refs/tags/${tag}`)).toEqual({
|
|
tag,
|
|
channel: "prod",
|
|
revision,
|
|
}),
|
|
);
|
|
|
|
it.each(["abcdef0", revision])(
|
|
"publishes a matching manual commit %s only to dev",
|
|
(commit) => {
|
|
expect(
|
|
publish("workflow_dispatch", "refs/heads/main", `dev-${commit}`),
|
|
).toEqual({ tag: `dev-${commit}`, channel: "dev", revision });
|
|
expect(
|
|
publish("workflow_dispatch", "refs/tags/v1.2.3", `dev-${commit}`)
|
|
.channel,
|
|
).toBe("dev");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"v1.2.3-rc.1",
|
|
"v1.2.3+build",
|
|
"v01.2.3",
|
|
"v1.2",
|
|
"prod",
|
|
"latest",
|
|
"dev-abcdef0",
|
|
"release/v1.2.3",
|
|
"v1.2.3\nchannel=prod",
|
|
"v1.2.3\n",
|
|
])("rejects non-release Git tag %s before publication", (tag) =>
|
|
expect(() => publish("push", `refs/tags/${tag}`)).toThrow(),
|
|
);
|
|
|
|
it.each([
|
|
"v1.2.3",
|
|
"prod",
|
|
"latest",
|
|
"dev",
|
|
"",
|
|
"dev-1234567",
|
|
"dev-abcdef",
|
|
"dev-ABCDEF0",
|
|
"dev-abcdef0\nchannel=prod",
|
|
"dev-abcdef0\n",
|
|
"dev-$(exit 1)",
|
|
])("rejects manual tag %s instead of promoting it", (tag) =>
|
|
expect(() =>
|
|
publish("workflow_dispatch", "refs/heads/main", tag),
|
|
).toThrow(),
|
|
);
|
|
|
|
it("does not publish branches or pull requests", () => {
|
|
expect(() => publish("push", "refs/heads/main")).toThrow();
|
|
expect(() => publish("pull_request", "refs/tags/v1.2.3")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("optional Dockhand deployment", () => {
|
|
const url = "https://dockhand.example.com/api/git/stacks/42/webhook";
|
|
const secret = "test-webhook-secret";
|
|
|
|
it("does not contact Dockhand when this channel has no configuration", async () => {
|
|
const send = vi.fn();
|
|
await expect(notifyDockhand({}, send)).resolves.toBe(false);
|
|
expect(send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([{ url }, { secret }])(
|
|
"rejects incomplete configuration",
|
|
async (config) => {
|
|
const send = vi.fn();
|
|
await expect(notifyDockhand(config, send)).rejects.toThrow();
|
|
expect(send).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("signs exactly the posted payload and refuses redirects", async () => {
|
|
const send = vi.fn().mockResolvedValue(new Response("{}"));
|
|
await expect(notifyDockhand({ url, secret }, send)).resolves.toBe(true);
|
|
const [target, request] = send.mock.calls[0];
|
|
expect(target.href).toBe(url);
|
|
expect(request.method).toBe("POST");
|
|
expect(request.body).toBe("{}");
|
|
expect(request.redirect).toBe("error");
|
|
expect(request.headers["X-Hub-Signature-256"]).toBe(
|
|
`sha256=${createHmac("sha256", secret).update(request.body).digest("hex")}`,
|
|
);
|
|
});
|
|
|
|
it("reports failed deployment requests", async () => {
|
|
const send = vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
new Response("private server details", { status: 403 }),
|
|
);
|
|
await expect(notifyDockhand({ url, secret }, send)).rejects.toThrow(
|
|
"HTTP 403",
|
|
);
|
|
});
|
|
});
|