feat: harden firewall policies telemetry and operations
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.
This commit is contained in:
Denozordec committed 2026-10-26 13:10:58 +07:00
1 parent aa475848fd
commit 9fa6269174
242 files changed
+19872 -3057

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { createMemoryDb, runMigrations } from '../../packages/db/dist/index.js'
import { canonicalCidr } from '../../packages/shared/dist/index.js'
import { seedFixture, benchmarkPrefix } from '../bench/fixture.mjs'
test('benchmark fixture satisfies migrated constraints at full 100-agent/50k scale', () => {
const { sqlite } = createMemoryDb()
try {
runMigrations(sqlite)
assert.equal(seedFixture(sqlite).integrity, 'ok')
for (const [table, expected] of [['agents', 100], ['agent_policy_sets', 100], ['ip_list_entries', 50_000]]) {
assert.equal(sqlite.prepare(`SELECT count(*) AS n FROM ${table}`).get().n, expected)
}
assert.equal(sqlite.prepare('SELECT priority FROM policy_rules WHERE id=?').get('bench-rule').priority, 10)
assert.deepEqual(sqlite.pragma('foreign_key_check'), [])
assert.throws(() => seedFixture(sqlite), /Fresh benchmark database required/)
assert.equal(sqlite.prepare('SELECT count(*) AS n FROM agents').get().n, 100)
const prefixes = Array.from({ length: 100_000 }, (_, i) => benchmarkPrefix(i, 11))
assert.equal(new Set(prefixes).size, 100_000)
for (const cidr of prefixes) assert.equal(canonicalCidr(cidr), cidr)
} finally { sqlite.close() }
})
+53
View File
@@ -0,0 +1,53 @@
// Schema definitions come from the compiled shared Zod contracts, with no new deps.
import { createRequire } from 'node:module'
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import * as shared from '../../packages/shared/dist/index.js'
const require = createRequire(resolve('packages/shared/package.json'))
const { z } = require('zod')
const definitions = {
Agent: 'agentSchema', AgentDiagnostics: 'agentDiagnosticsSchema', AgentPolicy: 'agentPolicySchema',
ApplyReport: 'applyReportBodySchema', ApplyReportV2: 'applyReportV2BodySchema', TelemetryQuality: 'telemetryQualitySchema', ApplyError: 'applyErrorSchema',
TimeseriesResponse: 'timeseriesResponseSchema', UiSummary: 'uiSummarySchema', ExplainResponse: 'explainResponseSchema',
CreateOverride: 'createOverrideBodySchema', IpOverride: 'ipOverrideSchema', IpList: 'ipListSchema',
CreateWebhook: 'webhookBodySchema', PatchWebhook: 'webhookPatchSchema',
EnrollWithCredential: 'enrollmentCredentialBodySchema', TokenRotationExchange: 'tokenRotationExchangeBodySchema',
CredentialStatus: 'credentialStatusSchema', TokenRotationAck: 'tokenRotationAckSchema',
AdminCredentialStatus: 'adminCredentialStatusSchema', ListImportSummary: 'listImportSummarySchema',
CreatePolicySnapshot: 'createPolicySnapshotSchema', PolicySnapshotDiffRequest: 'policySnapshotDiffSchema',
CreatePolicyRollout: 'createPolicyRolloutSchema', PolicyRolloutTransition: 'policyRolloutTransitionSchema', ReleasePolicyPin: 'releasePolicyPinSchema',
CreateAgentLabel: 'agentLabelBodySchema', PatchAgentLabel: 'patchAgentLabelSchema', PutAgentLabels: 'putAgentLabelsSchema',
PreviewLabelAssignment: 'previewLabelAssignmentSchema', ApplyLabelAssignment: 'applyLabelAssignmentSchema',
}
const schemas = Object.fromEntries(Object.entries(definitions).map(([name, exported]) => [name, z.toJSONSchema(shared[exported], { target: 'openapi-3.0', io: 'input', unrepresentable: 'any' })]))
const schemaRef = name => ({ $ref: `#/components/schemas/${name}` })
const operation = (summary, requestSchema, responseSchema, status = '200', security = 'bearerAuth') => ({
summary, tags: ['ops'], security: [{ [security]: [] }],
...(requestSchema ? { requestBody: { required: true, content: { 'application/json': { schema: schemaRef(requestSchema) } } } } : {}),
responses: { [status]: { description: summary, ...(responseSchema ? { content: { 'application/json': { schema: schemaRef(responseSchema) } } } : {}) }, '400': { description: 'Invalid request' }, '401': { description: 'Authentication required' }, '403': { description: 'Insufficient permission' }, '409': { description: 'Stale revision, conflicting operation or resource still referenced' } },
})
const idParameter = [{ name: 'id', in: 'path', required: true, schema: { type: 'string', minLength: 1, maxLength: 128 } }]
const pageParameters = [{ name: 'limit', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 50, default: 20 } }, { name: 'offset', in: 'query', schema: { type: 'integer', minimum: 0, maximum: 10000, default: 0 } }]
const paths = {
'/v1/agent/credentials': { get: operation('Agent credential status; no-store; approved current token only', null, 'CredentialStatus', '200', 'agentToken') },
'/v1/agent/token-rotation': { post: operation('Atomic token exchange; old-token retry only for exact completed exchange within 24 hours; no-store', 'TokenRotationExchange', 'TokenRotationAck', '200', 'agentToken') },
'/api/v1/agents/{id}/token-rotation': { parameters: idParameter, get: operation('Operator credential rotation status; agents read permission; no-store', null, 'AdminCredentialStatus'), post: operation('Request rotation for a supported approved client; agents write permission', null, null, '202') },
'/api/v1/policies/snapshots': { get: { ...operation('List immutable snapshot metadata; policies read permission'), parameters: pageParameters }, post: operation('Capture immutable effective policy for source agent; policies write permission', 'CreatePolicySnapshot', null, '201') },
'/api/v1/policies/snapshots/{id}': { parameters: idParameter, get: operation('Get immutable snapshot and payload; policies read permission'), delete: operation('Delete unreferenced snapshot; policies write permission') },
'/api/v1/policies/snapshots/{id}/diff': { parameters: idParameter, post: operation('Read-only bounded diff for up to 100 agents; policies write permission', 'PolicySnapshotDiffRequest') },
'/api/v1/policies/rollouts': { get: { ...operation('List rollout metadata; policies read permission'), parameters: pageParameters }, post: operation('Start explicitly selected canary rollout with generation checks; policies write permission', 'CreatePolicyRollout', null, '201') },
'/api/v1/policies/rollouts/{id}': { parameters: idParameter, get: operation('Get rollout and per-target acknowledgement; policies read permission'), delete: operation('Delete finished rollout after all pins released; policies write permission') },
'/api/v1/policies/rollouts/{id}/confirm': { parameters: idParameter, post: operation('Confirm rollout at expected version after fresh exact canary acknowledgement; policies write permission', 'PolicyRolloutTransition') },
'/api/v1/policies/rollouts/{id}/rollback': { parameters: idParameter, post: operation('Rollback to captured per-agent immutable policies; policies write permission', 'PolicyRolloutTransition') },
'/api/v1/agents/{id}/policy-pin/release': { parameters: idParameter, post: operation('Release selected rollout pin with expected generation; agents write permission', 'ReleasePolicyPin') },
'/api/v1/agent-labels': { get: { ...operation('List labels; agents read permission'), parameters: pageParameters }, post: operation('Create unique agent label; agents write permission', 'CreateAgentLabel', null, '201') },
'/api/v1/agent-labels/{id}': { parameters: idParameter, patch: operation('Update label; agents write permission', 'PatchAgentLabel'), delete: operation('Delete label without deleting assigned agents; agents write permission') },
'/api/v1/agents/{id}/labels': { parameters: idParameter, get: operation('Read agent label IDs; agents read permission'), put: operation('Replace agent labels atomically; agents write permission', 'PutAgentLabels') },
'/api/v1/agent-labels/preview-assignment': { post: operation('Preview add/replace policy-set assignment from explicit agent IDs or labels; policies read permission', 'PreviewLabelAssignment') },
'/api/v1/agent-labels/assign-policy-sets': { post: operation('Apply previously reviewed assignment with exact per-agent generations; policies write permission', 'ApplyLabelAssignment') },
}
const output = JSON.stringify({ 'x-generated-by': 'scripts/ci/generate-openapi-schemas.mjs; rebuild @evofw/shared first', components: { schemas }, paths }, null, 2) + '\n'
const path = resolve('docs/openapi.schemas.json')
if (process.argv.includes('--check')) {
if (readFileSync(path, 'utf8') !== output) throw new Error('OpenAPI schemas differ from shared contracts; rebuild shared and regenerate')
} else writeFileSync(path, output)
+100
View File
@@ -0,0 +1,100 @@
#!/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)'
+60
View File
@@ -0,0 +1,60 @@
"""Isolated namespace-only control-plane fixture for the shipped Linux agent."""
import http.server
import json
import pathlib
import sys
import socket
import threading
root = pathlib.Path(sys.argv[1]) if __name__ == "__main__" else pathlib.Path(".")
policy = {
"hash": "sha256:" + "1" * 64, "generation": 1, "apply_version": 2,
"default_action": "accept", "deny_cidrs": ["198.18.0.2/32", "2001:db8:1::2/128"],
"allow_cidrs": [], "port_rules": [], "sync_interval_sec": 60,
}
def acknowledgement(report):
"""The shipped durable sender only dequeues an exact ID+sequence ACK."""
return {"ok": True, "accepted": True, "report_id": report.get("report_id"), "sequence": report.get("sequence")}
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/v1/agent/policy":
result = dict(policy)
if (root / "invalid").exists():
result.update(hash="sha256:" + "2" * 64, deny_cidrs=["invalid-prefix"])
else:
result = {"ok": True}
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(result).encode())
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
if length > 1024 * 1024:
self.send_error(413)
return
report = json.loads(self.rfile.read(length))
with (root / "reports.jsonl").open("a") as handle:
handle.write(json.dumps(report) + "\n")
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(acknowledgement(report)).encode())
def log_message(self, *_):
pass
class IPv6Server(http.server.ThreadingHTTPServer):
address_family = socket.AF_INET6
if __name__ == "__main__":
server = http.server.ThreadingHTTPServer(("0.0.0.0", 0), Handler)
server6 = IPv6Server(("::", 0), Handler)
(root / "port6").write_text(str(server6.server_port))
threading.Thread(target=server6.serve_forever, daemon=True).start()
(root / "port").write_text(str(server.server_port))
server.serve_forever()
+21
View File
@@ -0,0 +1,21 @@
"""Pure contract regression; no sockets or firewall access."""
import importlib.util
import pathlib
import unittest
spec = importlib.util.spec_from_file_location("fixture", pathlib.Path(__file__).with_name("linux-agent-fixture.py"))
fixture = importlib.util.module_from_spec(spec)
spec.loader.exec_module(fixture)
class AcknowledgementTest(unittest.TestCase):
def test_exact_report_retry_has_stable_id_and_sequence_ack(self):
report = {"telemetry_version": 2, "report_id": "report-123", "sequence": 17}
expected = {"ok": True, "accepted": True, "report_id": "report-123", "sequence": 17}
self.assertEqual(fixture.acknowledgement(report), expected)
self.assertEqual(fixture.acknowledgement(dict(report)), expected)
self.assertNotEqual(fixture.acknowledgement({**report, "sequence": 18}), expected)
if __name__ == "__main__":
unittest.main()
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# All firewall mutations run only after unshare creates BOTH a network and mount
# namespace. Never installs/runs the agent against the runner's host firewall.
set -Eeuo pipefail
[[ $(uname -s) == Linux ]] || { echo 'Linux namespace runner required' >&2; exit 1; }
for command in unshare ip nft ipset iptables ip6tables python3 curl mount flock timeout; do command -v "$command" >/dev/null; done
temporary=$(mktemp -d)
trap 'rm -rf -- "$temporary"' EXIT
cp apps/api/src/agent-scripts/evofw-firewall.sh "$temporary/agent.sh"
cp scripts/ci/linux-agent-fixture.py "$temporary/server.py"
parent_net=$(readlink /proc/self/ns/net)
parent_mount=$(readlink /proc/self/ns/mnt)
for backend in nft ipset; do
rm -f "$temporary/port" "$temporary/port6" "$temporary/invalid"
unshare --net --mount --pid --fork --mount-proc env EVOFW_TEST_PARENT_NET="$parent_net" EVOFW_TEST_PARENT_MOUNT="$parent_mount" \
bash -s -- "$temporary" "$backend" <<'CHILD'
set -Eeuo pipefail
root=$1
backend=$2
unset http_proxy HTTP_PROXY https_proxy HTTPS_PROXY ALL_PROXY all_proxy
export NO_PROXY='*'
[[ $(readlink /proc/self/ns/net) != "$EVOFW_TEST_PARENT_NET" ]]
[[ $(readlink /proc/self/ns/mnt) != "$EVOFW_TEST_PARENT_MOUNT" ]]
mount --make-rprivate /
# Agent hardcoded config/state/log paths are private mounts, never host writes.
for directory in /etc /var/lib /var/log /run; do mount -t tmpfs tmpfs "$directory"; done
mkdir -p /etc/evofw /var/lib/evofw
ip link set lo up
ip netns add client
ip link add server0 type veth peer name client0
ip link set client0 netns client
ip addr add 198.18.0.1/24 dev server0
ip -6 addr add 2001:db8:1::1/64 dev server0 nodad
ip link set server0 up
ip -n client addr add 198.18.0.2/24 dev client0
ip -n client -6 addr add 2001:db8:1::2/64 dev client0 nodad
ip -n client link set client0 up
ip -n client link set lo up
python3 "$root/server.py" "$root" &
server_pid=$!
trap 'kill "$server_pid" 2>/dev/null || true; ip netns del client 2>/dev/null || true' EXIT
for attempt in {1..100}; do [[ ! -f "$root/port" ]] || break; sleep .05; done
port=$(cat "$root/port")
port6=$(cat "$root/port6")
cat > /etc/evofw/agent.conf <<CONF
EVOFW_CP_URL=http://127.0.0.1:$port
CLIENT_TOKEN=namespace-test-only
KERNEL_BACKEND=$backend
EVOFW_SKIP_SELF_UPDATE=1
CONF
nft add table inet unrelated
nft 'add chain inet unrelated input { type filter hook input priority 10; policy accept; }'
# A fresh connection through a veth verifies actual packet enforcement.
ip netns exec client curl --noproxy '*' --max-time 2 -fsS "http://198.18.0.1:$port/echo" >/dev/null
ip netns exec client curl --noproxy '*' --max-time 2 -gfsS "http://[2001:db8:1::1]:$port6/echo" >/dev/null
bash "$root/agent.sh"
python3 -c 'import json; assert json.load(open("/var/lib/evofw/telemetry-state.json"))["pending"] == [], "fixture ACK did not drain durable telemetry"'
[[ $(cat /var/lib/evofw/last_hash) == "sha256:$(printf '1%.0s' {1..64})" ]]
nft list table inet unrelated >/dev/null
if ip netns exec client curl --noproxy '*' --max-time 2 -fsS "http://198.18.0.1:$port/echo" >/dev/null; then echo 'deny did not block packet' >&2; exit 1; fi
if ip netns exec client curl --noproxy '*' --max-time 2 -gfsS "http://[2001:db8:1::1]:$port6/echo" >/dev/null; then echo 'IPv6 deny did not block packet' >&2; exit 1; fi
# Same hash must reconcile missing owned state, retaining unrelated rules.
if [[ "$backend" == nft ]]; then nft delete table inet evofw; else ipset flush evofw_deny_v4; fi
bash "$root/agent.sh"
if [[ "$backend" == nft ]]; then nft list table inet evofw >/dev/null; else ipset test evofw_deny_v4 198.18.0.2; fi
nft list table inet unrelated >/dev/null
touch "$root/invalid"
if bash "$root/agent.sh"; then echo 'invalid candidate unexpectedly applied' >&2; exit 1; fi
[[ $(cat /var/lib/evofw/last_hash) == "sha256:$(printf '1%.0s' {1..64})" ]]
if [[ "$backend" == nft ]]; then
nft list set inet evofw deny_v4 | grep -q '198.18.0.2'
nft list set inet evofw deny_v6 | grep -q '2001:db8:1::2'
else
ipset test evofw_deny_v4 198.18.0.2
ipset test evofw_deny_v6 2001:db8:1::2
fi
nft list table inet unrelated >/dev/null
echo "Linux namespace smoke passed ($backend): IPv4+IPv6 apply, real packet deny, drift repair, invalid-candidate last-good, unrelated table preservation"
CHILD
done
+12
View File
@@ -0,0 +1,12 @@
import { readFileSync } from 'node:fs'
const [file, digest, platform] = process.argv.slice(2)
const [os, architecture, variant] = platform.split('/')
const manifest = JSON.parse(readFileSync(file, 'utf8'))
let selected = digest
if (Array.isArray(manifest.manifests)) {
const matches = manifest.manifests.filter(entry => entry.platform?.os === os && entry.platform?.architecture === architecture && (entry.platform?.variant || undefined) === variant)
if (matches.length !== 1) throw new Error(`Expected one ${platform} manifest, found ${matches.length}`)
selected = matches[0].digest
}
if (!/^sha256:[0-9a-f]{64}$/.test(selected)) throw new Error('Invalid platform digest')
process.stdout.write(selected)
+23
View File
@@ -0,0 +1,23 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
test('mirror verification selects exactly one platform and refuses absent/ambiguous descriptors', () => {
const directory = mkdtempSync(join(tmpdir(), 'evofw-manifest-'))
const file = join(directory, 'index.json')
const digest = `sha256:${'a'.repeat(64)}`
const descriptor = { digest, platform: { os: 'linux', architecture: 'amd64' } }
const run = (manifest) => {
writeFileSync(file, JSON.stringify(manifest))
return spawnSync(process.execPath, ['scripts/ci/manifest-platform.mjs', file, digest, 'linux/amd64'], { encoding: 'utf8' })
}
try {
assert.equal(run({ manifests: [descriptor, { ...descriptor, platform: { os: 'linux', architecture: 'arm64' } }] }).stdout, digest)
assert.notEqual(run({ manifests: [] }).status, 0)
assert.notEqual(run({ manifests: [descriptor, descriptor] }).status, 0)
assert.equal(run({ schemaVersion: 2 }).stdout, digest)
assert.notEqual(run({ manifests: [{ ...descriptor, digest: 'invalid' }] }).status, 0)
} finally { rmSync(directory, { recursive: true, force: true }) }
})
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env sh
# Each workflow uses a fresh run-scoped builder, created by setup-buildx-action.
# Never remove another job's builder from a shared Docker daemon.
set -eu
name="${1:-evofw}"
config="${2:-deploy/docker/buildkitd.toml}"
docker buildx inspect "$name" --bootstrap >/dev/null
container="$(docker ps -q --filter "name=buildx_buildkit_${name}0")"
[ -n "$container" ] || { echo 'BuildKit container missing' >&2; exit 1; }
effective="$(mktemp)"
trap 'rm -f "$effective"' EXIT HUP INT TERM
docker exec "$container" cat /etc/buildkit/buildkitd.toml >"$effective"
for expected in 'reservedSpace = "2GB"' 'maxUsedSpace = "12GB"' 'minFreeSpace = "5GB"' 'keepDuration = "48h"'; do
grep -F "$expected" "$effective" >/dev/null || { echo 'BuildKit effective GC configuration mismatch' >&2; exit 1; }
done
+50
View File
@@ -0,0 +1,50 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { mkdtempSync, rmSync, writeFileSync, existsSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { resolve, join } from 'node:path'
import { spawnSync } from 'node:child_process'
const root = resolve(import.meta.dirname, '../..')
const Database = createRequire(join(root, 'packages/db/package.json'))('better-sqlite3')
const script = join(root, 'deploy/docker/evofw/sqlite-backup.mjs')
test('online backup includes committed WAL and restore verifies before offline replacement', () => {
const directory = mkdtempSync(join(tmpdir(), 'evofw-backup-test-'))
let db
const run = (...args) => spawnSync(process.execPath, [script, ...args], { cwd: directory, encoding: 'utf8', env: { ...process.env, NODE_PATH: join(root, 'packages/db/node_modules') } })
try {
const source = join(directory, 'source.db')
const backup = join(directory, 'backup.db')
const restored = join(directory, 'restored.db')
db = new Database(source)
db.pragma('journal_mode = WAL')
db.exec('CREATE TABLE records(id INTEGER PRIMARY KEY, value TEXT); INSERT INTO records VALUES(1,\'committed WAL\')')
assert.ok(existsSync(`${source}-wal`))
const result = run('backup', source, backup)
assert.equal(result.status, 0, result.stderr)
const snapshot = new Database(backup, { readonly: true })
assert.equal(snapshot.prepare('SELECT value FROM records').get().value, 'committed WAL')
snapshot.close()
assert.notEqual(run('backup', source, backup).status, 0, 'never overwrite an existing backup')
assert.notEqual(run('restore', backup, restored).status, 0, 'offline declaration required')
writeFileSync(restored, 'old offline data')
writeFileSync(`${restored}-wal`, 'stale')
writeFileSync(`${restored}-shm`, 'stale')
const recovery = run('restore', backup, restored, '--offline')
assert.equal(recovery.status, 0, recovery.stderr)
assert.ok(!existsSync(`${restored}-wal`))
const recovered = new Database(restored, { readonly: true })
assert.equal(recovered.prepare('SELECT value FROM records').get().value, 'committed WAL')
recovered.close()
const invalid = join(directory, 'invalid.db')
writeFileSync(invalid, 'not sqlite')
assert.notEqual(run('restore', invalid, restored, '--offline').status, 0)
const preserved = new Database(restored, { readonly: true })
assert.equal(preserved.prepare('SELECT count(*) AS n FROM records').get().n, 1)
preserved.close()
} finally {
db?.close()
rmSync(directory, { recursive: true, force: true })
}
})