# E2E Testing Pipeline using Native Woodpecker Services # No Docker-in-Docker - uses pre-built images for fast startup kind: pipeline type: docker name: e2e when: event: pull_request path: exclude: - "tools/**" - "onchain/test/FitnessEvaluator*" - "docs/**" - "formulas/**" - "evidence/**" - ".codeberg/**" - "*.md" clone: git: image: woodpeckerci/plugin-git settings: depth: 50 reference: /git-mirrors/harb.git netrc_machine: codeberg.org netrc_username: johba netrc_password: from_secret: codeberg_token # All background services - services get proper DNS resolution in Woodpecker # Note: Services can't depend on steps, so they wait internally for contracts.env services: # PostgreSQL for Ponder - name: postgres image: postgres:16-alpine environment: POSTGRES_USER: ponder POSTGRES_PASSWORD: ponder_local POSTGRES_DB: ponder_local # Anvil blockchain fork - name: anvil image: ghcr.io/foundry-rs/foundry:latest entrypoint: - anvil - --host=0.0.0.0 - --port=8545 - --fork-url=https://sepolia.base.org - --fork-block-number=20000000 - --chain-id=31337 - --accounts=10 - --balance=10000 # Ponder indexer - waits for contracts.env from bootstrap - name: ponder image: registry.niovi.voyage/harb/ponder-ci:latest commands: - | set -eu # Wait for contracts.env (bootstrap writes it after deploying) echo "=== Waiting for contracts.env ===" for i in $(seq 1 120); do if [ -f /woodpecker/src/contracts.env ]; then echo "Found contracts.env after $i attempts" break fi echo "Waiting for contracts.env... ($i/120)" sleep 3 done if [ ! -f /woodpecker/src/contracts.env ]; then echo "ERROR: contracts.env not found after 6 minutes" exit 1 fi # Source contract addresses from bootstrap . /woodpecker/src/contracts.env echo "=== Contract addresses ===" echo "KRAIKEN=$KRAIKEN" echo "STAKE=$STAKE" echo "START_BLOCK=$START_BLOCK" # Export env vars required by ponder export DATABASE_URL="$DATABASE_URL" export DATABASE_SCHEMA="ponder_ci_$START_BLOCK" export START_BLOCK="$START_BLOCK" export KRAIKEN_ADDRESS="$KRAIKEN" export STAKE_ADDRESS="$STAKE" export LM_ADDRESS="${LIQUIDITY_MANAGER:-0x0000000000000000000000000000000000000000}" export PONDER_NETWORK=BASE_SEPOLIA_LOCAL_FORK export PONDER_RPC_URL_BASE_SEPOLIA_LOCAL_FORK="$PONDER_RPC_URL_1" export PONDER_RPC_URL_1="$PONDER_RPC_URL_1" # Overlay kraiken-lib and ponder source from workspace # CI_WORKSPACE points to the repo checkout directory WS="${CI_WORKSPACE:-$(pwd)}" echo "=== Workspace: $WS ===" echo "=== Overlaying kraiken-lib from workspace ===" if [ -d "$WS/kraiken-lib/dist" ]; then cp -r "$WS/kraiken-lib/dist/." /app/kraiken-lib/dist/ cp -r "$WS/kraiken-lib/src/." /app/kraiken-lib/src/ echo "kraiken-lib updated from workspace (src + dist)" elif [ -d "$WS/kraiken-lib/src" ]; then cp -r "$WS/kraiken-lib/src/." /app/kraiken-lib/src/ echo "kraiken-lib/src updated (dist not available — may need rebuild)" else echo "WARNING: kraiken-lib not found at $WS/kraiken-lib" fi echo "=== Overlaying ponder source from workspace ===" # Copy individual source files (not the directory itself) to avoid nested src/src/ if [ -d "$WS/services/ponder/src" ]; then cp -r "$WS/services/ponder/src/." /app/services/ponder/src/ echo "ponder/src files updated from workspace" fi for f in ponder.schema.ts ponder.config.ts; do if [ -f "$WS/services/ponder/$f" ]; then cp "$WS/services/ponder/$f" /app/services/ponder/"$f" echo "ponder/$f updated from workspace" fi done echo "=== Starting Ponder (pre-built image + workspace overlay) ===" cd /app/services/ponder { echo "DATABASE_URL=${DATABASE_URL}" echo "PONDER_RPC_URL_1=${PONDER_RPC_URL_1}" echo "DATABASE_SCHEMA=${DATABASE_SCHEMA}" echo "START_BLOCK=${START_BLOCK}" } > .env.local # Use 'start' mode in CI — 'dev' mode watches for file changes and causes # a hot-restart loop when workspace overlay modifies source files exec npm run start # Webapp - waits for contracts.env from bootstrap - name: webapp image: registry.niovi.voyage/harb/webapp-ci:latest environment: CI: "true" commands: - | set -eu # Wait for contracts.env (bootstrap writes it after deploying) echo "=== Waiting for contracts.env ===" for i in $(seq 1 120); do if [ -f /woodpecker/src/contracts.env ]; then echo "Found contracts.env after $i attempts" break fi echo "Waiting for contracts.env... ($i/120)" sleep 3 done if [ ! -f /woodpecker/src/contracts.env ]; then echo "ERROR: contracts.env not found after 6 minutes" exit 1 fi # Source contract addresses from bootstrap . /woodpecker/src/contracts.env # Export environment variables for Vite export VITE_KRAIKEN_ADDRESS="$KRAIKEN" export VITE_STAKE_ADDRESS="$STAKE" export VITE_DEFAULT_CHAIN_ID=31337 export VITE_LOCAL_RPC_PROXY_TARGET=http://anvil:8545 export VITE_LOCAL_GRAPHQL_PROXY_TARGET=http://ponder:42069 # Default is the Sepolia SwapRouter; override via VITE_SWAP_ROUTER env var for other networks. export VITE_SWAP_ROUTER="${VITE_SWAP_ROUTER:-0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4}" export VITE_ENABLE_LOCAL_SWAP=true export VITE_BASE_PATH=/app/ # Overlay kraiken-lib from workspace (may be newer than baked-in image) WS="${CI_WORKSPACE:-$(pwd)}" echo "=== Overlaying kraiken-lib from workspace ===" if [ -d "$WS/kraiken-lib/dist" ]; then cp -r "$WS/kraiken-lib/dist/." /app/kraiken-lib/dist/ cp -r "$WS/kraiken-lib/src/." /app/kraiken-lib/src/ echo "kraiken-lib updated from workspace (src + dist)" elif [ -d /app/kraiken-lib/src ]; then echo "kraiken-lib/src found in image (using baked-in version)" else echo "ERROR: kraiken-lib/src not found!" exit 1 fi # Overlay webapp source from workspace (ensures CI tests current branch) echo "=== Overlaying webapp source from workspace ===" if [ -d "$WS/web-app/src" ]; then cp -r "$WS/web-app/src/." /app/web-app/src/ echo "webapp/src updated from workspace" fi for f in vite.config.ts vite.config.js; do if [ -f "$WS/web-app/$f" ]; then cp "$WS/web-app/$f" /app/web-app/"$f" echo "webapp/$f updated from workspace" fi done # Overlay @harb/web3 shared package from workspace if [ -d "$WS/packages/web3" ]; then mkdir -p /app/packages/web3 cp -r "$WS/packages/web3/." /app/packages/web3/ # Link @harb/web3 into web-app node_modules mkdir -p /app/web-app/node_modules/@harb ln -sf /app/packages/web3 /app/web-app/node_modules/@harb/web3 # Symlink wagmi/viem into packages dir so @harb/web3 can resolve them mkdir -p /app/packages/web3/node_modules ln -sf /app/web-app/node_modules/@wagmi /app/packages/web3/node_modules/@wagmi ln -sf /app/web-app/node_modules/viem /app/packages/web3/node_modules/viem echo "@harb/web3 linked with wagmi/viem deps" fi # Overlay @harb/utils shared package from workspace if [ -d "$WS/packages/utils" ]; then mkdir -p /app/packages/utils cp -r "$WS/packages/utils/." /app/packages/utils/ # Link @harb/utils into web-app node_modules mkdir -p /app/web-app/node_modules/@harb ln -sf /app/packages/utils /app/web-app/node_modules/@harb/utils # Symlink viem into packages dir so @harb/utils can resolve it mkdir -p /app/packages/utils/node_modules ln -sf /app/web-app/node_modules/viem /app/packages/utils/node_modules/viem echo "@harb/utils linked with viem dep" fi # Overlay @harb/analytics shared package from workspace if [ -d "$WS/packages/analytics" ]; then mkdir -p /app/packages/analytics cp -r "$WS/packages/analytics/." /app/packages/analytics/ mkdir -p /app/web-app/node_modules/@harb ln -sf /app/packages/analytics /app/web-app/node_modules/@harb/analytics echo "@harb/analytics linked for webapp" fi echo "=== Starting webapp (pre-built image + source overlay) ===" cd /app/web-app # Explicitly set CI=true to disable Vue DevTools in vite.config.ts # (prevents 500 errors from devtools path resolution in CI environment) export CI=true echo "CI=$CI (should be 'true' to disable Vue DevTools)" exec npm run dev -- --host 0.0.0.0 --port 5173 --base /app/ # Landing page - no contracts needed, starts immediately - name: landing image: registry.niovi.voyage/harb/landing-ci:latest commands: - | set -eu # Overlay landing source from workspace WS="${CI_WORKSPACE:-$(pwd)}" if [ -d "$WS/landing/src" ]; then cp -r "$WS/landing/src/." /app/landing/src/ echo "landing/src updated from workspace" fi for f in vite.config.ts vite.config.js; do if [ -f "$WS/landing/$f" ]; then cp "$WS/landing/$f" /app/landing/"$f" echo "landing/$f updated from workspace" fi done # Overlay @harb/web3 shared package if [ -d "$WS/packages/web3" ]; then mkdir -p /app/packages/web3 cp -r "$WS/packages/web3/." /app/packages/web3/ # Landing CI image doesn't have wagmi — install it cd /app/landing npm install --no-audit --no-fund @wagmi/vue viem 2>/dev/null || true # Link @harb/web3 mkdir -p /app/landing/node_modules/@harb ln -sf /app/packages/web3 /app/landing/node_modules/@harb/web3 # Symlink wagmi/viem into packages dir for resolution mkdir -p /app/packages/web3/node_modules ln -sf /app/landing/node_modules/@wagmi /app/packages/web3/node_modules/@wagmi 2>/dev/null || true ln -sf /app/landing/node_modules/viem /app/packages/web3/node_modules/viem 2>/dev/null || true echo "@harb/web3 linked for landing" fi # Overlay @harb/ui-shared shared package from workspace if [ -d "$WS/packages/ui-shared" ]; then mkdir -p /app/packages/ui-shared cp -r "$WS/packages/ui-shared/." /app/packages/ui-shared/ # Link @harb/ui-shared into landing node_modules mkdir -p /app/landing/node_modules/@harb ln -sf /app/packages/ui-shared /app/landing/node_modules/@harb/ui-shared # Symlink vue into packages dir so @harb/ui-shared can resolve it mkdir -p /app/packages/ui-shared/node_modules ln -sf /app/landing/node_modules/vue /app/packages/ui-shared/node_modules/vue 2>/dev/null || true echo "@harb/ui-shared linked for landing" fi # Overlay @harb/analytics shared package from workspace if [ -d "$WS/packages/analytics" ]; then mkdir -p /app/packages/analytics cp -r "$WS/packages/analytics/." /app/packages/analytics/ mkdir -p /app/landing/node_modules/@harb ln -sf /app/packages/analytics /app/landing/node_modules/@harb/analytics echo "@harb/analytics linked for landing" fi echo "=== Starting landing (pre-built image + source overlay) ===" cd /app/landing exec npm run dev -- --host 0.0.0.0 --port 5174 # Caddy proxy - waits for contracts.env to ensure other services are starting - name: caddy image: caddy:2.8-alpine commands: - | # Wait briefly for other services to start echo "=== Waiting for contracts.env before starting Caddy ===" for i in $(seq 1 120); do if [ -f /woodpecker/src/contracts.env ]; then echo "Found contracts.env, starting Caddy..." break fi echo "Waiting for contracts.env... ($i/120)" sleep 3 done printf '%s\n' ':8081 {' \ ' route /app* {' \ ' reverse_proxy webapp:5173' \ ' }' \ ' route /api/graphql* {' \ ' uri strip_prefix /api' \ ' reverse_proxy ponder:42069' \ ' }' \ ' route /api/rpc* {' \ ' uri strip_prefix /api/rpc' \ ' reverse_proxy anvil:8545' \ ' }' \ ' reverse_proxy landing:5174' \ '}' > /etc/caddy/Caddyfile exec caddy run --config /etc/caddy/Caddyfile steps: # Step 0: Install dependencies for onchain compilation - name: install-deps image: node:20-alpine commands: - | set -eu apk add --no-cache git echo "=== Installing uni-v3-lib dependencies ===" git submodule update --init --recursive cd onchain/lib/uni-v3-lib npm install # Step 1: Wait for base services and deploy contracts # Uses pre-built node-ci image with Foundry pre-installed (saves ~60s) - name: bootstrap image: registry.niovi.voyage/harb/node-ci:latest depends_on: - install-deps commands: - | # Create a bootstrap wrapper that runs under bash # (Woodpecker uses /bin/sh which lacks 'source' and bash-isms) export ANVIL_RPC=http://anvil:8545 export CONTRACT_ENV=/woodpecker/src/contracts.env export LOG_FILE=/dev/null export ONCHAIN_DIR="$PWD/onchain" export TXNBOT_FUND_VALUE=10ether export TXNBOT_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 export TXNBOT_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d exec bash scripts/ci-bootstrap.sh # Step 2: Wait for stack to be healthy (services run in background) - name: wait-for-stack image: alpine:3.20 depends_on: - bootstrap commands: - | set -eu apk add --no-cache curl bash echo "=== Waiting for DNS resolution (Docker embedded DNS can be slow under load) ===" for svc in ponder webapp landing caddy; do for attempt in $(seq 1 60); do if getent hosts "$svc" >/dev/null 2>&1; then echo "[dns] $svc resolved after $attempt attempts" break fi echo "[dns] ($attempt/60) waiting for $svc DNS..." sleep 5 done done echo "=== Waiting for stack to be healthy (max 7 min) ===" bash scripts/wait-for-service.sh http://ponder:42069/health 420 ponder # Wait for ponder to finish historical indexing (not just respond) # /ready returns 200 only when fully synced, 503 while indexing echo "=== Waiting for Ponder indexing to complete ===" for i in $(seq 1 120); do HTTP_CODE=$(curl -sf -o /dev/null -w '%{http_code}' --max-time 3 http://ponder:42069/ready 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "200" ]; then echo "[wait] Ponder fully indexed after $((i * 3))s" break fi if [ "$i" = "120" ]; then echo "[wait] WARNING: Ponder not fully indexed after 360s, continuing anyway" fi echo "[wait] ($i/120) Ponder indexing... (HTTP $HTTP_CODE)" sleep 3 done bash scripts/wait-for-service.sh http://webapp:5173/app/ 420 webapp bash scripts/wait-for-service.sh http://landing:5174/ 420 landing bash scripts/wait-for-service.sh http://caddy:8081/app/ 420 caddy echo "=== Stack is healthy ===" # Step 3: Run E2E tests — cross-browser matrix # Chromium runs all specs (01-07), then Firefox/WebKit/mobile run read-only specs (03,06,07). # The matrix is defined in playwright.config.ts via `projects`. - name: run-e2e-tests image: mcr.microsoft.com/playwright:v1.55.1-jammy depends_on: - wait-for-stack timeout: 1800 environment: STACK_BASE_URL: http://caddy:8081 STACK_RPC_URL: http://caddy:8081/api/rpc STACK_WEBAPP_URL: http://caddy:8081 STACK_GRAPHQL_URL: http://caddy:8081/api/graphql CI: "true" commands: - | set -eux echo "=== Checking system resources ===" free -h || true cat /proc/meminfo | grep -E 'MemTotal|MemAvail' || true echo "=== Verifying Playwright browsers ===" npx playwright install --dry-run 2>&1 || true ls -la /ms-playwright/ 2>/dev/null || echo "No /ms-playwright directory" echo "=== Installing test dependencies ===" npm config set fund false npm config set audit false npm ci --no-audit --no-fund echo "=== Running E2E tests — cross-browser matrix (workers=1 to limit memory) ===" npx playwright test --reporter=list --workers=1 # Step 4: Collect artifacts - name: collect-artifacts image: alpine:3.20 depends_on: - run-e2e-tests when: status: - success - failure commands: - | set -eu apk add --no-cache tar gzip mkdir -p artifacts if [ -d playwright-report ]; then tar -czf artifacts/playwright-report.tgz playwright-report echo "Playwright report archived" fi if [ -d test-results ]; then tar -czf artifacts/test-results.tgz test-results echo "Test results archived" fi ls -lh artifacts/ 2>/dev/null || echo "No artifacts"