refactor: consolidate CI and local dev orchestration (#108)

## Summary
- Extract shared bootstrap functions into `scripts/bootstrap-common.sh` (eliminates ~120 lines of duplicated forge/cast commands from e2e.yml)
- Create reusable `scripts/wait-for-service.sh` for health checks (replaces 60-line inline wait-for-stack)
- Merge dev and CI entrypoints into unified scripts branching on `CI` env var (delete `docker/ci-entrypoints/`)
- Replace 4 per-service CI Dockerfiles with parameterized `docker/Dockerfile.service-ci`
- Add `sync-tax-rates.mjs` to CI image builder stage
- Fix: CI now grants txnBot recenter access (was missing)
- Fix: txnBot funding parameterized (CI=10eth, local=1eth)
- Delete 5 obsolete migration docs and 4 DinD integration files

Net: -1540 lines removed

Closes #107

## Test plan
- [ ] E2E pipeline passes (bootstrap sources shared script, services use old images with commands override)
- [ ] build-ci-images pipeline builds all 4 services with unified Dockerfile
- [ ] Local dev stack boots via `./scripts/dev.sh start`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/harb/pulls/108
This commit is contained in:
johba 2026-02-03 12:07:28 +01:00
parent 4277f19b68
commit e5e1308e72
45 changed files with 882 additions and 2627 deletions

View file

@ -1,50 +0,0 @@
# syntax=docker/dockerfile:1.6
# Composite integration image that bundles the entire Harb stack for E2E testing
# This image runs docker-compose internally to orchestrate all services
FROM docker:27-dind
LABEL org.opencontainers.image.source="https://codeberg.org/johba/harb-ci"
LABEL org.opencontainers.image.description="Harb Stack integration container for E2E CI tests"
ENV DOCKER_TLS_CERTDIR="" \
COMPOSE_PROJECT_NAME=harb-ci \
HARB_ENV=BASE_SEPOLIA_LOCAL_FORK \
SKIP_WATCH=1
# Install docker-compose, bash, curl, and other essentials
RUN apk add --no-cache \
bash \
curl \
git \
docker-cli-compose \
shadow \
su-exec
# Create a non-root user for running the stack
RUN addgroup -g 1000 harb && \
adduser -D -u 1000 -G harb harb
WORKDIR /workspace
# Copy the entire project (will be mounted at runtime in CI, but needed for standalone usage)
COPY --chown=harb:harb . /workspace/
# Pre-build kraiken-lib to speed up startup
RUN cd /workspace && \
if [ -f scripts/build-kraiken-lib.sh ]; then \
./scripts/build-kraiken-lib.sh || echo "kraiken-lib build skipped"; \
fi
# Healthcheck: verify the stack is responding via Caddy
HEALTHCHECK --interval=5s --timeout=3s --retries=30 --start-period=120s \
CMD curl -f http://localhost:8081/api/graphql || exit 1
# Entrypoint script to start Docker daemon and the stack
COPY docker/integration-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8081
ENTRYPOINT ["/entrypoint.sh"]
CMD ["bash"]

View file

@ -1,57 +0,0 @@
# Production image for Landing page (Vite + Vue)
# Used in CI for E2E testing - contains all code baked in
FROM node:20-alpine AS builder
RUN apk add --no-cache git bash
WORKDIR /app
# Copy package files first for better caching
COPY package.json package-lock.json ./
COPY kraiken-lib/package.json kraiken-lib/package-lock.json ./kraiken-lib/
COPY landing/package.json landing/package-lock.json ./landing/
# Copy ABI files needed by kraiken-lib
COPY onchain/out/Kraiken.sol/Kraiken.json ./onchain/out/Kraiken.sol/
COPY onchain/out/Stake.sol/Stake.json ./onchain/out/Stake.sol/
# Install kraiken-lib dependencies and build
WORKDIR /app/kraiken-lib
RUN npm ci --ignore-scripts
COPY kraiken-lib/ ./
RUN ./node_modules/.bin/tsc
# Install landing dependencies
WORKDIR /app/landing
RUN npm ci
# Copy landing source
COPY landing/ ./
# Production image
FROM node:20-alpine
RUN apk add --no-cache dumb-init wget bash
WORKDIR /app
# Copy kraiken-lib (src for vite alias, dist for runtime)
COPY --from=builder /app/kraiken-lib/src ./kraiken-lib/src
COPY --from=builder /app/kraiken-lib/dist ./kraiken-lib/dist
COPY --from=builder /app/kraiken-lib/package.json ./kraiken-lib/
COPY --from=builder /app/landing ./landing
WORKDIR /app/landing
ENV NODE_ENV=development
ENV HOST=0.0.0.0
ENV PORT=5174
EXPOSE 5174
HEALTHCHECK --interval=5s --timeout=3s --retries=6 --start-period=10s \
CMD wget --spider -q http://127.0.0.1:5174/ || exit 1
# Landing doesn't need contract addresses - just serve static content
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5174"]

View file

@ -1,67 +0,0 @@
# Production image for Ponder indexer service
# Used in CI for E2E testing - contains all code baked in
FROM node:20-alpine AS builder
RUN apk add --no-cache git bash
WORKDIR /app
# Copy package files first for better caching
COPY package.json package-lock.json ./
COPY kraiken-lib/package.json kraiken-lib/package-lock.json ./kraiken-lib/
COPY services/ponder/package.json services/ponder/package-lock.json ./services/ponder/
# Copy ABI files needed by kraiken-lib
COPY onchain/out/Kraiken.sol/Kraiken.json ./onchain/out/Kraiken.sol/
COPY onchain/out/Stake.sol/Stake.json ./onchain/out/Stake.sol/
# Install kraiken-lib dependencies and build
WORKDIR /app/kraiken-lib
RUN npm ci --ignore-scripts
COPY kraiken-lib/ ./
RUN ./node_modules/.bin/tsc
# Install ponder dependencies
WORKDIR /app/services/ponder
RUN npm ci
# Copy ponder source
COPY services/ponder/ ./
# Copy shared config files needed by ponder
WORKDIR /app
COPY onchain/deployments*.json ./onchain/
# Production image
FROM node:20-alpine
RUN apk add --no-cache dumb-init wget postgresql-client bash
WORKDIR /app
# Copy kraiken-lib with full structure (needed for node_modules symlink resolution)
COPY --from=builder /app/kraiken-lib ./kraiken-lib
# Copy ponder with all node_modules
COPY --from=builder /app/services/ponder ./services/ponder
# Copy onchain artifacts
COPY --from=builder /app/onchain ./onchain
# Copy entrypoint
COPY docker/ci-entrypoints/ponder-ci-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /app/services/ponder
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=42069
EXPOSE 42069
HEALTHCHECK --interval=5s --timeout=3s --retries=12 --start-period=20s \
CMD wget --spider -q http://127.0.0.1:42069/ || exit 1
ENTRYPOINT ["dumb-init", "--", "/entrypoint.sh"]

View file

@ -0,0 +1,113 @@
# Unified CI image for Harb services (ponder, webapp, landing, txnBot).
# Parameterized via build args — eliminates per-service Dockerfile duplication.
#
# Usage:
# docker build -f docker/Dockerfile.service-ci \
# --build-arg SERVICE_DIR=services/ponder \
# --build-arg SERVICE_PORT=42069 \
# --build-arg ENTRYPOINT_SCRIPT=containers/ponder-entrypoint.sh \
# -t ponder-ci .
# ── Build args (declared early for builder stage) ──────────────────
ARG SERVICE_DIR
ARG NPM_INSTALL_CMD=ci
# ── Builder stage ──────────────────────────────────────────────────
FROM node:20-alpine AS builder
RUN apk add --no-cache git bash
WORKDIR /app
# Copy root package files
COPY package.json package-lock.json ./
# Copy kraiken-lib package files
COPY kraiken-lib/package.json kraiken-lib/package-lock.json ./kraiken-lib/
# Copy ABI files needed by kraiken-lib
COPY onchain/out/Kraiken.sol/Kraiken.json ./onchain/out/Kraiken.sol/
COPY onchain/out/Stake.sol/Stake.json ./onchain/out/Stake.sol/
# Copy Stake.sol for sync-tax-rates + the script itself
COPY onchain/src/Stake.sol ./onchain/src/
COPY scripts/sync-tax-rates.mjs ./scripts/
# Install kraiken-lib dependencies, run sync-tax-rates, and build
WORKDIR /app/kraiken-lib
RUN npm ci --ignore-scripts
COPY kraiken-lib/ ./
RUN node ../scripts/sync-tax-rates.mjs && ./node_modules/.bin/tsc
# Install service dependencies
ARG SERVICE_DIR
ARG NPM_INSTALL_CMD
WORKDIR /app/${SERVICE_DIR}
COPY ${SERVICE_DIR}/package.json ./
# Use glob pattern to optionally copy package-lock.json (txnBot has none)
COPY ${SERVICE_DIR}/package-lock.jso[n] ./
RUN if [ "$NPM_INSTALL_CMD" = "install" ]; then npm install; else npm ci; fi
# Copy service source
COPY ${SERVICE_DIR}/ ./
# Copy onchain deployment artifacts (glob handles missing files)
WORKDIR /app
COPY onchain/deployments*.jso[n] ./onchain/
# ── Runtime stage ──────────────────────────────────────────────────
FROM node:20-alpine
RUN apk add --no-cache dumb-init wget bash
WORKDIR /app
# Copy kraiken-lib (src for Vite alias, dist for runtime, package.json for resolution)
COPY --from=builder /app/kraiken-lib/src ./kraiken-lib/src
COPY --from=builder /app/kraiken-lib/dist ./kraiken-lib/dist
COPY --from=builder /app/kraiken-lib/package.json ./kraiken-lib/
# Copy service with all node_modules
ARG SERVICE_DIR
COPY --from=builder /app/${SERVICE_DIR} ./${SERVICE_DIR}
# Copy onchain artifacts
COPY --from=builder /app/onchain ./onchain
# Create placeholder deployments-local.json if not present
RUN test -f /app/onchain/deployments-local.json || \
(mkdir -p /app/onchain && echo '{"contracts":{}}' > /app/onchain/deployments-local.json)
# Conditionally create symlinks for Vite path resolution (webapp only)
ARG NEEDS_SYMLINKS=false
RUN if [ "$NEEDS_SYMLINKS" = "true" ]; then \
ln -sf /app/web-app /web-app && \
ln -sf /app/kraiken-lib /kraiken-lib && \
ln -sf /app/onchain /onchain; \
fi
# Copy entrypoint script
# For services with entrypoints (ponder, webapp, txnbot): pass the actual entrypoint
# For landing (no entrypoint): defaults to entrypoint-common.sh which is just helpers
ARG ENTRYPOINT_SCRIPT=containers/entrypoint-common.sh
COPY ${ENTRYPOINT_SCRIPT} /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Set working directory to service
WORKDIR /app/${SERVICE_DIR}
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
ENV HOST=0.0.0.0
ARG SERVICE_PORT=8080
ENV PORT=${SERVICE_PORT}
EXPOSE ${SERVICE_PORT}
ARG HEALTHCHECK_PATH=/
ARG HEALTHCHECK_RETRIES=12
ARG HEALTHCHECK_START=20s
HEALTHCHECK --interval=5s --timeout=3s --retries=${HEALTHCHECK_RETRIES} --start-period=${HEALTHCHECK_START} \
CMD wget --spider -q http://127.0.0.1:${PORT}${HEALTHCHECK_PATH} || exit 1
ENTRYPOINT ["dumb-init", "--", "/entrypoint.sh"]

View file

@ -1,64 +0,0 @@
# Production image for Transaction Bot service
# Used in CI for E2E testing - contains all code baked in
FROM node:20-alpine AS builder
RUN apk add --no-cache git bash
WORKDIR /app
# Copy package files first for better caching
COPY package.json package-lock.json ./
COPY kraiken-lib/package.json kraiken-lib/package-lock.json ./kraiken-lib/
COPY services/txnBot/package.json ./services/txnBot/
# Copy ABI files needed by kraiken-lib
COPY onchain/out/Kraiken.sol/Kraiken.json ./onchain/out/Kraiken.sol/
COPY onchain/out/Stake.sol/Stake.json ./onchain/out/Stake.sol/
# Install kraiken-lib dependencies and build
WORKDIR /app/kraiken-lib
RUN npm ci --ignore-scripts
COPY kraiken-lib/ ./
RUN ./node_modules/.bin/tsc
# Install txnBot dependencies (no lock file for txnBot)
WORKDIR /app/services/txnBot
RUN npm install
# Copy txnBot source
COPY services/txnBot/ ./
# Copy shared config files
WORKDIR /app
COPY onchain/deployments*.json ./onchain/
# Production image
FROM node:20-alpine
RUN apk add --no-cache dumb-init wget bash
WORKDIR /app
# Copy built artifacts
COPY --from=builder /app/kraiken-lib/dist ./kraiken-lib/dist
COPY --from=builder /app/kraiken-lib/package.json ./kraiken-lib/
COPY --from=builder /app/services/txnBot ./services/txnBot
COPY --from=builder /app/onchain ./onchain
# Copy entrypoint
COPY docker/ci-entrypoints/txnbot-ci-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /app/services/txnBot
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=43069
EXPOSE 43069
HEALTHCHECK --interval=5s --timeout=3s --retries=4 --start-period=10s \
CMD wget --spider -q http://127.0.0.1:43069/status || exit 1
ENTRYPOINT ["dumb-init", "--", "/entrypoint.sh"]

View file

@ -1,78 +0,0 @@
# Production image for Web App (Vite + Vue)
# Used in CI for E2E testing - contains all code baked in
# Includes filesystem symlinks for Vite path resolution in Docker
FROM node:20-alpine AS builder
RUN apk add --no-cache git bash
WORKDIR /app
# Copy package files first for better caching
COPY package.json package-lock.json ./
COPY kraiken-lib/package.json kraiken-lib/package-lock.json ./kraiken-lib/
COPY web-app/package.json web-app/package-lock.json ./web-app/
# Copy ABI files needed by kraiken-lib
COPY onchain/out/Kraiken.sol/Kraiken.json ./onchain/out/Kraiken.sol/
COPY onchain/out/Stake.sol/Stake.json ./onchain/out/Stake.sol/
# Install kraiken-lib dependencies and build
WORKDIR /app/kraiken-lib
RUN npm ci --ignore-scripts
COPY kraiken-lib/ ./
RUN ./node_modules/.bin/tsc
# Install webapp dependencies
WORKDIR /app/web-app
RUN npm ci
# Copy webapp source
COPY web-app/ ./
# Production image
FROM node:20-alpine
RUN apk add --no-cache dumb-init wget bash
WORKDIR /app
# Copy kraiken-lib (src for vite alias, dist for runtime)
COPY --from=builder /app/kraiken-lib/src ./kraiken-lib/src
COPY --from=builder /app/kraiken-lib/dist ./kraiken-lib/dist
COPY --from=builder /app/kraiken-lib/package.json ./kraiken-lib/
COPY --from=builder /app/web-app ./web-app
# Copy ABI files needed by kraiken-lib at compile time
COPY --from=builder /app/onchain/out ./onchain/out
# Create placeholder deployments-local.json for Vite compilation
# Actual contract addresses are provided via VITE_* environment variables at runtime
RUN mkdir -p /app/onchain && \
echo '{"contracts":{}}' > /app/onchain/deployments-local.json
# Create symlinks so Vite's path resolution works when base (/app/) is a prefix of root (/app/web-app)
# Vite's internal removeBase() can strip the /app/ prefix from filesystem paths, producing
# /web-app/src/... instead of /app/web-app/src/... — symlinks make both paths valid
RUN ln -s /app/web-app /web-app && \
ln -s /app/kraiken-lib /kraiken-lib && \
ln -s /app/onchain /onchain
# Copy entrypoint
COPY docker/ci-entrypoints/webapp-ci-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /app/web-app
ENV NODE_ENV=development
ENV HOST=0.0.0.0
ENV PORT=5173
# Disable Vue DevTools in CI builds - vite.config.ts checks for CI=true
ENV CI=true
EXPOSE 5173
HEALTHCHECK --interval=5s --timeout=3s --retries=84 --start-period=15s \
CMD wget --spider -q http://127.0.0.1:5173/app/ || exit 1
ENTRYPOINT ["dumb-init", "--", "/entrypoint.sh"]

View file

@ -1,32 +0,0 @@
#!/bin/bash
set -euo pipefail
# Change to the ponder directory (Woodpecker runs from /woodpecker/src/)
cd /app/services/ponder
echo "[ponder-ci] Starting Ponder indexer..."
# Required environment variables (set by Woodpecker from bootstrap step)
: "${DATABASE_URL:?DATABASE_URL is required}"
: "${PONDER_RPC_URL_1:?PONDER_RPC_URL_1 is required}"
# Optional with defaults
export PONDER_RPC_TIMEOUT=${PONDER_RPC_TIMEOUT:-20000}
export HOST=${HOST:-0.0.0.0}
export PORT=${PORT:-42069}
# Create .env.local from environment
cat > .env.local <<EOF
DATABASE_URL=${DATABASE_URL}
PONDER_RPC_URL_1=${PONDER_RPC_URL_1}
DATABASE_SCHEMA=${DATABASE_SCHEMA:-ponder_ci}
START_BLOCK=${START_BLOCK:-0}
EOF
echo "[ponder-ci] Environment configured:"
echo " DATABASE_URL: ${DATABASE_URL}"
echo " PONDER_RPC_URL_1: ${PONDER_RPC_URL_1}"
echo " START_BLOCK: ${START_BLOCK:-0}"
# Run ponder in dev mode (indexes and serves GraphQL)
exec npm run dev

View file

@ -1,35 +0,0 @@
#!/bin/bash
set -euo pipefail
echo "[txnbot-ci] Starting Transaction Bot..."
# Required environment variables (set by Woodpecker from bootstrap step)
: "${TXNBOT_PRIVATE_KEY:?TXNBOT_PRIVATE_KEY is required}"
: "${RPC_URL:?RPC_URL is required}"
: "${KRAIKEN_ADDRESS:?KRAIKEN_ADDRESS is required}"
: "${STAKE_ADDRESS:?STAKE_ADDRESS is required}"
: "${LIQUIDITY_MANAGER_ADDRESS:?LIQUIDITY_MANAGER_ADDRESS is required}"
# Create txnBot.env file from environment
cat > /tmp/txnBot.env <<EOF
TXNBOT_PRIVATE_KEY=${TXNBOT_PRIVATE_KEY}
RPC_URL=${RPC_URL}
KRAIKEN_ADDRESS=${KRAIKEN_ADDRESS}
STAKE_ADDRESS=${STAKE_ADDRESS}
LIQUIDITY_MANAGER_ADDRESS=${LIQUIDITY_MANAGER_ADDRESS}
POOL_ADDRESS=${POOL_ADDRESS:-}
WETH_ADDRESS=${WETH_ADDRESS:-0x4200000000000000000000000000000000000006}
EOF
export TXN_BOT_ENV_FILE=/tmp/txnBot.env
echo "[txnbot-ci] Environment configured:"
echo " RPC_URL: ${RPC_URL}"
echo " KRAIKEN_ADDRESS: ${KRAIKEN_ADDRESS}"
# Build TypeScript
echo "[txnbot-ci] Building TypeScript..."
npm run build
# Run the bot
exec npm run start

View file

@ -1,33 +0,0 @@
#!/bin/bash
set -euo pipefail
# Change to the webapp directory (Woodpecker runs from /woodpecker/src/)
cd /app/web-app
echo "[webapp-ci] Starting Web App..."
# Required environment variables (set by Woodpecker from bootstrap step)
: "${VITE_KRAIKEN_ADDRESS:?VITE_KRAIKEN_ADDRESS is required}"
: "${VITE_STAKE_ADDRESS:?VITE_STAKE_ADDRESS is required}"
# Disable Vue DevTools in CI to avoid path resolution issues
# vite.config.ts checks for CI=true to skip vite-plugin-vue-devtools
export CI=true
# Defaults for CI environment
export VITE_DEFAULT_CHAIN_ID=${VITE_DEFAULT_CHAIN_ID:-31337}
export VITE_LOCAL_RPC_URL=${VITE_LOCAL_RPC_URL:-/api/rpc}
export VITE_LOCAL_RPC_PROXY_TARGET=${VITE_LOCAL_RPC_PROXY_TARGET:-http://anvil:8545}
export VITE_LOCAL_GRAPHQL_PROXY_TARGET=${VITE_LOCAL_GRAPHQL_PROXY_TARGET:-http://ponder:42069}
export VITE_LOCAL_TXN_PROXY_TARGET=${VITE_LOCAL_TXN_PROXY_TARGET:-http://txn-bot:43069}
export VITE_SWAP_ROUTER=${VITE_SWAP_ROUTER:-0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4}
export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/api/graphql}
export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/api/txn}
echo "[webapp-ci] Environment configured:"
echo " VITE_KRAIKEN_ADDRESS: ${VITE_KRAIKEN_ADDRESS}"
echo " VITE_STAKE_ADDRESS: ${VITE_STAKE_ADDRESS}"
echo " VITE_DEFAULT_CHAIN_ID: ${VITE_DEFAULT_CHAIN_ID}"
# Run Vite dev server
exec npm run dev -- --host 0.0.0.0 --port 5173 --base /app/

View file

@ -1,42 +0,0 @@
#!/bin/bash
set -euo pipefail
echo "[integration] Starting Docker daemon..."
# Start Docker daemon in the background
dockerd-entrypoint.sh dockerd &
DOCKERD_PID=$!
# Wait for Docker daemon to be ready
echo "[integration] Waiting for Docker daemon..."
timeout 30 sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done'
echo "[integration] Docker daemon ready"
echo "[integration] Starting Harb stack..."
cd /workspace
# Build kraiken-lib if not already built
if [ ! -d "kraiken-lib/dist" ] || [ -z "$(ls -A kraiken-lib/dist 2>/dev/null)" ]; then
echo "[integration] Building kraiken-lib..."
./scripts/build-kraiken-lib.sh
fi
# Start the stack using dev.sh
echo "[integration] Launching stack via dev.sh..."
./scripts/dev.sh start
echo "[integration] Stack started successfully"
echo "[integration] Health endpoint: http://localhost:8081/api/graphql"
echo "[integration] Keeping container alive..."
# Keep the container running and forward signals to dockerd
trap "echo '[integration] Shutting down...'; ./scripts/dev.sh stop; kill $DOCKERD_PID; exit 0" SIGTERM SIGINT
# Wait for dockerd or run custom command if provided
if [ $# -gt 0 ]; then
echo "[integration] Executing: $*"
exec "$@"
else
wait $DOCKERD_PID
fi