refactor: default stack is Zebra + Zallet, Zaino behind an indexer profile #56
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| permissions: {} | |
| jobs: | |
| contract-validation: | |
| name: Contract validation (all networks) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Stage local config files for compose render | |
| # The live config/<net>/{zallet,zaino}.toml files are gitignored, but | |
| # docker compose config validates bind-mount source existence. Copy the | |
| # templates into place. | |
| run: | | |
| for net in mainnet testnet regtest; do | |
| cp config/$net/zallet.toml.example config/$net/zallet.toml | |
| cp config/$net/zaino.toml.example config/$net/zaino.toml | |
| done | |
| - name: Validate compose for every network | |
| run: | | |
| docker compose --env-file .env.mainnet config --quiet | |
| docker compose --env-file .env.testnet config --quiet | |
| docker compose --env-file .env.regtest config --quiet | |
| docker compose --env-file .env.mainnet --profile monitoring config --quiet | |
| docker compose --env-file .env.testnet --profile monitoring config --quiet | |
| docker compose --env-file .env.regtest --profile monitoring config --quiet | |
| docker compose --env-file .env.mainnet --profile indexer config --quiet | |
| docker compose --env-file .env.testnet --profile indexer config --quiet | |
| docker compose --env-file .env.regtest --profile indexer config --quiet | |
| - name: Install Python deps (contract parser + JSON Schema validator) | |
| run: python3 -m pip install --quiet pyyaml jsonschema | |
| - name: Validate z3-contract.yaml against z3-contract.schema.json | |
| run: | | |
| python3 -c ' | |
| import json, sys, yaml, jsonschema | |
| contract = yaml.safe_load(open("z3-contract.yaml")) | |
| schema = json.load(open("z3-contract.schema.json")) | |
| jsonschema.validate(contract, schema) | |
| print("PASS: z3-contract.yaml validates against z3-contract.schema.json") | |
| ' | |
| - name: Validate platform contract against resolved compose | |
| run: ./scripts/validate-contract.py | |
| - name: Validate contract inventory parity (env vars across compose and .env.example) | |
| run: ./scripts/validate-contract-parity.py | |
| zallet-config-permission-smoke: | |
| name: Zallet config permission smoke | |
| runs-on: ubuntu-latest | |
| needs: contract-validation | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Fresh setup files are readable by Zallet's compose user | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| ./scripts/setup-network.sh mainnet | |
| test "$(stat -c '%a' config/mainnet/zaino.toml)" = "644" | |
| test "$(stat -c '%a' config/mainnet/zallet.toml)" = "644" | |
| ZALLET_USER="$(docker compose --env-file .env.mainnet config --format json \ | |
| | python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["zallet"]["user"])')" | |
| docker run --rm --user "$ZALLET_USER" -v "$PWD/config/mainnet":/c:ro alpine \ | |
| sh -c 'cat /c/zallet.toml >/dev/null' | |
| - name: Existing restrictive files are repaired by setup-network.sh | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| ./scripts/setup-network.sh testnet | |
| chmod 600 config/testnet/zaino.toml config/testnet/zallet.toml | |
| ./scripts/setup-network.sh testnet | |
| test "$(stat -c '%a' config/testnet/zaino.toml)" = "644" | |
| test "$(stat -c '%a' config/testnet/zallet.toml)" = "644" | |
| ZALLET_USER="$(docker compose --env-file .env.testnet config --format json \ | |
| | python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["zallet"]["user"])')" | |
| docker run --rm --user "$ZALLET_USER" -v "$PWD/config/testnet":/c:ro alpine \ | |
| sh -c 'cat /c/zallet.toml >/dev/null' | |
| - name: Regtest pwhash rewrite keeps files readable | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| ./scripts/regtest-init.sh --prepare-only | |
| if grep -q '__GENERATED_BY_INIT_SH__' config/regtest/zallet.toml; then | |
| echo "regtest zallet.toml still has the pwhash placeholder" >&2 | |
| exit 1 | |
| fi | |
| test "$(stat -c '%a' config/regtest/zaino.toml)" = "644" | |
| test "$(stat -c '%a' config/regtest/zallet.toml)" = "644" | |
| test "$(stat -c '%a' config/regtest/zebra.toml)" = "644" | |
| ZALLET_USER="$(docker compose --env-file .env.regtest config --format json \ | |
| | python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["zallet"]["user"])')" | |
| docker run --rm --user "$ZALLET_USER" -v "$PWD/config/regtest":/c:ro alpine \ | |
| sh -c 'cat /c/zallet.toml >/dev/null' | |
| regtest-smoke: | |
| name: Regtest smoke test | |
| runs-on: ubuntu-latest | |
| needs: contract-validation | |
| # External identifiers for the regtest stack. Keep aligned with | |
| # z3-contract.yaml networks.regtest.{external_network,volumes.cookie}. | |
| # Values are literal constants, not user input — safe in run: blocks. | |
| env: | |
| COOKIE_VOLUME: z3-regtest-cookie | |
| EXTERNAL_NETWORK: z3-regtest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Stage regtest files with setup scripts | |
| run: | | |
| umask 077 | |
| ./scripts/regtest-init.sh --prepare-only | |
| - name: Start Zebra (regtest) | |
| run: | | |
| docker compose --env-file .env.regtest up -d zebra | |
| echo "Waiting for Zebra RPC on host port 29232..." | |
| for i in $(seq 1 60); do | |
| if curl -sf -X POST -H "Content-Type: application/json" \ | |
| -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \ | |
| http://127.0.0.1:29232 > /dev/null 2>&1; then | |
| echo "Zebra is ready" | |
| break | |
| fi | |
| if [ "$i" -eq 60 ]; then echo "Zebra failed to start" && exit 1; fi | |
| sleep 2 | |
| done | |
| - name: Verify Zebra is serving RPC | |
| run: | | |
| curl -sf -X POST -H "Content-Type: application/json" \ | |
| -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \ | |
| http://127.0.0.1:29232 | jq . | |
| - name: Mine the activation blocks | |
| # config/regtest activates NU5 at height 2; mine two blocks so the | |
| # network is past activation before wallet init. | |
| run: | | |
| curl -sf -u zebra:zebra -X POST -H "Content-Type: application/json" \ | |
| -d '{"jsonrpc":"2.0","method":"generate","params":[2],"id":1}' \ | |
| http://127.0.0.1:29232 | jq . | |
| - name: Verify Zallet accepts compose command | |
| run: docker compose --env-file .env.regtest run --rm --no-deps zallet --help | |
| - name: Generate the encryption identity in-container | |
| # Exercises the real generate-encryption-identity command against the | |
| # pinned image and asserts the key lands in the data volume owner-only | |
| # (mode 0600). The distroless image ships no data dir, so a fresh named | |
| # volume is root-owned; chown it to the zallet uid first, as the init | |
| # scripts do. | |
| run: | | |
| docker run --rm -v z3-regtest-zallet:/d busybox chown 1000:1000 /d | |
| docker compose --env-file .env.regtest run --rm --no-deps zallet \ | |
| --datadir /var/lib/zallet --config /etc/zallet/zallet.toml \ | |
| generate-encryption-identity | |
| MODE="$(docker run --rm -v z3-regtest-zallet:/d busybox stat -c '%a' /d/identity.txt)" | |
| test "$MODE" = "600" | |
| echo "identity.txt present in z3-regtest-zallet volume, mode $MODE" | |
| - name: Initialize the wallet and generate a mnemonic | |
| run: | | |
| docker compose --env-file .env.regtest run --rm zallet \ | |
| --datadir /var/lib/zallet --config /etc/zallet/zallet.toml \ | |
| init-wallet-encryption | |
| docker compose --env-file .env.regtest run --rm zallet \ | |
| --datadir /var/lib/zallet --config /etc/zallet/zallet.toml \ | |
| generate-mnemonic | |
| - name: Start Zallet and rpc-router, verify wallet RPC | |
| run: | | |
| docker compose --env-file .env.regtest up -d zallet rpc-router | |
| echo "Waiting for Zallet wallet RPC on host port 50232..." | |
| for i in $(seq 1 30); do | |
| if RESULT=$(curl -sf -u zebra:zebra -X POST -H "Content-Type: application/json" \ | |
| -d '{"jsonrpc":"2.0","method":"getwalletinfo","params":[],"id":1}' \ | |
| http://127.0.0.1:50232 2>/dev/null); then | |
| echo "Zallet wallet RPC is ready" | |
| echo "$RESULT" | jq . | |
| break | |
| fi | |
| if [ "$i" -eq 30 ]; then | |
| echo "Zallet wallet RPC never responded" | |
| docker compose --env-file .env.regtest logs zallet rpc-router --tail 50 | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| echo "Asserting rpc-router proxies chain RPCs..." | |
| curl -sf -u zebra:zebra -X POST -H "Content-Type: application/json" \ | |
| -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \ | |
| http://127.0.0.1:8181 | jq .result.blocks | |
| - name: Assert Zallet config is readable by the compose user | |
| # Guards the config-readability contract at the same UID/GID declared | |
| # for the zallet service. Catches script regressions and compose user | |
| # changes that would make the bind-mounted config unreadable. | |
| run: | | |
| ZALLET_USER="$(docker compose --env-file .env.regtest config --format json \ | |
| | python3 -c 'import json, sys; print(json.load(sys.stdin)["services"]["zallet"]["user"])')" | |
| docker run --rm --user "$ZALLET_USER" -v "$PWD/config/regtest":/c:ro alpine \ | |
| sh -c 'cat /c/zallet.toml >/dev/null && echo "compose user read OK"' | |
| - name: Verify the cookie volume is attachable | |
| # Regtest disables cookie auth (ZEBRA_RPC__ENABLE_COOKIE_AUTH=false) so | |
| # Zebra does not write /auth/.cookie. This check only verifies that the | |
| # published volume can be mounted. | |
| run: | | |
| docker run --rm -v "$COOKIE_VOLUME":/auth:ro alpine \ | |
| sh -c 'test -d /auth && echo "Cookie volume mounted read-only at /auth"' | |
| - name: Verify the external network is attachable | |
| run: | | |
| docker run --rm --network "$EXTERNAL_NETWORK" alpine \ | |
| sh -c 'apk add --no-cache bind-tools >/dev/null 2>&1 && nslookup zebra && nslookup zallet' | |
| - name: Collect logs on failure | |
| if: failure() | |
| run: | | |
| echo "=== Container status ===" | |
| docker compose --env-file .env.regtest ps 2>&1 || true | |
| echo "=== Zebra logs ===" | |
| docker compose --env-file .env.regtest logs zebra --tail 50 2>&1 || true | |
| echo "=== Zallet logs ===" | |
| docker compose --env-file .env.regtest logs zallet --tail 50 2>&1 || true | |
| echo "=== rpc-router logs ===" | |
| docker compose --env-file .env.regtest logs rpc-router --tail 50 2>&1 || true | |
| - name: Tear down | |
| if: always() | |
| run: docker compose --env-file .env.regtest --profile "*" down -v --remove-orphans 2>&1 || true | |
| cookie-permission-smoke: | |
| # Regtest disables cookie auth, so the regtest smoke job does not exercise | |
| # the cookie path. This job boots testnet Zebra and checks that the cookie | |
| # file is readable by service runtime users. | |
| name: Cookie permission smoke (testnet) | |
| runs-on: ubuntu-latest | |
| needs: contract-validation | |
| # External cookie volume for the testnet stack. Keep aligned with | |
| # z3-contract.yaml networks.testnet.volumes.cookie. | |
| env: | |
| COOKIE_VOLUME: z3-testnet-cookie | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Stage operator files for testnet boot | |
| run: | | |
| cp config/testnet/zallet.toml.example config/testnet/zallet.toml | |
| cp config/testnet/zaino.toml.example config/testnet/zaino.toml | |
| - name: Start testnet zebra and cookie-permissions sidecar | |
| # The sidecar chmods the cookie 0644 so services that mount the shared | |
| # volume can read it. | |
| run: | | |
| docker compose --env-file .env.testnet up -d zebra cookie-permissions | |
| echo "Waiting up to 60s for Zebra to write the cookie file..." | |
| for i in $(seq 1 30); do | |
| if docker run --rm -v "$COOKIE_VOLUME":/auth:ro alpine test -f /auth/.cookie 2>/dev/null; then | |
| echo "Cookie file present after ~$((i*2))s" | |
| break | |
| fi | |
| if [ "$i" -eq 30 ]; then | |
| echo "Cookie file never appeared" | |
| docker compose --env-file .env.testnet logs zebra --tail 50 | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| # Give the sidecar one polling cycle to chmod the cookie. | |
| sleep 6 | |
| - name: Assert cookie file is readable by both Zaino (uid 1000) and Zallet (uid 1000) | |
| # Test the runtime UIDs used by the services that read the shared | |
| # cookie volume. | |
| run: | | |
| docker run --rm --user 1000:1000 -v "$COOKIE_VOLUME":/auth:ro alpine \ | |
| sh -c 'cat /auth/.cookie >/dev/null && echo "uid 1000 read OK"' | |
| docker run --rm --user 10001:10001 -v "$COOKIE_VOLUME":/auth:ro alpine \ | |
| sh -c 'cat /auth/.cookie >/dev/null && echo "uid 10001 read OK"' | |
| - name: Tear down | |
| if: always() | |
| run: docker compose --env-file .env.testnet --profile "*" down -v --remove-orphans 2>&1 || true | |
| rpc-router-tests: | |
| name: rpc-router cargo test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Install Rust toolchain | |
| run: | | |
| rustup toolchain install 1.85 --profile minimal --no-self-update | |
| rustup default 1.85 | |
| - name: cargo test | |
| working-directory: rpc-router | |
| run: cargo test --locked --all-targets |