feat(cms): improve catalog, editorial recovery and operations
CI / check (push) Successful in 52s
CI / deploy (push) Successful in 2m10s
CI / publish-container (push) Failing after 1m18s

This commit is contained in:
Simo committed 2026-09-09 19:36:15 +02:00
1 parent 2fead134e6
commit c389c3893d
122 files changed
+8137 -703

No files matched your search

+7 -3
View File
@@ -93,6 +93,7 @@ for managed_name in epicnext-cms epicnext-cms-app; do
done
cp "$deploy_dir/.env" .env
pnpm install --frozen-lockfile
pnpm exec playwright install chromium
echo "Building $image"
DOCKER_BUILDKIT=1 docker build --network=host --progress=plain --cache-from epicnext-cms:latest \
@@ -159,8 +160,11 @@ candidate_attempted=1
)
healthy
node scripts/verify-deployed-release.mjs http://127.0.0.1:3002/api/health "$sha"
# Publish the latest alias only after health and release checks pass.
docker tag "$image" epicnext-cms:latest
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3002 pnpm test:e2e
# Publish the latest alias only after HTTP and browser checks pass.
verified_image="$(docker inspect --format '{{.Image}}' epicnext-cms-app)"
docker tag "$verified_image" "epicnext-cms:verified-$sha"
docker tag "$verified_image" epicnext-cms:latest
cutover_started=0
if [ "$backup_created" -eq 1 ]; then docker rm "$backup_name" || true; fi
if [ "$secondary_backup_created" -eq 1 ]; then docker rm "$secondary_backup" || true; fi
@@ -168,7 +172,7 @@ if [ "$secondary_backup_created" -eq 1 ]; then docker rm "$secondary_backup" ||
echo "Deployment verified: $sha"
# Retain the current and previous releases; do not remove arbitrary named tags.
while IFS= read -r tag; do
if [[ "$tag" =~ ^epicnext-cms:[0-9a-f]{40}$ ]] && [ "$tag" != "$image" ]; then
if [[ "$tag" =~ ^epicnext-cms:(verified-)?[0-9a-f]{40}$ ]] && [ "$tag" != "$image" ] && [ "$tag" != "epicnext-cms:verified-$sha" ]; then
tagged_image="$(docker image inspect --format '{{.Id}}' "$tag" 2>/dev/null || true)"
if [ -n "$tagged_image" ] && [ "$tagged_image" != "$previous_image" ]; then docker image rm "$tag" || true; fi
fi
+11
View File
@@ -260,6 +260,13 @@ async function publishScheduledArticles(): Promise<void> {
}
}
async function reportWorkerHeartbeat(): Promise<void> {
if (!redis) return;
await redis
.set("cms:jobs-worker:heartbeat", new Date().toISOString(), "EX", 600)
.catch((error) => captureWorkerError(error, "WorkerHeartbeat"));
}
async function main() {
new Cron("* * * * *", () => {
runCatalogExport().catch((e) =>
@@ -307,6 +314,10 @@ async function main() {
module: "jobs",
});
new Cron("* * * * *", () => {
void reportWorkerHeartbeat();
});
await reportWorkerHeartbeat();
await Promise.all([
backupEmulatorJar(),
cleanupOldLogs(),
+11 -1
View File
@@ -21,7 +21,17 @@ trap 'rm -rf -- "$DOCKER_CONFIG" "$context"' EXIT
git archive HEAD | tar -x -C "$context"
printf '%s' "$REGISTRY_TOKEN" | docker login "$registry" --username "$REGISTRY_USER" --password-stdin
unset REGISTRY_TOKEN
docker build --network=host --build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" "$context"
# On the shared runner, publish the exact image already verified by deployment.
local_image="epicnext-cms:$sha"
local_revision="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$local_image" 2>/dev/null || true)"
local_id="$(docker image inspect --format '{{.Id}}' "$local_image" 2>/dev/null || true)"
verified_id="$(docker image inspect --format '{{.Id}}' "epicnext-cms:verified-$sha" 2>/dev/null || true)"
if [[ "$local_revision" = "$sha" && -n "$local_id" && "$local_id" = "$verified_id" ]]; then
docker tag "$verified_id" "$image"
echo "Reusing verified release image $local_image"
else
docker build --network=host --build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" "$context"
fi
docker build --network=host --target migrations -t "$image-migrations" "$context"
node scripts/verify-portable-image.mjs "$image" "$sha"
# Publish only after the same application image passed both runtime configurations.
+2 -24
View File
@@ -2,6 +2,7 @@
import { execFileSync } from "node:child_process";
import { createServer } from "node:http";
import { setTimeout as delay } from "node:timers/promises";
import { verifyRuntimeMetadata } from "./verify-runtime-metadata.mjs";
const [image, release] = process.argv.slice(2);
if (!image || !/^[0-9a-f]{40}$/.test(release ?? ""))
@@ -95,30 +96,7 @@ try {
await delay(1000);
}
if (!ready) throw new Error(`${hotel}: expected HTTP release not served`);
const page = await fetch(`${base}/login`, {
signal: AbortSignal.timeout(30000),
});
const html = await page.text();
if (
!page.ok ||
!html.includes(`Fixture ${hotel}`) ||
!html.includes(base)
)
throw new Error(
`${hotel}: hotel name/domain were not resolved at runtime`,
);
const manifest = await fetch(`${base}/manifest.webmanifest`, {
signal: AbortSignal.timeout(15000),
});
if ((await manifest.json()).name !== `Fixture ${hotel}`)
throw new Error(`${hotel}: manifest contains build-time settings`);
for (const path of ["/robots.txt", "/sitemap.xml"]) {
const response = await fetch(base + path, {
signal: AbortSignal.timeout(15000),
});
if (!response.ok || !(await response.text()).includes(base))
throw new Error(`${hotel}: ${path} contains build-time domain`);
}
await verifyRuntimeMetadata(base, hotel);
const avatar = await fetch(
`${base}/api/imaging/avatar?figure=hd-180-1&img_format=png`,
{ signal: AbortSignal.timeout(15000) },
+19
View File
@@ -0,0 +1,19 @@
// Metadata handlers can resolve runtime settings without a working application DB.
export async function verifyRuntimeMetadata(base, hotel) {
const manifest = await fetch(`${base}/manifest.webmanifest`, {
signal: AbortSignal.timeout(15000),
});
if (!manifest.ok)
throw new Error(`${hotel}: manifest HTTP ${manifest.status}`);
if ((await manifest.json()).name !== `Fixture ${hotel}`)
throw new Error(`${hotel}: manifest contains build-time settings`);
for (const path of ["/robots.txt", "/sitemap.xml"]) {
const response = await fetch(base + path, {
signal: AbortSignal.timeout(15000),
});
if (!response.ok)
throw new Error(`${hotel}: ${path} HTTP ${response.status}`);
if (!(await response.text()).includes(base))
throw new Error(`${hotel}: ${path} contains build-time domain`);
}
}