distributed 4.4.0

CQRS/ES framework for Rust using Plain Old Rust Structs — append-only events, replay, snapshots, outbox, service bus, and pluggable infrastructure
Documentation
name: celld (live + e2e-celld)

# Reusable workflow: referenced via `uses: ./.github/workflows/integration-celld.yaml`
# from both the PR-quality and push-to-main pipelines.
#
# Local parity:
#   make -C tests/e2e-celld test
#   make -C tests/e2e-ui up && make -C tests/e2e-ui up-celld-nats
#   WATCH=0 WATCH_WORKER=0 make -C tests/e2e-celld run
#   E2E_UI_ORIGIN=http://localhost:5180 npx --prefix tests/e2e-ui playwright test \
#     todos.user.spec.ts chat.user.spec.ts --project chromium-user
#
# Default `cargo test` (quality) still runs fixture-only celld checks and
# skips live HTTP unless CELLD_URL is set. This job sets CELLD_URL / NATS_URL.
on:
  workflow_call:

env:
  CARGO_TERM_COLOR: always
  # Zitadel owns :18080 in the browser topology.
  CELLD_HTTP_PORT: "18880"
  CELLD_URL: http://127.0.0.1:18880
  NATS_PORT: "14222"
  NATS_URL: nats://127.0.0.1:14222
  DISTRIBUTED_INTERNAL_SECRET: test-only-internal-secret-change-me-2026
  AZURE_STORAGE_USE_EMULATOR: "true"
  AZURE_STORAGE_ACCOUNT_NAME: devstoreaccount1
  AZURE_STORAGE_ACCOUNT_KEY: Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==

jobs:
  e2e-celld:
    name: e2e-celld workspace tests
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: tests/e2e-celld
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
        with:
          toolchain: stable
      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: tests/e2e-celld -> target
          shared-key: e2e-celld-workspace
      - name: Run e2e-celld workspace tests
        run: cargo test --workspace --verbose

  live:
    name: celld live (Azurite + worker + NATS)
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
        with:
          persist-credentials: false

      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
        with:
          toolchain: stable
          targets: wasm32-unknown-unknown

      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: |
            . -> target
            tests/celld/worker -> tests/celld/worker/target
          shared-key: celld-live

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
          cache-dependency-path: |
            js/package-lock.json
            tests/e2e-ui/package-lock.json
            tests/e2e-ui/ui/package-lock.json

      - name: Install host tools
        run: sudo apt-get update && sudo apt-get install -y jq openssl curl

      - name: Install esbuild
        run: npm install -g esbuild

      - name: Install wasm-pack and worker-build
        run: |
          cargo install wasm-pack --locked || cargo install wasm-pack
          cargo install worker-build --locked || cargo install worker-build

      - name: Install celld CLI
        run: |
          curl -fsSL https://celld.dev/install.sh | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: Bring up Postgres + Zitadel and bootstrap OIDC
        run: make -C tests/e2e-ui up

      - name: Bring up Azurite + celld + NATS
        run: |
          command -v celld
          command -v worker-build
          make -C tests/e2e-ui up-celld-nats

      - name: Live celld HTTP + NATS profile tests
        run: make -C tests/e2e-ui test-celld

      - name: Build e2e-celld API and UI
        run: |
          cargo build --manifest-path tests/e2e-celld/Cargo.toml \
            -p e2e-celld-runner --bin e2e-celld
          make -C tests/e2e-ui ui-install
          npm install --prefix tests/e2e-ui

      - name: Start e2e-celld API + UI
        run: |
          set -euo pipefail
          set -a
          # shellcheck disable=SC1091
          . tests/e2e-ui/e2e-ui.env
          set +a
          export BIND=127.0.0.1:8791
          export CELLD_URL=http://127.0.0.1:18880
          export NATS_URL=nats://127.0.0.1:14222
          export AUTH_URL=http://localhost:5180
          export AUTH_USE_SECURE_COOKIES=false
          export AUTH_TRUST_HOST=true

          tests/e2e-celld/target/debug/e2e-celld \
            > tests/e2e-celld/.ci-runner.log 2>&1 &
          echo $! > tests/e2e-celld/.ci-runner.pid

          ok=0
          for i in $(seq 1 120); do
            code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
              "http://127.0.0.1:8791/graphql" \
              -H 'content-type: application/json' \
              -d '{"query":"{ __typename }"}' 2>/dev/null || echo 000)
            if [ "$code" = "200" ] || [ "$code" = "401" ]; then ok=1; break; fi
            sleep 0.5
          done
          if [ "$ok" != "1" ]; then
            echo "e2e-celld API failed to become ready"
            tail -120 tests/e2e-celld/.ci-runner.log
            exit 1
          fi

          cd tests/e2e-ui/ui
          PUBLIC_E2E_PROFILE=celld-nats \
            E2E_API_ORIGIN=http://127.0.0.1:8791 \
            npm run dev -- --host localhost --port 5180 \
            > ../.ci-celld-ui.log 2>&1 &
          echo $! > ../.ci-celld-ui.pid
          cd ../../..

          ok=0
          for i in $(seq 1 60); do
            code=$(curl -s -o /dev/null -w '%{http_code}' \
              "http://localhost:5180/" 2>/dev/null || echo 000)
            if [ "$code" = "200" ] || [ "$code" = "302" ] || [ "$code" = "303" ]; then
              ok=1
              break
            fi
            sleep 0.5
          done
          if [ "$ok" != "1" ]; then
            echo "e2e-celld UI failed to become ready (last HTTP $code)"
            tail -100 tests/e2e-ui/.ci-celld-ui.log
            exit 1
          fi

      - name: Install Playwright + Chromium
        working-directory: tests/e2e-ui
        run: npx playwright install chromium --with-deps

      - name: Red-team private HTTP boundaries
        run: |
          set -euo pipefail
          assert_status() {
            expected="$1"
            shift
            actual=$(curl -sS -o /tmp/celld-red-team-response -w '%{http_code}' "$@")
            if [ "$actual" != "$expected" ]; then
              echo "expected HTTP $expected, got $actual"
              sed -n '1,40p' /tmp/celld-red-team-response
              exit 1
            fi
          }

          assert_status 401 -X POST \
            http://127.0.0.1:8791/internal/outbox/drain \
            -H 'content-type: application/json' \
            -d '{"kind":"todo","id":"red-team"}'
          assert_status 401 -X POST \
            http://127.0.0.1:8791/internal/outbox/drain \
            -H 'content-type: application/json' \
            -H 'x-distributed-internal-secret: forged-red-team-secret-000000' \
            -d '{"kind":"todo","id":"red-team"}'
          assert_status 400 -X POST \
            http://127.0.0.1:8791/internal/outbox/drain \
            -H 'content-type: application/json' \
            -H "x-distributed-internal-secret: $DISTRIBUTED_INTERNAL_SECRET" \
            -d '{"kind":"todo","id":"red-team","outbox":[]}'
          assert_status 401 -X POST \
            http://127.0.0.1:8791/zitadel.scrape.v1 \
            -H 'content-type: application/json' \
            -d '{}'
          assert_status 401 \
            http://127.0.0.1:18880/todo/red-team
          assert_status 401 -X POST \
            http://127.0.0.1:18880/todo/red-team/outbox.complete \
            -H 'content-type: application/json' \
            -d '{"ids":["forged"]}'

      - name: Todo + Chat browser lifecycle through celld
        working-directory: tests/e2e-ui
        run: >-
          npx playwright test todos.user.spec.ts chat.user.spec.ts
          --project chromium-user
        env:
          E2E_UI_ORIGIN: http://localhost:5180
          E2E_API_ORIGIN: http://127.0.0.1:8791
          CI: true

      - name: Upload celld Playwright report
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: celld-playwright-report
          path: |
            tests/e2e-ui/playwright-report
            tests/e2e-ui/test-results
          if-no-files-found: ignore
          retention-days: 7

      - name: Dump logs on failure
        if: failure()
        run: |
          echo '=== celld compose ==='
          docker compose -f tests/celld/docker-compose.yml ps -a || true
          docker compose -f tests/celld/docker-compose.yml logs --tail=200 || true
          echo '=== NATS profile compose ==='
          docker compose -f tests/e2e-ui/celld-nats-profile/docker-compose.yml ps -a || true
          docker compose -f tests/e2e-ui/celld-nats-profile/docker-compose.yml logs --tail=80 || true
          echo '=== e2e-celld API ==='
          tail -180 tests/e2e-celld/.ci-runner.log || true
          echo '=== e2e-celld UI ==='
          tail -100 tests/e2e-ui/.ci-celld-ui.log || true
          echo '=== Postgres + Zitadel ==='
          docker compose -f tests/e2e-ui/docker/docker-compose.yml ps -a || true
          docker compose -f tests/e2e-ui/docker/docker-compose.yml logs --tail=100 || true

      - name: Tear down
        if: always()
        run: |
          [ -f tests/e2e-ui/.ci-celld-ui.pid ] && \
            kill "$(cat tests/e2e-ui/.ci-celld-ui.pid)" 2>/dev/null || true
          [ -f tests/e2e-celld/.ci-runner.pid ] && \
            kill "$(cat tests/e2e-celld/.ci-runner.pid)" 2>/dev/null || true
          lsof -ti:5180 2>/dev/null | xargs -r kill -9 2>/dev/null || true
          lsof -ti:8791 2>/dev/null | xargs -r kill -9 2>/dev/null || true
          make -C tests/e2e-ui down-celld-nats || true
          make -C tests/e2e-ui down-celld || true
          docker compose -f tests/e2e-ui/docker/docker-compose.yml down -v || true