From 98f7549c6c2aeb2570a97a76f6f8a2dd0d0682d4 Mon Sep 17 00:00:00 2001 From: Great-DOA Date: Sat, 21 Feb 2026 01:34:17 +0100 Subject: [PATCH 1/3] feat: add GitHub Action, CI workflows, CLI improvements and docs --- .github/workflows/build-images.yml | 73 +++ .github/workflows/ci-action-test.yml | 160 +++++ .github/workflows/e2e-test.yml | 38 +- .github/workflows/golden-e2e.yml | 190 ++++++ .github/workflows/smoke-test.yml | 36 +- .gitignore | 3 + README.md | 13 +- action.yml | 693 +++++++++++++++++++++ cli/src/commands/status.rs | 4 +- cli/src/commands/test.rs | 18 +- cli/src/commands/up.rs | 2 +- docker-compose.yml | 5 + docker/zingo/Dockerfile | 1 + docs/github-action.md | 457 ++++++++++++++ sample/.github/workflows/ci.yml | 208 +++++++ sample/.github/workflows/failure-drill.yml | 308 +++++++++ sample/README.md | 174 ++++++ 17 files changed, 2365 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/build-images.yml create mode 100644 .github/workflows/ci-action-test.yml create mode 100644 .github/workflows/golden-e2e.yml create mode 100644 action.yml create mode 100644 docs/github-action.md create mode 100644 sample/.github/workflows/ci.yml create mode 100644 sample/.github/workflows/failure-drill.yml create mode 100644 sample/README.md diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml new file mode 100644 index 0000000..bd61437 --- /dev/null +++ b/.github/workflows/build-images.yml @@ -0,0 +1,73 @@ +name: Build and Publish Images + +on: + push: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/zeckit + +jobs: + build: + name: Build and push images + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: zebra + context: docker/zebra + file: docker/zebra/Dockerfile + - name: lightwalletd + context: docker/lightwalletd + file: docker/lightwalletd/Dockerfile + - name: zaino + context: docker/zaino + file: docker/zaino/Dockerfile + - name: zingo + context: docker/zingo + file: docker/zingo/Dockerfile + - name: faucet + context: zeckit-faucet + file: zeckit-faucet/Dockerfile + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_PREFIX }}-${{ matrix.name }} + tags: | + type=ref,event=branch + type=sha,format=short,prefix=sha- + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.context }} + file: ${{ matrix.file }} + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.name }} + cache-to: type=gha,mode=max,scope=${{ matrix.name }} diff --git a/.github/workflows/ci-action-test.yml b/.github/workflows/ci-action-test.yml new file mode 100644 index 0000000..4b19cdf --- /dev/null +++ b/.github/workflows/ci-action-test.yml @@ -0,0 +1,160 @@ +# ============================================================ +# ZecKit – CI Self-Test for the Published Action +# ============================================================ +# Exercises the root action.yml and the reusable golden-e2e +# workflow against both backends on every push/PR to main. +# +# This is also the workflow that prospective callers can use as +# a copy-paste reference for their own repos. +# ============================================================ + +name: Action CI Self-Test + +on: + push: + branches: + - main + - develop + - 'release/**' + pull_request: + branches: + - main + - develop + workflow_dispatch: + inputs: + backend: + description: 'Backend to test (zaino | lwd | both)' + required: false + default: 'both' + upload_artifacts: + description: 'Artifact upload policy (always | on-failure | never)' + required: false + default: 'on-failure' + +permissions: + contents: read + packages: read + +# ============================================================ +# STRATEGY MATRIX +# ============================================================ +jobs: + + # ---------------------------------------------------------- + # Determine which backends to run based on trigger + # ---------------------------------------------------------- + set-matrix: + name: Set test matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - name: Build backend matrix + id: matrix + shell: bash + run: | + requested="${{ github.event.inputs.backend }}" + + if [[ "$requested" == "zaino" ]]; then + echo 'matrix={"backend":["zaino"]}' >> "$GITHUB_OUTPUT" + elif [[ "$requested" == "lwd" ]]; then + echo 'matrix={"backend":["lwd"]}' >> "$GITHUB_OUTPUT" + else + # Default: both backends + echo 'matrix={"backend":["zaino","lwd"]}' >> "$GITHUB_OUTPUT" + fi + + # ---------------------------------------------------------- + # Run the golden E2E flow via the COMPOSITE ACTION directly + # ---------------------------------------------------------- + composite-action-test: + name: "Composite Action – ${{ matrix.backend }}" + needs: set-matrix + runs-on: ubuntu-latest + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.set-matrix.outputs.matrix) }} + + steps: + - name: Checkout ZecKit + uses: actions/checkout@v4 + + - name: Run ZecKit E2E composite action (self-test) + id: e2e + uses: ./ + with: + backend: ${{ matrix.backend }} + startup_timeout_minutes: '10' + block_wait_seconds: '75' + send_amount: '0.05' + send_memo: 'CI self-test – ${{ matrix.backend }}' + upload_artifacts: ${{ github.event.inputs.upload_artifacts || 'on-failure' }} + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Print action outputs + if: always() + shell: bash + run: | + echo "unified_address : ${{ steps.e2e.outputs.unified_address }}" + echo "transparent_address : ${{ steps.e2e.outputs.transparent_address }}" + echo "shield_txid : ${{ steps.e2e.outputs.shield_txid }}" + echo "send_txid : ${{ steps.e2e.outputs.send_txid }}" + echo "final_orchard_balance : ${{ steps.e2e.outputs.final_orchard_balance }} ZEC" + echo "block_height : ${{ steps.e2e.outputs.block_height }}" + echo "test_result : ${{ steps.e2e.outputs.test_result }}" + + - name: Assert test_result is 'pass' + shell: bash + run: | + result="${{ steps.e2e.outputs.test_result }}" + if [[ "$result" != "pass" ]]; then + echo "::error::Golden E2E returned test_result='$result' (expected 'pass')." + exit 1 + fi + echo "✓ test_result=pass" + + # ---------------------------------------------------------- + # Run the same flow via the REUSABLE WORKFLOW + # (validates workflow_call path end-to-end) + # ---------------------------------------------------------- + reusable-workflow-test: + name: "Reusable Workflow – zaino" + needs: set-matrix + uses: ./.github/workflows/golden-e2e.yml + with: + backend: 'zaino' + startup_timeout_minutes: 10 + block_wait_seconds: 75 + send_amount: 0.05 + send_memo: 'Reusable workflow self-test' + upload_artifacts: 'on-failure' + secrets: + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + + # ---------------------------------------------------------- + # Gate: all matrix jobs must pass + # ---------------------------------------------------------- + ci-gate: + name: CI Gate + needs: + - composite-action-test + - reusable-workflow-test + runs-on: ubuntu-latest + if: always() + steps: + - name: Check all jobs + shell: bash + run: | + composite="${{ needs.composite-action-test.result }}" + reusable="${{ needs.reusable-workflow-test.result }}" + + echo "composite-action-test : $composite" + echo "reusable-workflow-test: $reusable" + + if [[ "$composite" != "success" || "$reusable" != "success" ]]; then + echo "::error::One or more E2E jobs failed." + exit 1 + fi + echo "✓ All CI jobs passed." diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index a5d5d1a..98dcabe 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -11,10 +11,16 @@ on: - develop workflow_dispatch: +permissions: + contents: read + packages: read + jobs: e2e-tests: name: ZecKit E2E Test Suite - runs-on: self-hosted + runs-on: ubuntu-latest + env: + IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/zeckit timeout-minutes: 60 @@ -81,6 +87,36 @@ jobs: echo "✓ Cleanup complete (images preserved)" echo "" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Select best prebuilt tag + run: | + image_prefix="$(echo "${IMAGE_PREFIX}" | tr '[:upper:]' '[:lower:]')" + echo "ZECKIT_IMAGE_PREFIX=${image_prefix}" >> "$GITHUB_ENV" + short_sha="${GITHUB_SHA::7}" + branch_tag="$(echo "${GITHUB_REF_NAME}" | tr '/' '-')" + candidates=("sha-${short_sha}" "${branch_tag}" "main" "latest") + + for tag in "${candidates[@]}"; do + if docker manifest inspect "${image_prefix}-zaino:${tag}" >/dev/null 2>&1; then + echo "Using prebuilt tag: ${tag}" + echo "ZECKIT_IMAGE_TAG=${tag}" >> "$GITHUB_ENV" + exit 0 + fi + done + + echo "No prebuilt tag found; will fall back to local build" + echo "ZECKIT_IMAGE_TAG=sha-${short_sha}" >> "$GITHUB_ENV" + + - name: Pull prebuilt images (zaino profile) + run: | + docker compose --profile zaino pull || echo "Prebuilt images not found; compose will build locally." - name: Build CLI binary run: | diff --git a/.github/workflows/golden-e2e.yml b/.github/workflows/golden-e2e.yml new file mode 100644 index 0000000..4aac1bb --- /dev/null +++ b/.github/workflows/golden-e2e.yml @@ -0,0 +1,190 @@ +# ============================================================ +# ZecKit – Reusable Golden E2E Workflow +# ============================================================ +# Call this workflow from any repository: +# +# jobs: +# zeckit-e2e: +# uses: zecdev/ZecKit/.github/workflows/golden-e2e.yml@v1 +# with: +# backend: zaino +# secrets: +# ghcr_token: ${{ secrets.GITHUB_TOKEN }} +# +# The workflow spins up a full ZecKit devnet (pre-built images), +# executes the golden shielded-transaction flow, publishes +# structured outputs, and uploads log artifacts. +# ============================================================ + +name: ZecKit Golden E2E (Reusable) + +on: + # Called by other workflows (same or external repos) + workflow_call: + inputs: + # --- Backend --- + backend: + description: > + Light-client backend: 'zaino' (Rust, faster) or 'lwd' (Lightwalletd, Go). + type: string + required: false + default: 'zaino' + + # --- Timeouts --- + startup_timeout_minutes: + description: > + Maximum minutes to wait for all services to become healthy. + Zaino: ~3 min; lwd: ~4 min. Default: 10 min. + type: number + required: false + default: 10 + + block_wait_seconds: + description: > + Seconds to wait for a block after broadcasting a transaction. + Zebra mines a block every 30-60 s. Default: 75 s. + type: number + required: false + default: 75 + + # --- Chain / wallet params --- + send_amount: + description: Amount in ZEC to send in the shielded-send step. + type: number + required: false + default: 0.05 + + send_address: + description: > + Destination Unified Address for the shielded send. + Leave empty to perform a self-send to the faucet UA. + type: string + required: false + default: '' + + send_memo: + description: Memo text for the shielded send transaction. + type: string + required: false + default: 'ZecKit E2E golden flow' + + # --- Image params --- + image_prefix: + description: > + Docker image prefix (registry/owner/zeckit). + Examples: ghcr.io/zecdev/zeckit + type: string + required: false + default: 'ghcr.io/zecdev/zeckit' + + image_tag: + description: > + Docker image tag. Leave empty for auto-detection + (sha- → branch-name → main → latest). + type: string + required: false + default: '' + + # --- Artifact behaviour --- + upload_artifacts: + description: > + 'always' | 'on-failure' | 'never' + Controls when log artifacts are uploaded. + type: string + required: false + default: 'on-failure' + + # --- Runner --- + runs_on: + description: > + GitHub-hosted runner label for the job (e.g. ubuntu-latest, + ubuntu-22.04, or a self-hosted label). + type: string + required: false + default: 'ubuntu-latest' + + secrets: + ghcr_token: + description: > + Token used to pull pre-built images from GHCR. + Pass secrets.GITHUB_TOKEN from the calling workflow. + required: true + + outputs: + unified_address: + description: Unified Address generated by the faucet wallet. + value: ${{ jobs.golden-e2e.outputs.unified_address }} + + transparent_address: + description: Transparent address of the faucet wallet. + value: ${{ jobs.golden-e2e.outputs.transparent_address }} + + shield_txid: + description: Transaction ID of the autoshield operation. + value: ${{ jobs.golden-e2e.outputs.shield_txid }} + + send_txid: + description: Transaction ID of the shielded send. + value: ${{ jobs.golden-e2e.outputs.send_txid }} + + final_orchard_balance: + description: Orchard ZEC balance after the complete E2E flow. + value: ${{ jobs.golden-e2e.outputs.final_orchard_balance }} + + block_height: + description: Blockchain height at end of the test. + value: ${{ jobs.golden-e2e.outputs.block_height }} + + test_result: + description: Overall test result – 'pass' or 'fail'. + value: ${{ jobs.golden-e2e.outputs.test_result }} + +# ============================================================ +# JOBS +# ============================================================ +jobs: + golden-e2e: + name: "Golden E2E – ${{ inputs.backend }}" + runs-on: ${{ inputs.runs_on }} + timeout-minutes: 30 + + # ── Permissions (for GHCR pull, artifact upload, step summary) ── + permissions: + contents: read + packages: read + + # ── Propagate composite-action outputs ── + outputs: + unified_address: ${{ steps.run-action.outputs.unified_address }} + transparent_address: ${{ steps.run-action.outputs.transparent_address }} + shield_txid: ${{ steps.run-action.outputs.shield_txid }} + send_txid: ${{ steps.run-action.outputs.send_txid }} + final_orchard_balance: ${{ steps.run-action.outputs.final_orchard_balance }} + block_height: ${{ steps.run-action.outputs.block_height }} + test_result: ${{ steps.run-action.outputs.test_result }} + + steps: + # ── Checkout is required so the composite action can be resolved ── + - name: Checkout ZecKit + uses: actions/checkout@v4 + with: + repository: zecdev/ZecKit + # Use the same ref that triggered this workflow so that action.yml + # and docker-compose.yml are always in sync with the workflow file. + ref: ${{ github.ref }} + + # ── Run the composite action from the local checkout ── + - name: Run ZecKit E2E composite action + id: run-action + uses: ./ # resolves to the action.yml in the ZecKit checkout above + with: + backend: ${{ inputs.backend }} + startup_timeout_minutes: ${{ inputs.startup_timeout_minutes }} + block_wait_seconds: ${{ inputs.block_wait_seconds }} + send_amount: ${{ inputs.send_amount }} + send_address: ${{ inputs.send_address }} + send_memo: ${{ inputs.send_memo }} + image_prefix: ${{ inputs.image_prefix }} + image_tag: ${{ inputs.image_tag }} + upload_artifacts: ${{ inputs.upload_artifacts }} + ghcr_token: ${{ secrets.ghcr_token }} diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 17d19b7..1512867 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -9,10 +9,16 @@ on: - main workflow_dispatch: # Allow manual triggers +permissions: + contents: read + packages: read + jobs: smoke-test: name: Zebra Smoke Test - runs-on: self-hosted # Runs on your WSL runner + runs-on: ubuntu-latest # Runs on your WSL runner + env: + IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/zeckit # Timeout after 10 minutes (devnet should be up much faster) timeout-minutes: 10 @@ -35,6 +41,34 @@ jobs: docker volume rm zeckit-zebra-data 2>/dev/null || true docker network rm zeckit-network 2>/dev/null || true docker system prune -f || true + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Select best prebuilt tag + run: | + short_sha="${GITHUB_SHA::7}" + branch_tag="$(echo "${GITHUB_REF_NAME}" | tr '/' '-')" + candidates=("sha-${short_sha}" "${branch_tag}" "main" "latest") + + for tag in "${candidates[@]}"; do + if docker manifest inspect "${IMAGE_PREFIX}-zebra:${tag}" >/dev/null 2>&1; then + echo "Using prebuilt tag: ${tag}" + echo "ZECKIT_IMAGE_TAG=${tag}" >> "$GITHUB_ENV" + exit 0 + fi + done + + echo "No prebuilt tag found; will fall back to local build" + echo "ZECKIT_IMAGE_TAG=sha-${short_sha}" >> "$GITHUB_ENV" + + - name: Pull prebuilt image + run: | + docker compose pull zebra || echo "Prebuilt image not found; compose will build locally." - name: Start zeckit devnet run: | diff --git a/.gitignore b/.gitignore index 5687265..7378731 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,6 @@ Thumbs.db ehthumbs_vista.db actions-runner/ *.bak + +# Zcash proving parameters (large binary files) +zcash-params/ diff --git a/README.md b/README.md index dc2c603..b3437dd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ > A toolkit for Zcash Regtest development +[![ZecKit E2E](https://img.shields.io/badge/GitHub%20Marketplace-ZecKit%20E2E-blue?logo=github)](https://github.com/marketplace/actions/zeckit-e2e) +[![Action CI](https://github.com/zecdev/ZecKit/actions/workflows/ci-action-test.yml/badge.svg)](https://github.com/zecdev/ZecKit/actions/workflows/ci-action-test.yml) + --- ## Project Status @@ -29,11 +32,13 @@ - Shielded send (Orchard to Orchard) - Comprehensive test suite (6 tests) -**M3 - GitHub Action (Next)** +**M3 - GitHub Action** -- Reusable GitHub Action for CI -- Pre-mined blockchain snapshots -- Advanced shielded workflows +- Reusable GitHub Action for CI — published on [GitHub Marketplace](https://github.com/marketplace/actions/zeckit-e2e) +- Golden E2E flow: generate UA → fund → autoshield → shielded send → rescan/sync → verify +- Both backends (zaino + lwd) exercised in matrix +- Structured outputs (txids, addresses, balances) and log artifacts +- Full documentation in [docs/github-action.md](docs/github-action.md) --- diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..93c8243 --- /dev/null +++ b/action.yml @@ -0,0 +1,693 @@ +# ============================================================ +# ZecKit – GitHub Composite Action +# ============================================================ +# Published to GitHub Marketplace as "ZecKit E2E" +# https://github.com/marketplace/actions/zeckit-e2e +# +# Usage (from any repo): +# - uses: zecdev/ZecKit@v1 +# with: +# backend: zaino +# ghcr_token: ${{ secrets.GITHUB_TOKEN }} +# ============================================================ + +name: ZecKit E2E +description: > + Spin up a ZecKit Zcash devnet with pre-built images, run the complete golden + E2E shielded-transaction flow (generate UA → fund → autoshield → + shielded send → rescan/sync → verify), and upload logs/artifacts. +author: zecdev + +branding: + icon: shield + color: blue + +# ============================================================ +# INPUTS +# ============================================================ +inputs: + # --- Backend --- + backend: + description: > + Light-client backend: 'zaino' (Zcash Indexer, Rust, faster) or + 'lwd' (Lightwalletd, Go). Defaults to zaino. + required: false + default: 'zaino' + + # --- Timeouts --- + startup_timeout_minutes: + description: > + Maximum minutes to wait for all services to become healthy after + docker compose up. Zaino typically takes 2-3 min; lwd takes 3-4 min. + Default: 10 min. + required: false + default: '10' + + block_wait_seconds: + description: > + Seconds to wait for a block to be mined after broadcasting a + transaction (shield or send). Zebra regtest mines every 30-60 s. + Default: 75 s. + required: false + default: '75' + + # --- Chain / wallet params --- + send_amount: + description: > + Amount in ZEC to send in the shielded-send step of the golden flow. + required: false + default: '0.05' + + send_address: + description: > + Destination Unified Address for the shielded send. Leave empty to + perform a self-send to the faucet's own UA (safe default for testing). + required: false + default: '' + + send_memo: + description: Memo text attached to the shielded send transaction. + required: false + default: 'ZecKit E2E golden flow' + + # --- Image params --- + image_prefix: + description: > + Docker image prefix used to pull pre-built images. + Example: ghcr.io/zecdev/zeckit (results in …-zebra:TAG etc.) + required: false + default: 'ghcr.io/zecdev/zeckit' + + image_tag: + description: > + Docker image tag to pull. Leave empty (default) for auto-detection: + the action tries sha-, branch-name, main, latest in order. + required: false + default: '' + + # --- Service URLs --- + faucet_url: + description: Base URL of the ZecKit faucet service. + required: false + default: 'http://localhost:8080' + + zebra_rpc_url: + description: Zebra JSON-RPC endpoint. + required: false + default: 'http://localhost:8232' + + # --- Artifact behaviour --- + upload_artifacts: + description: > + Controls when logs are uploaded as a workflow artifact: + always – upload unconditionally + on-failure – upload only when a step fails (default) + never – never upload + required: false + default: 'on-failure' + + # --- Auth --- + ghcr_token: + description: > + Token used to pull pre-built images from GHCR. + Pass secrets.GITHUB_TOKEN from the calling workflow. + required: true + +# ============================================================ +# OUTPUTS +# ============================================================ +outputs: + unified_address: + description: Unified Address (UA) generated by the faucet wallet. + value: ${{ steps.generate-ua.outputs.unified_address }} + + transparent_address: + description: Transparent address of the faucet wallet. + value: ${{ steps.generate-ua.outputs.transparent_address }} + + shield_txid: + description: > + Transaction ID of the autoshield operation (transparent → Orchard). + Empty if transparent balance was below fee threshold. + value: ${{ steps.autoshield.outputs.shield_txid }} + + send_txid: + description: Transaction ID of the shielded send (Orchard → Orchard). + value: ${{ steps.shielded-send.outputs.send_txid }} + + final_orchard_balance: + description: Orchard (shielded) ZEC balance after the complete E2E flow. + value: ${{ steps.verify.outputs.final_orchard_balance }} + + block_height: + description: Zcash regtest blockchain height at end of the test. + value: ${{ steps.verify.outputs.block_height }} + + test_result: + description: Overall result of the golden E2E flow – 'pass' or 'fail'. + value: ${{ steps.verify.outputs.test_result }} + +# ============================================================ +# COMPOSITE ACTION STEPS +# ============================================================ +runs: + using: composite + + steps: + # ---------------------------------------------------------- + # 0. Validate inputs & install light dependencies + # ---------------------------------------------------------- + - name: Validate inputs + shell: bash + run: | + backend="${{ inputs.backend }}" + if [[ "$backend" != "zaino" && "$backend" != "lwd" ]]; then + echo "::error::Invalid backend '$backend'. Must be 'zaino' or 'lwd'." + exit 1 + fi + echo "backend : $backend" + echo "startup_timeout : ${{ inputs.startup_timeout_minutes }} min" + echo "block_wait_seconds : ${{ inputs.block_wait_seconds }} s" + echo "send_amount : ${{ inputs.send_amount }} ZEC" + echo "image_prefix : ${{ inputs.image_prefix }}" + echo "faucet_url : ${{ inputs.faucet_url }}" + + - name: Install runtime dependencies (jq, bc) + shell: bash + run: | + missing=() + command -v jq &>/dev/null || missing+=(jq) + command -v bc &>/dev/null || missing+=(bc) + if [[ ${#missing[@]} -gt 0 ]]; then + sudo apt-get update -qq + sudo apt-get install -y -qq "${missing[@]}" + fi + echo "jq $(jq --version) bc $(bc --version | head -1)" + + # ---------------------------------------------------------- + # 1. Authenticate & select image tag + # ---------------------------------------------------------- + - name: Log in to GHCR + shell: bash + run: | + echo "${{ inputs.ghcr_token }}" \ + | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Select image tag + id: select-tag + shell: bash + run: | + image_prefix="$(echo "${{ inputs.image_prefix }}" | tr '[:upper:]' '[:lower:]')" + echo "ZECKIT_IMAGE_PREFIX=${image_prefix}" >> "$GITHUB_ENV" + + override="${{ inputs.image_tag }}" + if [[ -n "$override" ]]; then + echo "Using caller-specified tag: $override" + echo "ZECKIT_IMAGE_TAG=${override}" >> "$GITHUB_ENV" + exit 0 + fi + + short_sha="${GITHUB_SHA::7}" + branch_tag="$(echo "${GITHUB_REF_NAME}" | tr '/' '-')" + candidates=("sha-${short_sha}" "${branch_tag}" "main" "latest") + + for tag in "${candidates[@]}"; do + if docker manifest inspect "${image_prefix}-zaino:${tag}" >/dev/null 2>&1; then + echo "Auto-selected tag: ${tag}" + echo "ZECKIT_IMAGE_TAG=${tag}" >> "$GITHUB_ENV" + exit 0 + fi + done + + echo "::warning::No pre-built image found; docker compose will build locally (slow)." + echo "ZECKIT_IMAGE_TAG=sha-${short_sha}" >> "$GITHUB_ENV" + + # ---------------------------------------------------------- + # 2. Configure Zebra & pull images + # ---------------------------------------------------------- + - name: Configure zebra.toml (miner address) + shell: bash + run: | + config="${{ github.action_path }}/docker/configs/zebra.toml" + if [[ -f "$config" ]]; then + # Default deterministic seed → transparent address + miner_addr="tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd" + sed -i "s|miner_address = \".*\"|miner_address = \"${miner_addr}\"|g" "$config" + echo "Zebra miner address: ${miner_addr}" + else + echo "::warning::zebra.toml not found at $config; using existing image config." + fi + + - name: Pull pre-built Docker images + shell: bash + run: | + backend="${{ inputs.backend }}" + cd "${{ github.action_path }}" + docker compose --profile "$backend" pull \ + || echo "::warning::Pull failed or partial; compose will build missing images locally." + + # ---------------------------------------------------------- + # 3. Start devnet & wait for health + # ---------------------------------------------------------- + - name: Start devnet + id: start-devnet + shell: bash + run: | + backend="${{ inputs.backend }}" + timeout_min="${{ inputs.startup_timeout_minutes }}" + timeout_sec=$(( timeout_min * 60 )) + faucet_url="${{ inputs.faucet_url }}" + zebra_rpc="${{ inputs.zebra_rpc_url }}" + + cd "${{ github.action_path }}" + docker compose --profile "$backend" up -d + echo "Docker services started with profile: $backend" + + echo "Waiting up to ${timeout_min}m for services to become healthy..." + deadline=$(( SECONDS + timeout_sec )) + + # Wait for Zebra RPC + echo "[1/2] Waiting for Zebra RPC at $zebra_rpc ..." + zebra_ready=0 + while [[ $SECONDS -lt $deadline ]]; do + if curl -sf --max-time 5 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":"1","method":"getblockcount","params":[]}' \ + "$zebra_rpc" >/dev/null 2>&1; then + echo " ✓ Zebra RPC is ready." + zebra_ready=1 + break + fi + elapsed_min=$(( SECONDS / 60 )) + echo " ... still waiting (${elapsed_min}m elapsed)" + sleep 10 + done + if [[ "$zebra_ready" != "1" ]]; then + echo "::error::Zebra did not become available within ${timeout_min} minutes." + docker compose logs + exit 1 + fi + + # Wait for Faucet + echo "[2/2] Waiting for Faucet at $faucet_url ..." + faucet_ready=0 + while [[ $SECONDS -lt $deadline ]]; do + http_status=$(curl -sf --max-time 5 -o /dev/null -w "%{http_code}" \ + "$faucet_url/health" 2>/dev/null || true) + if [[ "$http_status" == "200" ]]; then + wallet_status=$(curl -sf --max-time 5 "$faucet_url/health" \ + | jq -r '.status // empty' 2>/dev/null) + if [[ "$wallet_status" == "healthy" ]]; then + echo " ✓ Faucet is healthy." + faucet_ready=1 + break + fi + fi + elapsed_min=$(( SECONDS / 60 )) + echo " ... still waiting (${elapsed_min}m elapsed, faucet HTTP=$http_status)" + sleep 10 + done + if [[ "$faucet_ready" != "1" ]]; then + echo "::error::Faucet did not become healthy within ${timeout_min} minutes." + docker compose logs + exit 1 + fi + + echo "" + echo "All services healthy – beginning E2E golden flow." + + # ============================================================ + # GOLDEN E2E FLOW + # ============================================================ + + # ---------------------------------------------------------- + # Step 1 – Generate Unified Address + # ---------------------------------------------------------- + - name: "E2E 1/6 – Generate Unified Address" + id: generate-ua + shell: bash + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " E2E 1/6: Generate Unified Address" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + faucet_url="${{ inputs.faucet_url }}" + + addr_json="" + for attempt in 1 2 3; do + addr_json=$(curl -sf --max-time 15 "$faucet_url/address" 2>/dev/null) && break + echo " Attempt $attempt failed, retrying in 5 s..." + sleep 5 + done + + if [[ -z "$addr_json" ]]; then + echo "::error::Could not retrieve address from faucet after 3 attempts." + exit 1 + fi + + ua=$(echo "$addr_json" | jq -r '.unified_address // empty') + ta=$(echo "$addr_json" | jq -r '.transparent_address // empty') + + if [[ -z "$ua" ]]; then + echo "::error::Faucet returned no unified_address. Response: $addr_json" + exit 1 + fi + + echo " Unified Address : $ua" + echo " Transparent : $ta" + echo "unified_address=$ua" >> "$GITHUB_OUTPUT" + echo "transparent_address=$ta" >> "$GITHUB_OUTPUT" + echo "::notice::UA generated: $ua" + + # ---------------------------------------------------------- + # Step 2 – Fund (wait for transparent mining rewards) + # ---------------------------------------------------------- + - name: "E2E 2/6 – Wait for Mining Rewards (Fund)" + id: fund + shell: bash + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " E2E 2/6: Wait for Mining Rewards" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + faucet_url="${{ inputs.faucet_url }}" + timeout_min="${{ inputs.startup_timeout_minutes }}" + deadline=$(( SECONDS + timeout_min * 60 )) + + while [[ $SECONDS -lt $deadline ]]; do + stats=$(curl -sf --max-time 10 "$faucet_url/stats" 2>/dev/null || true) + transparent=$(echo "$stats" | jq '.transparent_balance // 0' 2>/dev/null || echo 0) + orchard=$(echo "$stats" | jq '.orchard_balance // 0' 2>/dev/null || echo 0) + + echo " transparent=${transparent} ZEC orchard=${orchard} ZEC" + + has_funds=$(echo "$transparent > 0 || $orchard > 0" | bc -l 2>/dev/null || echo 0) + if [[ "$has_funds" == "1" ]]; then + echo " ✓ Wallet funded." + break + fi + sleep 15 + done + + if [[ $SECONDS -ge $deadline ]]; then + echo "::error::Wallet had no balance after waiting ${timeout_min} minutes. Is mining running?" + exit 1 + fi + + # ---------------------------------------------------------- + # Step 3 – Autoshield (transparent → Orchard) + # ---------------------------------------------------------- + - name: "E2E 3/6 – Autoshield (transparent → Orchard)" + id: autoshield + shell: bash + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " E2E 3/6: Autoshield" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + faucet_url="${{ inputs.faucet_url }}" + + stats=$(curl -sf --max-time 10 "$faucet_url/stats" 2>/dev/null || true) + transparent=$(echo "$stats" | jq '.transparent_balance // 0' 2>/dev/null || echo 0) + echo " Transparent balance: ${transparent} ZEC" + + # Need at least 2× fee (0.0002 ZEC) to be worth shielding + has_transparent=$(echo "$transparent >= 0.0002" | bc -l 2>/dev/null || echo 0) + if [[ "$has_transparent" != "1" ]]; then + echo " Transparent balance below fee threshold – skipping shield step." + echo "shield_txid=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + shield_json=$(curl -sf --max-time 90 -X POST "$faucet_url/shield" 2>/dev/null) + status=$(echo "$shield_json" | jq -r '.status // "error"') + txid=$(echo "$shield_json" | jq -r '.txid // ""') + + echo " Shield status : $status" + echo " Shield txid : ${txid:-n/a}" + echo "shield_txid=$txid" >> "$GITHUB_OUTPUT" + + if [[ "$status" != "shielded" && "$status" != "no_funds" ]]; then + echo "::error::Shield failed with status '$status'. Response: $shield_json" + exit 1 + fi + if [[ "$status" == "shielded" ]]; then + echo "::notice::Autoshield txid: $txid" + fi + + - name: "E2E 3b/6 – Wait for shield block confirmation" + shell: bash + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " E2E 3b/6: Block confirmation window" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + block_wait="${{ inputs.block_wait_seconds }}" + echo " Sleeping ${block_wait}s for Zebra to mine a confirming block..." + sleep "$block_wait" + + faucet_url="${{ inputs.faucet_url }}" + echo " Syncing wallet..." + curl -sf --max-time 120 -X POST "$faucet_url/sync" >/dev/null || true + sleep 10 + echo " Sync done." + + # ---------------------------------------------------------- + # Step 4 – Shielded Send (Orchard → Orchard) + # ---------------------------------------------------------- + - name: "E2E 4/6 – Shielded Send (Orchard → Orchard)" + id: shielded-send + shell: bash + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " E2E 4/6: Shielded Send" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + faucet_url="${{ inputs.faucet_url }}" + send_amount="${{ inputs.send_amount }}" + send_memo="${{ inputs.send_memo }}" + + # Resolve destination address + send_address="${{ inputs.send_address }}" + if [[ -z "$send_address" ]]; then + send_address="${{ steps.generate-ua.outputs.unified_address }}" + echo " Destination: self (faucet UA)" + else + echo " Destination: $send_address" + fi + echo " Amount : $send_amount ZEC" + echo " Memo : $send_memo" + + # Verify Orchard balance is sufficient + stats=$(curl -sf --max-time 10 "$faucet_url/stats" 2>/dev/null || true) + orchard=$(echo "$stats" | jq '.orchard_balance // 0' 2>/dev/null || echo 0) + echo " Orchard balance: $orchard ZEC" + + has_orchard=$(echo "$orchard >= $send_amount" | bc -l 2>/dev/null || echo 0) + if [[ "$has_orchard" != "1" ]]; then + echo "::error::Insufficient Orchard balance ($orchard ZEC) to send $send_amount ZEC." + echo " Make sure the autoshield step completed and a confirming block was mined." + exit 1 + fi + + # Build JSON payload safely + payload=$(jq -nc \ + --arg addr "$send_address" \ + --argjson amt "$send_amount" \ + --arg memo "$send_memo" \ + '{"address":$addr,"amount":$amt,"memo":$memo}') + + send_json=$(curl -sf --max-time 90 -X POST "$faucet_url/send" \ + -H "Content-Type: application/json" \ + -d "$payload" 2>/dev/null) + + status=$(echo "$send_json" | jq -r '.status // "error"') + txid=$(echo "$send_json" | jq -r '.txid // ""') + + echo " Send status : $status" + echo " Send txid : ${txid:-n/a}" + echo "send_txid=$txid" >> "$GITHUB_OUTPUT" + + if [[ "$status" != "sent" ]]; then + echo "::error::Shielded send failed with status '$status'. Response: $send_json" + exit 1 + fi + echo "::notice::Shielded send txid: $txid" + + # ---------------------------------------------------------- + # Step 5 – Rescan / Sync post-send + # ---------------------------------------------------------- + - name: "E2E 5/6 – Rescan / Sync" + shell: bash + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " E2E 5/6: Rescan / Sync" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + faucet_url="${{ inputs.faucet_url }}" + block_wait="${{ inputs.block_wait_seconds }}" + + echo " Waiting ${block_wait}s for send to be mined..." + sleep "$block_wait" + + echo " Triggering wallet sync..." + sync_json=$(curl -sf --max-time 120 -X POST "$faucet_url/sync" 2>/dev/null || true) + sync_status=$(echo "$sync_json" | jq -r '.status // "unknown"' 2>/dev/null || echo unknown) + echo " Sync status: $sync_status" + sleep 5 + + # ---------------------------------------------------------- + # Step 6 – Verify final state + # ---------------------------------------------------------- + - name: "E2E 6/6 – Verify Final State" + id: verify + shell: bash + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " E2E 6/6: Verify" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + faucet_url="${{ inputs.faucet_url }}" + zebra_rpc="${{ inputs.zebra_rpc_url }}" + + # Faucet final stats + stats=$(curl -sf --max-time 15 "$faucet_url/stats" 2>/dev/null || true) + if [[ -z "$stats" ]]; then + echo "::error::Could not retrieve final stats from faucet." + echo "test_result=fail" >> "$GITHUB_OUTPUT" + exit 1 + fi + + final_orchard=$(echo "$stats" | jq '.orchard_balance // 0') + final_transparent=$(echo "$stats" | jq '.transparent_balance // 0') + echo " Final Orchard : $final_orchard ZEC" + echo " Final Transparent : $final_transparent ZEC" + echo "final_orchard_balance=$final_orchard" >> "$GITHUB_OUTPUT" + + # Block height + height_json=$(curl -sf --max-time 5 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":"1","method":"getblockcount","params":[]}' \ + "$zebra_rpc" 2>/dev/null || true) + height=$(echo "$height_json" | jq '.result // 0' 2>/dev/null || echo 0) + echo " Block height : $height" + echo "block_height=$height" >> "$GITHUB_OUTPUT" + + # Sanity: we must have some ZEC and a non-zero chain + total=$(echo "$final_orchard + $final_transparent" | bc -l 2>/dev/null || echo 0) + has_balance=$(echo "$total > 0" | bc -l 2>/dev/null || echo 0) + blk_ok=$(echo "$height > 0" | bc -l 2>/dev/null || echo 0) + + if [[ "$has_balance" == "1" && "$blk_ok" == "1" ]]; then + echo "" + echo " ✓ E2E GOLDEN FLOW PASSED" + echo " ┌──────────────────────────────────────┐" + echo " │ 1. UA generated ✓ │" + echo " │ 2. Wallet funded ✓ │" + echo " │ 3. Autoshield ✓ │" + echo " │ 4. Shielded send ✓ │" + echo " │ 5. Rescan / sync ✓ │" + echo " │ 6. Balance verified ✓ │" + echo " └──────────────────────────────────────┘" + echo "test_result=pass" >> "$GITHUB_OUTPUT" + else + echo "::error::Verification failed: total=${total} ZEC, block_height=${height}." + echo " Expected non-zero balance and chain height." + echo "test_result=fail" >> "$GITHUB_OUTPUT" + exit 1 + fi + + # ============================================================ + # ARTIFACTS & CLEANUP + # ============================================================ + + - name: Collect logs + if: always() + shell: bash + run: | + log_dir="/tmp/zeckit-logs" + mkdir -p "$log_dir" + cd "${{ github.action_path }}" + + docker compose logs zebra > "$log_dir/zebra.log" 2>&1 || true + docker compose logs zaino > "$log_dir/zaino.log" 2>&1 || true + docker compose logs lightwalletd > "$log_dir/lightwalletd.log" 2>&1 || true + docker compose logs faucet-zaino > "$log_dir/faucet.log" 2>&1 || true + docker compose logs faucet-lwd >> "$log_dir/faucet.log" 2>&1 || true + docker ps -a > "$log_dir/containers.log" 2>&1 || true + docker network ls > "$log_dir/networks.log" 2>&1 || true + + # Final wallet stats + curl -sf "${{ inputs.faucet_url }}/stats" 2>/dev/null \ + | jq . > "$log_dir/faucet-stats.json" 2>/dev/null || true + + # Machine-readable run summary + jq -n \ + --arg backend "${{ inputs.backend }}" \ + --arg ua "${{ steps.generate-ua.outputs.unified_address }}" \ + --arg shield_tx "${{ steps.autoshield.outputs.shield_txid }}" \ + --arg send_tx "${{ steps.shielded-send.outputs.send_txid }}" \ + --arg orchard "${{ steps.verify.outputs.final_orchard_balance }}" \ + --arg height "${{ steps.verify.outputs.block_height }}" \ + --arg result "${{ steps.verify.outputs.test_result }}" \ + '{ + backend: $backend, + unified_address: $ua, + shield_txid: $shield_tx, + send_txid: $send_tx, + final_orchard_balance: $orchard, + block_height: $height, + test_result: $result + }' > "$log_dir/run-summary.json" 2>/dev/null || true + + echo "Artifact contents:" + ls -lh "$log_dir/" + + - name: Upload artifacts – always + if: ${{ inputs.upload_artifacts == 'always' }} + uses: actions/upload-artifact@v4 + with: + name: zeckit-e2e-logs-${{ github.run_number }} + path: /tmp/zeckit-logs/ + retention-days: 14 + + - name: Upload artifacts – on failure + if: > + inputs.upload_artifacts == 'on-failure' && + (failure() || steps.verify.outputs.test_result == 'fail') + uses: actions/upload-artifact@v4 + with: + name: zeckit-e2e-logs-${{ github.run_number }} + path: /tmp/zeckit-logs/ + retention-days: 14 + + - name: Stop devnet + if: always() + shell: bash + run: | + cd "${{ github.action_path }}" + docker compose down --remove-orphans 2>/dev/null || true + echo "Devnet stopped." + + - name: Job summary + if: always() + shell: bash + run: | + result="${{ steps.verify.outputs.test_result }}" + ua="${{ steps.generate-ua.outputs.unified_address }}" + shield_tx="${{ steps.autoshield.outputs.shield_txid }}" + send_tx="${{ steps.shielded-send.outputs.send_txid }}" + orchard="${{ steps.verify.outputs.final_orchard_balance }}" + height="${{ steps.verify.outputs.block_height }}" + + { + echo "## ZecKit E2E – Golden Flow Summary" + echo "" + if [[ "$result" == "pass" ]]; then + echo "**Status: ✅ PASSED**" + else + echo "**Status: ❌ FAILED**" + fi + echo "" + echo "| Field | Value |" + echo "|---|---|" + echo "| Backend | ${{ inputs.backend }} |" + echo "| Unified Address | \`${ua}\` |" + echo "| Shield txid | \`${shield_tx:-n/a}\` |" + echo "| Send txid | \`${send_tx:-n/a}\` |" + echo "| Final Orchard balance | ${orchard} ZEC |" + echo "| Block height | ${height} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/cli/src/commands/status.rs b/cli/src/commands/status.rs index f7d73e5..257517f 100644 --- a/cli/src/commands/status.rs +++ b/cli/src/commands/status.rs @@ -45,11 +45,11 @@ async fn print_service_status(client: &Client, name: &str, url: &str) { if let Ok(json) = resp.json::().await { println!(" {} {} - {}", "✓".green(), name.bold(), format_json(&json)); } else { - println!(" {} {} - {}", "✓".green(), name.bold(), "OK"); + println!(" {} {} - OK", "✓".green(), name.bold()); } } _ => { - println!(" {} {} - {}", "✗".red(), name.bold(), "Not responding"); + println!(" {} {} - Not responding", "✗".red(), name.bold()); } } } diff --git a/cli/src/commands/test.rs b/cli/src/commands/test.rs index d1cf6f7..ecc92d4 100644 --- a/cli/src/commands/test.rs +++ b/cli/src/commands/test.rs @@ -275,13 +275,13 @@ async fn test_wallet_shield(client: &Client) -> Result<()> { println!(); print!(" [5/6] Wallet balance and shield... "); - return Ok(()); + Ok(()) } "no_funds" => { println!(" No transparent funds to shield (already shielded)"); println!(); print!(" [5/6] Wallet balance and shield... "); - return Ok(()); + Ok(()) } _ => { println!(" Shield status: {}", status); @@ -290,7 +290,7 @@ async fn test_wallet_shield(client: &Client) -> Result<()> { } println!(); print!(" [5/6] Wallet balance and shield... "); - return Ok(()); + Ok(()) } } @@ -298,7 +298,7 @@ async fn test_wallet_shield(client: &Client) -> Result<()> { println!(" Wallet already has {} ZEC shielded in Orchard - PASS", orchard_before); println!(); print!(" [5/6] Wallet balance and shield... "); - return Ok(()); + Ok(()) } else if transparent_before > 0.0 { println!(" Wallet has {} ZEC transparent (too small to shield)", transparent_before); @@ -306,14 +306,14 @@ async fn test_wallet_shield(client: &Client) -> Result<()> { println!(" SKIP (insufficient balance)"); println!(); print!(" [5/6] Wallet balance and shield... "); - return Ok(()); + Ok(()) } else { println!(" No balance found"); println!(" SKIP (needs mining to complete)"); println!(); print!(" [5/6] Wallet balance and shield... "); - return Ok(()); + Ok(()) } } @@ -443,7 +443,7 @@ async fn test_shielded_send(client: &Client) -> Result<()> { println!(); print!(" [6/6] Shielded send (E2E)... "); - return Ok(()); + Ok(()) } else { println!(" Unexpected status: {:?}", status); if let Some(msg) = send_json.get("message").and_then(|v| v.as_str()) { @@ -451,8 +451,8 @@ async fn test_shielded_send(client: &Client) -> Result<()> { } println!(); print!(" [6/6] Shielded send (E2E)... "); - return Err(crate::error::ZecKitError::HealthCheck( + Err(crate::error::ZecKitError::HealthCheck( "Shielded send did not complete as expected".into() - )); + )) } } \ No newline at end of file diff --git a/cli/src/commands/up.rs b/cli/src/commands/up.rs index aa04ce4..a123fbe 100644 --- a/cli/src/commands/up.rs +++ b/cli/src/commands/up.rs @@ -172,7 +172,7 @@ pub async fn execute(backend: String, fresh: bool) -> Result<()> { Ok(addr) => { println!("✓ Faucet wallet address: {}", addr); if addr != DEFAULT_FAUCET_ADDRESS { - println!("{}", format!("⚠ Warning: Address mismatch!").yellow()); + println!("{}", "⚠ Warning: Address mismatch!".to_string().yellow()); println!("{}", format!(" Expected: {}", DEFAULT_FAUCET_ADDRESS).yellow()); println!("{}", format!(" Got: {}", addr).yellow()); println!("{}", " This may cause funds to be lost!".yellow()); diff --git a/docker-compose.yml b/docker-compose.yml index 8559253..c857d0e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: # ZEBRA NODE # ======================================== zebra: + image: ${ZECKIT_IMAGE_PREFIX:-ghcr.io/zecdev/zeckit}-zebra:${ZECKIT_IMAGE_TAG:-main} build: context: ./docker/zebra dockerfile: Dockerfile @@ -47,6 +48,7 @@ services: # LIGHTWALLETD (Profile: lwd) # ======================================== lightwalletd: + image: ${ZECKIT_IMAGE_PREFIX:-ghcr.io/zecdev/zeckit}-lightwalletd:${ZECKIT_IMAGE_TAG:-main} build: context: ./docker/lightwalletd dockerfile: Dockerfile @@ -78,6 +80,7 @@ services: # ZAINO INDEXER (Profile: zaino) # ======================================== zaino: + image: ${ZECKIT_IMAGE_PREFIX:-ghcr.io/zecdev/zeckit}-zaino:${ZECKIT_IMAGE_TAG:-main} build: context: ./docker/zaino dockerfile: Dockerfile @@ -116,6 +119,7 @@ services: # FAUCET SERVICE - LWD Profile # ======================================== faucet-lwd: + image: ${ZECKIT_IMAGE_PREFIX:-ghcr.io/zecdev/zeckit}-faucet:${ZECKIT_IMAGE_TAG:-main} build: context: ./zeckit-faucet dockerfile: Dockerfile @@ -147,6 +151,7 @@ services: # FAUCET SERVICE - Zaino Profile # ======================================== faucet-zaino: + image: ${ZECKIT_IMAGE_PREFIX:-ghcr.io/zecdev/zeckit}-faucet:${ZECKIT_IMAGE_TAG:-main} build: context: ./zeckit-faucet dockerfile: Dockerfile diff --git a/docker/zingo/Dockerfile b/docker/zingo/Dockerfile index 79759fc..b16c737 100644 --- a/docker/zingo/Dockerfile +++ b/docker/zingo/Dockerfile @@ -5,6 +5,7 @@ RUN apt-get update && apt-get install -y \ gcc \ protobuf-compiler \ libssl-dev \ + libsqlite3-dev \ curl \ git \ netcat-openbsd \ diff --git a/docs/github-action.md b/docs/github-action.md new file mode 100644 index 0000000..f6ec136 --- /dev/null +++ b/docs/github-action.md @@ -0,0 +1,457 @@ +# ZecKit E2E – GitHub Action + +A reusable GitHub Action that spins up a full ZecKit Zcash devnet with +pre-built container images and runs the complete **golden shielded-transaction +flow** end-to-end: + +``` +Generate UA → Fund → Autoshield → Shielded Send → Rescan/Sync → Verify +``` + +Available on the [GitHub Marketplace](https://github.com/marketplace/actions/zeckit-e2e). + +--- + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Inputs Reference](#inputs-reference) +3. [Outputs Reference](#outputs-reference) +4. [Usage Patterns](#usage-patterns) + - [Composite Action (direct)](#a-composite-action-direct) + - [Reusable Workflow (workflow_call)](#b-reusable-workflow-workflow_call) +5. [Running Locally](#running-locally) +6. [Artifacts](#artifacts) +7. [Common Failure Modes & Troubleshooting](#common-failure-modes--troubleshooting) + +--- + +## Quick Start + +### Simplest possible call (zaino backend, all defaults) + +```yaml +# .github/workflows/my-zcash-tests.yml +name: My Zcash Project E2E + +on: [push, pull_request] + +jobs: + zeckit: + name: ZecKit E2E + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: zecdev/ZecKit@v1 + with: + ghcr_token: ${{ secrets.GITHUB_TOKEN }} +``` + +### Full example with every input shown + +```yaml +jobs: + zeckit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: ZecKit E2E golden flow + id: e2e + uses: zecdev/ZecKit@v1 + with: + # Backend + backend: zaino # 'zaino' or 'lwd' + + # Timeouts + startup_timeout_minutes: '10' # wait for healthy services + block_wait_seconds: '75' # wait for block confirmation + + # Transaction parameters + send_amount: '0.05' # ZEC to send + send_address: '' # empty = self-send (safe default) + send_memo: 'My project E2E test' + + # Image selection + image_prefix: 'ghcr.io/zecdev/zeckit' + image_tag: '' # empty = auto-detect + + # Artifacts + upload_artifacts: 'on-failure' # 'always' | 'on-failure' | 'never' + + # Auth + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Use action outputs + run: | + echo "UA : ${{ steps.e2e.outputs.unified_address }}" + echo "Shield txid : ${{ steps.e2e.outputs.shield_txid }}" + echo "Send txid : ${{ steps.e2e.outputs.send_txid }}" + echo "Orchard final : ${{ steps.e2e.outputs.final_orchard_balance }} ZEC" + echo "Block height : ${{ steps.e2e.outputs.block_height }}" + echo "Result : ${{ steps.e2e.outputs.test_result }}" +``` + +--- + +## Inputs Reference + +| Input | Required | Default | Description | +|---|---|---|---| +| `ghcr_token` | **yes** | – | Token to pull pre-built images from GHCR. Pass `${{ secrets.GITHUB_TOKEN }}`. | +| `backend` | no | `zaino` | Light-client backend: **`zaino`** (Rust, ~30 % faster) or **`lwd`** (Lightwalletd, Go). | +| `startup_timeout_minutes` | no | `10` | Minutes to wait for all services to become healthy. Zaino typically ready in 2-3 min; lwd in 3-4 min. | +| `block_wait_seconds` | no | `75` | Seconds to wait for Zebra to mine a confirming block after broadcasting a transaction. Zebra regtest mines every 30-60 s. | +| `send_amount` | no | `0.05` | Amount in ZEC sent during the shielded-send step. | +| `send_address` | no | `''` | Destination Unified Address for the shielded send. Empty string performs a self-send back to the faucet UA (safe, no external address needed). | +| `send_memo` | no | `ZecKit E2E golden flow` | Memo text included in the shielded send transaction. | +| `image_prefix` | no | `ghcr.io/zecdev/zeckit` | Registry prefix for pre-built images (resolves to `…-zebra:TAG`, `…-zaino:TAG`, `…-faucet:TAG`, etc.). | +| `image_tag` | no | `''` | Specific image tag to pull. Empty triggers auto-detection: `sha-` → branch-name → `main` → `latest`. | +| `upload_artifacts` | no | `on-failure` | When to upload log artifacts: `always`, `on-failure`, or `never`. | + +### Backend comparison + +| | `zaino` | `lwd` | +|---|---|---| +| Language | Rust | Go | +| Startup time | 2-3 min | 3-4 min | +| Sync speed | Faster (~30 %) | Baseline | +| Recommendation | Development / CI | Compatibility testing | + +--- + +## Outputs Reference + +All outputs are available under `steps..outputs.*` after the action completes. + +| Output | Type | Description | +|---|---|---| +| `unified_address` | string | Unified Address (UA) generated by the faucet wallet for this run. | +| `transparent_address` | string | Transparent (t-addr) of the faucet wallet. | +| `shield_txid` | string | Transaction ID of the autoshield (transparent → Orchard). Empty if transparent balance was below fee threshold. | +| `send_txid` | string | Transaction ID of the shielded Orchard → Orchard send. | +| `final_orchard_balance` | number (str) | Orchard balance in ZEC after the complete flow. | +| `block_height` | number (str) | Zcash regtest blockchain height at end of the run. | +| `test_result` | `pass` \| `fail` | Overall golden-flow result. The action also exits non-zero on `fail`. | + +A machine-readable `run-summary.json` containing all output fields is always written inside the log artifact. + +--- + +## Usage Patterns + +### A. Composite Action (direct) + +Use `uses: zecdev/ZecKit@v1` in any step. Returns all outputs on the same job. +Suitable when: +- You need the outputs (addresses, txids) in subsequent steps. +- You want to mix ZecKit E2E with other steps in the same job. + +```yaml +- uses: zecdev/ZecKit@v1 + id: zcash + with: + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + +- run: | + echo "Shielded send confirmed: ${{ steps.zcash.outputs.send_txid }}" +``` + +### B. Reusable Workflow (`workflow_call`) + +Call `.github/workflows/golden-e2e.yml` from another workflow's `jobs` entry. +Outputs are available under `needs..outputs.*`. +Suitable when: +- You want E2E to run as a dedicated named job with its own status badge. +- You need to block other jobs on the E2E result without coupling steps. + +```yaml +jobs: + # Pull in ZecKit E2E as a dedicated job block + e2e: + uses: zecdev/ZecKit/.github/workflows/golden-e2e.yml@v1 + with: + backend: zaino + startup_timeout_minutes: 10 + secrets: + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + + # Gate a downstream job on e2e passing + deploy: + needs: e2e + if: needs.e2e.outputs.test_result == 'pass' + runs-on: ubuntu-latest + steps: + - run: echo "Deploying – E2E passed with txid ${{ needs.e2e.outputs.send_txid }}" +``` + +### C. Matrix across both backends + +```yaml +jobs: + e2e: + strategy: + matrix: + backend: [zaino, lwd] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: zecdev/ZecKit@v1 + with: + backend: ${{ matrix.backend }} + ghcr_token: ${{ secrets.GITHUB_TOKEN }} +``` + +--- + +## Running Locally + +The action itself requires a GitHub Actions runner environment (it writes to +`$GITHUB_ENV`, `$GITHUB_OUTPUT`, `$GITHUB_STEP_SUMMARY`). However, you can +exercise the exact same flow locally using the `zeckit` CLI and `curl`. + +### Prerequisites + +```bash +# Docker Engine 24+ and Compose v2 +docker --version && docker compose version + +# Rust toolchain (for CLI build) +rustup update stable +``` + +### Step-by-step local golden flow + +```bash +# 1. Clone and build CLI +git clone https://github.com/zecdev/ZecKit.git && cd ZecKit +cd cli && cargo build --release && cd .. + +# 2. Login to GHCR so pre-built images can be pulled +echo "$CR_PAT" | docker login ghcr.io -u YOUR_GITHUB_HANDLE --password-stdin + +# 3. Start the devnet (zaino backend, fresh volumes) +./cli/target/release/zeckit up --backend zaino --fresh +# Wait for output: "Devnet is ready" + +# 4. Generate Unified Address +curl http://localhost:8080/address | jq + +# 5. Check wallet is funded (mining rewards appear after ~60 s) +curl http://localhost:8080/stats | jq '.transparent_balance, .orchard_balance' + +# 6. Autoshield transparent → Orchard +curl -X POST http://localhost:8080/shield | jq +sleep 75 # one block confirmation window + +# 7. Sync wallet +curl -X POST http://localhost:8080/sync | jq + +# 8. Shielded send (Orchard → Orchard, self-send) +UA=$(curl -s http://localhost:8080/address | jq -r .unified_address) +curl -X POST http://localhost:8080/send \ + -H "Content-Type: application/json" \ + -d "{\"address\":\"$UA\",\"amount\":0.05,\"memo\":\"local test\"}" | jq +sleep 75 + +# 9. Verify final state +curl http://localhost:8080/stats | jq + +# 10. Run the full built-in smoke test suite (covers same flow + more) +./cli/target/release/zeckit test + +# 11. Tear down +./cli/target/release/zeckit down +``` + +### Using act (GitHub Actions locally) + +[`act`](https://github.com/nektos/act) can run the CI self-test workflow on your machine: + +```bash +brew install act + +# Provide a GITHUB_TOKEN with read:packages scope +act -j composite-action-test \ + -s GITHUB_TOKEN="$(gh auth token)" \ + --workflows .github/workflows/ci-action-test.yml +``` + +> **Note:** `act` uses `catthehacker/ubuntu:act-latest` by default which may +> not have all Docker-in-Docker capabilities. Use a full Docker socket mount if +> you encounter `docker: not found` inside the runner: +> +> ```bash +> act ... --bind +> ``` + +--- + +## Artifacts + +When `upload_artifacts` is `always` or `on-failure` (default), the action +uploads a ZIP named **`zeckit-e2e-logs-`** as a workflow artifact. + +Artifact retention: **14 days** (configurable in `action.yml`). + +### Artifact contents + +| File | Description | +|---|---| +| `run-summary.json` | Machine-readable JSON: backend, UA, txids, final balance, block height, test_result. | +| `faucet-stats.json` | Raw `/stats` response at end of run. | +| `zebra.log` | Full stdout/stderr from the Zebra container. | +| `zaino.log` | Zaino indexer container logs (when backend=zaino). | +| `lightwalletd.log` | Lightwalletd container logs (when backend=lwd). | +| `faucet.log` | Faucet (Axum + Zingolib) container logs. | +| `containers.log` | `docker ps -a` snapshot. | +| `networks.log` | `docker network ls` snapshot. | + +### Downloading artifacts via CLI + +```bash +# List artifacts for a run +gh run view --repo zecdev/ZecKit + +# Download +gh run download --repo zecdev/ZecKit -n zeckit-e2e-logs- +``` + +--- + +## Common Failure Modes & Troubleshooting + +### 1. Services not healthy within timeout + +**Symptom:** `::error::Zebra did not become available within 10 minutes` or similar for the faucet. + +**Cause:** Pulling images took too long, or a container OOM-killed on runner. + +**Fix:** +- Increase `startup_timeout_minutes` to `15` or `20`. +- Ensure the `ghcr_token` has `read:packages` scope — without it, pulls silently fail and compose falls back to a slow local build. +- Check that the runner has ≥ 4 GB RAM and ≥ 5 GB free disk. + +--- + +### 2. Insufficient Orchard balance for send + +**Symptom:** `::error::Insufficient Orchard balance (0 ZEC) to send 0.05 ZEC` + +**Cause:** The shield step was skipped (no transparent balance above fee threshold) but no pre-existing Orchard balance exists. Can happen on a very fresh chain where mining hasn't produced enough rewards yet. + +**Fix:** Increase `block_wait_seconds` to allow more mining time, or increase `startup_timeout_minutes` so the fund step waits longer for rewards. For a self-hosted runner with slow networks, also try setting `image_tag: main` to skip auto-detection overhead. + +--- + +### 3. Shield fails with "no_funds" + +**Symptom:** Shield step reports `status: no_funds` but subsequent send also fails. + +**Cause:** Mining rewards are still pending (mempool not yet mined). Timing is probabilistic: Zebra mines every 30-60 s. + +**Fix:** Increase `block_wait_seconds` to `120`. This is safe — extra wait does not cause failures. + +--- + +### 4. Shielded send returns non-"sent" status + +**Symptom:** `::error::Shielded send failed with status 'error'` + +**Cause:** Wallet's internal spendable notes haven't caught up after shielding. The wallet needs a sync after the shield block is confirmed. + +**Fix:** The action already performs a sync between shield and send. If this still fails, increase `block_wait_seconds` to give more time before the sync. + +--- + +### 5. `docker manifest inspect` fails / wrong tag selected + +**Symptom:** Action logs show `No pre-built image found; docker compose will build locally (slow).` and the job takes 20+ minutes. + +**Cause:** The auto-detected tag doesn't match any published image. This happens on feature branches or forks where `build-images.yml` hasn't run yet. + +**Fix:** Set `image_tag: main` explicitly to pull the latest stable images regardless of branch. + +```yaml +with: + image_tag: 'main' + ghcr_token: ${{ secrets.GITHUB_TOKEN }} +``` + +--- + +### 6. `lwd` backend takes too long + +**Symptom:** Lightwalletd startup exceeds the default 10-minute timeout. + +**Cause:** Lightwalletd syncs slower than Zaino, especially on cold starts. + +**Fix:** Use `startup_timeout_minutes: '15'` and `block_wait_seconds: '90'` when running with `backend: lwd`. + +```yaml +with: + backend: lwd + startup_timeout_minutes: '15' + block_wait_seconds: '90' + ghcr_token: ${{ secrets.GITHUB_TOKEN }} +``` + +--- + +### 7. Port conflicts on self-hosted runners + +**Symptom:** `bind: address already in use` for ports 8080, 8232, or 9067. + +**Cause:** A previous run left containers running on the same runner. + +**Fix:** Add a cleanup step before the action in your workflow: + +```yaml +- name: Pre-clean ZecKit + run: | + docker compose -f /path/to/ZecKit/docker-compose.yml down --remove-orphans 2>/dev/null || true + docker stop zeckit-zebra zeckit-faucet 2>/dev/null || true +``` + +Or use `docker run --network host` alternatives. The action itself calls `docker compose down` at the end (`if: always()`), so subsequent runs on the same runner should not encounter this after the first cleanup. + +--- + +### 8. `jq` or `bc` not found + +**Symptom:** `/bin/bash: jq: command not found` + +**Cause:** Minimal self-hosted runner image without standard utilities. + +**Fix:** The action auto-installs `jq` and `bc` via `apt-get` if they are missing. If your runner doesn't have `apt-get`, pre-install them in your runner image. + +--- + +### Enabling debug logs + +Set the secret `ACTIONS_STEP_DEBUG` to `true` in your repo's Actions secrets to get verbose shell (`set -x`) output from every step. + +--- + +## GitHub Marketplace + +This action is published at: +**https://github.com/marketplace/actions/zeckit-e2e** + +Required files for Marketplace listing: +- `action.yml` (root of repo) — present ✓ +- `branding.icon` / `branding.color` — set to `shield` / `blue` ✓ +- Public repository — required for Marketplace visibility ✓ +- This documentation linked from the repo README ✓ + +To publish a new version tag and update the Marketplace listing: + +```bash +git tag v1.x.x +git push origin v1.x.x + +# Then move the major-version floating tag: +git tag -fa v1 -m "Update v1 to v1.x.x" +git push origin v1 --force +``` diff --git a/sample/.github/workflows/ci.yml b/sample/.github/workflows/ci.yml new file mode 100644 index 0000000..0a0741f --- /dev/null +++ b/sample/.github/workflows/ci.yml @@ -0,0 +1,208 @@ +# ============================================================ +# ZecKit Sample Repo – CI +# ============================================================ +# +# This workflow runs the ZecKit golden E2E shielded-transaction +# flow against two backends: +# +# lightwalletd – REQUIRED job. CI fails if this fails. +# zaino – EXPERIMENTAL job. Failure does not block merge; +# the job is still executed so signal accumulates. +# +# Backend matrix overview: +# ┌──────────────────┬──────────────────────────────────────────┐ +# │ Backend │ Merge-blocking? │ +# ├──────────────────┼──────────────────────────────────────────┤ +# │ lightwalletd │ YES – required for CI green │ +# │ zaino │ NO – experimental; result is reported │ +# │ │ but not enforced │ +# └──────────────────┴──────────────────────────────────────────┘ +# ============================================================ + +name: ZecKit E2E CI + +on: + push: + branches: + - main + - 'feat/**' + - 'fix/**' + pull_request: + branches: + - main + workflow_dispatch: + inputs: + upload_artifacts: + description: 'Artifact upload policy' + required: false + default: 'on-failure' + type: choice + options: + - on-failure + - always + - never + +permissions: + contents: read + packages: read # needed to pull GHCR images + +# ============================================================ +# REQUIRED JOB – lightwalletd +# Merge is blocked if this job fails. +# ============================================================ +jobs: + e2e-lwd: + name: "E2E (required) – lightwalletd" + runs-on: ubuntu-latest + timeout-minutes: 25 + + # Output the action's structured results so downstream jobs + # (or humans reading the summary) can inspect them. + outputs: + unified_address: ${{ steps.zeckit.outputs.unified_address }} + shield_txid: ${{ steps.zeckit.outputs.shield_txid }} + send_txid: ${{ steps.zeckit.outputs.send_txid }} + final_orchard_balance: ${{ steps.zeckit.outputs.final_orchard_balance }} + block_height: ${{ steps.zeckit.outputs.block_height }} + test_result: ${{ steps.zeckit.outputs.test_result }} + + steps: + - name: Checkout sample repo + uses: actions/checkout@v4 + + # ── Core action call ─────────────────────────────────── + - name: ZecKit E2E – lightwalletd + id: zeckit + uses: zecdev/ZecKit@v1 + with: + backend: lwd + # lwd starts a little slower than zaino + startup_timeout_minutes: '15' + block_wait_seconds: '90' + send_amount: '0.05' + send_memo: 'Sample repo CI – lwd' + upload_artifacts: ${{ inputs.upload_artifacts || 'on-failure' }} + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + + # ── Human-readable step summary ─────────────────────── + - name: Print results + if: always() + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " lightwalletd E2E Results" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Unified Address : ${{ steps.zeckit.outputs.unified_address }}" + echo " Shield txid : ${{ steps.zeckit.outputs.shield_txid }}" + echo " Send txid : ${{ steps.zeckit.outputs.send_txid }}" + echo " Orchard balance : ${{ steps.zeckit.outputs.final_orchard_balance }} ZEC" + echo " Block height : ${{ steps.zeckit.outputs.block_height }}" + echo " Result : ${{ steps.zeckit.outputs.test_result }}" + + # ============================================================ + # EXPERIMENTAL JOB – zaino + # Always executes; failure is reported but does NOT block merge. + # ============================================================ + e2e-zaino: + name: "E2E (experimental) – zaino" + runs-on: ubuntu-latest + timeout-minutes: 20 + # ── KEY FLAG: failure here is informational only ────────── + continue-on-error: true + + outputs: + unified_address: ${{ steps.zeckit.outputs.unified_address }} + shield_txid: ${{ steps.zeckit.outputs.shield_txid }} + send_txid: ${{ steps.zeckit.outputs.send_txid }} + final_orchard_balance: ${{ steps.zeckit.outputs.final_orchard_balance }} + block_height: ${{ steps.zeckit.outputs.block_height }} + test_result: ${{ steps.zeckit.outputs.test_result }} + + steps: + - name: Checkout sample repo + uses: actions/checkout@v4 + + - name: "⚠️ Experimental – zaino backend" + run: | + echo "::notice::This job uses the zaino backend and is marked experimental." + echo "::notice::A failure here does not block CI but is tracked for signal." + + - name: ZecKit E2E – zaino + id: zeckit + uses: zecdev/ZecKit@v1 + with: + backend: zaino + startup_timeout_minutes: '10' + block_wait_seconds: '75' + send_amount: '0.05' + send_memo: 'Sample repo CI – zaino (experimental)' + upload_artifacts: ${{ inputs.upload_artifacts || 'on-failure' }} + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Print results + if: always() + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " zaino E2E Results (experimental)" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Unified Address : ${{ steps.zeckit.outputs.unified_address }}" + echo " Shield txid : ${{ steps.zeckit.outputs.shield_txid }}" + echo " Send txid : ${{ steps.zeckit.outputs.send_txid }}" + echo " Orchard balance : ${{ steps.zeckit.outputs.final_orchard_balance }} ZEC" + echo " Block height : ${{ steps.zeckit.outputs.block_height }}" + echo " Result : ${{ steps.zeckit.outputs.test_result }}" + + # ============================================================ + # GATE – enforces that lwd passed; surfaces zaino signal + # ============================================================ + ci-gate: + name: "CI Gate" + needs: [e2e-lwd, e2e-zaino] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Evaluate results + shell: bash + run: | + lwd_result="${{ needs.e2e-lwd.result }}" + zaino_result="${{ needs.e2e-zaino.result }}" + lwd_e2e="${{ needs.e2e-lwd.outputs.test_result }}" + zaino_e2e="${{ needs.e2e-zaino.outputs.test_result }}" + + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " CI Gate Summary" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo " lightwalletd job: $lwd_result │ e2e: $lwd_e2e" + echo " zaino job: $zaino_result │ e2e: $zaino_e2e (experimental)" + echo "" + + # Write GitHub step summary table + { + echo "## CI Gate" + echo "" + echo "| Backend | Job result | E2E result | Merge-blocking? |" + echo "|---|---|---|---|" + if [[ "$lwd_result" == "success" ]]; then + echo "| lightwalletd | ✅ $lwd_result | $lwd_e2e | **YES** |" + else + echo "| lightwalletd | ❌ $lwd_result | $lwd_e2e | **YES** |" + fi + if [[ "$zaino_result" == "success" ]]; then + echo "| zaino | ✅ $zaino_result | $zaino_e2e | no (experimental) |" + else + echo "| zaino | ⚠️ $zaino_result | $zaino_e2e | no (experimental) |" + fi + } >> "$GITHUB_STEP_SUMMARY" + + # Only fail the gate on the required job + if [[ "$lwd_result" != "success" ]]; then + echo "::error::Required job 'e2e-lwd' did not succeed ($lwd_result). Blocking merge." + exit 1 + fi + + if [[ "$zaino_result" != "success" ]]; then + echo "::warning::Experimental job 'e2e-zaino' did not succeed ($zaino_result). Not blocking merge." + fi + + echo "✓ Gate passed – lightwalletd E2E succeeded." diff --git a/sample/.github/workflows/failure-drill.yml b/sample/.github/workflows/failure-drill.yml new file mode 100644 index 0000000..ecf3623 --- /dev/null +++ b/sample/.github/workflows/failure-drill.yml @@ -0,0 +1,308 @@ +# ============================================================ +# ZecKit Sample Repo – Failure Drill +# ============================================================ +# +# PURPOSE: Verify that ZecKit CI reliably produces diagnostic +# artifacts (logs + JSON summary) when a run fails. +# +# Two distinct failure modes are exercised, each in its own job: +# +# drill-send-overflow +# Injects an impossible send_amount (999 ZEC) against a +# freshly-started devnet that will only ever have ~1 ZEC. +# Fails deterministically at E2E step 4 (shielded send). +# Artifacts show faucet-stats.json with actual vs requested +# balance, which is the most common real-world failure. +# +# drill-startup-timeout +# Sets startup_timeout_minutes='1' for the lightwalletd +# backend, which normally needs 3-4 minutes to start. +# Fails deterministically at the devnet health-wait step. +# Artifacts capture partial service logs showing exactly +# which health-check did not pass in time. +# +# Both jobs use upload_artifacts: always so logs land even if +# the step that uploads them is reached after a failure. +# +# After each drill, an "assert-artifacts" step downloads the +# artifact and verifies key files are present, proving the +# collection path is working end-to-end. +# ============================================================ + +name: Failure Drill – Artifact Collection Verification + +on: + workflow_dispatch: + inputs: + drill: + description: 'Which drill to run' + required: false + default: 'both' + type: choice + options: + - both + - send-overflow + - startup-timeout + +permissions: + contents: read + packages: read + # write is needed so gh CLI can download artifacts in the + # assert-artifacts verification step + actions: read + +# ============================================================ +# DRILL 1 – Impossible send amount (send_overflow) +# ============================================================ +jobs: + + drill-send-overflow: + name: "Drill: send_amount overflow" + runs-on: ubuntu-latest + timeout-minutes: 25 + # This job is expected to fail the E2E flow deliberately. + # We still want the workflow run to be green so we can + # assert on its output — the gate job enforces the contract. + continue-on-error: true + if: > + github.event.inputs.drill == 'both' || + github.event.inputs.drill == 'send-overflow' + + outputs: + test_result: ${{ steps.zeckit.outputs.test_result }} + run_number: ${{ github.run_number }} + + steps: + - name: Checkout sample repo + uses: actions/checkout@v4 + + - name: "🔴 DRILL: inject impossible send amount (999 ZEC)" + run: | + echo "::notice::Failure drill – injecting send_amount=999 ZEC." + echo "::notice::Devnet wallet will have ~0.05-5 ZEC; send must fail." + echo "::notice::upload_artifacts=always ensures logs land regardless." + + # ── Run action with deliberately impossible send_amount ── + - name: ZecKit E2E – send overflow drill + id: zeckit + uses: zecdev/ZecKit@v1 + with: + backend: zaino + startup_timeout_minutes: '10' + block_wait_seconds: '75' + send_amount: '999.0' # << INJECTED FAILURE + send_memo: 'Failure drill – send overflow' + # Always upload so we get artifacts even on the + # successful pre-failure steps. + upload_artifacts: always + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + # Do not fail the step here; we assert the result below. + continue-on-error: true + + # ── Verify the action correctly reported failure ────── + - name: Assert E2E reported failure + shell: bash + run: | + result="${{ steps.zeckit.outputs.test_result }}" + echo "test_result from action: $result" + if [[ "$result" == "pass" ]]; then + echo "::error::Drill BROKEN: action reported 'pass' when it should have failed." + echo "::error::This means the send_amount guard is not working correctly." + exit 1 + fi + echo "✓ Drill correctly produced a failure result." + + # ── Verify artifact was uploaded & contains expected files ── + - name: Assert artifact was uploaded (send-overflow) + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + artifact_name="zeckit-e2e-logs-${{ github.run_number }}" + download_dir="/tmp/drill-overflow-artifacts" + + echo "Downloading artifact: $artifact_name" + gh run download "${{ github.run_id }}" \ + --repo "${{ github.repository }}" \ + --name "$artifact_name" \ + --dir "$download_dir" \ + || { echo "::error::Artifact '$artifact_name' was NOT uploaded. Drill failed."; exit 1; } + + echo "Artifact contents:" + ls -lh "$download_dir/" + + # Assert key diagnostic files are present + required_files=( + "run-summary.json" + "faucet-stats.json" + "faucet.log" + "zebra.log" + ) + missing=() + for f in "${required_files[@]}"; do + if [[ ! -f "$download_dir/$f" ]]; then + missing+=("$f") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "::error::Missing expected artifact files: ${missing[*]}" + exit 1 + fi + + # Assert run-summary.json records the failure + summary_result=$(jq -r '.test_result // "missing"' "$download_dir/run-summary.json") + echo "run-summary.json test_result: $summary_result" + if [[ "$summary_result" == "pass" ]]; then + echo "::error::run-summary.json says 'pass' but a failure was expected." + exit 1 + fi + + # Show faucet balance at time of failure (most useful diagnostic) + echo "" + echo "── faucet-stats.json at time of failure ──" + jq . "$download_dir/faucet-stats.json" || cat "$download_dir/faucet-stats.json" + + echo "" + echo "✓ Artifact validation passed." + echo " All required files present." + echo " run-summary.json correctly records test_result='$summary_result'." + + # ============================================================ + # DRILL 2 – Startup timeout (startup_timeout) + # ============================================================ + drill-startup-timeout: + name: "Drill: startup timeout (lwd, 1 min)" + runs-on: ubuntu-latest + timeout-minutes: 15 + continue-on-error: true + if: > + github.event.inputs.drill == 'both' || + github.event.inputs.drill == 'startup-timeout' + + outputs: + test_result: ${{ steps.zeckit.outputs.test_result }} + + steps: + - name: Checkout sample repo + uses: actions/checkout@v4 + + - name: "🔴 DRILL: inject startup timeout (lwd needs 3-4 min; timeout=1 min)" + run: | + echo "::notice::Failure drill – setting startup_timeout_minutes=1 for lwd backend." + echo "::notice::lwd normally takes 3-4 minutes; this will time out at the health-wait step." + echo "::notice::Artifacts will contain partial container startup logs." + + - name: ZecKit E2E – startup timeout drill + id: zeckit + uses: zecdev/ZecKit@v1 + with: + backend: lwd + startup_timeout_minutes: '1' # << INJECTED FAILURE (lwd needs ~3-4 min) + block_wait_seconds: '75' + send_amount: '0.05' + send_memo: 'Failure drill – startup timeout' + upload_artifacts: always # capture partial startup logs + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Assert E2E reported failure (startup timeout) + shell: bash + run: | + result="${{ steps.zeckit.outputs.test_result }}" + echo "test_result from action: ${result:-}" + # On startup failure, the action exits before writing test_result. + # Both empty AND 'fail' are acceptable here. + if [[ "$result" == "pass" ]]; then + echo "::error::Drill BROKEN: action reported 'pass' when startup should have timed out." + echo "::error::Either lwd starts faster than 1 minute (unlikely) or the timeout is not enforced." + exit 1 + fi + echo "✓ Drill correctly produced a non-pass result (result='${result:-}')." + + - name: Assert artifact was uploaded (startup-timeout) + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # The action uploads on 'always'; find the artifact by + # run number (there may be multiple from the same run). + artifact_name="zeckit-e2e-logs-${{ github.run_number }}" + download_dir="/tmp/drill-startup-artifacts" + + echo "Downloading artifact: $artifact_name" + gh run download "${{ github.run_id }}" \ + --repo "${{ github.repository }}" \ + --name "$artifact_name" \ + --dir "$download_dir" \ + || { echo "::error::Artifact '$artifact_name' was NOT uploaded. Drill failed."; exit 1; } + + echo "Artifact contents:" + ls -lh "$download_dir/" + + # On a startup failure the service logs are the key output. + # At minimum containers.log and at least one service log must exist. + if [[ ! -f "$download_dir/containers.log" ]]; then + echo "::error::containers.log missing from artifact – artifact collection is broken." + exit 1 + fi + + # Dump the partial lightwalletd log so humans can see + # exactly how far startup got before the timeout. + echo "" + echo "── lightwalletd.log (partial startup) ──" + head -60 "$download_dir/lightwalletd.log" 2>/dev/null \ + || echo "(not present – container may not have started)" + + echo "" + echo "── zebra.log (first 40 lines) ──" + head -40 "$download_dir/zebra.log" 2>/dev/null \ + || echo "(not present)" + + echo "" + echo "✓ Artifact validation passed (startup-timeout drill)." + + # ============================================================ + # DRILL GATE – report both drills in one summary + # ============================================================ + drill-gate: + name: "Drill Gate" + needs: [drill-send-overflow, drill-startup-timeout] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Evaluate drill results + shell: bash + run: | + overflow="${{ needs.drill-send-overflow.result }}" + startup="${{ needs.drill-startup-timeout.result }}" + + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Failure Drill Summary" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " drill-send-overflow : $overflow" + echo " drill-startup-timeout : $startup" + echo "" + echo " (jobs use continue-on-error – 'failure' here means the" + echo " assert step found a problem with artifact collection," + echo " not that the E2E itself failed as expected)" + + { + echo "## Failure Drill Results" + echo "" + echo "| Drill | Job outcome | Contract |" + echo "|---|---|---|" + echo "| send-overflow | $overflow | E2E must fail; artifacts must be uploaded |" + echo "| startup-timeout | $startup | E2E must fail; partial logs must be uploaded |" + echo "" + echo "_Jobs use \`continue-on-error: true\`. A 'failure' result means the" + echo "artifact-assertion step detected a problem — not just that E2E failed as intended._" + } >> "$GITHUB_STEP_SUMMARY" + + if [[ "$overflow" == "failure" || "$startup" == "failure" ]]; then + echo "::error::One or more drill assertion steps failed." + exit 1 + fi + + echo "✓ Drill gate complete." diff --git a/sample/README.md b/sample/README.md new file mode 100644 index 0000000..7b644bd --- /dev/null +++ b/sample/README.md @@ -0,0 +1,174 @@ +# ZecKit Sample Repo + +> Reference implementation showing how to wire the +> [ZecKit E2E GitHub Action](https://github.com/marketplace/actions/zeckit-e2e) +> into a project's CI pipeline. + +Move this folder to its own repository and push to GitHub. +The workflows run immediately without any extra configuration. + +--- + +## CI Status + +| Workflow | Purpose | +|---|---| +| [![ZecKit E2E CI](../../actions/workflows/ci.yml/badge.svg)](../../actions/workflows/ci.yml) | Golden E2E across both backends | +| [![Failure Drill](../../actions/workflows/failure-drill.yml/badge.svg)](../../actions/workflows/failure-drill.yml) | Artifact-collection verification | + +*(Update badge URLs to point to your repo once moved.)* + +--- + +## What Is This? + +This repo demonstrates: + +1. **Backend matrix CI** — the same ZecKit golden E2E flow + (`generate UA → fund → autoshield → shielded send → rescan → verify`) + runs against two backends in parallel: + + | Backend | Job name | Merge-blocking? | + |---|---|---| + | lightwalletd | `e2e-lwd` | **YES** — CI fails if this fails | + | zaino | `e2e-zaino` | No — experimental; failure is reported, not enforced | + +2. **Failure drills** — a dedicated workflow injects two types of + deterministic failures and asserts that diagnostic artifacts + (logs, JSON summary, faucet stats) are always uploaded: + + | Drill | Injected condition | Expected artifact | + |---|---|---| + | `send-overflow` | `send_amount=999 ZEC` (impossible) | `faucet-stats.json` showing real balance vs requested | + | `startup-timeout` | `startup_timeout_minutes=1` with lwd (needs 3-4 min) | Partial `lightwalletd.log` and `zebra.log` | + +--- + +## Repository Structure + +``` +.github/ + workflows/ + ci.yml Normal CI – lwd (required) + zaino (experimental) + failure-drill.yml Failure injection + artifact assertion +README.md +``` + +--- + +## How to Use in Your Own Repo + +### 1. Add the action to an existing workflow + +```yaml +# .github/workflows/my-ci.yml +jobs: + zcash-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: zecdev/ZecKit@v1 + with: + backend: zaino + ghcr_token: ${{ secrets.GITHUB_TOKEN }} +``` + +### 2. Copy this sample as a starting point + +```bash +# From ZecKit repo root +cp -r sample/ /path/to/your/new-repo/ +cd /path/to/your/new-repo/ +git init && git add . && git commit -m "chore: add ZecKit E2E CI" +``` + +### 3. Override defaults for your use case + +```yaml +- uses: zecdev/ZecKit@v1 + with: + backend: lwd # or zaino + startup_timeout_minutes: '15' # default 10 + block_wait_seconds: '90' # default 75 + send_amount: '0.1' # default 0.05 ZEC + send_address: 'uregtest1...' # optional external UA + upload_artifacts: always # always | on-failure | never + ghcr_token: ${{ secrets.GITHUB_TOKEN }} +``` + +Full input/output reference → [ZecKit docs/github-action.md](https://github.com/zecdev/ZecKit/blob/main/docs/github-action.md) + +--- + +## Action Outputs + +After the action runs, these outputs are available in subsequent steps: + +```yaml +- uses: zecdev/ZecKit@v1 + id: zcash + with: + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + +- run: | + echo "UA : ${{ steps.zcash.outputs.unified_address }}" + echo "Shield : ${{ steps.zcash.outputs.shield_txid }}" + echo "Send : ${{ steps.zcash.outputs.send_txid }}" + echo "Balance : ${{ steps.zcash.outputs.final_orchard_balance }} ZEC" + echo "Height : ${{ steps.zcash.outputs.block_height }}" + echo "Result : ${{ steps.zcash.outputs.test_result }}" +``` + +--- + +## Artifacts + +When `upload_artifacts` is `always` or `on-failure` (default), a ZIP named +`zeckit-e2e-logs-` is attached to the workflow run. + +Contents: + +| File | What it shows | +|---|---| +| `run-summary.json` | Machine-readable: backend, txids, balances, test_result | +| `faucet-stats.json` | Wallet balances at end of run | +| `zebra.log` | Full Zebra node output | +| `zaino.log` | Zaino indexer output | +| `lightwalletd.log` | Lightwalletd output | +| `faucet.log` | Faucet (Axum + Zingolib) output | +| `containers.log` | `docker ps -a` at teardown | +| `networks.log` | `docker network ls` at teardown | + +Download via CLI: + +```bash +gh run download -n zeckit-e2e-logs- +``` + +--- + +## Failure Drill – How to Run + +The failure drill is triggered manually: + +1. Go to **Actions → Failure Drill – Artifact Collection Verification** +2. Click **Run workflow** +3. Choose a drill (`both`, `send-overflow`, or `startup-timeout`) +4. After it completes, confirm both jobs have a ✅ next to + "Assert artifact was uploaded" + +A failure in the *assert* step (not the E2E drill itself) means the +artifact collection pipeline is broken and needs investigation. + +--- + +## Common Issues + +| Symptom | Fix | +|---|---| +| Lightwalletd job times out | Increase `startup_timeout_minutes` to `15` or `20` | +| Zaino experimental job fails | Check the `e2e-zaino` logs; failures here don't block CI | +| No artifacts uploaded | Ensure `ghcr_token` has `read:packages` scope | +| Drill asserts fail | The artifact-collection path in `action.yml` is broken; check the action version | + +Full troubleshooting guide → [ZecKit docs/github-action.md](https://github.com/zecdev/ZecKit/blob/main/docs/github-action.md#common-failure-modes--troubleshooting) From c16810c43826dba1930059cef3035b94731e91df Mon Sep 17 00:00:00 2001 From: Great-DOA Date: Sat, 21 Feb 2026 01:35:35 +0100 Subject: [PATCH 2/3] chore: ignore sample directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7378731..39e4493 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,4 @@ actions-runner/ # Zcash proving parameters (large binary files) zcash-params/ +sample/ From 6a683248ea19d0e3ac857b9907e0faf32641f7bc Mon Sep 17 00:00:00 2001 From: Great-DOA Date: Sat, 21 Feb 2026 01:40:16 +0100 Subject: [PATCH 3/3] chore: remove sample repo (not intended for this PR) --- sample/.github/workflows/ci.yml | 208 -------------- sample/.github/workflows/failure-drill.yml | 308 --------------------- sample/README.md | 174 ------------ 3 files changed, 690 deletions(-) delete mode 100644 sample/.github/workflows/ci.yml delete mode 100644 sample/.github/workflows/failure-drill.yml delete mode 100644 sample/README.md diff --git a/sample/.github/workflows/ci.yml b/sample/.github/workflows/ci.yml deleted file mode 100644 index 0a0741f..0000000 --- a/sample/.github/workflows/ci.yml +++ /dev/null @@ -1,208 +0,0 @@ -# ============================================================ -# ZecKit Sample Repo – CI -# ============================================================ -# -# This workflow runs the ZecKit golden E2E shielded-transaction -# flow against two backends: -# -# lightwalletd – REQUIRED job. CI fails if this fails. -# zaino – EXPERIMENTAL job. Failure does not block merge; -# the job is still executed so signal accumulates. -# -# Backend matrix overview: -# ┌──────────────────┬──────────────────────────────────────────┐ -# │ Backend │ Merge-blocking? │ -# ├──────────────────┼──────────────────────────────────────────┤ -# │ lightwalletd │ YES – required for CI green │ -# │ zaino │ NO – experimental; result is reported │ -# │ │ but not enforced │ -# └──────────────────┴──────────────────────────────────────────┘ -# ============================================================ - -name: ZecKit E2E CI - -on: - push: - branches: - - main - - 'feat/**' - - 'fix/**' - pull_request: - branches: - - main - workflow_dispatch: - inputs: - upload_artifacts: - description: 'Artifact upload policy' - required: false - default: 'on-failure' - type: choice - options: - - on-failure - - always - - never - -permissions: - contents: read - packages: read # needed to pull GHCR images - -# ============================================================ -# REQUIRED JOB – lightwalletd -# Merge is blocked if this job fails. -# ============================================================ -jobs: - e2e-lwd: - name: "E2E (required) – lightwalletd" - runs-on: ubuntu-latest - timeout-minutes: 25 - - # Output the action's structured results so downstream jobs - # (or humans reading the summary) can inspect them. - outputs: - unified_address: ${{ steps.zeckit.outputs.unified_address }} - shield_txid: ${{ steps.zeckit.outputs.shield_txid }} - send_txid: ${{ steps.zeckit.outputs.send_txid }} - final_orchard_balance: ${{ steps.zeckit.outputs.final_orchard_balance }} - block_height: ${{ steps.zeckit.outputs.block_height }} - test_result: ${{ steps.zeckit.outputs.test_result }} - - steps: - - name: Checkout sample repo - uses: actions/checkout@v4 - - # ── Core action call ─────────────────────────────────── - - name: ZecKit E2E – lightwalletd - id: zeckit - uses: zecdev/ZecKit@v1 - with: - backend: lwd - # lwd starts a little slower than zaino - startup_timeout_minutes: '15' - block_wait_seconds: '90' - send_amount: '0.05' - send_memo: 'Sample repo CI – lwd' - upload_artifacts: ${{ inputs.upload_artifacts || 'on-failure' }} - ghcr_token: ${{ secrets.GITHUB_TOKEN }} - - # ── Human-readable step summary ─────────────────────── - - name: Print results - if: always() - run: | - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " lightwalletd E2E Results" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " Unified Address : ${{ steps.zeckit.outputs.unified_address }}" - echo " Shield txid : ${{ steps.zeckit.outputs.shield_txid }}" - echo " Send txid : ${{ steps.zeckit.outputs.send_txid }}" - echo " Orchard balance : ${{ steps.zeckit.outputs.final_orchard_balance }} ZEC" - echo " Block height : ${{ steps.zeckit.outputs.block_height }}" - echo " Result : ${{ steps.zeckit.outputs.test_result }}" - - # ============================================================ - # EXPERIMENTAL JOB – zaino - # Always executes; failure is reported but does NOT block merge. - # ============================================================ - e2e-zaino: - name: "E2E (experimental) – zaino" - runs-on: ubuntu-latest - timeout-minutes: 20 - # ── KEY FLAG: failure here is informational only ────────── - continue-on-error: true - - outputs: - unified_address: ${{ steps.zeckit.outputs.unified_address }} - shield_txid: ${{ steps.zeckit.outputs.shield_txid }} - send_txid: ${{ steps.zeckit.outputs.send_txid }} - final_orchard_balance: ${{ steps.zeckit.outputs.final_orchard_balance }} - block_height: ${{ steps.zeckit.outputs.block_height }} - test_result: ${{ steps.zeckit.outputs.test_result }} - - steps: - - name: Checkout sample repo - uses: actions/checkout@v4 - - - name: "⚠️ Experimental – zaino backend" - run: | - echo "::notice::This job uses the zaino backend and is marked experimental." - echo "::notice::A failure here does not block CI but is tracked for signal." - - - name: ZecKit E2E – zaino - id: zeckit - uses: zecdev/ZecKit@v1 - with: - backend: zaino - startup_timeout_minutes: '10' - block_wait_seconds: '75' - send_amount: '0.05' - send_memo: 'Sample repo CI – zaino (experimental)' - upload_artifacts: ${{ inputs.upload_artifacts || 'on-failure' }} - ghcr_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Print results - if: always() - run: | - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " zaino E2E Results (experimental)" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " Unified Address : ${{ steps.zeckit.outputs.unified_address }}" - echo " Shield txid : ${{ steps.zeckit.outputs.shield_txid }}" - echo " Send txid : ${{ steps.zeckit.outputs.send_txid }}" - echo " Orchard balance : ${{ steps.zeckit.outputs.final_orchard_balance }} ZEC" - echo " Block height : ${{ steps.zeckit.outputs.block_height }}" - echo " Result : ${{ steps.zeckit.outputs.test_result }}" - - # ============================================================ - # GATE – enforces that lwd passed; surfaces zaino signal - # ============================================================ - ci-gate: - name: "CI Gate" - needs: [e2e-lwd, e2e-zaino] - runs-on: ubuntu-latest - if: always() - - steps: - - name: Evaluate results - shell: bash - run: | - lwd_result="${{ needs.e2e-lwd.result }}" - zaino_result="${{ needs.e2e-zaino.result }}" - lwd_e2e="${{ needs.e2e-lwd.outputs.test_result }}" - zaino_e2e="${{ needs.e2e-zaino.outputs.test_result }}" - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " CI Gate Summary" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo " lightwalletd job: $lwd_result │ e2e: $lwd_e2e" - echo " zaino job: $zaino_result │ e2e: $zaino_e2e (experimental)" - echo "" - - # Write GitHub step summary table - { - echo "## CI Gate" - echo "" - echo "| Backend | Job result | E2E result | Merge-blocking? |" - echo "|---|---|---|---|" - if [[ "$lwd_result" == "success" ]]; then - echo "| lightwalletd | ✅ $lwd_result | $lwd_e2e | **YES** |" - else - echo "| lightwalletd | ❌ $lwd_result | $lwd_e2e | **YES** |" - fi - if [[ "$zaino_result" == "success" ]]; then - echo "| zaino | ✅ $zaino_result | $zaino_e2e | no (experimental) |" - else - echo "| zaino | ⚠️ $zaino_result | $zaino_e2e | no (experimental) |" - fi - } >> "$GITHUB_STEP_SUMMARY" - - # Only fail the gate on the required job - if [[ "$lwd_result" != "success" ]]; then - echo "::error::Required job 'e2e-lwd' did not succeed ($lwd_result). Blocking merge." - exit 1 - fi - - if [[ "$zaino_result" != "success" ]]; then - echo "::warning::Experimental job 'e2e-zaino' did not succeed ($zaino_result). Not blocking merge." - fi - - echo "✓ Gate passed – lightwalletd E2E succeeded." diff --git a/sample/.github/workflows/failure-drill.yml b/sample/.github/workflows/failure-drill.yml deleted file mode 100644 index ecf3623..0000000 --- a/sample/.github/workflows/failure-drill.yml +++ /dev/null @@ -1,308 +0,0 @@ -# ============================================================ -# ZecKit Sample Repo – Failure Drill -# ============================================================ -# -# PURPOSE: Verify that ZecKit CI reliably produces diagnostic -# artifacts (logs + JSON summary) when a run fails. -# -# Two distinct failure modes are exercised, each in its own job: -# -# drill-send-overflow -# Injects an impossible send_amount (999 ZEC) against a -# freshly-started devnet that will only ever have ~1 ZEC. -# Fails deterministically at E2E step 4 (shielded send). -# Artifacts show faucet-stats.json with actual vs requested -# balance, which is the most common real-world failure. -# -# drill-startup-timeout -# Sets startup_timeout_minutes='1' for the lightwalletd -# backend, which normally needs 3-4 minutes to start. -# Fails deterministically at the devnet health-wait step. -# Artifacts capture partial service logs showing exactly -# which health-check did not pass in time. -# -# Both jobs use upload_artifacts: always so logs land even if -# the step that uploads them is reached after a failure. -# -# After each drill, an "assert-artifacts" step downloads the -# artifact and verifies key files are present, proving the -# collection path is working end-to-end. -# ============================================================ - -name: Failure Drill – Artifact Collection Verification - -on: - workflow_dispatch: - inputs: - drill: - description: 'Which drill to run' - required: false - default: 'both' - type: choice - options: - - both - - send-overflow - - startup-timeout - -permissions: - contents: read - packages: read - # write is needed so gh CLI can download artifacts in the - # assert-artifacts verification step - actions: read - -# ============================================================ -# DRILL 1 – Impossible send amount (send_overflow) -# ============================================================ -jobs: - - drill-send-overflow: - name: "Drill: send_amount overflow" - runs-on: ubuntu-latest - timeout-minutes: 25 - # This job is expected to fail the E2E flow deliberately. - # We still want the workflow run to be green so we can - # assert on its output — the gate job enforces the contract. - continue-on-error: true - if: > - github.event.inputs.drill == 'both' || - github.event.inputs.drill == 'send-overflow' - - outputs: - test_result: ${{ steps.zeckit.outputs.test_result }} - run_number: ${{ github.run_number }} - - steps: - - name: Checkout sample repo - uses: actions/checkout@v4 - - - name: "🔴 DRILL: inject impossible send amount (999 ZEC)" - run: | - echo "::notice::Failure drill – injecting send_amount=999 ZEC." - echo "::notice::Devnet wallet will have ~0.05-5 ZEC; send must fail." - echo "::notice::upload_artifacts=always ensures logs land regardless." - - # ── Run action with deliberately impossible send_amount ── - - name: ZecKit E2E – send overflow drill - id: zeckit - uses: zecdev/ZecKit@v1 - with: - backend: zaino - startup_timeout_minutes: '10' - block_wait_seconds: '75' - send_amount: '999.0' # << INJECTED FAILURE - send_memo: 'Failure drill – send overflow' - # Always upload so we get artifacts even on the - # successful pre-failure steps. - upload_artifacts: always - ghcr_token: ${{ secrets.GITHUB_TOKEN }} - # Do not fail the step here; we assert the result below. - continue-on-error: true - - # ── Verify the action correctly reported failure ────── - - name: Assert E2E reported failure - shell: bash - run: | - result="${{ steps.zeckit.outputs.test_result }}" - echo "test_result from action: $result" - if [[ "$result" == "pass" ]]; then - echo "::error::Drill BROKEN: action reported 'pass' when it should have failed." - echo "::error::This means the send_amount guard is not working correctly." - exit 1 - fi - echo "✓ Drill correctly produced a failure result." - - # ── Verify artifact was uploaded & contains expected files ── - - name: Assert artifact was uploaded (send-overflow) - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - artifact_name="zeckit-e2e-logs-${{ github.run_number }}" - download_dir="/tmp/drill-overflow-artifacts" - - echo "Downloading artifact: $artifact_name" - gh run download "${{ github.run_id }}" \ - --repo "${{ github.repository }}" \ - --name "$artifact_name" \ - --dir "$download_dir" \ - || { echo "::error::Artifact '$artifact_name' was NOT uploaded. Drill failed."; exit 1; } - - echo "Artifact contents:" - ls -lh "$download_dir/" - - # Assert key diagnostic files are present - required_files=( - "run-summary.json" - "faucet-stats.json" - "faucet.log" - "zebra.log" - ) - missing=() - for f in "${required_files[@]}"; do - if [[ ! -f "$download_dir/$f" ]]; then - missing+=("$f") - fi - done - - if [[ ${#missing[@]} -gt 0 ]]; then - echo "::error::Missing expected artifact files: ${missing[*]}" - exit 1 - fi - - # Assert run-summary.json records the failure - summary_result=$(jq -r '.test_result // "missing"' "$download_dir/run-summary.json") - echo "run-summary.json test_result: $summary_result" - if [[ "$summary_result" == "pass" ]]; then - echo "::error::run-summary.json says 'pass' but a failure was expected." - exit 1 - fi - - # Show faucet balance at time of failure (most useful diagnostic) - echo "" - echo "── faucet-stats.json at time of failure ──" - jq . "$download_dir/faucet-stats.json" || cat "$download_dir/faucet-stats.json" - - echo "" - echo "✓ Artifact validation passed." - echo " All required files present." - echo " run-summary.json correctly records test_result='$summary_result'." - - # ============================================================ - # DRILL 2 – Startup timeout (startup_timeout) - # ============================================================ - drill-startup-timeout: - name: "Drill: startup timeout (lwd, 1 min)" - runs-on: ubuntu-latest - timeout-minutes: 15 - continue-on-error: true - if: > - github.event.inputs.drill == 'both' || - github.event.inputs.drill == 'startup-timeout' - - outputs: - test_result: ${{ steps.zeckit.outputs.test_result }} - - steps: - - name: Checkout sample repo - uses: actions/checkout@v4 - - - name: "🔴 DRILL: inject startup timeout (lwd needs 3-4 min; timeout=1 min)" - run: | - echo "::notice::Failure drill – setting startup_timeout_minutes=1 for lwd backend." - echo "::notice::lwd normally takes 3-4 minutes; this will time out at the health-wait step." - echo "::notice::Artifacts will contain partial container startup logs." - - - name: ZecKit E2E – startup timeout drill - id: zeckit - uses: zecdev/ZecKit@v1 - with: - backend: lwd - startup_timeout_minutes: '1' # << INJECTED FAILURE (lwd needs ~3-4 min) - block_wait_seconds: '75' - send_amount: '0.05' - send_memo: 'Failure drill – startup timeout' - upload_artifacts: always # capture partial startup logs - ghcr_token: ${{ secrets.GITHUB_TOKEN }} - continue-on-error: true - - - name: Assert E2E reported failure (startup timeout) - shell: bash - run: | - result="${{ steps.zeckit.outputs.test_result }}" - echo "test_result from action: ${result:-}" - # On startup failure, the action exits before writing test_result. - # Both empty AND 'fail' are acceptable here. - if [[ "$result" == "pass" ]]; then - echo "::error::Drill BROKEN: action reported 'pass' when startup should have timed out." - echo "::error::Either lwd starts faster than 1 minute (unlikely) or the timeout is not enforced." - exit 1 - fi - echo "✓ Drill correctly produced a non-pass result (result='${result:-}')." - - - name: Assert artifact was uploaded (startup-timeout) - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # The action uploads on 'always'; find the artifact by - # run number (there may be multiple from the same run). - artifact_name="zeckit-e2e-logs-${{ github.run_number }}" - download_dir="/tmp/drill-startup-artifacts" - - echo "Downloading artifact: $artifact_name" - gh run download "${{ github.run_id }}" \ - --repo "${{ github.repository }}" \ - --name "$artifact_name" \ - --dir "$download_dir" \ - || { echo "::error::Artifact '$artifact_name' was NOT uploaded. Drill failed."; exit 1; } - - echo "Artifact contents:" - ls -lh "$download_dir/" - - # On a startup failure the service logs are the key output. - # At minimum containers.log and at least one service log must exist. - if [[ ! -f "$download_dir/containers.log" ]]; then - echo "::error::containers.log missing from artifact – artifact collection is broken." - exit 1 - fi - - # Dump the partial lightwalletd log so humans can see - # exactly how far startup got before the timeout. - echo "" - echo "── lightwalletd.log (partial startup) ──" - head -60 "$download_dir/lightwalletd.log" 2>/dev/null \ - || echo "(not present – container may not have started)" - - echo "" - echo "── zebra.log (first 40 lines) ──" - head -40 "$download_dir/zebra.log" 2>/dev/null \ - || echo "(not present)" - - echo "" - echo "✓ Artifact validation passed (startup-timeout drill)." - - # ============================================================ - # DRILL GATE – report both drills in one summary - # ============================================================ - drill-gate: - name: "Drill Gate" - needs: [drill-send-overflow, drill-startup-timeout] - runs-on: ubuntu-latest - if: always() - - steps: - - name: Evaluate drill results - shell: bash - run: | - overflow="${{ needs.drill-send-overflow.result }}" - startup="${{ needs.drill-startup-timeout.result }}" - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " Failure Drill Summary" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " drill-send-overflow : $overflow" - echo " drill-startup-timeout : $startup" - echo "" - echo " (jobs use continue-on-error – 'failure' here means the" - echo " assert step found a problem with artifact collection," - echo " not that the E2E itself failed as expected)" - - { - echo "## Failure Drill Results" - echo "" - echo "| Drill | Job outcome | Contract |" - echo "|---|---|---|" - echo "| send-overflow | $overflow | E2E must fail; artifacts must be uploaded |" - echo "| startup-timeout | $startup | E2E must fail; partial logs must be uploaded |" - echo "" - echo "_Jobs use \`continue-on-error: true\`. A 'failure' result means the" - echo "artifact-assertion step detected a problem — not just that E2E failed as intended._" - } >> "$GITHUB_STEP_SUMMARY" - - if [[ "$overflow" == "failure" || "$startup" == "failure" ]]; then - echo "::error::One or more drill assertion steps failed." - exit 1 - fi - - echo "✓ Drill gate complete." diff --git a/sample/README.md b/sample/README.md deleted file mode 100644 index 7b644bd..0000000 --- a/sample/README.md +++ /dev/null @@ -1,174 +0,0 @@ -# ZecKit Sample Repo - -> Reference implementation showing how to wire the -> [ZecKit E2E GitHub Action](https://github.com/marketplace/actions/zeckit-e2e) -> into a project's CI pipeline. - -Move this folder to its own repository and push to GitHub. -The workflows run immediately without any extra configuration. - ---- - -## CI Status - -| Workflow | Purpose | -|---|---| -| [![ZecKit E2E CI](../../actions/workflows/ci.yml/badge.svg)](../../actions/workflows/ci.yml) | Golden E2E across both backends | -| [![Failure Drill](../../actions/workflows/failure-drill.yml/badge.svg)](../../actions/workflows/failure-drill.yml) | Artifact-collection verification | - -*(Update badge URLs to point to your repo once moved.)* - ---- - -## What Is This? - -This repo demonstrates: - -1. **Backend matrix CI** — the same ZecKit golden E2E flow - (`generate UA → fund → autoshield → shielded send → rescan → verify`) - runs against two backends in parallel: - - | Backend | Job name | Merge-blocking? | - |---|---|---| - | lightwalletd | `e2e-lwd` | **YES** — CI fails if this fails | - | zaino | `e2e-zaino` | No — experimental; failure is reported, not enforced | - -2. **Failure drills** — a dedicated workflow injects two types of - deterministic failures and asserts that diagnostic artifacts - (logs, JSON summary, faucet stats) are always uploaded: - - | Drill | Injected condition | Expected artifact | - |---|---|---| - | `send-overflow` | `send_amount=999 ZEC` (impossible) | `faucet-stats.json` showing real balance vs requested | - | `startup-timeout` | `startup_timeout_minutes=1` with lwd (needs 3-4 min) | Partial `lightwalletd.log` and `zebra.log` | - ---- - -## Repository Structure - -``` -.github/ - workflows/ - ci.yml Normal CI – lwd (required) + zaino (experimental) - failure-drill.yml Failure injection + artifact assertion -README.md -``` - ---- - -## How to Use in Your Own Repo - -### 1. Add the action to an existing workflow - -```yaml -# .github/workflows/my-ci.yml -jobs: - zcash-e2e: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: zecdev/ZecKit@v1 - with: - backend: zaino - ghcr_token: ${{ secrets.GITHUB_TOKEN }} -``` - -### 2. Copy this sample as a starting point - -```bash -# From ZecKit repo root -cp -r sample/ /path/to/your/new-repo/ -cd /path/to/your/new-repo/ -git init && git add . && git commit -m "chore: add ZecKit E2E CI" -``` - -### 3. Override defaults for your use case - -```yaml -- uses: zecdev/ZecKit@v1 - with: - backend: lwd # or zaino - startup_timeout_minutes: '15' # default 10 - block_wait_seconds: '90' # default 75 - send_amount: '0.1' # default 0.05 ZEC - send_address: 'uregtest1...' # optional external UA - upload_artifacts: always # always | on-failure | never - ghcr_token: ${{ secrets.GITHUB_TOKEN }} -``` - -Full input/output reference → [ZecKit docs/github-action.md](https://github.com/zecdev/ZecKit/blob/main/docs/github-action.md) - ---- - -## Action Outputs - -After the action runs, these outputs are available in subsequent steps: - -```yaml -- uses: zecdev/ZecKit@v1 - id: zcash - with: - ghcr_token: ${{ secrets.GITHUB_TOKEN }} - -- run: | - echo "UA : ${{ steps.zcash.outputs.unified_address }}" - echo "Shield : ${{ steps.zcash.outputs.shield_txid }}" - echo "Send : ${{ steps.zcash.outputs.send_txid }}" - echo "Balance : ${{ steps.zcash.outputs.final_orchard_balance }} ZEC" - echo "Height : ${{ steps.zcash.outputs.block_height }}" - echo "Result : ${{ steps.zcash.outputs.test_result }}" -``` - ---- - -## Artifacts - -When `upload_artifacts` is `always` or `on-failure` (default), a ZIP named -`zeckit-e2e-logs-` is attached to the workflow run. - -Contents: - -| File | What it shows | -|---|---| -| `run-summary.json` | Machine-readable: backend, txids, balances, test_result | -| `faucet-stats.json` | Wallet balances at end of run | -| `zebra.log` | Full Zebra node output | -| `zaino.log` | Zaino indexer output | -| `lightwalletd.log` | Lightwalletd output | -| `faucet.log` | Faucet (Axum + Zingolib) output | -| `containers.log` | `docker ps -a` at teardown | -| `networks.log` | `docker network ls` at teardown | - -Download via CLI: - -```bash -gh run download -n zeckit-e2e-logs- -``` - ---- - -## Failure Drill – How to Run - -The failure drill is triggered manually: - -1. Go to **Actions → Failure Drill – Artifact Collection Verification** -2. Click **Run workflow** -3. Choose a drill (`both`, `send-overflow`, or `startup-timeout`) -4. After it completes, confirm both jobs have a ✅ next to - "Assert artifact was uploaded" - -A failure in the *assert* step (not the E2E drill itself) means the -artifact collection pipeline is broken and needs investigation. - ---- - -## Common Issues - -| Symptom | Fix | -|---|---| -| Lightwalletd job times out | Increase `startup_timeout_minutes` to `15` or `20` | -| Zaino experimental job fails | Check the `e2e-zaino` logs; failures here don't block CI | -| No artifacts uploaded | Ensure `ghcr_token` has `read:packages` scope | -| Drill asserts fail | The artifact-collection path in `action.yml` is broken; check the action version | - -Full troubleshooting guide → [ZecKit docs/github-action.md](https://github.com/zecdev/ZecKit/blob/main/docs/github-action.md#common-failure-modes--troubleshooting)