use canonical version in interface metadata test #746
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Contract CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| deploy-script: | |
| name: Deployment script checks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run deploy.sh manifest and drift tests | |
| run: bash tests/deploy/test_deploy.sh | |
| fmt: | |
| name: Format check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.81.0" | |
| components: rustfmt | |
| - uses: Swatinem/rust-cache@v2 | |
| - run: cargo fmt --all -- --check | |
| clippy: | |
| name: Clippy | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.81.0" | |
| components: clippy | |
| - uses: Swatinem/rust-cache@v2 | |
| - run: cargo clippy --all-targets -- -D warnings | |
| contract-build-lints: | |
| name: Contract-only dead-code check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.81.0" | |
| targets: wasm32-unknown-unknown | |
| - uses: Swatinem/rust-cache@v2 | |
| - run: tools/check_contract_build_lints.sh | |
| test: | |
| name: Unit tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.81.0" | |
| - uses: Swatinem/rust-cache@v2 | |
| - run: cargo test --workspace | |
| production-acceptance: | |
| name: Production acceptance report | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.81.0" | |
| components: rustfmt, clippy | |
| targets: wasm32-unknown-unknown | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Run production acceptance suite | |
| run: scripts/production-acceptance.sh --output target/production-readiness-report.md | |
| - name: Upload production readiness report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: production-readiness-report | |
| path: target/production-readiness-report.md | |
| retention-days: 14 | |
| error-discriminants: | |
| name: Error discriminant stability | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check Error enum discriminants are append-only | |
| run: tools/check_error_discriminants.sh "${{ github.event.pull_request.base.sha }}" HEAD | |
| build: | |
| name: WASM build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.81.0" | |
| targets: wasm32-unknown-unknown | |
| - uses: Swatinem/rust-cache@v2 | |
| - run: cargo build --target wasm32-unknown-unknown --release -p ledgerlens-score --locked | |
| - name: Record WASM byte-size evidence | |
| run: | | |
| mkdir -p target | |
| { | |
| echo "# WASM Size Evidence" | |
| echo "" | |
| echo "Path: target/wasm32-unknown-unknown/release/ledgerlens_score.wasm" | |
| echo "Bytes: $(wc -c < target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | tr -d ' ')" | |
| echo "SHA-256: $(sha256sum target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | awk '{print $1}')" | |
| } > target/wasm-size-report.md | |
| - name: Upload wasm artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ledgerlens-score-wasm | |
| path: target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | |
| retention-days: 7 | |
| - name: Upload WASM size report artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ledgerlens-score-wasm-size-report | |
| path: target/wasm-size-report.md | |
| retention-days: 7 | |
| deploy-script-tests: | |
| name: Deploy script failure-mode tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - run: tests/deploy_script_rpc_failures.sh | |
| audit: | |
| name: Security audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| # cargo-audit 0.22 requires Rust 1.88+ and understands the CVSS 4.0 | |
| # records now present in the live RustSec advisory database. This job | |
| # only inspects Cargo.lock; contract builds remain pinned to Rust 1.81. | |
| toolchain: "1.88.0" | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install cargo-audit | |
| run: cargo +1.88.0 install cargo-audit --version 0.22.2 --locked | |
| - name: Run security audit | |
| # RUSTSEC-2026-0009 affects time's RFC parsing. time is only pulled in | |
| # through serde_with -> stellar-xdr; the contract does not invoke that | |
| # parsing path. The fixed time release requires Edition 2024 and cannot | |
| # be parsed by the Rust 1.81 toolchain required for deployable Soroban | |
| # WASM. Keep this exception narrow and remove it when Soroban permits a | |
| # newer release toolchain. | |
| run: cargo +1.88.0 audit --ignore RUSTSEC-2026-0009 | |
| # ── Supply-chain integrity checks ─────────────────────────────────────────── | |
| # | |
| # Verifies that: | |
| # 1. Every crate in Cargo.lock originates from a pinned, expected registry | |
| # (crates.io or git with an exact rev) — no path-only or unpinned | |
| # sources that could silently change between runs. | |
| # 2. The release WASM artifact is signed with a detached SHA-256 manifest | |
| # so downstream consumers can verify provenance. | |
| # 3. The toolchain pin in rust-toolchain.toml matches what CI actually uses. | |
| # | |
| # Failure modes that cause this job to fail (acceptance criteria): | |
| # • Any Cargo.lock entry whose `source` field is absent (local path crates | |
| # that have been accidentally published) or points to an unexpected registry. | |
| # • The WASM artifact SHA-256 in the signed manifest does not match the | |
| # freshly built binary. | |
| # • The active rustc version does not match rust-toolchain.toml. | |
| # | |
| # See docs/supply-chain.md for the full policy and manual audit procedure. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| supply-chain: | |
| name: Supply-chain integrity | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@fcf085fcb917fba9e37c9eaa60e2e5dc4d0cd05f # 1.81.0 | |
| with: | |
| toolchain: "1.81.0" | |
| targets: wasm32-unknown-unknown | |
| # ── Step 1: Verify the active toolchain matches rust-toolchain.toml ────── | |
| - name: Verify toolchain pin | |
| run: | | |
| EXPECTED=$(grep '^channel' rust-toolchain.toml | sed 's/.*= *"\(.*\)"/\1/') | |
| ACTUAL=$(rustc --version | awk '{print $2}') | |
| echo "rust-toolchain.toml channel : $EXPECTED" | |
| echo "Active rustc version : $ACTUAL" | |
| if [ "$EXPECTED" != "$ACTUAL" ]; then | |
| echo "ERROR: toolchain mismatch — CI used $ACTUAL but rust-toolchain.toml pins $EXPECTED" | |
| exit 1 | |
| fi | |
| echo "OK: toolchain pin verified." | |
| # ── Step 2: Verify Cargo.lock registry sources ──────────────────────────── | |
| # Each non-workspace crate in Cargo.lock must declare a `source` that is | |
| # either: | |
| # (a) "registry+https://github.com/rust-lang/crates.io-index" | |
| # (b) a git source with an explicit `rev` lock | |
| # Path-only entries (workspace members) are explicitly allowed. | |
| # Any entry from an unknown registry causes the job to fail. | |
| - name: Verify Cargo.lock registry sources | |
| run: | | |
| python3 - <<'EOF' | |
| import re, sys | |
| with open("Cargo.lock") as f: | |
| content = f.read() | |
| ALLOWED_REGISTRY = "registry+https://github.com/rust-lang/crates.io-index" | |
| # Parse [[package]] blocks | |
| blocks = re.split(r'\n\[\[package\]\]', content) | |
| violations = [] | |
| for block in blocks: | |
| name_m = re.search(r'name\s*=\s*"([^"]+)"', block) | |
| src_m = re.search(r'source\s*=\s*"([^"]+)"', block) | |
| if not name_m: | |
| continue | |
| name = name_m.group(1) | |
| if not src_m: | |
| # No source = workspace path crate; allowed. | |
| continue | |
| src = src_m.group(1) | |
| if src == ALLOWED_REGISTRY: | |
| continue | |
| if src.startswith("git+") and "rev=" in src: | |
| continue # pinned git source | |
| violations.append(f" {name}: {src}") | |
| if violations: | |
| print("ERROR: Cargo.lock contains unexpected or unpinned registry sources:") | |
| for v in violations: | |
| print(v) | |
| print("") | |
| print("Only crates.io and git sources with a pinned rev are permitted.") | |
| print("See docs/supply-chain.md for the policy.") | |
| sys.exit(1) | |
| print(f"OK: all Cargo.lock registry sources verified.") | |
| EOF | |
| # ── Step 3: Build WASM and produce a signed SHA-256 manifest ───────────── | |
| - name: Build release WASM | |
| run: cargo build --target wasm32-unknown-unknown --release -p ledgerlens-score --locked | |
| - name: Sign WASM artifact (SHA-256 manifest) | |
| run: | | |
| WASM=target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | |
| MANIFEST=target/ledgerlens_score.wasm.sha256 | |
| sha256sum "$WASM" > "$MANIFEST" | |
| echo "Manifest written:" | |
| cat "$MANIFEST" | |
| - name: Verify WASM artifact against manifest | |
| run: | | |
| MANIFEST=target/ledgerlens_score.wasm.sha256 | |
| sha256sum --check "$MANIFEST" | |
| echo "OK: WASM artifact integrity verified." | |
| - name: Upload WASM SHA-256 manifest | |
| uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.1 | |
| with: | |
| name: ledgerlens-score-wasm-sha256-manifest | |
| path: target/ledgerlens_score.wasm.sha256 | |
| retention-days: 90 | |
| # | |
| # Builds the release WASM twice in two completely independent containers and | |
| # asserts the resulting files are byte-identical by comparing their SHA-256 | |
| # digests. A mismatch means something in the toolchain or build inputs is | |
| # non-deterministic — the job fails and surfaces the differing hashes. | |
| # | |
| # Both jobs must complete before the comparison step, so they run in parallel | |
| # first and the compare step depends on both via `needs`. | |
| # | |
| # See docs/reproducible-builds.md for the local verification procedure. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| repro-build-1: | |
| name: Reproducible build (run 1) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.81.0" | |
| targets: wasm32-unknown-unknown | |
| # Deliberately no Swatinem/rust-cache — each run must be a cold, | |
| # independent build to prove there is no cache-based non-determinism. | |
| - name: Build WASM (run 1) | |
| run: cargo build --target wasm32-unknown-unknown --release -p ledgerlens-score --locked | |
| - name: Compute SHA-256 (run 1) | |
| id: hash1 | |
| run: | | |
| SHA=$(sha256sum target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | awk '{print $1}') | |
| echo "sha256=$SHA" >> "$GITHUB_OUTPUT" | |
| echo "Run 1 SHA-256: $SHA" | |
| - name: Upload WASM artifact (run 1) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: wasm-run-1 | |
| path: target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | |
| retention-days: 1 | |
| repro-build-2: | |
| name: Reproducible build (run 2) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.81.0" | |
| targets: wasm32-unknown-unknown | |
| # No cache — independent cold build, same as run 1. | |
| - name: Build WASM (run 2) | |
| run: cargo build --target wasm32-unknown-unknown --release -p ledgerlens-score --locked | |
| - name: Compute SHA-256 (run 2) | |
| id: hash2 | |
| run: | | |
| SHA=$(sha256sum target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | awk '{print $1}') | |
| echo "sha256=$SHA" >> "$GITHUB_OUTPUT" | |
| echo "Run 2 SHA-256: $SHA" | |
| - name: Upload WASM artifact (run 2) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: wasm-run-2 | |
| path: target/wasm32-unknown-unknown/release/ledgerlens_score.wasm | |
| retention-days: 1 | |
| repro-verify: | |
| name: Verify WASM reproducibility | |
| needs: [repro-build-1, repro-build-2] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download WASM artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: wasm-artifacts | |
| - name: Compare SHA-256 digests | |
| run: | | |
| SHA1=$(sha256sum wasm-artifacts/wasm-run-1/ledgerlens_score.wasm | awk '{print $1}') | |
| SHA2=$(sha256sum wasm-artifacts/wasm-run-2/ledgerlens_score.wasm | awk '{print $1}') | |
| echo "Run 1 SHA-256: $SHA1" | |
| echo "Run 2 SHA-256: $SHA2" | |
| if [ "$SHA1" != "$SHA2" ]; then | |
| echo "" | |
| echo "ERROR: WASM output is NOT reproducible." | |
| echo " Run 1: $SHA1" | |
| echo " Run 2: $SHA2" | |
| echo "" | |
| echo "The two independent builds produced different bytecode." | |
| echo "Check for non-deterministic build inputs (timestamps, paths," | |
| echo "env vars, or codegen flags). See docs/reproducible-builds.md." | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "SUCCESS: Both builds produced identical WASM output." | |
| echo " SHA-256: $SHA1" |