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

157
scripts/bootstrap-common.sh Executable file
View file

@ -0,0 +1,157 @@
#!/usr/bin/env bash
# Shared bootstrap functions for local dev and CI.
# Source this file after setting these variables:
# ANVIL_RPC - Anvil JSON-RPC URL (required)
# DEPLOYER_PK - Deployer private key (defaults to Anvil account 0)
# DEPLOYER_ADDR - Deployer address (defaults to Anvil account 0)
# TXNBOT_ADDRESS - TxnBot wallet address (optional)
# TXNBOT_PRIVATE_KEY- TxnBot private key (optional)
# TXNBOT_FUND_VALUE - Amount to fund txnBot (default: 1ether)
# CONTRACT_ENV - Path to write contracts.env (required)
# LOG_FILE - Log file for cast/forge output (default: /dev/null)
# ONCHAIN_DIR - Path to onchain/ directory (required)
# KRAIKEN_LIB_DIR - Path to kraiken-lib/ directory (optional, for CI build)
set -euo pipefail
# ── Constants ──────────────────────────────────────────────────────────
FEE_DEST=0xf6a3eef9088A255c32b6aD2025f83E57291D9011
WETH=0x4200000000000000000000000000000000000006
SWAP_ROUTER=0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4
MAX_UINT=0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
DEFAULT_DEPLOYER_PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
DEFAULT_DEPLOYER_ADDR=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
DEFAULT_TXNBOT_PK=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
DEFAULT_TXNBOT_ADDR=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
# ── Defaults ───────────────────────────────────────────────────────────
DEPLOYER_PK=${DEPLOYER_PK:-$DEFAULT_DEPLOYER_PK}
DEPLOYER_ADDR=${DEPLOYER_ADDR:-$DEFAULT_DEPLOYER_ADDR}
TXNBOT_FUND_VALUE=${TXNBOT_FUND_VALUE:-1ether}
LOG_FILE=${LOG_FILE:-/dev/null}
# ── Helpers ────────────────────────────────────────────────────────────
bootstrap_log() {
echo "[bootstrap] $*"
}
# ── Functions ──────────────────────────────────────────────────────────
wait_for_rpc() {
for _ in {1..120}; do
if cast chain-id --rpc-url "$ANVIL_RPC" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
bootstrap_log "Timed out waiting for Anvil at $ANVIL_RPC"
return 1
}
run_forge_script() {
bootstrap_log "Deploying contracts to fork"
pushd "$ONCHAIN_DIR" >/dev/null
forge script script/DeployLocal.sol --fork-url "$ANVIL_RPC" --broadcast >>"$LOG_FILE" 2>&1
popd >/dev/null
}
extract_addresses() {
local run_file
run_file="$(ls -t "$ONCHAIN_DIR/broadcast/DeployLocal.sol"/*/run-latest.json 2>/dev/null | head -n1)"
if [[ -z "$run_file" ]]; then
bootstrap_log "Deployment artifact not found"
exit 1
fi
bootstrap_log "Using artifact $run_file"
LIQUIDITY_MANAGER="$(jq -r '.transactions[] | select(.contractName=="LiquidityManager") | .contractAddress' "$run_file" | head -n1)"
KRAIKEN="$(jq -r '.transactions[] | select(.contractName=="Kraiken") | .contractAddress' "$run_file" | head -n1)"
STAKE="$(jq -r '.transactions[] | select(.contractName=="Stake") | .contractAddress' "$run_file" | head -n1)"
DEPLOY_BLOCK="$(jq -r '.receipts[0].blockNumber' "$run_file" | xargs printf "%d")"
if [[ -z "$LIQUIDITY_MANAGER" || "$LIQUIDITY_MANAGER" == "null" ]]; then
bootstrap_log "LiquidityManager address missing"
exit 1
fi
}
write_contracts_env() {
cat >"$CONTRACT_ENV" <<EOCONTRACTS
LIQUIDITY_MANAGER=$LIQUIDITY_MANAGER
KRAIKEN=$KRAIKEN
STAKE=$STAKE
EOCONTRACTS
}
fund_liquidity_manager() {
bootstrap_log "Funding LiquidityManager"
cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \
"$LIQUIDITY_MANAGER" --value 0.1ether >>"$LOG_FILE" 2>&1
}
grant_recenter_access() {
bootstrap_log "Granting recenter access to deployer"
cast rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1
cast send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \
"$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$DEPLOYER_ADDR" >>"$LOG_FILE" 2>&1
cast rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1
if [[ -n "${TXNBOT_ADDRESS:-}" ]]; then
bootstrap_log "Granting recenter access to txnBot ($TXNBOT_ADDRESS)"
cast rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1
cast send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \
"$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$TXNBOT_ADDRESS" >>"$LOG_FILE" 2>&1
cast rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1
fi
}
call_recenter() {
local recenter_pk="$DEPLOYER_PK"
local recenter_addr="$DEPLOYER_ADDR"
if [[ -n "${TXNBOT_ADDRESS:-}" ]]; then
recenter_pk="$TXNBOT_PRIVATE_KEY"
recenter_addr="$TXNBOT_ADDRESS"
fi
bootstrap_log "Calling recenter() via $recenter_addr"
cast send --rpc-url "$ANVIL_RPC" --private-key "$recenter_pk" \
"$LIQUIDITY_MANAGER" "recenter()" >>"$LOG_FILE" 2>&1
}
seed_application_state() {
bootstrap_log "Wrapping ETH to WETH"
cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \
"$WETH" "deposit()" --value 0.02ether >>"$LOG_FILE" 2>&1
bootstrap_log "Approving router"
cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \
"$WETH" "approve(address,uint256)" "$SWAP_ROUTER" "$MAX_UINT" >>"$LOG_FILE" 2>&1
bootstrap_log "Executing initial KRK swap"
cast send --legacy --gas-limit 300000 --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \
"$SWAP_ROUTER" "exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
"($WETH,$KRAIKEN,10000,$DEPLOYER_ADDR,10000000000000000,0,0)" >>"$LOG_FILE" 2>&1
}
fund_txn_bot_wallet() {
if [[ -z "${TXNBOT_ADDRESS:-}" ]]; then
return
fi
bootstrap_log "Funding txnBot wallet $TXNBOT_ADDRESS with $TXNBOT_FUND_VALUE"
cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \
"$TXNBOT_ADDRESS" --value "$TXNBOT_FUND_VALUE" >>"$LOG_FILE" 2>&1 || true
local wei hex
wei="$(cast --to-unit "$TXNBOT_FUND_VALUE" wei)"
hex="$(cast --to-hex "$wei")"
cast rpc --rpc-url "$ANVIL_RPC" anvil_setBalance "$TXNBOT_ADDRESS" "$hex" >>"$LOG_FILE" 2>&1
}
write_deployments_json() {
local target="${1:-$ONCHAIN_DIR/deployments-local.json}"
cat >"$target" <<EODEPLOYMENTS
{
"contracts": {
"Kraiken": "$KRAIKEN",
"Stake": "$STAKE",
"LiquidityManager": "$LIQUIDITY_MANAGER"
}
}
EODEPLOYMENTS
}

View file

@ -1,5 +1,6 @@
#!/bin/bash
# Build and push CI images for E2E testing
# Build CI images locally using the unified Dockerfile.service-ci
# For CI pipeline builds, see .woodpecker/build-ci-images.yml
set -euo pipefail
cd "$(dirname "$0")/.."
@ -15,7 +16,12 @@ echo "Tag: $TAG"
echo ""
echo "=== Building ponder-ci ==="
docker build \
-f docker/Dockerfile.ponder-ci \
-f docker/Dockerfile.service-ci \
--build-arg SERVICE_DIR=services/ponder \
--build-arg SERVICE_PORT=42069 \
--build-arg ENTRYPOINT_SCRIPT=containers/ponder-entrypoint.sh \
--build-arg HEALTHCHECK_RETRIES=12 \
--build-arg HEALTHCHECK_START=20s \
-t "$REGISTRY/harb/ponder-ci:$TAG" \
.
@ -23,7 +29,15 @@ docker build \
echo ""
echo "=== Building webapp-ci ==="
docker build \
-f docker/Dockerfile.webapp-ci \
-f docker/Dockerfile.service-ci \
--build-arg SERVICE_DIR=web-app \
--build-arg SERVICE_PORT=5173 \
--build-arg HEALTHCHECK_PATH=/app/ \
--build-arg HEALTHCHECK_RETRIES=84 \
--build-arg HEALTHCHECK_START=15s \
--build-arg ENTRYPOINT_SCRIPT=containers/webapp-entrypoint.sh \
--build-arg NODE_ENV=development \
--build-arg NEEDS_SYMLINKS=true \
-t "$REGISTRY/harb/webapp-ci:$TAG" \
.
@ -31,7 +45,13 @@ docker build \
echo ""
echo "=== Building landing-ci ==="
docker build \
-f docker/Dockerfile.landing-ci \
-f docker/Dockerfile.service-ci \
--build-arg SERVICE_DIR=landing \
--build-arg SERVICE_PORT=5174 \
--build-arg ENTRYPOINT_SCRIPT=containers/landing-ci-entrypoint.sh \
--build-arg NODE_ENV=development \
--build-arg HEALTHCHECK_RETRIES=6 \
--build-arg HEALTHCHECK_START=10s \
-t "$REGISTRY/harb/landing-ci:$TAG" \
.
@ -39,7 +59,14 @@ docker build \
echo ""
echo "=== Building txnbot-ci ==="
docker build \
-f docker/Dockerfile.txnbot-ci \
-f docker/Dockerfile.service-ci \
--build-arg SERVICE_DIR=services/txnBot \
--build-arg SERVICE_PORT=43069 \
--build-arg HEALTHCHECK_PATH=/status \
--build-arg HEALTHCHECK_RETRIES=4 \
--build-arg HEALTHCHECK_START=10s \
--build-arg ENTRYPOINT_SCRIPT=containers/txnbot-entrypoint.sh \
--build-arg NPM_INSTALL_CMD=install \
-t "$REGISTRY/harb/txnbot-ci:$TAG" \
.

View file

@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
REGISTRY="${REGISTRY:-registry.niovi.voyage}"
IMAGE_NAME="${IMAGE_NAME:-harb/integration}"
TAG="${TAG:-latest}"
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
echo "Building integration image: ${FULL_IMAGE}"
echo "This may take 5-10 minutes on first build..."
# Build kraiken-lib first (required by the image)
echo "=== Building kraiken-lib ==="
./scripts/build-kraiken-lib.sh
# Build the integration image
echo "=== Building Docker image ==="
docker build \
-f docker/Dockerfile.integration \
-t "${FULL_IMAGE}" \
--progress=plain \
.
echo ""
echo "✓ Image built successfully: ${FULL_IMAGE}"
echo ""
echo "To test locally:"
echo " docker run --rm --privileged -p 8081:8081 ${FULL_IMAGE}"
echo ""
echo "To push to registry:"
echo " docker push ${FULL_IMAGE}"

74
scripts/ci-bootstrap.sh Executable file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env bash
# CI bootstrap script — runs under bash to support 'source' and bash-isms.
# Env vars ANVIL_RPC, CONTRACT_ENV, LOG_FILE, ONCHAIN_DIR, TXNBOT_FUND_VALUE,
# TXNBOT_ADDRESS, TXNBOT_PRIVATE_KEY must be set before calling this script.
set -euo pipefail
echo "=== Foundry version ==="
forge --version
cast --version
# Source shared bootstrap functions
source scripts/bootstrap-common.sh
echo "=== Waiting for Anvil ==="
wait_for_rpc
echo "=== Deploying contracts ==="
run_forge_script
extract_addresses
echo "=== Contract Deployment Complete ==="
echo "KRAIKEN: $KRAIKEN"
echo "STAKE: $STAKE"
echo "LIQUIDITY_MANAGER: $LIQUIDITY_MANAGER"
echo "DEPLOY_BLOCK: $DEPLOY_BLOCK"
# Build kraiken-lib BEFORE writing contracts.env
# (services wait for contracts.env, so kraiken-lib must be ready first)
echo "=== Building kraiken-lib (shared dependency) ==="
cd kraiken-lib
npm ci --ignore-scripts
if [[ -f ../scripts/sync-tax-rates.mjs ]]; then
node ../scripts/sync-tax-rates.mjs
fi
./node_modules/.bin/tsc
cd ..
# Get current block number as start block
START_BLOCK=$(cast block-number --rpc-url "$ANVIL_RPC")
# Write contracts.env with CI-specific extra vars
{
echo "KRAIKEN=$KRAIKEN"
echo "STAKE=$STAKE"
echo "LIQUIDITY_MANAGER=$LIQUIDITY_MANAGER"
echo "START_BLOCK=$START_BLOCK"
echo "PONDER_RPC_URL_1=http://anvil:8545"
echo "DATABASE_URL=postgres://ponder:ponder_local@postgres:5432/ponder_local"
echo "RPC_URL=http://anvil:8545"
} > "$CONTRACT_ENV"
# Write deployments-local.json for E2E tests
write_deployments_json "$ONCHAIN_DIR/deployments-local.json"
echo "=== deployments-local.json written ==="
cat "$ONCHAIN_DIR/deployments-local.json"
echo "=== Funding LiquidityManager ==="
fund_liquidity_manager
echo "=== Granting recenter access ==="
grant_recenter_access
echo "=== Calling recenter() to seed liquidity ==="
call_recenter
echo "=== Seeding application state (initial swap) ==="
seed_application_state
echo "=== Funding txnBot ==="
fund_txn_bot_wallet
echo "TXNBOT_PRIVATE_KEY=$TXNBOT_PRIVATE_KEY" >> "$CONTRACT_ENV"
echo "=== Bootstrap complete ==="

View file

@ -34,7 +34,8 @@ fi
container_name() {
local service="$1"
echo "${PROJECT_NAME}_${service}_1"
# docker compose v2 uses hyphens; v1 used underscores
echo "${PROJECT_NAME}-${service}-1"
}
# Check Docker disk usage and warn if approaching limits
@ -201,6 +202,13 @@ start_stack() {
wait_for_healthy "$(container_name caddy)" "$CADDY_TIMEOUT" || exit 1
# Smoke test: verify end-to-end connectivity through Caddy
echo " Running smoke test..."
./scripts/wait-for-service.sh http://localhost:8081/app/ 30 "caddy-proxy" || {
echo " [!!] Smoke test failed — Caddy proxy not serving /app/"
exit 1
}
if [[ -z "${SKIP_WATCH:-}" ]]; then
echo "Watching for kraiken-lib changes..."
./scripts/watch-kraiken-lib.sh &

26
scripts/wait-for-service.sh Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Wait for an HTTP service to respond with 2xx.
# Usage: wait-for-service.sh <url> [timeout_seconds] [label]
set -euo pipefail
URL="${1:?Usage: wait-for-service.sh <url> [timeout_seconds] [label]}"
TIMEOUT="${2:-120}"
LABEL="${3:-$URL}"
INTERVAL=5
ATTEMPTS=$((TIMEOUT / INTERVAL))
if (( ATTEMPTS < 1 )); then ATTEMPTS=1; fi
for i in $(seq 1 "$ATTEMPTS"); do
if curl -sf --max-time 3 "$URL" > /dev/null 2>&1; then
echo "[wait] $LABEL healthy after $((i * INTERVAL))s"
exit 0
fi
echo "[wait] ($i/$ATTEMPTS) $LABEL not ready..."
sleep "$INTERVAL"
done
echo "[wait] ERROR: $LABEL not healthy after ${TIMEOUT}s"
echo "--- Diagnostic: $URL ---"
curl -v --max-time 5 "$URL" 2>&1 | head -20 || true
exit 1