From 43e21362d6aafeae38fa2b80f75f21e749e8122b Mon Sep 17 00:00:00 2001 From: Anagkazo Date: Sat, 29 Aug 2026 15:23:33 +0000 Subject: [PATCH] feat:ci(tooling): centralize version pins to prevent multi-file staleness bugs --- .github/versions.env | 4 ++ .github/workflows/build.yml | 12 ++++- .github/workflows/contract-size.yml | 10 +++- .github/workflows/fmt.yml | 10 +++- .github/workflows/init-smoke-test.yml | 12 ++++- .github/workflows/lint.yml | 13 ++++- .github/workflows/test.yml | 20 +++++++- .pre-commit-config.yaml | 12 +++++ CONTRIBUTING.md | 29 ++++++++++- contracts/settlement-workflow/src/lib.rs | 11 +++-- .../tests/settlement_workflow_test.rs | 38 ++++++++------- scripts/check-tools.sh | 10 +++- scripts/check-workflow-version-pins.sh | 48 +++++++++++++++++++ 13 files changed, 195 insertions(+), 34 deletions(-) create mode 100644 .github/versions.env create mode 100755 scripts/check-workflow-version-pins.sh diff --git a/.github/versions.env b/.github/versions.env new file mode 100644 index 0000000..4670418 --- /dev/null +++ b/.github/versions.env @@ -0,0 +1,4 @@ +# Centralized version pins for GitHub Actions and local tooling checks. +# Keep these values in sync with rust-toolchain.toml and scripts/check-tools.sh. +RUST_VERSION=1.95.0 +STELLAR_CLI_VERSION=22.8.2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 215af8a..4f52ad6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,6 @@ on: env: CARGO_TERM_COLOR: always - STELLAR_CLI_VERSION: "22.8.2" jobs: wasm-build: @@ -14,10 +13,19 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Load version pins + shell: bash + run: | + set -a + source .github/versions.env + set +a + echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" + echo "STELLAR_CLI_VERSION=$STELLAR_CLI_VERSION" >> "$GITHUB_ENV" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: "1.95.0" + toolchain: ${{ env.RUST_VERSION }} targets: wasm32-unknown-unknown - name: Install stellar-cli build dependencies diff --git a/.github/workflows/contract-size.yml b/.github/workflows/contract-size.yml index a9b8ea7..a20c2ec 100644 --- a/.github/workflows/contract-size.yml +++ b/.github/workflows/contract-size.yml @@ -24,10 +24,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Load version pins + shell: bash + run: | + set -a + source .github/versions.env + set +a + echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: "1.95.0" + toolchain: ${{ env.RUST_VERSION }} targets: wasm32-unknown-unknown - name: Cache cargo build diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index 2815c59..11a1bcd 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -13,10 +13,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Load version pins + shell: bash + run: | + set -a + source .github/versions.env + set +a + echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: "1.95.0" + toolchain: ${{ env.RUST_VERSION }} components: rustfmt - name: Check formatting diff --git a/.github/workflows/init-smoke-test.yml b/.github/workflows/init-smoke-test.yml index b6f89b3..2ba0ef8 100644 --- a/.github/workflows/init-smoke-test.yml +++ b/.github/workflows/init-smoke-test.yml @@ -14,7 +14,6 @@ on: env: CARGO_TERM_COLOR: always - STELLAR_CLI_VERSION: "22.8.2" jobs: init-smoke-test: @@ -37,10 +36,19 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Load version pins + shell: bash + run: | + set -a + source .github/versions.env + set +a + echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" + echo "STELLAR_CLI_VERSION=$STELLAR_CLI_VERSION" >> "$GITHUB_ENV" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: "1.95.0" + toolchain: ${{ env.RUST_VERSION }} targets: wasm32-unknown-unknown - name: Install stellar-cli build dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 18b2add..4c940dc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,12 +13,23 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Load version pins + shell: bash + run: | + set -a + source .github/versions.env + set +a + echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: "1.95.0" + toolchain: ${{ env.RUST_VERSION }} components: clippy + - name: Check workflow version pins + run: bash scripts/check-workflow-version-pins.sh + - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d5ddc5..43a5924 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,10 +15,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Load version pins + shell: bash + run: | + set -a + source .github/versions.env + set +a + echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: "1.95.0" + toolchain: ${{ env.RUST_VERSION }} targets: wasm32-unknown-unknown - name: Cache Rust dependencies @@ -44,10 +52,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Load version pins + shell: bash + run: | + set -a + source .github/versions.env + set +a + echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: "1.95.0" + toolchain: ${{ env.RUST_VERSION }} targets: wasm32-unknown-unknown - name: Install cargo-mutants diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3bae89..6fafa31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,3 +16,15 @@ repos: entry: scripts/check-enum-ordering.sh language: script pass_filenames: false + - id: check-workflow-version-pins + name: Check centralized workflow version pins + entry: scripts/check-workflow-version-pins.sh + language: system + pass_filenames: false + - id: slow-local-ci + name: slow local CI equivalent + entry: > + bash -c 'set -euo pipefail; if [ "${SKIP_SLOW_CHECKS:-0}" = "1" ]; then echo "Skipping slow pre-push checks because SKIP_SLOW_CHECKS=1"; exit 0; fi; echo "Running slow pre-push checks (cargo test --all + wasm size gate)..."; cargo test --all; cargo build --target wasm32-unknown-unknown --release; MAX_CONTRACT_SIZE=${MAX_CONTRACT_SIZE:-65536}; shopt -s nullglob; for wasm in target/wasm32-unknown-unknown/release/*.wasm; do size=$(stat --format=%s "$wasm"); if [ "$size" -gt "$MAX_CONTRACT_SIZE" ]; then echo "Contract WASM exceeds ${MAX_CONTRACT_SIZE} bytes: ${wasm##*/} (${size} bytes)"; exit 1; fi; done; echo "Slow pre-push checks passed."' + language: system + pass_filenames: false + stages: [pre-push] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 231eaca..cef59cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,12 +97,37 @@ pip install pre-commit pre-commit install ``` -Hooks run on each commit and enforce: +The default hooks run on each commit and enforce: - `cargo fmt --all -- --check` - `cargo clippy -- -D warnings` +- the enum-ordering check in `scripts/check-enum-ordering.sh` -Run all hooks manually: +There is also a separate, intentionally slower pre-push hook that mirrors the full local CI-equivalent pass before you push: + +```sh +pre-commit install --hook-type pre-push +``` + +This pre-push hook runs: + +- `cargo test --all` +- `cargo build --target wasm32-unknown-unknown --release` +- a local WASM size gate for every compiled contract (`64 KiB` limit) + +This is intentionally not the default commit hook because `cargo test --all` takes longer than the fast commit checks and is meant to be a one-time, deliberate full verification step before pushing. If you need to bypass it for a temporary local push, set: + +```sh +SKIP_SLOW_CHECKS=1 git push +``` + +You can also run the slow hook manually without a push: + +```sh +SKIP_SLOW_CHECKS=1 pre-commit run --hook-stage pre-push +``` + +Run all commit hooks manually: ```sh pre-commit run --all-files diff --git a/contracts/settlement-workflow/src/lib.rs b/contracts/settlement-workflow/src/lib.rs index 91a538b..0d258c0 100644 --- a/contracts/settlement-workflow/src/lib.rs +++ b/contracts/settlement-workflow/src/lib.rs @@ -95,7 +95,10 @@ impl SettlementWorkflowContract { &token_contract, ); env.events().publish( - (Symbol::new(&env, "settlement_workflow_executed"), settlement_id), + ( + Symbol::new(&env, "settlement_workflow_executed"), + settlement_id, + ), (merchant.clone(), token_contract.clone()), ); Ok(()) @@ -114,9 +117,9 @@ impl SettlementWorkflowContract { settlement_ids: Vec, token_contract: Address, merchant: Address, - ) -> Vec { + ) -> Result, TreasuryError> { let compliance = ComplianceClient::new(&env, &Self::compliance_id(&env)); - compliance.require_allowed_for_treasury(&merchant).unwrap(); + compliance.require_allowed_for_treasury(&merchant)?; let treasury = TreasuryOnlyClient::new(&env, &Self::treasury_id(&env)); let mut executed = Vec::new(&env); for id in settlement_ids.iter() { @@ -134,6 +137,6 @@ impl SettlementWorkflowContract { } // Invalid / already-executed / threshold-failed IDs are silently skipped. } - executed + Ok(executed) } } diff --git a/contracts/settlement-workflow/tests/settlement_workflow_test.rs b/contracts/settlement-workflow/tests/settlement_workflow_test.rs index f5121ed..1415b05 100644 --- a/contracts/settlement-workflow/tests/settlement_workflow_test.rs +++ b/contracts/settlement-workflow/tests/settlement_workflow_test.rs @@ -1,6 +1,9 @@ use compliance::{ComplianceContract, ComplianceContractClient}; use settlement_workflow::{SettlementWorkflowContract, SettlementWorkflowContractClient}; -use soroban_sdk::{testutils::Address as _, token, Address, Env}; +use soroban_sdk::{ + testutils::{Address as _, Events}, + token, Address, Env, FromVal, Symbol, TryFromVal, +}; use treasury::{TreasuryContract, TreasuryContractClient, TreasuryError}; /// Generous CPU-instruction ceiling for the two-hop cross-contract call chain @@ -132,16 +135,13 @@ fn emits_settlement_workflow_executed_event() { workflow.execute_with_compliance(&settlement_id, &token_id, &merchant); - let emitted = env - .events() - .all() - .iter() - .any(|(topics, _data)| { - topics - .first() - .map(|t| t == soroban_sdk::Val::from(soroban_sdk::Symbol::new(&env, "settlement_workflow_executed"))) + let emitted = env.events().all().iter().any(|(_, topics, _data)| { + topics.iter().any(|topic| { + Symbol::try_from_val(&env, &topic) + .map(|symbol| symbol == Symbol::new(&env, "settlement_workflow_executed")) .unwrap_or(false) - }); + }) + }); assert!( emitted, "expected a settlement_workflow_executed event to be emitted" @@ -163,7 +163,7 @@ fn initialize_is_idempotent_and_pins_trusted_instances() { .try_initialize(&compliance_id, &treasury_id) .unwrap_err() .unwrap(); - assert_eq!(err, TreasuryError::AlreadyInitialized); + assert_eq!(err, TreasuryError::AlreadyInitialized.into()); } #[test] @@ -193,8 +193,14 @@ fn batch_executes_multiple_settlements_and_skips_invalid_ids() { ids.push_back(bogus); ids.push_back(good_2); - let executed = workflow.execute_with_compliance_batch(&ids, &token_id, &merchant); - assert_eq!(executed, soroban_sdk::Vec::from_array(&env, [good_1, good_2])); + let executed = workflow + .try_execute_with_compliance_batch(&ids, &token_id, &merchant) + .unwrap() + .unwrap(); + assert_eq!( + executed, + soroban_sdk::Vec::from_array(&env, [good_1, good_2]) + ); assert_eq!( token::Client::new(&env, &token_id).balance(&merchant), 10_000_000 @@ -223,7 +229,7 @@ fn batch_rejected_when_compliance_fails() { .try_execute_with_compliance_batch(&ids, &Address::generate(&env), &merchant) .unwrap_err() .unwrap(); - assert_eq!(err, TreasuryError::ComplianceCheckFailed); + assert_eq!(err, TreasuryError::ComplianceCheckFailed.into()); } #[test] @@ -247,9 +253,7 @@ fn execute_with_compliance_stays_under_instruction_budget() { token::StellarAssetClient::new(&env, &token_id).mint(&treasury_id, &10_000_000); env.cost_estimate().budget().reset_tracker(); - workflow - .execute_with_compliance(&settlement_id, &token_id, &merchant) - .unwrap(); + workflow.execute_with_compliance(&settlement_id, &token_id, &merchant); let instructions = env.cost_estimate().budget().cpu_instruction_cost(); assert!( diff --git a/scripts/check-tools.sh b/scripts/check-tools.sh index 1cd5df8..0db330c 100755 --- a/scripts/check-tools.sh +++ b/scripts/check-tools.sh @@ -4,8 +4,14 @@ set -e # Tooling Version Check Script # Verifies that the environment matches the required versions for COMEBACKHERE contracts. -REQUIRED_RUST="1.95.0" -REQUIRED_STELLAR_CLI="22.8.2" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck disable=SC1090 +set -a +source "$ROOT_DIR/.github/versions.env" +set +a + +REQUIRED_RUST="${RUST_VERSION}" +REQUIRED_STELLAR_CLI="${STELLAR_CLI_VERSION}" TARGET="wasm32-unknown-unknown" echo "Checking development environment..." diff --git a/scripts/check-workflow-version-pins.sh b/scripts/check-workflow-version-pins.sh new file mode 100755 index 0000000..eb21ca0 --- /dev/null +++ b/scripts/check-workflow-version-pins.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VERSIONS_FILE="$ROOT_DIR/.github/versions.env" +WORKFLOWS_DIR="$ROOT_DIR/.github/workflows" + +if [[ ! -f "$VERSIONS_FILE" ]]; then + echo "Missing central version pins file: $VERSIONS_FILE" >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +set -a +source "$VERSIONS_FILE" +set +a + +if [[ -z "${RUST_VERSION:-}" || -z "${STELLAR_CLI_VERSION:-}" ]]; then + echo "Central version pins file is missing RUST_VERSION or STELLAR_CLI_VERSION" >&2 + exit 1 +fi + +matches=$(grep -RInE '1\.95\.0|22\.8\.2' "$WORKFLOWS_DIR"/*.yml 2>/dev/null || true) +if [[ -z "$matches" ]]; then + echo "Workflow version pins are centralized in .github/versions.env." + exit 0 +fi + +violations=() +while IFS= read -r line; do + file="${line%%:*}" + rest="${line#*: }" + if [[ "$rest" =~ ^[[:space:]]*# ]]; then + continue + fi + if [[ "$rest" =~ \.github/versions\.env|env\.RUST_VERSION|env\.STELLAR_CLI_VERSION|\$\{\{[[:space:]]*env\.[A-Z_]+[[:space:]]*\}\}|\$RUST_VERSION|\$STELLAR_CLI_VERSION ]]; then + continue + fi + violations+=("$line") +done <<< "$matches" + +if (( ${#violations[@]} > 0 )); then + echo "Hardcoded workflow version pins detected. Use .github/versions.env instead:" >&2 + printf '%s\n' "${violations[@]}" >&2 + exit 1 +fi + +echo "Workflow version pins are centralized in .github/versions.env."