ci: deploy the verified local image
All checks were successful
Test and deploy / Verify production build (push) Successful in 10m25s

Build and test the Next.js application once with persistent BuildKit caches, smoke-test the immutable runtime image, and activate that same image through the shared production Docker daemon with serialized rollback-safe deployment.
This commit is contained in:
2026-09-15 15:00:19 +03:30
parent 46495eb927
commit dcce6d219d
8 changed files with 319 additions and 130 deletions

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -Eeuo pipefail
image="${1:?runtime image is required}"
container_name="portfolio-smoke-${GITEA_RUN_ID:-$$}"
cleanup() {
docker rm --force "$container_name" >/dev/null 2>&1 || true
}
trap cleanup EXIT
cleanup
docker run --detach --name "$container_name" "$image" >/dev/null
for _ in {1..30}; do
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name")"
case "$status" in
healthy)
break
;;
unhealthy|exited|dead)
docker logs "$container_name" >&2
exit 1
;;
esac
sleep 2
done
if [[ "$(docker inspect --format '{{.State.Health.Status}}' "$container_name")" != "healthy" ]]; then
docker logs "$container_name" >&2
echo "Runtime image did not become healthy within 60 seconds." >&2
exit 1
fi
docker exec -i "$container_name" node <<'NODE'
const origin = 'http://127.0.0.1:3000';
async function expectOk(path, options) {
const response = await fetch(`${origin}${path}`, options);
if (!response.ok) throw new Error(`${path} returned ${response.status}`);
return response;
}
const english = await expectOk('/');
await expectOk('/fa');
await expectOk('/projects/interanet-solutions');
await expectOk('/fa/projects/interanet-solutions');
const html = await english.text();
const staticAsset = html.match(/(?:src|href)="(\/_next\/static\/[^"?#]+)/)?.[1];
if (!staticAsset) throw new Error('No built static asset was found in the home page.');
await expectOk(staticAsset);
const optimized = await expectOk(
'/_next/image?url=%2Fimages%2Favatar.png&w=256&q=75',
{ headers: { Accept: 'image/webp' } },
);
const contentType = optimized.headers.get('content-type') ?? '';
if (!/^image\/(?:webp|png)$/.test(contentType)) {
throw new Error(`Unexpected optimized image type: ${contentType}`);
}
if ((await optimized.arrayBuffer()).byteLength === 0) {
throw new Error('Optimized image response was empty.');
}
NODE