Files
EvoFirewall/scripts/ci/image-smoke.sh
T
Denozordec ce90fa9701
quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 27s
quality / web (push) Successful in 48s
quality / api (push) Successful in 1m13s
quality / linux-agent-smoke (push) Successful in 22s
quality / docker-check (push) Successful in 4m43s
CD / quality (push) Successful in 7m42s
CD / publish (push) Successful in 5m17s
fix(ci): restore backups through remote Docker API
2026-10-26 14:28:36 +07:00

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=$?
if docker inspect "$container" >/dev/null 2>&1; then docker logs "$container" >>"$tmp/container.log" 2>&1 || true; fi
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)'