name: CI on: push: branches: [main] pull_request: branches: [main] # Least-privilege default for the auto-injected GITHUB_TOKEN. None of the CI # jobs write back to the repo — coverage uploads via CODECOV_TOKEN, everything # else is build/test/lint — so a read-only token is sufficient (OpenSSF # Scorecard: Token-Permissions). permissions: contents: read env: CARGO_TERM_COLOR: always RUSTFLAGS: "-D warnings" # Network-flake resilience: retry transient registry/DNS failures at the tool # level so a blip fetching crates.io / npm / PyPI inside any build step (cargo, # napi, maturin, wasm-pack, npm ci, pip) retries automatically instead of # failing the job and needing a manual re-run. Cargo treats "couldn't resolve # host" / connect / timeout as spurious and retries with backoff; 10 attempts # ride out a transient DNS blip on a runner. Complements the setup-action / # cache retries (which only covered toolchain download + cache restore). CARGO_NET_RETRY: "10" CARGO_NET_GIT_FETCH_WITH_CLI: "true" npm_config_fetch_retries: "5" npm_config_fetch_retry_maxtimeout: "120000" PIP_RETRIES: "5" PIP_DEFAULT_TIMEOUT: "120" jobs: rust: name: Rust ${{ matrix.os }} runs-on: ${{ matrix.os }} timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs) strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Install Rust toolchain uses: ./.github/actions/setup-rust with: components: rustfmt, clippy - name: Cache cargo uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore timeout-minutes: 6 - name: Warm cargo registry (retry transient DNS/registry flakes) shell: bash # CARGO_NET_RETRY rides out short blips, but a longer runner DNS outage # outlasts cargo's rapid in-process retries: the crates.io index fetch # the first cargo step does ("Could not resolve host: index.crates.io") # then fails the whole job. Pre-fetch the dependency graph here with real # backoff so clippy/build/test resolve from the warmed local cache. run: | for attempt in 1 2 3 4 5; do if cargo fetch; then exit 0; fi echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..." sleep $((attempt * 20)) done echo "::error::cargo fetch still failing after 5 attempts" exit 1 - name: Format check run: cargo fmt --all -- --check - name: Clippy (workspace, all targets) run: cargo clippy -p wickra-core -p wickra -p wickra-data -p wickra-wasm --all-targets -- -D warnings - name: Build run: cargo build -p wickra-core -p wickra -p wickra-data --verbose - name: Tests (default features) run: cargo test -p wickra-core -p wickra -p wickra-data --verbose - name: Tests (live-binance feature) run: cargo test -p wickra-data --features live-binance --verbose - name: Compile benches (smoke) run: cargo build -p wickra --benches --verbose - name: Compile examples # All runnable examples now live in the dedicated wickra-examples crate # (examples/rust/src/bin/*.rs) which enables the live-binance feature # on its wickra-data dep, so a single --bins build covers backtest, # live_binance, fetch_btcusdt, multi_timeframe, parallel_assets and # streaming. run: cargo build -p wickra-examples --bins # Syntax/parse smoke for the non-Rust examples. The Rust examples are built # in the `rust` job above (`cargo build -p wickra-examples --bins`); the Node, # browser-WASM and Python examples otherwise have no build gate, so a broken # edit could land unnoticed. This is a parse-only smoke — actually running the # examples needs the built native binding / wasm module / wheel, which the # binding jobs provide separately. examples-smoke: name: Examples (syntax smoke) runs-on: ubuntu-latest timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs) steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Node id: setup_node continue-on-error: true uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "22" - name: Wait before Node retry if: steps.setup_node.outcome == 'failure' shell: bash run: | echo "::warning::setup-node failed (likely CDN flake), waiting 30s before retry..." sleep 30 - name: Set up Node (retry) if: steps.setup_node.outcome == 'failure' uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "22" - name: Set up Python id: setup_python continue-on-error: true uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" - name: Wait before Python retry if: steps.setup_python.outcome == 'failure' shell: bash run: | echo "::warning::setup-python failed (likely CDN flake), waiting 30s before retry..." sleep 30 - name: Set up Python (retry) if: steps.setup_python.outcome == 'failure' uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" - name: Node examples — syntax check run: | shopt -s nullglob count=0 for f in examples/node/*.js examples/wasm/*.js; do echo "node --check $f" node --check "$f" count=$((count + 1)) done echo "checked $count Node/WASM .js files" - name: WASM demo module scripts — syntax check # The .html demos embed an ES module; extract it and parse-check so a # broken edit to the in-page strategy logic fails CI. run: | shopt -s nullglob count=0 for f in examples/wasm/*.html; do node -e 'const fs=require("fs");const h=fs.readFileSync(process.argv[1],"utf8");const m=h.match(/