Skip to content

docs: document Zebra latest image override (#50) #55

docs: document Zebra latest image override (#50)

docs: document Zebra latest image override (#50) #55

Workflow file for this run

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
- 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 block 1 (required for Zaino sync)
run: |
curl -sf -u zebra:zebra -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"generate","params":[1],"id":1}' \
http://127.0.0.1:29232 | jq .
- name: Start Zaino and verify JSON-RPC proxy
run: |
docker compose --env-file .env.regtest up -d zaino
echo "Waiting for Zaino JSON-RPC on host port 28237..."
for i in $(seq 1 30); do
if RESULT=$(curl -sf -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' \
http://127.0.0.1:28237 2>/dev/null); then
echo "Zaino is ready"
echo "$RESULT" | jq .
break
fi
if [ "$i" -eq 30 ]; then
echo "Zaino failed to start"
docker compose --env-file .env.regtest logs zaino --tail 20
exit 1
fi
sleep 2
done
- 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: 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 zaino'
- 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 "=== Zaino logs ==="
docker compose --env-file .env.regtest logs zaino --tail 50 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 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