11dd659b5f
Relicenses Wickra from PolyForm Noncommercial 1.0.0 to the dual, OSI-approved **MIT OR Apache-2.0** (the de-facto Rust convention). Wickra becomes permissive, commercial-use-permitted open source; users may choose either license. ## Changes - Replace `LICENSE` (PolyForm) with `LICENSE-MIT` + `LICENSE-APACHE` (full texts). - Cargo: workspace `license = "MIT OR Apache-2.0"` (SPDX) + all 7 sub-crates switched from `license-file.workspace` to `license.workspace`. - `deny.toml`: drop PolyForm from the allowlist. - Python: `pyproject.toml` PEP 639 SPDX expression; remove the non-commercial classifier (verified: sdist metadata emits `License-Expression: MIT OR Apache-2.0`). - Node: `package.json`, the 6 platform manifests and both lockfiles. - README + Python/Node/WASM binding READMEs, CONTRIBUTING, CITATION.cff, PR template, and the WASM `pkg.license` step in `release.yml`. - SECURITY.md: refresh supported versions 0.1.x -> 0.4.x. - CHANGELOG: note the relicense under [Unreleased]. ## Notes - No code changes; metadata/text only. `cargo build` and `cargo deny check licenses` pass locally. - GitHub will auto-detect "MIT, Apache-2.0" once this lands (currently NOASSERTION). - Matching downstream changes (org `.github` profile, webpage, docs) are in separate PRs; merge those together with the relicense release so the live sites and org profile do not claim MIT before the packages do.
777 lines
33 KiB
YAML
777 lines
33 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
workflow_dispatch:
|
|
|
|
# Least-privilege default for the auto-injected GITHUB_TOKEN. The publish jobs
|
|
# (cargo/python/node) push to external registries via their own secrets
|
|
# (CARGO_REGISTRY_TOKEN / PYPI_API_TOKEN / NPM_TOKEN), not the GITHUB_TOKEN, so
|
|
# they need no repo write. The jobs that genuinely write through the
|
|
# GITHUB_TOKEN — github-release (contents: write), node-/wasm-publish and
|
|
# attestations (id-token / attestations: write) — declare those rights in their
|
|
# own job-level permissions blocks, which override this default (OpenSSF
|
|
# Scorecard: Token-Permissions).
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
# Network-flake resilience: retry transient registry/DNS failures at the tool
|
|
# level so a blip fetching crates.io / npm inside any build or publish step
|
|
# (cargo, napi, maturin, wasm-pack, npm) retries automatically instead of
|
|
# failing the job. Cargo treats "couldn't resolve host" / connect / timeout as
|
|
# spurious and retries with backoff; 10 attempts ride out a transient DNS blip.
|
|
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:
|
|
# --------------------------------------------------------------------------
|
|
# 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
|
|
with:
|
|
persist-credentials: false
|
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
|
timeout-minutes: 6
|
|
|
|
# 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
|
|
|
|
# CycloneDX SBOM per published crate. Attached to the GitHub Release
|
|
# alongside the .crate / .whl / .tgz artefacts so downstream
|
|
# consumers can audit the published dependency tree without
|
|
# re-resolving Cargo.lock.
|
|
- name: Install cargo-cyclonedx
|
|
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-cyclonedx
|
|
|
|
- name: Generate CycloneDX SBOMs
|
|
run: |
|
|
# cargo-cyclonedx walks the whole workspace in a single pass and
|
|
# writes a <package>.cdx.json next to each member's Cargo.toml; it
|
|
# has no -p/--package selector. Collect the three crates.io crates
|
|
# (the .crate files published by this job) into the upload dir.
|
|
cargo cyclonedx --format json --top-level
|
|
mkdir -p sboms
|
|
cp crates/wickra-core/wickra-core.cdx.json sboms/
|
|
cp crates/wickra-data/wickra-data.cdx.json sboms/
|
|
cp crates/wickra/wickra.cdx.json sboms/
|
|
ls -lh sboms/
|
|
|
|
- name: Upload SBOMs
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: sboms
|
|
path: sboms/*.cdx.json
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 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
|
|
with:
|
|
persist-credentials: false
|
|
- name: Set up Python
|
|
id: setup_python
|
|
continue-on-error: true
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: "3.11"
|
|
- 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.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
|
|
with:
|
|
persist-credentials: false
|
|
- 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 }
|
|
- { host: windows-11-arm, target: aarch64-pc-windows-msvc }
|
|
runs-on: ${{ matrix.host }}
|
|
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"
|
|
|
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- 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 Node deps
|
|
working-directory: bindings/node
|
|
run: npm ci
|
|
|
|
- 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
|
|
# `id-token: write` lets npm publish embed a Sigstore provenance
|
|
# attestation generated from the GitHub Actions OIDC token. The npm
|
|
# registry then shows a "Verified provenance" badge and lets
|
|
# consumers verify the package was built from this exact workflow
|
|
# run.
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
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"
|
|
registry-url: "https://registry.npmjs.org"
|
|
|
|
- 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"
|
|
registry-url: "https://registry.npmjs.org"
|
|
|
|
- name: Install Node deps
|
|
working-directory: bindings/node
|
|
run: npm ci
|
|
|
|
- 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 --provenance)
|
|
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 --provenance)
|
|
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 --provenance
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "::warning::first attempt failed (rc=$rc); retrying after 30s"
|
|
sleep 30
|
|
npm publish --access public --ignore-scripts --provenance
|
|
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`)
|
|
# --------------------------------------------------------------------------
|
|
# Note: this job's npm publish call uses `--provenance` (see below),
|
|
# which requires the `id-token: write` permission set at the job level.
|
|
wasm-publish:
|
|
name: Publish wickra-wasm to npm
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
# `id-token: write` lets npm publish embed a Sigstore provenance
|
|
# attestation generated from the GitHub Actions OIDC token (same
|
|
# mechanism as the node-publish job above).
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
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"
|
|
registry-url: "https://registry.npmjs.org"
|
|
|
|
- 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"
|
|
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@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
|
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 <support@wickra.org>';
|
|
pkg.repository = { type: 'git', url: 'https://github.com/wickra-lib/wickra' };
|
|
pkg.homepage = 'https://github.com/wickra-lib/wickra';
|
|
pkg.bugs = { url: 'https://github.com/wickra-lib/wickra/issues' };
|
|
pkg.license = 'MIT OR Apache-2.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 --provenance 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.
|
|
#
|
|
# The release is created as a DRAFT here and only flipped to published by the
|
|
# downstream publish-release job, after the provenance bundle is attached. That
|
|
# ordering (draft -> attach everything -> publish) makes the pipeline compatible
|
|
# with GitHub release immutability, which locks assets at publish time (P24):
|
|
# the old "publish, then upload provenance" order would have the provenance
|
|
# upload rejected once immutability is enabled.
|
|
# --------------------------------------------------------------------------
|
|
github-release:
|
|
name: Attach assets to the draft GitHub Release
|
|
needs: [cargo-publish, python-publish, node-publish, wasm-publish]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
# Expose the resolved tag so the attestations job can attach the provenance
|
|
# bundle to this same release without re-resolving it.
|
|
outputs:
|
|
tag: ${{ steps.tag.outputs.tag }}
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve target tag
|
|
id: tag
|
|
# Pass the (potentially attacker-influenceable on a tag push) ref context
|
|
# through the environment instead of interpolating it into the shell
|
|
# script, so a crafted tag name cannot inject commands (zizmor:
|
|
# template-injection).
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
REF: ${{ github.ref }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
run: |
|
|
if [ "$EVENT_NAME" = "push" ] && [[ "$REF" == refs/tags/* ]]; then
|
|
tag="$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/ \;
|
|
# CycloneDX SBOMs (one per published crate).
|
|
find artifacts -type f -name "*.cdx.json" -exec cp {} release-assets/ \;
|
|
ls -lh release-assets/
|
|
echo "asset-count=$(ls release-assets/ | wc -l)"
|
|
|
|
- name: Create / update the draft 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
|
|
# Created as a draft; publish-release flips it to published + latest once
|
|
# the provenance bundle is attached (P24, immutability-ready).
|
|
draft: true
|
|
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.
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Build provenance attestations (findings P13.2)
|
|
# --------------------------------------------------------------------------
|
|
attestations:
|
|
name: Attest build provenance
|
|
needs: [cargo-publish, python-wheels, python-sdist, github-release]
|
|
runs-on: ubuntu-latest
|
|
# Signed SLSA build-provenance attestations for the published crates and
|
|
# Python wheels/sdist. npm tarballs already carry inline Sigstore provenance
|
|
# from `npm publish --provenance`, so they are covered there.
|
|
#
|
|
# The job stays isolated from the *publishes*: cargo/PyPI/npm all run upstream
|
|
# of github-release, so a Sigstore hiccup here can never block or corrupt a
|
|
# publish (the isolation the SBOM step lacked before #79). It additionally
|
|
# `needs: github-release` so the (still-draft) GitHub Release already exists
|
|
# when it attaches the provenance bundle as a release asset (P21.1e) — OpenSSF
|
|
# Scorecard's Signed-Releases check scans release *assets* (*.intoto.jsonl),
|
|
# not GitHub's separate attestations store, so the bundle has to live on the
|
|
# release. The release is published afterwards by the publish-release job
|
|
# whether or not this attestation succeeds (P24), so a failure here still only
|
|
# costs the provenance asset, never the release.
|
|
permissions:
|
|
id-token: write # OIDC for keyless Sigstore signing
|
|
attestations: write # write the attestations to this repo
|
|
contents: write # upload the provenance bundle as a release asset
|
|
steps:
|
|
- name: Download crate files
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: crate-files
|
|
path: artifacts/crates
|
|
- name: Download wheels + sdist
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: wheels-*
|
|
path: artifacts/python
|
|
merge-multiple: true
|
|
- name: Attest build provenance
|
|
id: attest
|
|
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
|
with:
|
|
subject-path: |
|
|
artifacts/crates/*.crate
|
|
artifacts/python/*.whl
|
|
artifacts/python/*.tar.gz
|
|
|
|
# Attach the Sigstore provenance bundle to the GitHub Release as a
|
|
# `*.intoto.jsonl` asset so OpenSSF Scorecard's Signed-Releases check finds
|
|
# signed provenance on the release itself (P21.1e). attest-build-provenance
|
|
# writes a single JSONL bundle covering every subject above; copy it to a
|
|
# `.intoto.jsonl`-suffixed name and upload with --clobber so re-runs are
|
|
# idempotent. github.token has contents: write here, which is all gh needs.
|
|
- name: Attach provenance bundle to the GitHub Release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ needs.github-release.outputs.tag }}
|
|
BUNDLE: ${{ steps.attest.outputs.bundle-path }}
|
|
run: |
|
|
if [ -z "$TAG" ]; then
|
|
echo "::error::no tag resolved from github-release; cannot attach provenance."
|
|
exit 1
|
|
fi
|
|
if [ -z "$BUNDLE" ] || [ ! -f "$BUNDLE" ]; then
|
|
echo "::error::attestation bundle not found at '$BUNDLE'."
|
|
exit 1
|
|
fi
|
|
dest="wickra-${TAG}.provenance.intoto.jsonl"
|
|
cp "$BUNDLE" "$dest"
|
|
echo "Uploading $dest to release $TAG"
|
|
gh release upload "$TAG" "$dest" --clobber --repo "${{ github.repository }}"
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Publish the drafted release LAST (P24 — immutability-ready).
|
|
#
|
|
# github-release creates the release as a draft and attestations attaches the
|
|
# provenance bundle to it; only now, with every asset in place, is it flipped to
|
|
# published + latest. With GitHub release immutability enabled, assets lock at
|
|
# this publish step — so the provenance bundle and every build artefact are
|
|
# already present and never need a (rejected) post-publish upload.
|
|
#
|
|
# `if: always() && needs.github-release.result == 'success'` preserves the old
|
|
# robustness: the release is published whenever the draft was created, even if
|
|
# the attestations job hit a Sigstore hiccup — that only costs the provenance
|
|
# asset, exactly as before. If github-release was skipped (a publish job failed)
|
|
# there is no draft, so this is skipped too and no release is published.
|
|
# --------------------------------------------------------------------------
|
|
publish-release:
|
|
name: Publish the GitHub Release
|
|
needs: [github-release, attestations]
|
|
if: always() && needs.github-release.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write # flip the draft release to published
|
|
steps:
|
|
- name: Flip the draft release to published (latest)
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ needs.github-release.outputs.tag }}
|
|
run: |
|
|
if [ -z "$TAG" ]; then
|
|
echo "::error::no tag resolved from github-release; cannot publish."
|
|
exit 1
|
|
fi
|
|
echo "::notice::publishing release $TAG (draft -> published, latest)"
|
|
gh release edit "$TAG" --draft=false --latest=true --repo "${{ github.repository }}" |