a4d8c40dc2
python-wheels built only glibc Linux (x86_64/aarch64), macOS, and Windows x64 — Alpine/musl users and Windows arm64 had no wheel. Add musllinux_1_2 wheels for x86_64 and aarch64 Linux and an aarch64 wheel on the windows-11-arm runner. The upload artifact name now includes the manylinux value so the glibc and musl builds of the same architecture do not collide. The Node release matrix already covers linux-arm64 and win32-arm64 (added in B11); Node musl is left out, as B11 documented, because it needs a cross/container setup.
465 lines
19 KiB
YAML
465 lines
19 KiB
YAML
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
|
|
# The publish jobs run with long-lived registry tokens. Binding them to a
|
|
# protected GitHub environment lets the org require a reviewer to approve
|
|
# each release and restrict which tags/branches may deploy, so the secrets
|
|
# are not reachable from an arbitrary workflow run. The `release`
|
|
# environment and its protection rules are configured under repo
|
|
# Settings -> Environments.
|
|
environment: release
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # 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/<name>-<version>.crate.
|
|
#
|
|
# No --allow-dirty: `actions/checkout` gives a clean tree and nothing
|
|
# above mutates it. No --no-verify: every crate was just published to
|
|
# crates.io in the steps above, so the verification build resolves its
|
|
# workspace dependencies from the registry and confirms each .crate
|
|
# actually builds before it is attached to the release.
|
|
- name: Build .crate files for release attachment
|
|
run: |
|
|
cargo package -p wickra-core
|
|
cargo package -p wickra-data
|
|
cargo package -p wickra
|
|
|
|
- name: Upload .crate files
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
with:
|
|
name: crate-files
|
|
path: target/package/*.crate
|
|
|
|
# --------------------------------------------------------------------------
|
|
# PyPI: cross-platform wheels + sdist
|
|
# --------------------------------------------------------------------------
|
|
python-wheels:
|
|
name: Build wheels (${{ matrix.target }}/${{ matrix.manylinux }} on ${{ matrix.os }})
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# glibc Linux (manylinux)
|
|
- { os: ubuntu-latest, target: x86_64, manylinux: auto }
|
|
- { os: ubuntu-latest, target: aarch64, manylinux: auto }
|
|
# musl Linux (Alpine and other musl distros)
|
|
- { os: ubuntu-latest, target: x86_64, manylinux: musllinux_1_2 }
|
|
- { os: ubuntu-latest, target: aarch64, manylinux: musllinux_1_2 }
|
|
# macOS
|
|
- { os: macos-latest, target: x86_64, manylinux: auto }
|
|
- { os: macos-latest, target: aarch64, manylinux: auto }
|
|
# Windows
|
|
- { os: windows-latest, target: x64, manylinux: auto }
|
|
- { os: windows-11-arm, target: aarch64, manylinux: auto }
|
|
runs-on: ${{ matrix.os }}
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.11"
|
|
- uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
|
with:
|
|
working-directory: bindings/python
|
|
target: ${{ matrix.target }}
|
|
args: --release --strip --out dist
|
|
manylinux: ${{ matrix.manylinux }}
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
with:
|
|
# Include manylinux in the name so the glibc and musl x86_64/aarch64
|
|
# builds do not collide on the same artifact name.
|
|
name: wheels-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux }}
|
|
path: bindings/python/dist/*
|
|
|
|
python-sdist:
|
|
name: Build Python sdist
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
- uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
|
with:
|
|
working-directory: bindings/python
|
|
command: sdist
|
|
args: --out dist
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
with:
|
|
name: wheels-sdist
|
|
path: bindings/python/dist/*
|
|
|
|
python-publish:
|
|
name: Publish to PyPI
|
|
needs: [python-wheels, python-sdist]
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
steps:
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
with:
|
|
path: dist
|
|
pattern: wheels-*
|
|
merge-multiple: true
|
|
|
|
- name: Upload to PyPI
|
|
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
|
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "20"
|
|
|
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
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
|
|
environment: release
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
with:
|
|
path: bindings/node/artifacts
|
|
pattern: bindings-*
|
|
|
|
# The npm/<platform>/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"
|
|
# --ignore-scripts: a per-platform package must never run lifecycle
|
|
# scripts during publish (npm runs prepublishOnly/prepare/etc. from
|
|
# the package being published — a malicious or stray script would
|
|
# execute with the npm token in the environment).
|
|
(cd "$dir" && npm publish --access public --ignore-scripts)
|
|
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 --ignore-scripts)
|
|
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). --ignore-scripts for the same reason as publish:
|
|
# packing must not execute lifecycle scripts from the packed dir.
|
|
for d in npm/*/; do
|
|
(cd "$d" && npm pack --ignore-scripts)
|
|
done
|
|
|
|
- name: Upload Node tarballs
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
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
|
|
environment: release
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "20"
|
|
registry-url: "https://registry.npmjs.org"
|
|
|
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
with:
|
|
targets: wasm32-unknown-unknown
|
|
|
|
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # 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 <kingchencp@gmail.com>';
|
|
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
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@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
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. |