Files
wickra/.github/workflows/release.yml
T
kingchenc 8ccb885906 D4: pass --ignore-scripts to per-platform npm publish/pack
The main npm package already publishes and packs with --ignore-scripts,
but the per-platform subpackage loop did not: `npm publish --access
public`, its retry, and the per-platform `npm pack` all ran lifecycle
scripts from the package directory with the npm token in scope.

Add --ignore-scripts to all three, matching the main package, so no
prepublish/prepare hook can execute during a release.
2026-05-22 12:32:23 +02:00

446 lines
16 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
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/<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@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/<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@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 <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@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.