Files
wickra/.github/workflows/release.yml
T
kingchenc 070be2eb27 release: 0.2.6 (docs.rs fix + README table reordering) (#36)
* fix(docs-rs): rename `doc_auto_cfg` to `doc_cfg` after Rust 1.92 merge

`doc_auto_cfg` was removed in Rust 1.92.0 and folded back into
`doc_cfg` (rust-lang/rust#138907). docs.rs builds with the latest
nightly and sets `--cfg docsrs`, so the previous

    #![cfg_attr(docsrs, feature(doc_auto_cfg))]

aborts compilation with E0557 on every published 0.2.x. GitHub CI
never tripped this — stable rustc ignores the line because nothing
sets the `docsrs` cfg there.

Switch all three published library crates (`wickra`, `wickra-core`,
`wickra-data`) to the merged-into `doc_cfg` gate. Same intent, same
on-docs.rs output, builds again on nightly.

* docs(readme): float Wickra to the top of the comparison tables

Reorders the "Why Wickra exists" library-comparison table and the two
benchmark headers so Wickra is the first row (with a ★ marker) instead
of the last. The previous order placed Wickra at the bottom, which
buries the only row a reader landing on the README is here to compare
against. Same column data, same ★/winner annotations, just the row
order flipped and a ★ prefix on the Wickra label.

Mirrored across the umbrella README and every binding README so the
crates.io / PyPI / npm landing pages stay in sync.

* release: bump workspace + bindings to 0.2.6

Workspace, every binding (Python, Node, Node platform stubs), the
release.yml comment and the CHANGELOG all move together to 0.2.6 so
the next tagged release lines every artefact up.

0.2.6 carries two changes from the [0.2.6] CHANGELOG entry:
- fix(docs-rs): swap the now-removed `doc_auto_cfg` feature gate for
  the merged-into `doc_cfg` so docs.rs nightly builds resume.
- docs(readme): float ★ Wickra to the top of every comparison table
  across the umbrella + binding READMEs.

wickra-win32-arm64-msvc stays excluded for this release with the same
npm spam-filter rationale that held for 0.2.5.
2026-05-24 03:20:13 +02:00

506 lines
21 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Sync root README into bindings/python so it ships with the wheel
shell: bash
run: cp README.md bindings/python/README.md
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Sync root README into bindings/python so it ships in the sdist
run: cp README.md bindings/python/README.md
- uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: bindings/python
command: sdist
args: --out dist
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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 }
# NOTE: aarch64-pc-windows-msvc is temporarily skipped for 0.2.6.
# The wickra-win32-arm64-msvc npm subpackage name is blocked by the
# npm spam-detection filter for new accounts (same situation that
# affected wickra-win32-x64-msvc through 0.1.4 until npm Support
# unblocked it). A support ticket is open; once the new arm64 name
# is unblocked this matrix entry will be restored alongside the
# corresponding optionalDependencies / napi.triples / npm/<target>
# entries in a follow-up release.
# - { host: windows-11-arm, target: aarch64-pc-windows-msvc }
runs-on: ${{ matrix.host }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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. Skip versions that are
# already on npm. A first-attempt 403 from npm's spam filter is
# tolerated for a single 30-second retry — that historically clears
# rate-limit-driven false positives. Anything that still fails after
# the retry is a *real* failure (the platform binary will be missing
# from `optionalDependencies` and Windows-style installs will break,
# exactly the regression that produced audit finding R20) — fail the
# job loudly so the release does not silently land in a half-published
# state. Previously this loop swallowed the second-attempt failure with
# a `::warning::` and `return 0`; that mask is removed.
- 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")
fail=0
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 "::error::$pkgname could not be published — the release would land with a missing platform binary; failing the job."
return 1
fi
return 0
}
for dir in npm/*/; do
publish_dir "$dir" || fail=1
done
exit $fail
- name: Sync root README into bindings/node so it ships with the npm tarball
# npm reads README.md from the package directory at publish time. Copy
# the canonical root README in just before the publish so every
# registry shows the same project page.
shell: bash
run: cp ../../README.md README.md
working-directory: bindings/node
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.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
- name: Install wasm-pack (latest, via prebuilt binary)
# See the matching note in ci.yml: jetli's default installs an old
# 0.10.x wasm-pack whose build subcommand rejects --features.
uses: taiki-e/install-action@6c1f7cf125e42770ff087ea443901b487cc5471a # v2.79.5
with:
tool: wasm-pack
- name: Sync root README into bindings/wasm so wasm-pack ships it in pkg/
# wasm-pack copies the crate's README.md into the generated pkg/
# directory it then publishes. Refresh it from the canonical root
# README right before the build.
run: cp README.md bindings/wasm/README.md
- 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 = 'PolyForm-Noncommercial-1.0.0';
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
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.