#!/usr/bin/env bash # CI Docker mining proof — Linux agent connects and reports hashrate > 0. # Uses docker/data test wallet + fleet secret (see docker/README.md). set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="${ROOT}/docker/docker-compose.yml" BASE_URL="${AETHERFORGE_DOCKER_BASE_URL:-http://127.0.0.1:18989}" E2E_USER="${AETHERFORGE_E2E_USER:-testuser}" E2E_PASS="${AETHERFORGE_E2E_PASS:-testpass}" EXPECTED_WORKER="${AETHERFORGE_DOCKER_WORKER:-docker-e2e-linux}" WAIT_SECONDS="${AETHERFORGE_DOCKER_WAIT_SEC:-180}" POLL_INTERVAL="${AETHERFORGE_DOCKER_POLL_SEC:-5}" AUTH_HEADER="Authorization: Basic $(printf '%s:%s' "$E2E_USER" "$E2E_PASS" | base64 | tr -d '\n')" DEADLINE=0 log() { printf '[ci-docker-mining] %s\n' "$*"; } fail() { log "ERROR: $*"; exit 1; } require_docker() { if ! command -v docker >/dev/null 2>&1; then fail "docker not found — install Docker Engine 24+ or run on a GHA ubuntu-latest runner" fi if ! docker compose version >/dev/null 2>&1; then fail "docker compose plugin not found" fi } teardown() { local code=$? log "Tearing down compose (exit=$code)…" docker compose -f "$COMPOSE_FILE" down --rmi local -v --remove-orphans 2>/dev/null || true if [[ $code -ne 0 ]]; then log "Recent server logs:" docker compose -f "$COMPOSE_FILE" logs --tail=80 server 2>/dev/null || true log "Recent agent logs:" docker compose -f "$COMPOSE_FILE" logs --tail=80 agent 2>/dev/null || true fi exit "$code" } wait_for_health() { while (( SECONDS < DEADLINE )); do if curl -sf "${BASE_URL}/api/v1/health" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'; then log "Server healthy at ${BASE_URL}/api/v1/health" return 0 fi sleep "$POLL_INTERVAL" done fail "server not healthy within ${WAIT_SECONDS}s (${BASE_URL}/api/v1/health)" } assert_mining() { local agents_json stats_json while (( SECONDS < DEADLINE )); do agents_json="$(curl -sf -H "$AUTH_HEADER" "${BASE_URL}/api/v1/agents" || true)" stats_json="$(curl -sf -H "$AUTH_HEADER" "${BASE_URL}/api/v1/dashboard/stats" || true)" if [[ -n "$agents_json" && -n "$stats_json" ]]; then local online_hr online_hr="$(python3 - <<'PY' "$agents_json" "$EXPECTED_WORKER" import json, sys agents = json.loads(sys.argv[1]) worker = sys.argv[2] hits = [] for a in agents: if a.get("status") != "online": continue hr = max( float(a.get("hashrate_15s") or 0), float(a.get("hashrate_1m") or 0), float(a.get("hashrate_15m") or 0), ) if hr > 0: hits.append((a.get("id"), a.get("worker_name") or a.get("name"), hr)) if worker: named = [h for h in hits if h[1] == worker] if named: print(f"{named[0][0]}|{named[0][1]}|{named[0][2]:.2f}") sys.exit(0) if hits: print(f"{hits[0][0]}|{hits[0][1]}|{hits[0][2]:.2f}") PY )" || true)" if [[ -n "$online_hr" ]]; then IFS='|' read -r agent_id worker_name hashrate <<<"$online_hr" log "PASS: online agent ${agent_id} (${worker_name}) hashrate=${hashrate} H/s" log "Fleet stats: $(printf '%s' "$stats_json" | python3 -c 'import json,sys; s=json.load(sys.stdin); print(f"online={s.get(\"online_agents\",0)} total_hr={s.get(\"total_hashrate\",0):.2f}")')" return 0 fi local online_count total_hr online_count="$(printf '%s' "$stats_json" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("online_agents",0))')" total_hr="$(printf '%s' "$stats_json" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("total_hashrate",0))')" log "Waiting… online_agents=${online_count} total_hashrate=${total_hr} (need online + hashrate>0)" else log "Waiting… agents/stats API not ready" fi sleep "$POLL_INTERVAL" done fail "no online agent with hashrate>0 within ${WAIT_SECONDS}s (expected worker: ${EXPECTED_WORKER})" } main() { require_docker trap teardown EXIT cd "$ROOT" log "Starting docker compose (build may take several minutes)…" docker compose -f "$COMPOSE_FILE" up --build -d DEADLINE=$((SECONDS + WAIT_SECONDS)) log "Waiting up to ${WAIT_SECONDS}s for health + mining proof…" wait_for_health assert_mining log "Docker mining proof succeeded" } main "$@"