d59cd44043
* docs: standardise language naming and add binding security sections Canonical binding list everywhere: Rust, Python, Node.js, WASM, C, C++, C#, Go, Java, R. Use C# (not .NET) as the language label, WASM (not WebAssembly) in prose, and frame the C ABI as a hub rather than a list item. - Bump stale indicator counts (200+ -> 514) and family count (sixteen -> twenty-four) in the Node/Python/WASM and docs READMEs. - Add a short Security section to all eight binding READMEs. - Relabel benchmark rows (C -> C / C++, C# / .NET -> C#). - Fix the 'language stecker' wording in the C#/Go/R API intros. - Documentation only; no code or public API changes. * release.yml: extend install snippets and expose version output Add the missing registry installs to the release body (dotnet, go, Gradle/ Maven Central, r-universe) alongside cargo/pip/npm, and expose a v-stripped 'version' output from the tag step for the Gradle coordinate. Also fix the C-ABI language order in the assets note (C# before Go). * release.yml: correct the release body (10 languages, all registries) Reframe the tagline to '10 languages' (native Rust/Python/Node.js/WASM + a C ABI hub for C, C++, C#, Go, Java, R) instead of '4 language registries', note that C#/Java/Go/R publish to NuGet/Maven/Go/r-universe via their own jobs, and tidy the Node.js label and the C-ABI hub list.
1095 lines
47 KiB
YAML
1095 lines
47 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
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@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
|
||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
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@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
|
||
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.
|
||
# --------------------------------------------------------------------------
|
||
# --------------------------------------------------------------------------
|
||
# C ABI native libraries (bindings/c) — built per target on a native runner
|
||
# (no cross toolchain needed) and attached to the GitHub Release as the
|
||
# distribution channel. There is no package registry for the C ABI.
|
||
# --------------------------------------------------------------------------
|
||
c-abi-build:
|
||
name: C ABI library (${{ 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- 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: Build the C ABI library (cdylib + staticlib)
|
||
run: cargo build -p wickra-c --release --target ${{ matrix.target }}
|
||
|
||
- name: Package header + libraries
|
||
shell: bash
|
||
run: |
|
||
set -e
|
||
dir="wickra-c-${{ matrix.target }}"
|
||
mkdir -p "$dir/include" "$dir/lib"
|
||
cp bindings/c/include/wickra.h bindings/c/include/wickra.hpp "$dir/include/"
|
||
for f in libwickra.so libwickra.a libwickra.dylib wickra.dll wickra.dll.lib wickra.lib; do
|
||
src="target/${{ matrix.target }}/release/$f"
|
||
[ -f "$src" ] && cp "$src" "$dir/lib/"
|
||
done
|
||
tar -czf "$dir.tar.gz" "$dir"
|
||
echo "packaged $dir:"; ls -lR "$dir"
|
||
|
||
- name: Upload artifact
|
||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||
with:
|
||
name: c-abi-${{ matrix.target }}
|
||
path: wickra-c-${{ matrix.target }}.tar.gz
|
||
if-no-files-found: error
|
||
|
||
# Pack and publish the .NET binding to NuGet. Independent of the GitHub-release
|
||
# job so a C# hiccup never blocks the C/C++ asset release. Authentication uses
|
||
# NuGet Trusted Publishing (OIDC) — no long-lived API key. The 'wickra-release'
|
||
# trusted-publishing policy on nuget.org (owner KingchenC, repo wickra-lib/wickra,
|
||
# workflow release.yml) exchanges the GitHub OIDC token for a short-lived key.
|
||
csharp-publish:
|
||
name: Publish to NuGet
|
||
needs: c-abi-build
|
||
runs-on: ubuntu-latest
|
||
environment: release
|
||
permissions:
|
||
contents: read
|
||
id-token: write # request the GitHub OIDC token for trusted publishing
|
||
steps:
|
||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- name: Download the C ABI native libraries
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
pattern: c-abi-*
|
||
path: c-abi-artifacts
|
||
|
||
- name: Stage native libraries into runtimes/<rid>/native
|
||
shell: bash
|
||
run: |
|
||
set -e
|
||
declare -A RID=(
|
||
[x86_64-unknown-linux-gnu]=linux-x64
|
||
[aarch64-unknown-linux-gnu]=linux-arm64
|
||
[x86_64-apple-darwin]=osx-x64
|
||
[aarch64-apple-darwin]=osx-arm64
|
||
[x86_64-pc-windows-msvc]=win-x64
|
||
[aarch64-pc-windows-msvc]=win-arm64
|
||
)
|
||
base=bindings/csharp/Wickra/runtimes
|
||
for target in "${!RID[@]}"; do
|
||
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
|
||
if [ -z "$archive" ]; then
|
||
echo "::error::missing native artifact for $target"; exit 1
|
||
fi
|
||
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
|
||
dest="$base/${RID[$target]}/native"; mkdir -p "$dest"
|
||
for f in libwickra.so libwickra.dylib wickra.dll; do
|
||
src=$(find "$tmp" -name "$f" | head -1)
|
||
[ -n "$src" ] && cp "$src" "$dest/"
|
||
done
|
||
echo "staged ${RID[$target]}:"; ls -l "$dest"
|
||
done
|
||
|
||
- name: Pack
|
||
shell: bash
|
||
run: |
|
||
version="${GITHUB_REF_NAME#v}"
|
||
dotnet pack bindings/csharp/Wickra/Wickra.csproj -c Release -p:Version="$version" -o nupkg
|
||
|
||
# Exchange the GitHub OIDC token for a short-lived (~1h) NuGet API key.
|
||
# 'user' is the nuget.org profile name (the package owner), not an email.
|
||
- name: NuGet login (OIDC -> temporary API key)
|
||
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
|
||
id: nuget_login
|
||
with:
|
||
user: KingchenC
|
||
|
||
# Pass the temporary key through the environment (not string-interpolated
|
||
# into the script) so it cannot be parsed as shell — avoids template injection.
|
||
- name: Push to NuGet
|
||
shell: bash
|
||
env:
|
||
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
|
||
run: |
|
||
dotnet nuget push "nupkg/*.nupkg" --api-key "$NUGET_API_KEY" \
|
||
--source https://api.nuget.org/v3/index.json --skip-duplicate
|
||
|
||
- name: Upload the NuGet package as a build artifact
|
||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||
with:
|
||
name: nuget-package
|
||
path: nupkg/*.nupkg
|
||
if-no-files-found: error
|
||
|
||
# Publish the Java binding to Maven Central. Independent of the GitHub-release
|
||
# job so a Java hiccup never blocks the C/C++ asset release. The Maven Central
|
||
# namespace (org.wickra) is verified; authentication uses the Sonatype Central
|
||
# Portal token (CENTRAL_USERNAME / CENTRAL_PASSWORD) and artifacts are signed
|
||
# with the GPG key (GPG_PRIVATE_KEY / GPG_PASSPHRASE). The pom version must
|
||
# match the release tag (kept in sync by the version-bump checklist).
|
||
java-publish:
|
||
name: Publish to Maven Central
|
||
needs: c-abi-build
|
||
runs-on: ubuntu-latest
|
||
environment: release
|
||
steps:
|
||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- name: Download the C ABI native libraries
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
pattern: c-abi-*
|
||
path: c-abi-artifacts
|
||
|
||
- name: Stage native libraries into resources/native/<os>-<arch>
|
||
shell: bash
|
||
run: |
|
||
set -e
|
||
declare -A PLAT=(
|
||
[x86_64-unknown-linux-gnu]=linux-x64
|
||
[aarch64-unknown-linux-gnu]=linux-arm64
|
||
[x86_64-apple-darwin]=osx-x64
|
||
[aarch64-apple-darwin]=osx-arm64
|
||
[x86_64-pc-windows-msvc]=win-x64
|
||
[aarch64-pc-windows-msvc]=win-arm64
|
||
)
|
||
base=bindings/java/src/main/resources/native
|
||
for target in "${!PLAT[@]}"; do
|
||
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
|
||
if [ -z "$archive" ]; then
|
||
echo "::error::missing native artifact for $target"; exit 1
|
||
fi
|
||
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
|
||
dest="$base/${PLAT[$target]}"; mkdir -p "$dest"
|
||
for f in libwickra.so libwickra.dylib wickra.dll; do
|
||
src=$(find "$tmp" -name "$f" | head -1)
|
||
[ -n "$src" ] && cp "$src" "$dest/"
|
||
done
|
||
echo "staged ${PLAT[$target]}:"; ls -l "$dest"
|
||
done
|
||
|
||
# setup-java writes a settings.xml with the 'central' server credentials
|
||
# (mapped from the env vars below) and imports the GPG signing key.
|
||
- name: Set up JDK 22 + Maven Central credentials + GPG
|
||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||
with:
|
||
distribution: temurin
|
||
java-version: "22"
|
||
server-id: central
|
||
server-username: CENTRAL_USERNAME
|
||
server-password: CENTRAL_PASSWORD
|
||
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||
gpg-passphrase: GPG_PASSPHRASE
|
||
|
||
- name: Deploy to Maven Central
|
||
env:
|
||
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
|
||
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
|
||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||
run: mvn -B -f bindings/java -Prelease deploy -DskipTests
|
||
|
||
# Mirror the in-repo Go module (bindings/go) to the standalone wickra-go
|
||
# repository so `go get github.com/wickra-lib/wickra-go` builds with no extra
|
||
# steps. The in-repo module keeps the prebuilt libraries git-ignored; the
|
||
# mirror commits them per platform under lib/<goos>_<goarch>/ alongside the
|
||
# vendored header. Independent of the GitHub-release job so a Go hiccup never
|
||
# blocks the C/C++ asset release. The push uses a fine-grained PAT
|
||
# (WICKRA_GO_MIRROR_TOKEN, contents:write on wickra-go); the mirror is a
|
||
# derived artifact, so its bot commit is intentionally unsigned.
|
||
go-mirror:
|
||
name: Mirror the Go module to wickra-go
|
||
needs: c-abi-build
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- name: Download the C ABI native libraries
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
pattern: c-abi-*
|
||
path: c-abi-artifacts
|
||
|
||
- name: Assemble the wickra-go module tree
|
||
shell: bash
|
||
run: |
|
||
set -e
|
||
out=wickra-go-module
|
||
mkdir -p "$out/include" "$out/lib"
|
||
# Single-package Go source + vendored C ABI header.
|
||
cp bindings/go/*.go "$out/"
|
||
cp bindings/go/include/wickra.h "$out/include/"
|
||
cp bindings/go/README.md "$out/"
|
||
# Ship the dual license so pkg.go.dev detects a redistributable license.
|
||
cp LICENSE-MIT LICENSE-APACHE "$out/"
|
||
# Standalone module path (the in-repo go.mod points at the subdir).
|
||
printf 'module github.com/wickra-lib/wickra-go\n\ngo 1.23\n' > "$out/go.mod"
|
||
# Point install instructions at the standalone module path.
|
||
sed -i 's#github.com/wickra-lib/wickra/bindings/go#github.com/wickra-lib/wickra-go#g' \
|
||
"$out/README.md" "$out"/*.go
|
||
# Stage each prebuilt library under lib/<goos>_<goarch>/ (committed in
|
||
# the mirror, unlike the in-repo lib/.gitignore). "<dir> <libfile>".
|
||
declare -A GO=(
|
||
[x86_64-unknown-linux-gnu]="linux_amd64 libwickra.so"
|
||
[aarch64-unknown-linux-gnu]="linux_arm64 libwickra.so"
|
||
[x86_64-apple-darwin]="darwin_amd64 libwickra.dylib"
|
||
[aarch64-apple-darwin]="darwin_arm64 libwickra.dylib"
|
||
[x86_64-pc-windows-msvc]="windows_amd64 wickra.dll"
|
||
[aarch64-pc-windows-msvc]="windows_arm64 wickra.dll"
|
||
)
|
||
for target in "${!GO[@]}"; do
|
||
read -r dir libfile <<< "${GO[$target]}"
|
||
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
|
||
if [ -z "$archive" ]; then
|
||
echo "::error::missing native artifact for $target"; exit 1
|
||
fi
|
||
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
|
||
src=$(find "$tmp" -name "$libfile" | head -1)
|
||
if [ -z "$src" ]; then
|
||
echo "::error::missing $libfile for $target"; exit 1
|
||
fi
|
||
mkdir -p "$out/lib/$dir"; cp "$src" "$out/lib/$dir/"
|
||
done
|
||
echo "assembled module:"; find "$out" -type f | sort
|
||
|
||
- name: Push to wickra-go and tag the release
|
||
shell: bash
|
||
env:
|
||
MIRROR_TOKEN: ${{ secrets.WICKRA_GO_MIRROR_TOKEN }}
|
||
run: |
|
||
set -e
|
||
version="${GITHUB_REF_NAME#v}"
|
||
remote="https://x-access-token:${MIRROR_TOKEN}@github.com/wickra-lib/wickra-go.git"
|
||
git clone -q "$remote" mirror-repo
|
||
cd mirror-repo
|
||
git config user.name "wickra-bot"
|
||
git config user.email "support@wickra.org"
|
||
git symbolic-ref HEAD refs/heads/main
|
||
# Replace all tracked content with the freshly assembled tree.
|
||
find . -mindepth 1 -maxdepth 1 -not -name .git -exec rm -rf {} +
|
||
cp -r ../wickra-go-module/. .
|
||
git add -A
|
||
if git diff --cached --quiet; then
|
||
echo "wickra-go already up to date; tagging only"
|
||
else
|
||
git -c commit.gpgsign=false commit -q -m "Release v$version"
|
||
fi
|
||
git push origin HEAD:refs/heads/main
|
||
git tag "v$version"
|
||
git push origin "v$version"
|
||
|
||
github-release:
|
||
name: Attach assets to the draft GitHub Release
|
||
needs: [cargo-publish, python-publish, node-publish, wasm-publish, c-abi-build]
|
||
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 }}
|
||
version: ${{ steps.tag.outputs.version }}
|
||
steps:
|
||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||
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 "version=${tag#v}" >> "$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 for 10 languages: native Rust, Python, Node.js, WASM plus a C ABI hub for C, C++, C#, Go, Java, R.
|
||
|
||
### Install
|
||
|
||
```bash
|
||
cargo add wickra
|
||
pip install wickra
|
||
npm install wickra
|
||
npm install wickra-wasm
|
||
dotnet add package Wickra
|
||
go get github.com/wickra-lib/wickra-go
|
||
```
|
||
|
||
Java (Gradle, Maven Central):
|
||
|
||
```gradle
|
||
implementation("org.wickra:wickra:${{ steps.tag.outputs.version }}")
|
||
```
|
||
|
||
R (r-universe):
|
||
|
||
```r
|
||
install.packages("wickra", repos = "https://wickra-lib.r-universe.dev")
|
||
```
|
||
|
||
### Attached assets
|
||
|
||
Pre-built artefacts for every supported platform — the same files this
|
||
workflow run published to crates.io, PyPI, and npm. C# ships to NuGet,
|
||
Java to Maven Central, Go to the wickra-go module, and R to r-universe
|
||
via their own release jobs.
|
||
|
||
- `*.whl` / `wickra-*.tar.gz` — Python wheels + sdist (5 platforms, ABI3 ≥ 3.9)
|
||
- `wickra.*.node` — native Node.js 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)
|
||
- `wickra-c-<target>.tar.gz` — C ABI: `include/wickra.h` + `wickra.hpp`
|
||
and the cdylib/staticlib per target (linux/macos/windows × x64/arm64),
|
||
the hub for C, C++, C#, Go, Java and R
|
||
|
||
### 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 }}" |