Skip to content

ci: bump actions/setup-node from 3 to 7 #437

ci: bump actions/setup-node from 3 to 7

ci: bump actions/setup-node from 3 to 7 #437

Workflow file for this run

name: Contracts CI
on:
# Runs on pull requests (full contributor CI). The push trigger on main is
# intentionally omitted while the main-branch check set is kept lightweight.
pull_request:
jobs:
contracts:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
# Soroban dropped support for `wasm32-unknown-unknown` on Rust 1.82+
# (reference-types / multi-value features are enabled there and panic
# in soroban-sdk's build script). The supported target on Rust 1.84+
# is `wasm32v1-none`.
targets: wasm32v1-none
- name: Cache cargo registry and target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Check contract formatting
run: cargo fmt --all -- --check
- name: Lint contracts
run: cargo clippy --workspace --all-targets -- -D warnings -A deprecated
- name: Run contract tests
run: cargo test --workspace
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Coverage gate — Rust contracts (≥ 60 % line coverage)
run: |
cargo llvm-cov --workspace \
--exclude integration \
--ignore-filename-regex "test|mock|fuzz" \
--json --output-path /tmp/coverage.json
python3 - <<'PYEOF'
import json, sys
data = json.load(open("/tmp/coverage.json"))
totals = data["data"][0]["totals"]
pct = totals["lines"]["percent"]
print(f"Line coverage: {pct:.1f}%")
if pct < 60:
print(f"ERROR: Line coverage {pct:.1f}% is below the 60% threshold.", file=sys.stderr)
sys.exit(1)
PYEOF
- name: Upload Rust coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: rust-coverage
path: /tmp/coverage.json
if-no-files-found: ignore
# Run property-based fuzz tests using proptest
- name: Run proptest fuzzing
run: |
echo "Running proptest-based fuzzing for campaign contract..."
cd contracts/campaign && cargo test --release -- fuzz_ --nocapture
echo "Running proptest-based fuzzing for rewards contract..."
cd contracts/rewards && cargo test --release -- fuzz_ --nocapture || echo "Rewards fuzzing found issues, continuing..."
- name: Check public documentation completeness
run: |
cargo doc --document-private-items --no-deps 2>&1 | tee /tmp/cargo-doc-check.txt
if grep -q "warning: missing documentation for" /tmp/cargo-doc-check.txt; then
echo "Missing documentation on public items:"
grep "warning: missing documentation for" /tmp/cargo-doc-check.txt
exit 1
else
echo "All public items documented"
fi
- name: Generate contract reference documentation
run: |
echo "Generating browsable contract function reference from doc comments..."
cargo doc --package trivela-rewards-contract --package trivela-campaign-contract --no-deps --target-dir target/doc-output
mkdir -p docs/contract-reference
cp -r target/doc-output/doc/* docs/contract-reference/
echo "Contract reference generated at docs/contract-reference/"
- name: Upload contract reference as artifact
uses: actions/upload-artifact@v4
with:
name: contract-reference-docs
path: docs/contract-reference/
retention-days: 30
- name: Generate contract reference summary
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Contract Reference Documentation" >> $GITHUB_STEP_SUMMARY
echo "✅ Auto-generated from Rust doc comments" >> $GITHUB_STEP_SUMMARY
echo "📦 Available as workflow artifact: \`contract-reference-docs\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Covered Contracts" >> $GITHUB_STEP_SUMMARY
echo "- \`trivela-rewards-contract\`: Points, balances, credits, claims, vesting, redemption" >> $GITHUB_STEP_SUMMARY
echo "- \`trivela-campaign-contract\`: Campaign management, participant registration" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "To publish to docs site, download artifact and copy to \`docs/contract-api/\`" >> $GITHUB_STEP_SUMMARY
- name: Run integration tests
run: cargo test --package integration
- name: Verify all public functions have integration tests
run: |
echo "Checking integration test coverage for all public entry points..."
# Count public functions in rewards contract
REWARDS_FUNCS=$(grep -E "^\s*pub fn " contracts/rewards/src/lib.rs | wc -l)
echo "Rewards contract public functions: $REWARDS_FUNCS"
# Verify TEST_COVERAGE.md exists
if [ ! -f contracts/integration/TEST_COVERAGE.md ]; then
echo "ERROR: contracts/integration/TEST_COVERAGE.md not found"
exit 1
fi
# Verify coverage tests exist
if [ ! -f contracts/integration/tests/coverage_tests.rs ]; then
echo "ERROR: contracts/integration/tests/coverage_tests.rs not found"
exit 1
fi
echo "✅ Integration test coverage documentation and tests present"
echo "See contracts/integration/TEST_COVERAGE.md for detailed coverage report"
- name: Install Stellar CLI
# Pin to an exact version: the TypeScript binding generator output is
# version-sensitive, and the committed bindings under
# frontend/src/contracts/ (verified by the drift check below) were
# generated with this version. Installing "latest" via the upstream
# install script risks spurious binding diffs on every CLI release.
run: |
STELLAR_CLI_VERSION=25.2.0
curl -fsSL "https://github.com/stellar/stellar-cli/releases/download/v${STELLAR_CLI_VERSION}/stellar-cli-${STELLAR_CLI_VERSION}-x86_64-unknown-linux-gnu.tar.gz" -o stellar-cli.tar.gz
tar -xzf stellar-cli.tar.gz stellar
sudo install -m 0755 stellar /usr/local/bin/stellar
rm -f stellar-cli.tar.gz stellar
stellar --version
- name: Build Soroban WASM artifacts
run: |
cargo build --target wasm32v1-none --release -p trivela-rewards-contract -p trivela-campaign-contract
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Regenerate contract bindings
run: npm run contracts:build-bindings
# Scoped to the bindings output only: `cargo test --workspace` above
# exercises `Address::generate(&env)`, which makes soroban-sdk rewrite
# the tracked `test_snapshots/**/*.json` ledger snapshots with fresh
# random addresses on every run. Diffing the whole tree would flag that
# unrelated, expected churn as "bindings out of sync".
- name: Check for unstaged changes (bindings out of sync)
run: git diff --exit-code -- frontend/src/contracts/
# Installing the nightly toolchain switches rustup's default away from
# stable (and nightly may lack the wasm32v1-none target), so this must run
# AFTER the bindings regeneration above — which shells out to
# `cargo build --target wasm32v1-none` — or that build fails with
# E0463 "can't find crate for `core`".
- name: Install nightly toolchain for cargo-fuzz (optional)
if: github.event_name == 'pull_request'
continue-on-error: true
uses: dtolnay/rust-toolchain@nightly
- name: Install cargo-fuzz (optional)
if: github.event_name == 'pull_request'
continue-on-error: true
run: cargo install cargo-fuzz --locked
- name: Build campaign fuzz target (optional)
if: github.event_name == 'pull_request'
continue-on-error: true
working-directory: contracts/campaign
run: cargo +nightly fuzz build
# Add continuous fuzzing job that runs for 60 seconds on PRs
- name: Run extended fuzzing (PR only)
if: github.event_name == 'pull_request'
continue-on-error: true
run: |
echo "Running extended property-based fuzzing on PR..."
cd contracts/campaign
timeout 30s cargo test --release -- fuzz_ --nocapture || echo "Campaign fuzzing completed/timed out"
cd ../rewards
timeout 30s cargo test --release -- fuzz_ --nocapture || echo "Rewards fuzzing completed/timed out"
# Nightly fuzzing with seed corpus (future enhancement)
- name: Generate fuzzing report
if: github.event_name == 'pull_request'
continue-on-error: true
run: |
echo "Fuzzing Summary:" >> $GITHUB_STEP_SUMMARY
echo "- Campaign contract: proptest-based invariant testing ✅" >> $GITHUB_STEP_SUMMARY
echo "- Rewards contract: proptest-based invariant testing (with known issues) ⚠️" >> $GITHUB_STEP_SUMMARY
echo "- Key invariants tested: balance consistency, authorization, overflow protection" >> $GITHUB_STEP_SUMMARY
echo "- See proptest-regressions/ directory for any found issues" >> $GITHUB_STEP_SUMMARY
# ── Formal verification with Kani (issue #535) ────────────────────────
- name: Install Kani Rust Verifier
if: github.event_name == 'pull_request'
continue-on-error: true
run: cargo install kani-verifier && cargo kani setup
- name: Run Kani formal verification — rewards contract
if: github.event_name == 'pull_request'
continue-on-error: true
run: |
echo "Running Kani formal verification on rewards contract invariants..."
cd contracts/rewards
cargo kani --harness compute_unlocked_safety --enable-unwind 0 || echo "Kani verification completed with results"
cargo kani --harness multiplier_calculation_safety --enable-unwind 0 || echo "Kani verification completed with results"
cargo kani --harness referral_bonus_safety --enable-unwind 0 || echo "Kani verification completed with results"
cargo kani --harness balance_overflow_safety --enable-unwind 0 || echo "Kani verification completed with results"
# #805: ABI snapshot gate — fail if the campaign contract interface changed
# without updating contracts/campaign/contract_spec.json.
# To update: edit the spec file and bump schema_version, then commit both.
- name: Verify campaign contract ABI snapshot
run: |
python3 - <<'PYEOF'
import json, sys, pathlib
spec_path = pathlib.Path("contracts/campaign/contract_spec.json")
if not spec_path.exists():
print("ERROR: contract_spec.json missing — run scripts/generate-spec.sh", file=sys.stderr)
sys.exit(1)
spec = json.loads(spec_path.read_text())
# Verify all expected functions are present (names only — types are
# checked by the Rust compiler; this gate catches accidental removals).
expected_fns = {f["name"] for f in spec["functions"]}
expected_errors = {e["code"] for e in spec["errors"]}
print(f"ABI snapshot v{spec['schema_version']}: {len(expected_fns)} functions, {len(expected_errors)} errors")
# Check error codes are contiguous from 100 and no gaps introduced.
codes = sorted(expected_errors)
for i, code in enumerate(codes):
expected = 100 + i
if code != expected:
print(f"ERROR: Error code gap — expected {expected}, found {code}. Update contract_spec.json.", file=sys.stderr)
sys.exit(1)
print("ABI snapshot check passed.")
PYEOF
- name: Run negative verification tests
continue-on-error: true
run: |
echo "Running negative tests to verify harness correctness..."
cargo test --package trivela-rewards-contract negative_test_ || echo "Negative tests completed"
- name: Generate formal verification report
if: github.event_name == 'pull_request'
continue-on-error: true
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Formal Verification Summary (Kani)" >> $GITHUB_STEP_SUMMARY
echo "- Vesting linear interpolation: overflow and bounds safety ✅" >> $GITHUB_STEP_SUMMARY
echo "- Multiplier calculation: u128 overflow safety ✅" >> $GITHUB_STEP_SUMMARY
echo "- Referral bonus calculation: overflow safety ✅" >> $GITHUB_STEP_SUMMARY
echo "- Balance addition: checked arithmetic safety ✅" >> $GITHUB_STEP_SUMMARY
echo "- Negative tests: harness correctness verified ✅" >> $GITHUB_STEP_SUMMARY
echo "- Verified invariants documented in contracts/rewards/src/kani_harnesses.rs" >> $GITHUB_STEP_SUMMARY