Publish the native release as a container image and document deployment
Checks / web (push) Failing after 19s
Private ROM checks / private-rom (push) Failing after 25m51s

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>
This commit is contained in:
kawaandClaude Opus 5.5 committed 2026-10-18 00:27:23 +02:00
1 parent d3e3faff72
commit b33725adfa
9 files changed
+584

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
import { appendFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { pathToFileURL } from "node:url";
/** Keep manual test builds out of the production channel. */
export function imagePublication({ event, ref, inputTag, revision }) {
if (!/^[0-9a-f]{40}$/.test(revision))
throw new Error("Expected the full SHA-1 of the checked-out commit.");
let tag, channel;
if (event === "workflow_dispatch") {
tag = inputTag ?? "";
const commit = /^dev-([0-9a-f]{7,40})$/.exec(tag)?.[1];
if (!commit || tag !== tag.trim())
throw new Error(
"Manual image tags must be dev-<commit SHA> (7-40 lowercase hex characters).",
);
if (!revision.startsWith(commit))
throw new Error(
"The dev tag does not match the checked-out commit. Select the matching source branch or Git tag.",
);
channel = "dev";
} else if (event === "push" && ref?.startsWith("refs/tags/")) {
tag = ref.slice("refs/tags/".length);
if (
!/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(tag) ||
tag.length > 128 ||
tag !== tag.trim()
)
throw new Error(
"Production requires a stable Git version tag: vMAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH.",
);
channel = "prod";
} else {
throw new Error(
"Image publication requires a version tag push or a manual dev build.",
);
}
return { tag, channel, revision };
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
const env = process.env;
const revision = execFileSync("git", ["rev-parse", "HEAD"], {
encoding: "utf8",
}).trim();
const publication = imagePublication({
event: env.EVENT_NAME,
ref: env.GIT_REF,
inputTag: env.INPUT_TAG,
revision,
});
const registry = new URL(env.SERVER_URL).host;
const image = `${registry}/${env.REPOSITORY.toLowerCase()}`;
const source = `${env.SERVER_URL.replace(/\/$/, "")}/${env.REPOSITORY}`;
const outputs = { registry, image, source, ...publication };
appendFileSync(
env.GITHUB_OUTPUT,
Object.entries(outputs)
.map(([key, value]) => `${key}=${value}\n`)
.join(""),
);
console.log(
`Publishing ${image}:${publication.tag} and :${publication.channel} from ${revision}`,
);
}
+53
View File
@@ -0,0 +1,53 @@
import { createHmac } from "node:crypto";
import { pathToFileURL } from "node:url";
export async function notifyDockhand({ url, secret }, send = fetch) {
if (!url && !secret) return false;
if (!url || !secret)
throw new Error(
"Configure both Dockhand webhook URL and secret for this channel.",
);
const target = new URL(url);
if (!["http:", "https:"].includes(target.protocol))
throw new Error("Dockhand webhook requires an HTTP(S) URL.");
const body = "{}";
const signature = createHmac("sha256", secret).update(body).digest("hex");
const response = await send(target, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Hub-Signature-256": `sha256=${signature}`,
},
body,
redirect: "error",
signal: AbortSignal.timeout(60_000),
});
if (!response.ok)
throw new Error(
`Dockhand rejected the deployment request (HTTP ${response.status}).`,
);
return true;
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
try {
const sent = await notifyDockhand({
url: process.env.DOCKHAND_WEBHOOK_URL,
secret: process.env.DOCKHAND_WEBHOOK_SECRET,
});
console.log(
sent
? "Dockhand deployment requested. Check the stack's deployment status in Dockhand."
: "Dockhand webhook not configured for this channel; image publication is complete.",
);
} catch {
// Do not print the URL, secret or server response into the job log.
console.error(
"Dockhand deployment request failed. The image was already published; check this channel's webhook configuration and connectivity.",
);
process.exitCode = 1;
}
}