quality / changes (push) Successful in 6s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 28s
quality / web (push) Successful in 57s
quality / api (push) Successful in 1m35s
quality / linux-agent-smoke (push) Successful in 24s
quality / docker-check (push) Failing after 3m20s
CD / quality (push) Failing after 6m56s
CD / publish (push) Skipped
128 lines
5.4 KiB
Bash
128 lines
5.4 KiB
Bash
#!/usr/bin/env sh
|
|
# Runs only disposable named volumes. IMAGE must already be loaded locally.
|
|
set -eu
|
|
image="${1:?Usage: image-smoke.sh IMAGE [PREVIOUS_IMAGE]}"
|
|
previous="${2:-}"
|
|
if [ -n "$previous" ]; then
|
|
printf '%s' "$previous" | grep -Eq '@sha256:[0-9a-f]{64}$' || { echo 'PREVIOUS_IMAGE must be pinned by digest' >&2; exit 1; }
|
|
elif [ "${REQUIRE_UPGRADE:-false}" = true ] && [ "${FIRST_RELEASE:-false}" != true ]; then
|
|
echo 'Release requires a digest-pinned previous image; set FIRST_RELEASE=true only for an explicitly approved first release' >&2
|
|
exit 1
|
|
fi
|
|
id="evofw-smoke-$(date +%s)-$$"
|
|
volume="$id-data"
|
|
restore_volume="$id-restore"
|
|
container="$id"
|
|
marker_id=''
|
|
marker_name="$id-persistence"
|
|
tmp="$(mktemp -d)"
|
|
cleanup() {
|
|
result=$?
|
|
docker logs "$container" >>"$tmp/container.log" 2>&1 || true
|
|
if [ "$result" -ne 0 ]; then cat "$tmp/container.log" >&2; fi
|
|
docker rm -f "$container" >/dev/null 2>&1 || true
|
|
docker volume rm "$volume" "$restore_volume" >/dev/null 2>&1 || true
|
|
if [ -n "${SMOKE_ARTIFACT_DIR:-}" ]; then mkdir -p "$SMOKE_ARTIFACT_DIR"; cp "$tmp/container.log" "$SMOKE_ARTIFACT_DIR/"; fi
|
|
rm -rf "$tmp"
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
docker volume create "$volume" >/dev/null
|
|
docker volume create "$restore_volume" >/dev/null
|
|
start() {
|
|
docker run -d --name "$container" --mount "type=volume,src=$2,dst=/data" \
|
|
-e EVOFW_ALLOW_UNSAFE=true -e AUTH_REQUIRED=false "$1" >/dev/null
|
|
attempts=0
|
|
until docker exec "$container" node -e "fetch('http://127.0.0.1:8080/ready',{signal:AbortSignal.timeout(1000)}).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"; do
|
|
attempts=$((attempts + 1))
|
|
[ "$attempts" -lt 120 ] || { docker logs "$container"; exit 1; }
|
|
[ "$(docker inspect -f '{{.State.Running}}' "$container")" = true ] || { docker logs "$container"; exit 1; }
|
|
sleep 1
|
|
done
|
|
}
|
|
marker() {
|
|
value="$(docker exec -i "$container" node --input-type=module - "$1" "$marker_id" "$marker_name" <<'MARKER'
|
|
import assert from 'node:assert/strict'
|
|
const [mode, existingId, name] = process.argv.slice(2)
|
|
assert.ok(mode === 'write' || mode === 'read')
|
|
const call = async (path, options = {}) => {
|
|
const response = await fetch(`http://127.0.0.1:8080/api/v1${path}`, { ...options, signal: AbortSignal.timeout(10000) })
|
|
assert.ok(response.ok, `Persistence marker HTTP ${response.status}`)
|
|
return response.json()
|
|
}
|
|
let id = existingId
|
|
if (mode === 'write' && !id) {
|
|
const created = await call('/lists', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, type: 'static', entries: ['192.0.2.123/32'] }) })
|
|
assert.ok(typeof created.id === 'string' && created.id.length > 0, 'Marker creation did not return an ID')
|
|
id = created.id
|
|
}
|
|
assert.ok(id, 'Persistence marker ID is missing')
|
|
const stored = await call(`/lists/${encodeURIComponent(id)}`)
|
|
assert.equal(stored.id, id)
|
|
assert.equal(stored.name, name)
|
|
assert.equal(stored.type, 'static')
|
|
assert.deepEqual(stored.entries, ['192.0.2.123/32'], 'Marker entries did not survive')
|
|
assert.equal(stored.entry_count, 1)
|
|
process.stdout.write(id)
|
|
MARKER
|
|
)"
|
|
if [ "$1" = write ]; then marker_id="$value"; fi
|
|
}
|
|
stop() {
|
|
docker stop -t 30 "$container" >/dev/null
|
|
[ "$(docker inspect -f '{{.State.ExitCode}}' "$container")" = 0 ]
|
|
[ "$(docker inspect -f '{{.State.OOMKilled}}' "$container")" = false ]
|
|
docker logs "$container" >>"$tmp/container.log" 2>&1
|
|
docker rm "$container" >/dev/null
|
|
}
|
|
if [ -n "$previous" ]; then
|
|
# Explicit previous version/digest is preferable to a mutable latest tag.
|
|
docker pull "$previous"
|
|
start "$previous" "$volume"
|
|
marker write
|
|
stop
|
|
start "$image" "$volume"
|
|
marker read
|
|
echo 'upgrade: passed'
|
|
else
|
|
if [ "${FIRST_RELEASE:-false}" = true ]; then
|
|
echo 'upgrade: first-release exception explicitly selected (no previous deployment)'
|
|
else
|
|
echo 'upgrade: NOT RUN (development smoke only; not release validation)'
|
|
fi
|
|
start "$image" "$volume"
|
|
fi
|
|
docker exec -i "$container" node --input-type=module <<'NODE'
|
|
import assert from 'node:assert/strict'
|
|
import { existsSync, readdirSync } from 'node:fs'
|
|
for (const path of ['/health', '/ready', '/api/v1/agents']) {
|
|
const response = await fetch(`http://127.0.0.1:8080${path}`)
|
|
assert.equal(response.status, 200, path)
|
|
await response.json()
|
|
}
|
|
for (const path of ['/', '/agents']) {
|
|
const response = await fetch(`http://127.0.0.1:8080${path}`)
|
|
assert.equal(response.status, 200, path)
|
|
assert.match(await response.text(), /<html/i)
|
|
}
|
|
for (const path of ['/v1/agent/install.sh', '/v1/agent/uninstall.sh', '/v1/agent/sync-script', '/v1/agent/mikrotik-install.rsc']) {
|
|
const response = await fetch(`http://127.0.0.1:8080${path}`)
|
|
assert.equal(response.status, 200, path)
|
|
assert.ok((await response.text()).length > 100, path)
|
|
}
|
|
assert.ok(existsSync('/app/node_modules/@evofw/db/migrations'))
|
|
assert.ok(existsSync('/app/dist/agent-scripts/install.sh'))
|
|
for (const name of ['src', 'test', '.turbo', 'pnpm-lock.yaml']) assert.ok(!existsSync(`/app/${name}`), name)
|
|
assert.ok(!readdirSync('/app/dist').some(name => name.endsWith('.sh') || name.endsWith('.rsc')), 'no duplicate root scripts')
|
|
NODE
|
|
marker write
|
|
sh deploy/docker/backup.sh "$container" "$tmp/backup.db"
|
|
stop
|
|
start "$image" "$volume"
|
|
marker read
|
|
stop
|
|
sh deploy/docker/restore.sh "$image" "$restore_volume" "$tmp/backup.db"
|
|
start "$image" "$restore_volume"
|
|
marker read
|
|
stop
|
|
echo 'image smoke: passed (migration, native SQLite, API, SPA, scripts, SIGTERM, persistence, online backup and restore)'
|