103 lines
4.2 KiB
Bash
103 lines
4.2 KiB
Bash
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
image="${1:?runtime image is required}"
|
|
container_name="portfolio-smoke-${GITEA_RUN_ID:-$$}"
|
|
volume_name="${container_name}-data"
|
|
|
|
cleanup() {
|
|
docker rm --force "$container_name" >/dev/null 2>&1 || true
|
|
docker volume rm "$volume_name" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
cleanup
|
|
docker volume create "$volume_name" >/dev/null
|
|
start_container() {
|
|
docker run --detach --name "$container_name" --mount "type=volume,source=$volume_name,target=/data" --env SITE_URL=http://127.0.0.1:3000 "$image" >/dev/null
|
|
}
|
|
start_container
|
|
|
|
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 "$container_name" node -e "fetch('http://127.0.0.1:3000/').then(async r=>{if(!r.ok||!(await r.text()).includes('Your Name'))process.exit(1)})"
|
|
# Exercise the packaged maintenance command and native SQLite driver in the image.
|
|
docker exec "$container_name" node maintenance.mjs init
|
|
docker exec --env OWNER_EMAIL=smoke@example.com --env OWNER_PASSWORD=Isolated-container-smoke-password-2026 "$container_name" node maintenance.mjs create-owner
|
|
|
|
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('/projects/sample-projects');
|
|
|
|
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);
|
|
|
|
|
|
await expectOk('/images/portfolio-placeholder.svg');
|
|
|
|
NODE
|
|
|
|
# Upload through the real authenticated endpoint, then replace the container.
|
|
docker exec -i "$container_name" node --input-type=module <<'NODE'
|
|
import { writeFileSync } from 'node:fs';
|
|
const origin = 'http://127.0.0.1:3000';
|
|
const login = await fetch(`${origin}/api/auth/sign-in/email`, {
|
|
method: 'POST', headers: { Origin: origin, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: 'smoke@example.com', password: 'Isolated-container-smoke-password-2026' }),
|
|
});
|
|
if (!login.ok) throw new Error('Container owner login failed');
|
|
const cookie = login.headers.getSetCookie().map(value => value.split(';')[0]).join('; ');
|
|
const form = new FormData();
|
|
form.set('file', new Blob(['%PDF-1.4\nContainer persistence check\n%%EOF'], { type: 'application/pdf' }), 'check.pdf');
|
|
const response = await fetch(`${origin}/api/admin/media`, { method: 'POST', headers: { Origin: origin, Cookie: cookie }, body: form });
|
|
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
|
|
const uploaded = await response.json();
|
|
writeFileSync('/data/smoke-media-key', uploaded.key);
|
|
NODE
|
|
docker rm --force "$container_name" >/dev/null
|
|
start_container
|
|
docker exec -i "$container_name" node --input-type=module <<'NODE'
|
|
import { readFileSync } from 'node:fs';
|
|
const origin = 'http://127.0.0.1:3000';
|
|
let ready = false;
|
|
for (let attempt = 0; attempt < 30; attempt++) {
|
|
try { if ((await fetch(`${origin}/api/health`)).ok) { ready = true; break; } } catch {}
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
if (!ready) throw new Error('Replacement container did not start');
|
|
const page = await fetch(`${origin}/projects/sample-projects`);
|
|
if (!page.ok) throw new Error('Database did not survive container replacement');
|
|
const key = readFileSync('/data/smoke-media-key', 'utf8');
|
|
const media = await fetch(`${origin}/media/${key}`);
|
|
if (!media.ok || !(await media.text()).includes('Container persistence check')) throw new Error('Upload did not survive container replacement');
|
|
console.log('Container startup, native SQLite, owner login, uploads, and replacement persistence passed.');
|
|
NODE
|