name: Release on: push: tags: ["v*"] workflow_dispatch: env: CARGO_TERM_COLOR: always jobs: # -------------------------------------------------------------------------- # crates.io: three crates in dependency order # -------------------------------------------------------------------------- cargo-publish: name: Publish to crates.io runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 # Idempotent publishing: if the version is already on crates.io we # treat that as success so re-runs of the workflow don't fail. - name: Publish wickra-core (idempotent) run: | out=$(cargo publish -p wickra-core --token "$CARGO_REGISTRY_TOKEN" 2>&1) && echo "$out" \ || (echo "$out" | grep -q "already uploaded\|already exists" && echo "skip: already published" \ || (echo "$out"; exit 1)) env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for crates.io index to refresh run: sleep 45 - name: Publish wickra-data (idempotent) run: | out=$(cargo publish -p wickra-data --token "$CARGO_REGISTRY_TOKEN" 2>&1) && echo "$out" \ || (echo "$out" | grep -q "already uploaded\|already exists" && echo "skip: already published" \ || (echo "$out"; exit 1)) env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for crates.io index to refresh run: sleep 45 - name: Publish wickra (idempotent) run: | out=$(cargo publish -p wickra --token "$CARGO_REGISTRY_TOKEN" 2>&1) && echo "$out" \ || (echo "$out" | grep -q "already uploaded\|already exists" && echo "skip: already published" \ || (echo "$out"; exit 1)) env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} # Produce .crate files for the GitHub Release attachments. `cargo package` # writes them to target/package/-.crate. - name: Build .crate files for release attachment run: | cargo package -p wickra-core --allow-dirty --no-verify cargo package -p wickra-data --allow-dirty --no-verify cargo package -p wickra --allow-dirty --no-verify - name: Upload .crate files uses: actions/upload-artifact@v4 with: name: crate-files path: target/package/*.crate # -------------------------------------------------------------------------- # PyPI: cross-platform wheels + sdist # -------------------------------------------------------------------------- python-wheels: name: Build wheels (${{ matrix.target }} on ${{ matrix.os }}) strategy: fail-fast: false matrix: include: - { os: ubuntu-latest, target: x86_64, manylinux: auto } - { os: ubuntu-latest, target: aarch64, manylinux: auto } - { os: macos-latest, target: x86_64, manylinux: auto } - { os: macos-latest, target: aarch64, manylinux: auto } - { os: windows-latest, target: x64, manylinux: auto } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - uses: PyO3/maturin-action@v1 with: working-directory: bindings/python target: ${{ matrix.target }} args: --release --strip --out dist manylinux: ${{ matrix.manylinux }} - uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.os }}-${{ matrix.target }} path: bindings/python/dist/* python-sdist: name: Build Python sdist runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: PyO3/maturin-action@v1 with: working-directory: bindings/python command: sdist args: --out dist - uses: actions/upload-artifact@v4 with: name: wheels-sdist path: bindings/python/dist/* python-publish: name: Publish to PyPI needs: [python-wheels, python-sdist] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: path: dist pattern: wheels-* merge-multiple: true - name: Upload to PyPI uses: PyO3/maturin-action@v1 with: command: upload args: --skip-existing dist/* env: MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} # -------------------------------------------------------------------------- # npm: per-platform native binaries + main meta-package # -------------------------------------------------------------------------- node-build: name: Node build (${{ matrix.target }}) strategy: fail-fast: false matrix: include: - { host: ubuntu-latest, target: x86_64-unknown-linux-gnu } - { host: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu } - { host: macos-latest, target: x86_64-apple-darwin } - { host: macos-latest, target: aarch64-apple-darwin } - { host: windows-latest, target: x86_64-pc-windows-msvc } - { host: windows-11-arm, target: aarch64-pc-windows-msvc } runs-on: ${{ matrix.host }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - name: Install Node deps working-directory: bindings/node run: npm install - name: Build native module working-directory: bindings/node run: npx napi build --platform --release --target ${{ matrix.target }} - name: Upload artifact uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: bindings/node/wickra.*.node if-no-files-found: error node-publish: name: Publish to npm needs: node-build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" registry-url: "https://registry.npmjs.org" - name: Install Node deps working-directory: bindings/node run: npm install - name: Download all platform binaries uses: actions/download-artifact@v4 with: path: bindings/node/artifacts pattern: bindings-* # The npm//package.json templates are committed to the # repo (see bindings/node/npm/), so `napi artifacts` only needs to # drop the freshly built .node binary into the matching directory. - name: Move binaries into platform package layout working-directory: bindings/node run: npx napi artifacts --dir artifacts # Publish each platform package individually so one failure doesn't kill # the others. Skip versions that are already on npm. Tolerate spam-filter # 403s with a one-time retry after a short delay (spam detection is # often rate-limit-based and clears on the next request). - name: Publish platform packages (idempotent) working-directory: bindings/node env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | set +e version=$(node -p "require('./package.json').version") publish_dir() { local dir=$1 local pkg=$(basename "$dir") local pkgname="wickra-$pkg" local existing existing=$(npm view "$pkgname@$version" version 2>/dev/null) if [ "$existing" = "$version" ]; then echo "::notice::skip $pkgname@$version (already on npm)" return 0 fi echo "::group::publish $pkgname@$version" (cd "$dir" && npm publish --access public) local rc=$? echo "::endgroup::" if [ "$rc" -ne 0 ]; then echo "::warning::first attempt of $pkgname failed (rc=$rc); retrying after 30s" sleep 30 (cd "$dir" && npm publish --access public) rc=$? fi if [ "$rc" -ne 0 ]; then echo "::warning::$pkgname could not be published; the main package will skip the missing optional dep" fi return 0 } for dir in npm/*/; do publish_dir "$dir" done - name: Publish main package to npm (idempotent) working-directory: bindings/node env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} shell: bash run: | # GitHub Actions defaults to `bash -e`. `npm view` on a not-yet- # published package returns 1, which would kill the step before # we got to publish. Force a graceful empty result instead. set +e version=$(node -p "require('./package.json').version") existing=$(npm view "wickra@$version" version 2>/dev/null) if [ "$existing" = "$version" ]; then echo "::notice::skip wickra@$version (already on npm)" exit 0 fi # --ignore-scripts so any leftover prepublish hooks (which would # otherwise try to republish the already-published platform # subpackages) can't sabotage the main publish. npm publish --access public --ignore-scripts rc=$? if [ "$rc" -ne 0 ]; then echo "::warning::first attempt failed (rc=$rc); retrying after 30s" sleep 30 npm publish --access public --ignore-scripts rc=$? fi exit $rc - name: Pack node tarballs for release attachment working-directory: bindings/node run: | # Main package npm pack --ignore-scripts # Each per-platform package (the binaries were already moved in by napi artifacts) for d in npm/*/; do (cd "$d" && npm pack) done - name: Upload Node tarballs uses: actions/upload-artifact@v4 with: name: node-tarballs path: | bindings/node/*.tgz bindings/node/npm/*/*.tgz # -------------------------------------------------------------------------- # WASM: wasm-pack build + npm publish (as `wickra-wasm`) # -------------------------------------------------------------------------- wasm-publish: name: Publish wickra-wasm to npm runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" registry-url: "https://registry.npmjs.org" - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown - uses: jetli/wasm-pack-action@v0.4.0 - name: Build WASM package (bundler target) run: wasm-pack build bindings/wasm --target bundler --release --features panic-hook - name: Enrich generated package.json with author + repo metadata working-directory: bindings/wasm/pkg run: | node -e " const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json')); pkg.author = 'kingchenc '; pkg.repository = { type: 'git', url: 'https://github.com/kingchenc/wickra' }; pkg.homepage = 'https://github.com/kingchenc/wickra'; pkg.bugs = { url: 'https://github.com/kingchenc/wickra/issues' }; pkg.license = 'SEE LICENSE IN LICENSE'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)); " - name: Pack wickra-wasm for release attachment working-directory: bindings/wasm/pkg run: npm pack - name: Upload WASM tarball uses: actions/upload-artifact@v4 with: name: wasm-tarball path: bindings/wasm/pkg/wickra-wasm-*.tgz - name: Publish wickra-wasm to npm (idempotent) working-directory: bindings/wasm/pkg run: | out=$(npm publish --access public 2>&1) && echo "$out" \ || (echo "$out" | grep -q "You cannot publish over" && echo "skip: version already on npm" \ || (echo "$out"; exit 1)) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # -------------------------------------------------------------------------- # GitHub Release: attach every built artefact to the tag's release page. # -------------------------------------------------------------------------- github-release: name: Attach assets to the GitHub Release needs: [cargo-publish, python-publish, node-publish, wasm-publish] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Resolve target tag id: tag run: | if [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref }}" == refs/tags/* ]]; then tag="${{ github.ref_name }}" else # workflow_dispatch / non-tag push: attach to the latest v* tag. tag=$(git tag --list 'v*' --sort=-v:refname | head -n1) fi if [ -z "$tag" ]; then echo "::error::no v* tag found to attach assets to" exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "::notice::attaching assets to release $tag" - name: Download all build artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: Stage release assets run: | set -e mkdir -p release-assets # Python wheels + sdist (5 wheel artifacts + 1 sdist artifact). find artifacts -type f -name "*.whl" -exec cp {} release-assets/ \; find artifacts -type f -name "*.tar.gz" -exec cp {} release-assets/ \; # Native Node binaries (one per platform). find artifacts -type f -name "*.node" -exec cp {} release-assets/ \; # Node npm-pack tarballs (main + per-platform). find artifacts -type f -name "wickra-*.tgz" -exec cp {} release-assets/ \; # Cargo .crate files (one per workspace member). find artifacts -type f -name "*.crate" -exec cp {} release-assets/ \; ls -lh release-assets/ echo "asset-count=$(ls release-assets/ | wc -l)" - name: Create / update GitHub Release with assets uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.tag.outputs.tag }} name: Wickra ${{ steps.tag.outputs.tag }} files: release-assets/* generate_release_notes: true fail_on_unmatched_files: false body: | Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries. ### Install ```bash cargo add wickra pip install wickra npm install wickra npm install wickra-wasm ``` ### Attached assets Pre-built artefacts for every supported platform — the same files that were uploaded to crates.io, PyPI, and npm by this workflow run. - `*.whl` / `wickra-*.tar.gz` — Python wheels + sdist (5 platforms, ABI3 ≥ 3.9) - `wickra.*.node` — native Node bindings (linux-x64-gnu, darwin-x64, darwin-arm64, win32-x64-msvc) - `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM) - `*.crate` — cargo source crates (wickra-core, wickra-data, wickra) ### Auto-generated changelog See below; GitHub computes it from the commits since the previous tag.