quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
Implement reliable agent apply, telemetry v2, analytics retention, guarded rollouts, enrollment, labels and webhooks. Improve ReUI frontend data safety and bounded list loading, and add Docker smoke, recovery and resource checks. Document local verification and pending native acceptance.
101 lines
4.5 KiB
Bash
101 lines
4.5 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"
|
|
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() {
|
|
docker exec "$container" node -e "const D=require('node:module').createRequire('/app/node_modules/@evofw/db/package.json')('better-sqlite3'); const db=new D('/data/app.db'); if(process.argv[1]==='write') db.prepare('INSERT OR REPLACE INTO settings(key,value) VALUES (?,?)').run('ci-smoke','survives'); else if(db.prepare('SELECT value FROM settings WHERE key=?').get('ci-smoke')?.value!=='survives')process.exitCode=1; db.close()" "$1"
|
|
}
|
|
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)'
|