167f7b3ffe
Adds a Java binding (`bindings/java`) over the C ABI hub — the fourth language stecker after C#, Go and R, reaching the hub through the Java Foreign Function & Memory API (Panama, `java.lang.foreign`, final in Java 22) rather than JNI or jextract. ## What's here - **`bindings/java`** — a Maven module (`org.wickra:wickra`) exposing all 514 indicators as idiomatic `AutoCloseable` classes. The downcall handles (`internal/NativeMethods.java`), the per-indicator wrappers and the output records are generated from `bindings/c/include/wickra.h` (same eight-archetype taxonomy as the C#/Go/R generators: scalar/batch, multi-output, bars, profile, values-profile, array-input). The opaque handle is a `MemorySegment` freed by a registered `java.lang.ref.Cleaner` action; multi-output returns a `record` (`null` at warmup), bars a `record[]`, profiles a record with a trailing `double[]`. The hand-written `WickraNative` resolves the native library (a bundled per-platform copy, or a `target/release` fallback for local development) and validates it against a sentinel symbol. repr(C) struct offsets are computed in the generator so the FFM reads land on the exact bytes. - **`examples/java`** — the full example suite mirroring C/C#/Go/R: streaming, backtest, multi_timeframe, parallel_assets (parallel streams), three strategies, and `FetchBtcusdt`/`LiveBinance`. - **CI** — a `java` job builds the C ABI library, sets up JDK 22 (Temurin, with a CDN-flake retry), runs the archetype test suite and the seven offline examples on Linux, macOS and Windows. - **Release** — a gated `java-publish` job (skipped until the `JAVA_PUBLISH_ENABLED` repository variable is set) stages the native libraries from the `wickra-c-<triple>.tar.gz` assets into the binding's resources and deploys to Maven Central with GPG signing. Independent of the GitHub-release job, like the NuGet job. - **Docs** — Java added to the README languages table, project layout, building/testing and comparison table, CONTRIBUTING, ARCHITECTURE, the examples index, the issue/PR templates, the About-description template, and the other binding READMEs. ## Requirements Java 22+ (the FFM API is final since Java 22). The binding requires `--enable-native-access=ALL-UNNAMED` at runtime; the test and example runners pass it automatically. No Rust crate or `Cargo.toml` change — the Java binding is standalone and additive. The generated `*.java` are committed (like the node `index.js`/`index.d.ts`); the generator stays private.
974 lines
38 KiB
YAML
974 lines
38 KiB
YAML
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 }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
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
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
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: "20"
|
|
|
|
- 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: "20"
|
|
|
|
- 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(/<script type="module">([\s\S]*?)<\/script>/);if(!m){console.error("no <script type=module> in "+process.argv[1]);process.exit(1);}fs.writeFileSync("module-check.mjs",m[1]);' "$f"
|
|
echo "node --check (module of) $f"
|
|
node --check module-check.mjs
|
|
count=$((count + 1))
|
|
done
|
|
rm -f module-check.mjs
|
|
echo "checked $count WASM .html module scripts"
|
|
|
|
- name: Python examples — byte-compile
|
|
run: |
|
|
shopt -s nullglob
|
|
count=0
|
|
for f in examples/python/*.py; do
|
|
echo "py_compile $f"
|
|
python -m py_compile "$f"
|
|
count=$((count + 1))
|
|
done
|
|
echo "compiled $count Python files"
|
|
|
|
# Clippy for the Python and Node bindings. These are kept out of the main
|
|
# `rust` job because PyO3 / napi build scripts need a Python interpreter and
|
|
# a Node toolchain on PATH, which the 3-OS matrix job does not provision.
|
|
# Ubuntu-only is sufficient: the lints are platform-independent.
|
|
clippy-bindings:
|
|
name: Clippy bindings
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
with:
|
|
components: clippy
|
|
|
|
- name: Set up Python
|
|
id: setup_python
|
|
continue-on-error: true
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Set up Node
|
|
id: setup_node
|
|
continue-on-error: true
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: "20"
|
|
|
|
- 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: "20"
|
|
|
|
- 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
|
|
# See the rust job: pre-fetch with backoff so the clippy index update
|
|
# can't fail the job on a transient "Could not resolve host" DNS blip.
|
|
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: Clippy (bindings, all targets)
|
|
run: cargo clippy -p wickra-node -p wickra-python --all-targets -- -D warnings
|
|
|
|
# Verify the crates still build and test on their declared minimum supported
|
|
# Rust version. The workspace pins rust-version = "1.86" — that floor is
|
|
# set by criterion 0.8.2 (the bench dev-dep), which itself rolled past the
|
|
# clap_lex 1.1.0 / edition2024 / Rust 1.85 floor and now needs 1.86; the
|
|
# earlier rayon-core 1.13.0 (1.80) and clap_lex (1.85) requirements are
|
|
# subsumed. bindings/node pins rust-version = "1.88" because napi-build
|
|
# 2.3.2 requires it (and that subsumes the older 1.77 floor needed for
|
|
# `cargo::` directives). Without this job an accidental use of a newer
|
|
# API would only surface for downstream users.
|
|
msrv:
|
|
name: ${{ matrix.name }}
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- name: MSRV workspace (Rust 1.86)
|
|
toolchain: "1.86"
|
|
packages: "-p wickra-core -p wickra -p wickra-data"
|
|
- name: MSRV node binding (Rust 1.88)
|
|
toolchain: "1.88"
|
|
packages: "-p wickra-node"
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust ${{ matrix.toolchain }}
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
with:
|
|
toolchain: ${{ matrix.toolchain }}
|
|
|
|
- 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
|
|
# See the rust job: pre-fetch with backoff so the first cargo step can't
|
|
# fail the job on a transient "Could not resolve host" DNS blip.
|
|
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: Build on MSRV
|
|
run: cargo build ${{ matrix.packages }} --verbose
|
|
|
|
- name: Test on MSRV
|
|
run: cargo test ${{ matrix.packages }} --verbose
|
|
|
|
# Code coverage for the pure-Rust crates, uploaded to Codecov.
|
|
coverage:
|
|
name: Coverage
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
with:
|
|
components: llvm-tools-preview
|
|
|
|
- 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: Install cargo-llvm-cov
|
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
|
with:
|
|
tool: cargo-llvm-cov
|
|
|
|
- name: Generate coverage (lcov)
|
|
run: >
|
|
cargo llvm-cov
|
|
-p wickra-core -p wickra -p wickra-data
|
|
--features wickra-data/live-binance
|
|
--lcov --output-path lcov.info
|
|
|
|
- name: Upload to Codecov
|
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
with:
|
|
files: lcov.info
|
|
fail_ci_if_error: false
|
|
env:
|
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
|
|
# Supply-chain audit: security advisories, license policy, banned crates,
|
|
# and source restrictions. Configured by deny.toml at the repo root.
|
|
supply-chain:
|
|
name: Supply-chain (cargo-deny)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: cargo-deny
|
|
uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20
|
|
with:
|
|
command: check
|
|
|
|
# Time-boxed fuzz smoke. Each target runs for ~30 s with libfuzzer; any panic
|
|
# fails the job. The goal is to catch a regression in the harness (e.g. a
|
|
# newly added indicator that panics on a particular input shape), not to
|
|
# discover novel bugs — long fuzz campaigns should be run on dedicated
|
|
# infrastructure with persistent corpora.
|
|
fuzz-smoke:
|
|
name: Fuzz (smoke)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install nightly Rust
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
with:
|
|
toolchain: nightly
|
|
|
|
- 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
|
|
with:
|
|
workspaces: fuzz
|
|
|
|
- name: Install cargo-fuzz
|
|
# Use the prebuilt binary from taiki-e/install-action instead of
|
|
# `cargo install cargo-fuzz --locked`. The latter resolves the
|
|
# version graph from cargo-fuzz's own Cargo.lock, which pins to
|
|
# rustix 0.36.5 — a version that still uses internal `rustc_*`
|
|
# attributes the modern nightly compiler rejects, so the install
|
|
# never gets off the ground. The prebuilt binary avoids the entire
|
|
# transitive-dep compile.
|
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
|
with:
|
|
tool: cargo-fuzz
|
|
|
|
- name: Fuzz csv_reader (30 s)
|
|
run: cargo +nightly fuzz run --target x86_64-unknown-linux-gnu csv_reader -- -max_total_time=30
|
|
working-directory: fuzz
|
|
|
|
- name: Fuzz binance_envelope (30 s)
|
|
run: cargo +nightly fuzz run --target x86_64-unknown-linux-gnu binance_envelope -- -max_total_time=30
|
|
working-directory: fuzz
|
|
|
|
- name: Fuzz indicator_update (30 s)
|
|
run: cargo +nightly fuzz run --target x86_64-unknown-linux-gnu indicator_update -- -max_total_time=30
|
|
working-directory: fuzz
|
|
|
|
- name: Fuzz indicator_update_candle (30 s)
|
|
run: cargo +nightly fuzz run --target x86_64-unknown-linux-gnu indicator_update_candle -- -max_total_time=30
|
|
working-directory: fuzz
|
|
|
|
- name: Fuzz tick_aggregator (30 s)
|
|
run: cargo +nightly fuzz run --target x86_64-unknown-linux-gnu tick_aggregator -- -max_total_time=30
|
|
working-directory: fuzz
|
|
|
|
python:
|
|
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
python-version: ["3.9", "3.11", "3.12", "3.13"]
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
|
|
- 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
|
|
|
|
# setup-python downloads the interpreter from the Actions tool cache /
|
|
# nodejs CDN and occasionally hangs or 5xx's on the Windows runners.
|
|
# Run it with continue-on-error, then retry once after a backoff so a
|
|
# single CDN flake does not fail the whole job (see also: GitHub
|
|
# Actions runner-images#7061).
|
|
- name: Set up Python
|
|
id: setup_python
|
|
continue-on-error: true
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: ${{ matrix.python-version }}
|
|
cache: pip
|
|
cache-dependency-path: .github/requirements/ci-dev-*.txt
|
|
|
|
- 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: ${{ matrix.python-version }}
|
|
cache: pip
|
|
cache-dependency-path: .github/requirements/ci-dev-*.txt
|
|
|
|
- name: Install Python dev dependencies
|
|
shell: bash
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
# Hash-locked dev tooling (OpenSSF Scorecard PinnedDependencies).
|
|
# Split by Python version: numpy ships no single release with wheels
|
|
# for both cp39 and cp313 (<=2.0.2 has cp39 only, >=2.1 drops cp39).
|
|
if [ "${{ matrix.python-version }}" = "3.9" ]; then
|
|
python -m pip install --require-hashes -r .github/requirements/ci-dev-py39.txt
|
|
else
|
|
python -m pip install --require-hashes -r .github/requirements/ci-dev-py3.txt
|
|
fi
|
|
|
|
- name: Build wheel
|
|
working-directory: bindings/python
|
|
run: maturin build --release --out dist
|
|
|
|
- name: Install wheel
|
|
shell: bash
|
|
working-directory: bindings/python
|
|
# --no-index forces pip to ignore PyPI; --no-deps skips re-resolving
|
|
# numpy (already installed in the previous step). Without --no-index
|
|
# pip prefers the PyPI 0.2.x wheel over our freshly built one when
|
|
# platform tags overlap (e.g. macOS arm64), so tests would run
|
|
# against the released package and miss any new symbols the PR adds.
|
|
run: python -m pip install --no-index --find-links dist --force-reinstall --no-deps wickra
|
|
|
|
- name: Run Python tests
|
|
working-directory: bindings/python
|
|
run: pytest -v
|
|
|
|
wasm:
|
|
name: WASM build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain (with wasm target)
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
with:
|
|
targets: wasm32-unknown-unknown
|
|
|
|
- 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: Install wasm-pack
|
|
# jetli/wasm-pack-action@v0.4.0 with no `version:` input installs an
|
|
# old wasm-pack (~0.10.x) whose `build` subcommand does not yet accept
|
|
# `--features`, so `wasm-pack build … --features panic-hook` fails
|
|
# with "Found argument '--features' which wasn't expected". Use the
|
|
# same taiki-e prebuilt-binary installer we already use for
|
|
# cargo-llvm-cov and cargo-fuzz; it tracks the latest wasm-pack
|
|
# release, which has `--features` as a top-level flag (since 0.12).
|
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
|
with:
|
|
tool: wasm-pack
|
|
|
|
- name: Build WASM package
|
|
run: wasm-pack build bindings/wasm --target web --release --features panic-hook
|
|
|
|
- name: Run WASM tests
|
|
run: wasm-pack test --node bindings/wasm
|
|
|
|
- name: Verify generated artefacts
|
|
run: |
|
|
test -f bindings/wasm/pkg/wickra_wasm.js
|
|
test -f bindings/wasm/pkg/wickra_wasm_bg.wasm
|
|
test -f bindings/wasm/pkg/wickra_wasm.d.ts
|
|
|
|
node:
|
|
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
node-version: ["18", "20"]
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
|
|
- 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
|
|
|
|
# setup-node downloads Node from nodejs.org and we've seen it fail on
|
|
# Windows runners with "Attempting to download 18..." followed by a
|
|
# silent hang or curl error. Retry once after a backoff so a single
|
|
# CDN flake does not fail the whole job.
|
|
- name: Set up Node
|
|
id: setup_node
|
|
continue-on-error: true
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: ${{ matrix.node-version }}
|
|
cache: npm
|
|
cache-dependency-path: bindings/node/package-lock.json
|
|
|
|
- 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: ${{ matrix.node-version }}
|
|
cache: npm
|
|
cache-dependency-path: bindings/node/package-lock.json
|
|
|
|
- name: Install Node dependencies
|
|
working-directory: bindings/node
|
|
run: npm ci
|
|
|
|
- name: Build native module
|
|
working-directory: bindings/node
|
|
# --platform puts the target triple into the filename so the loader's
|
|
# `wickra.<target>.node` lookup finds the freshly built binary instead
|
|
# of falling back to the per-platform npm subpackage (which doesn't
|
|
# exist yet for win32-x64-msvc).
|
|
run: npx napi build --platform --release
|
|
|
|
- name: Run Node tests
|
|
working-directory: bindings/node
|
|
run: node --test __tests__/
|
|
|
|
c-abi:
|
|
name: C ABI on ${{ matrix.os }}
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
|
|
- 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: Install cbindgen
|
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
|
with:
|
|
tool: cbindgen
|
|
|
|
- name: Build the C ABI library (cdylib + staticlib)
|
|
run: cargo build -p wickra-c --release
|
|
|
|
- name: Rust unit tests
|
|
run: cargo test -p wickra-c
|
|
|
|
# The generated header is platform-independent, so checking drift on one OS
|
|
# is enough — and avoids a spurious CRLF/LF diff on the Windows runner.
|
|
- name: Check the committed header is in sync with cbindgen
|
|
if: runner.os == 'Linux'
|
|
shell: bash
|
|
run: |
|
|
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
|
|
if ! git diff --quiet -- bindings/c/include/wickra.h; then
|
|
echo "::error::bindings/c/include/wickra.h is out of sync — run cbindgen and commit the result"
|
|
git --no-pager diff -- bindings/c/include/wickra.h
|
|
exit 1
|
|
fi
|
|
|
|
# The real cross-language test: a foreign C consumer links the generated
|
|
# header + the compiled library and runs. If this passes on all three OSes,
|
|
# every C-capable language can link the same way.
|
|
- name: Build and run the C smoke example (CMake + ctest)
|
|
shell: bash
|
|
run: |
|
|
cmake -S examples/c -B examples/c/build
|
|
cmake --build examples/c/build --config Release
|
|
ctest --test-dir examples/c/build -C Release --output-on-failure
|
|
|
|
csharp:
|
|
name: C# on ${{ matrix.os }}
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
|
|
- 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
|
|
|
|
# The binding links against the C ABI hub at runtime; build it first so the
|
|
# DllImportResolver finds target/release/wickra.{dll,so,dylib}. .NET 8 SDK is
|
|
# preinstalled on the GitHub runners, so no setup-dotnet step is needed.
|
|
- name: Build the C ABI library
|
|
run: cargo build -p wickra-c --release
|
|
|
|
- name: .NET info
|
|
run: dotnet --info
|
|
|
|
- name: Test the C# binding
|
|
run: dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj -c Release
|
|
|
|
- name: Build the C# examples
|
|
shell: bash
|
|
run: |
|
|
for d in streaming backtest multi_timeframe parallel_assets \
|
|
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze \
|
|
fetch_btcusdt live_binance; do
|
|
dotnet build "examples/csharp/$d" -c Release
|
|
done
|
|
|
|
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
|
- name: Run the offline C# examples
|
|
shell: bash
|
|
run: |
|
|
for d in streaming backtest multi_timeframe parallel_assets \
|
|
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
|
dotnet run --project "examples/csharp/$d" -c Release --no-build
|
|
done
|
|
|
|
go:
|
|
name: Go on ${{ matrix.os }}
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# Go is not reliably on PATH on every runner image, so install it
|
|
# explicitly. cache: false — the cargo build dominates and the modules
|
|
# have no external Go deps worth caching.
|
|
- name: Set up Go
|
|
id: setup-go
|
|
continue-on-error: true
|
|
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
|
with:
|
|
go-version: "stable"
|
|
cache: false
|
|
|
|
- name: Retry Go setup (CDN flake)
|
|
if: steps.setup-go.outcome == 'failure'
|
|
shell: bash
|
|
run: |
|
|
echo "::warning::setup-go failed (likely CDN flake), waiting 30s before retry..."
|
|
sleep 30
|
|
|
|
- name: Set up Go (retry)
|
|
if: steps.setup-go.outcome == 'failure'
|
|
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
|
with:
|
|
go-version: "stable"
|
|
cache: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
|
|
- 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
|
|
|
|
# The binding links against the C ABI hub via cgo; build it first and stage
|
|
# the platform library under bindings/go/lib so cgo's LDFLAGS find it. Go is
|
|
# preinstalled on the GitHub runners, so no setup-go step is needed.
|
|
- name: Build the C ABI library
|
|
run: cargo build -p wickra-c --release
|
|
|
|
- name: Stage the native library
|
|
shell: bash
|
|
run: |
|
|
mkdir -p bindings/go/lib
|
|
case "$RUNNER_OS" in
|
|
Linux) cp target/release/libwickra.so bindings/go/lib/ ;;
|
|
macOS) cp target/release/libwickra.dylib bindings/go/lib/ ;;
|
|
Windows) cp target/release/wickra.dll bindings/go/lib/ ;;
|
|
esac
|
|
|
|
- name: Go info
|
|
run: go version
|
|
|
|
- name: Check gofmt
|
|
shell: bash
|
|
run: |
|
|
unformatted="$(gofmt -l bindings/go examples/go)"
|
|
if [ -n "$unformatted" ]; then
|
|
echo "gofmt needed on:"; echo "$unformatted"; exit 1
|
|
fi
|
|
|
|
- name: Vet and test the Go binding
|
|
shell: bash
|
|
# On Windows there is no rpath; the loader resolves wickra.dll via PATH.
|
|
run: |
|
|
export PATH="$PWD/bindings/go/lib:$PATH"
|
|
cd bindings/go
|
|
go vet ./...
|
|
go test ./...
|
|
|
|
- name: Build the Go examples
|
|
shell: bash
|
|
run: |
|
|
export PATH="$PWD/bindings/go/lib:$PATH"
|
|
cd examples/go
|
|
go build ./...
|
|
|
|
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
|
- name: Run the offline Go examples
|
|
shell: bash
|
|
run: |
|
|
export PATH="$PWD/bindings/go/lib:$PATH"
|
|
cd examples/go
|
|
for d in streaming backtest multi_timeframe parallel_assets \
|
|
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
|
go run "./$d"
|
|
done
|
|
|
|
r:
|
|
name: R on ${{ matrix.os }}
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
env:
|
|
WICKRA_INCLUDE_DIR: ${{ github.workspace }}/bindings/c/include
|
|
WICKRA_LIB_DIR: ${{ github.workspace }}/target/release
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
|
|
- name: Cache cargo
|
|
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
continue-on-error: true
|
|
timeout-minutes: 6
|
|
|
|
# The binding compiles a thin .Call glue layer against the C ABI hub; build
|
|
# the library first. On Windows configure.win bundles a renamed copy
|
|
# (wickra_abi.dll) so the package's own wickra.dll does not collide with it.
|
|
- name: Build the C ABI library
|
|
run: cargo build -p wickra-c --release
|
|
|
|
- name: Set up R
|
|
uses: r-lib/actions/setup-r@a51a8012b0aab7c32ef9d19bf54da93f3254335e # v2
|
|
with:
|
|
r-version: "release"
|
|
use-public-rspm: true
|
|
|
|
# Use the repos configured by setup-r (use-public-rspm) so Linux installs
|
|
# binary packages — building testthat's deps from source is slow and flaky.
|
|
- name: Install test dependency
|
|
run: Rscript -e 'install.packages("testthat")'
|
|
|
|
- name: Install and test the R binding
|
|
shell: bash
|
|
# github.workspace is a backslash path on Windows; configure.win (sh) and
|
|
# mingw need forward slashes. On Linux/macOS the rpath points at
|
|
# WICKRA_LIB_DIR; export the loader path too as a belt-and-suspenders.
|
|
run: |
|
|
export WICKRA_INCLUDE_DIR="${WICKRA_INCLUDE_DIR//\\//}"
|
|
export WICKRA_LIB_DIR="${WICKRA_LIB_DIR//\\//}"
|
|
export LD_LIBRARY_PATH="$WICKRA_LIB_DIR:$LD_LIBRARY_PATH"
|
|
export DYLD_LIBRARY_PATH="$WICKRA_LIB_DIR:$DYLD_LIBRARY_PATH"
|
|
R CMD INSTALL bindings/r
|
|
Rscript -e 'library(testthat); library(wickra); test_dir("bindings/r/tests/testthat", stop_on_failure = TRUE)'
|
|
|
|
- name: Run the offline R examples
|
|
shell: bash
|
|
run: |
|
|
export WICKRA_LIB_DIR="${WICKRA_LIB_DIR//\\//}"
|
|
export LD_LIBRARY_PATH="$WICKRA_LIB_DIR:$LD_LIBRARY_PATH"
|
|
export DYLD_LIBRARY_PATH="$WICKRA_LIB_DIR:$DYLD_LIBRARY_PATH"
|
|
cd examples/r
|
|
for f in streaming backtest multi_timeframe parallel_assets \
|
|
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
|
Rscript "$f.R"
|
|
done
|
|
|
|
# fetch_btcusdt / live_binance need the network (and jsonlite / websocket);
|
|
# build-check that they parse without running them.
|
|
- name: Parse the network R examples
|
|
run: Rscript -e 'invisible(lapply(c("examples/r/fetch_btcusdt.R", "examples/r/live_binance.R"), parse)); cat("network R examples parse OK\n")'
|
|
|
|
java:
|
|
name: Java on ${{ matrix.os }}
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
|
|
- 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
|
|
|
|
# The binding links the C ABI hub at runtime through the Java FFM API; build
|
|
# it first so WickraNative's development fallback finds
|
|
# target/release/wickra.{so,dylib,dll}. JDK 22+ is required for the final FFM
|
|
# API, so it is installed explicitly (runners ship 17/21).
|
|
- name: Build the C ABI library
|
|
run: cargo build -p wickra-c --release
|
|
|
|
- name: Set up JDK 22
|
|
id: setup-java
|
|
continue-on-error: true
|
|
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: "22"
|
|
cache: maven
|
|
|
|
- name: Retry JDK setup (CDN flake)
|
|
if: steps.setup-java.outcome == 'failure'
|
|
shell: bash
|
|
run: |
|
|
echo "::warning::setup-java failed (likely CDN flake), waiting 30s before retry..."
|
|
sleep 30
|
|
|
|
- name: Set up JDK 22 (retry)
|
|
if: steps.setup-java.outcome == 'failure'
|
|
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: "22"
|
|
cache: maven
|
|
|
|
- name: Java info
|
|
run: java -version
|
|
|
|
# `install` runs the archetype test suite (the real FFI boundary check) and
|
|
# installs the binding to the local repo so the examples can resolve it.
|
|
- name: Test and install the Java binding
|
|
run: mvn -B -f bindings/java install
|
|
|
|
- name: Build the Java examples
|
|
run: mvn -B -f examples/java compile
|
|
|
|
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
|
- name: Run the offline Java examples
|
|
shell: bash
|
|
run: |
|
|
for cls in Streaming Backtest MultiTimeframe ParallelAssets \
|
|
StrategyRsiMeanReversion StrategyMacdAdx StrategyBollingerSqueeze; do
|
|
mvn -B -q -f examples/java exec:exec -Dexec.mainClass="org.wickra.examples.$cls"
|
|
done
|
|
|
|
# The cross-library benchmark has moved to a dedicated scheduled workflow
|
|
# (.github/workflows/bench.yml) — see audit finding R10. It runs nightly
|
|
# at 03:00 UTC and on-demand via `workflow_dispatch`, and is no longer on
|
|
# the every-push / every-PR critical path.
|