docs: make Lit Action examples standalone (#420) #1572
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Phala / dstack simulator validation | |
| # | |
| # Builds the dstack guest-agent from source, runs it as a local simulator, | |
| # and exercises the dstack::v1::dstack integration tests against it. | |
| # Then runs the dstack-verifier against a live simulator quote to confirm | |
| # that quote_verified=true — validating the get_quote() → dstack-verifier pipeline. | |
| # | |
| # Note on is_valid: the simulator's attestation.bin contains a synthetic OS image | |
| # hash that is not published on download.dstack.org, so os_image_hash_verified and | |
| # is_valid will always be false with the simulator. This is expected: the simulator | |
| # is a developer tool for testing the attestation API, not a real TDX environment. | |
| # We assert only quote_verified=true, which proves the pipeline is wired correctly. | |
| # | |
| # This job gates Phase 1 Workflow A: attestation mechanism (FR-2.5). | |
| # It runs on every push and pull_request so simulator regressions are caught early. | |
| # | |
| # Isolation: each run gets a unique mktemp dir for the simulator's sockets so | |
| # concurrent runners on the same self-hosted host do not conflict. | |
| name: Phala Simulator Validation | |
| on: | |
| push: | |
| branches: [main, next] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: phala-simulator-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| simulator: | |
| if: > | |
| github.event_name == 'push' || | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| name: dstack simulator attestation | |
| runs-on: self-hosted | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # ── 1. Build the dstack simulator (cached by dstack HEAD SHA) ─────────── | |
| # The verifier is pulled as the dstacktee/dstack-verifier Docker image | |
| # (same as verify-attestation.yml) — no source build or cache needed. | |
| - name: Get dstack HEAD SHA | |
| id: dstack-sha | |
| run: | | |
| SHA=$(git ls-remote https://github.com/Dstack-TEE/dstack.git HEAD | head -1 | cut -f1 | tr -d '[:space:]') | |
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | |
| - name: Restore simulator cache | |
| id: sim-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ github.workspace }}/dstack-cache/simulator | |
| key: dstack-simulator-${{ steps.dstack-sha.outputs.sha }}-${{ runner.os }} | |
| - name: Fix dstack cache permissions | |
| if: steps.sim-cache.outputs.cache-hit == 'true' | |
| run: | | |
| CACHE_DIR="${{ github.workspace }}/dstack-cache" | |
| [ -d "$CACHE_DIR" ] && chmod -R u+rwX "$CACHE_DIR" || true | |
| - name: Clone dstack repository | |
| if: steps.sim-cache.outputs.cache-hit != 'true' | |
| run: | | |
| DSTACK_BUILD=$(mktemp -d) | |
| echo "DSTACK_BUILD=$DSTACK_BUILD" >> "$GITHUB_ENV" | |
| git clone --depth 1 https://github.com/Dstack-TEE/dstack.git "$DSTACK_BUILD" | |
| # Use stable Rust for the dstack build (it may require features newer than 1.91). | |
| - name: Install Rust (stable, for dstack) | |
| if: steps.sim-cache.outputs.cache-hit != 'true' | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Build dstack simulator | |
| if: steps.sim-cache.outputs.cache-hit != 'true' | |
| run: cd "$DSTACK_BUILD/sdk/simulator" && bash build.sh | |
| - name: Populate simulator cache | |
| if: steps.sim-cache.outputs.cache-hit != 'true' | |
| run: | | |
| mkdir -p "${{ github.workspace }}/dstack-cache/simulator" | |
| cp "$DSTACK_BUILD/sdk/simulator/dstack-simulator" \ | |
| "$DSTACK_BUILD/sdk/simulator/appkeys.json" \ | |
| "$DSTACK_BUILD/sdk/simulator/app-compose.json" \ | |
| "$DSTACK_BUILD/sdk/simulator/sys-config.json" \ | |
| "$DSTACK_BUILD/sdk/simulator/attestation.bin" \ | |
| "$DSTACK_BUILD/sdk/simulator/dstack.toml" \ | |
| "${{ github.workspace }}/dstack-cache/simulator/" | |
| - name: Pull dstack-verifier image | |
| run: docker pull --platform linux/amd64 dstacktee/dstack-verifier:latest | |
| - name: Remove dstack build directory | |
| if: always() && env.DSTACK_BUILD != '' | |
| run: rm -rf "$DSTACK_BUILD" | |
| # Switch to the pinned toolchain the project declares in rust-toolchain.toml. | |
| - name: Install Rust 1.91 (lit-api-server toolchain) | |
| uses: dtolnay/rust-toolchain@1.91 | |
| - name: Install protobuf-compiler | |
| run: | | |
| for i in 1 2 3 4 5; do | |
| sudo apt-get update -qq && sudo apt-get install -y protobuf-compiler && break | |
| echo "apt lock held, waiting 10s (attempt $i/5)…" | |
| sleep 10 | |
| done | |
| # ── 2. Unique-socket simulator run, unit tests ────────────────────────── | |
| # Each CI run creates a fresh mktemp dir for the simulator's sockets. | |
| # This allows multiple runners on the same host to operate in parallel | |
| # without competing for /tmp/dstack/sdk/simulator/dstack.sock. | |
| - name: Run phala simulator tests | |
| run: | | |
| set -eu | |
| SIM_SRC="${{ github.workspace }}/dstack-cache/simulator" | |
| # Create an isolated temp dir for this run's sockets and data files. | |
| SIM_TMP=$(mktemp -d /tmp/dstack-sim-XXXXXX) | |
| SIM_SOCK="$SIM_TMP/dstack.sock" | |
| # Copy data files that the simulator reads via relative paths. | |
| cp "$SIM_SRC/appkeys.json" "$SIM_SRC/app-compose.json" \ | |
| "$SIM_SRC/sys-config.json" "$SIM_SRC/attestation.bin" \ | |
| "$SIM_SRC/dstack.toml" "$SIM_TMP/" | |
| # Start simulator; sockets are created relative to CWD ($SIM_TMP). | |
| # exec replaces shell with simulator so SIM_PID refers to actual process (avoids zombies). | |
| echo "Starting dstack simulator in $SIM_TMP..." | |
| sh -c "cd '$SIM_TMP' && exec '$SIM_SRC/dstack-simulator'" >> "$SIM_TMP/dstack-simulator.log" 2>&1 & | |
| SIM_PID=$! | |
| for i in $(seq 1 15); do | |
| [ -S "$SIM_SOCK" ] && echo "Socket ready." && break | |
| echo " waiting for dstack.sock ($i/15)..." | |
| sleep 1 | |
| done | |
| [ -S "$SIM_SOCK" ] || { | |
| echo "ERROR: dstack.sock never appeared" | |
| cat "$SIM_TMP/dstack-simulator.log" | |
| kill "$SIM_PID" 2>/dev/null || true | |
| rm -rf "$SIM_TMP" | |
| exit 1 | |
| } | |
| # Run the Rust unit tests against the simulator socket. | |
| (cd lit-api-server && DSTACK_SOCKET="$SIM_SOCK" cargo test --locked --features dstack -- dstack::v1::dstack::tests --nocapture) | |
| STATUS=$? | |
| # Teardown. | |
| kill "$SIM_PID" 2>/dev/null || true | |
| rm -rf "$SIM_TMP" | |
| exit "$STATUS" | |
| - name: Build lit-api-server | |
| run: cargo build --locked --manifest-path=lit-api-server/Cargo.toml --features dstack --bin lit-api-server | |
| # ── 3. dstack-verifier end-to-end attestation pipeline ───────────────── | |
| # Start simulator, start lit-api-server (attestation-only; fetches from simulator), | |
| # get attestation from lit-api-server's /attestation endpoint, run dstack-verifier. | |
| # Assert quote_verified=true; is_valid will be false (simulator uses a synthetic | |
| # OS image hash not published on download.dstack.org) — that is expected. | |
| - name: Verify attestation pipeline with dstack-verifier | |
| run: | | |
| set -eu | |
| SIM_SRC="${{ github.workspace }}/dstack-cache/simulator" | |
| API_BIN="$(pwd)/lit-api-server/target/debug/lit-api-server" | |
| SIM_TMP=$(mktemp -d /tmp/dstack-sim-XXXXXX) | |
| SIM_SOCK="$SIM_TMP/dstack.sock" | |
| cp "$SIM_SRC/appkeys.json" "$SIM_SRC/app-compose.json" \ | |
| "$SIM_SRC/sys-config.json" "$SIM_SRC/attestation.bin" \ | |
| "$SIM_SRC/dstack.toml" "$SIM_TMP/" | |
| # Start simulator (exec avoids zombie processes). | |
| sh -c "cd '$SIM_TMP' && exec '$SIM_SRC/dstack-simulator'" >> "$SIM_TMP/dstack-simulator.log" 2>&1 & | |
| SIM_PID=$! | |
| for i in $(seq 1 15); do | |
| [ -S "$SIM_SOCK" ] && break | |
| echo " waiting for simulator ($i/15)..." | |
| sleep 1 | |
| done | |
| [ -S "$SIM_SOCK" ] || { | |
| echo "ERROR: dstack.sock never appeared" | |
| cat "$SIM_TMP/dstack-simulator.log" | |
| kill "$SIM_PID" 2>/dev/null || true | |
| rm -rf "$SIM_TMP" | |
| exit 1 | |
| } | |
| # Copy branch-appropriate config (NodeConfig.toml is gitignored); main uses main.toml, others use next.toml. | |
| if [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| cp lit-api-server/NodeConfig.main.toml lit-api-server/NodeConfig.toml | |
| else | |
| cp lit-api-server/NodeConfig.next.toml lit-api-server/NodeConfig.toml | |
| fi | |
| # Pick a random free TCP port per run so concurrent jobs on the | |
| # same self-hosted host don't collide on the default Rocket port | |
| # (8000). ROCKET_PORT overrides lit-api-server/Rocket.toml. | |
| API_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1]); s.close()') | |
| echo "lit-api-server will bind to port $API_PORT" | |
| (cd lit-api-server && DSTACK_SOCKET="$SIM_SOCK" ROCKET_PORT="$API_PORT" "$API_BIN") >> "$SIM_TMP/lit-api-server.log" 2>&1 & | |
| API_PID=$! | |
| if ! kill -0 "$API_PID" 2>/dev/null; then | |
| echo "ERROR: lit-api-server failed to start" | |
| cat "$SIM_TMP/lit-api-server.log" | |
| kill "$SIM_PID" 2>/dev/null || true | |
| rm -rf "$SIM_TMP" | |
| exit 1 | |
| fi | |
| # Wait for /attestation to respond (up to 5 min). | |
| for i in $(seq 1 300); do | |
| if curl -sf "http://localhost:$API_PORT/attestation" >/dev/null 2>&1; then | |
| echo "lit-api-server /attestation ready." | |
| break | |
| fi | |
| echo " waiting for lit-api-server ($i/300)..." | |
| sleep 1 | |
| done | |
| if ! curl -sf "http://localhost:$API_PORT/attestation" >/dev/null 2>&1; then | |
| echo "ERROR: lit-api-server /attestation never responded" | |
| cat "$SIM_TMP/lit-api-server.log" | |
| kill "$SIM_PID" "$API_PID" 2>/dev/null || true | |
| rm -rf "$SIM_TMP" | |
| exit 1 | |
| fi | |
| # Get attestation from lit-api-server (which got it from the simulator). | |
| curl -sf "http://localhost:$API_PORT/attestation" > "$SIM_TMP/attestation.json" | |
| # Teardown simulator and lit-api-server. | |
| kill "$SIM_PID" "$API_PID" 2>/dev/null || true | |
| # Fix empty digests and normalise format (same script used by verify-attestation.yml). | |
| # Then inject spec_version=1 into vm_config if missing: the simulator omits it but | |
| # the Docker verifier's serde parser requires it to be present in VmConfig. | |
| # (The verifier will still report os_image_hash_verified=false and is_valid=false | |
| # because the simulator's synthetic OS hash is not published — that is expected.) | |
| python3 scripts/fix-attestation-event-log.py "$SIM_TMP/attestation.json" \ | |
| | python3 scripts/inject-spec-version.py \ | |
| > "$SIM_TMP/verify-request.json" | |
| # Run verifier via Docker (same image as verify-attestation.yml). | |
| # Exits 1 when is_valid=false (expected for simulator) — ignore and check quote_verified. | |
| docker run --rm \ | |
| -v "$SIM_TMP:/verify" \ | |
| -w /verify \ | |
| --platform linux/amd64 \ | |
| dstacktee/dstack-verifier:latest \ | |
| --verify /verify/verify-request.json || true | |
| # Assertion: quote_verified must be true. | |
| RESULT_FILE="$SIM_TMP/verify-request.json.verification.json" | |
| QUOTE_OK=$(python3 -c \ | |
| 'import sys,json; v=json.load(open(sys.argv[1]))["details"]["quote_verified"]; print("true" if v else "false")' \ | |
| "$RESULT_FILE") | |
| rm -rf "$SIM_TMP" | |
| [ "$QUOTE_OK" = "true" ] || { | |
| echo "ERROR: quote_verified=false — attestation pipeline is broken" | |
| exit 1 | |
| } | |
| echo "Attestation pipeline verified: quote_verified=true." |