2be39b8b98
OpenSSF Scorecard's Signed-Releases check scans the GitHub Release *assets* for signed/provenance files (`*.intoto.jsonl`, `*.sig`, ...). It does not look at GitHub's separate attestations store, so although the attestations job has signed the published bytes since v0.4.0, the v0.4.0 release assets carried no provenance file and the check stayed at 0. Attach the Sigstore provenance bundle (already produced by actions/attest-build-provenance) to the release as `wickra-<tag>.provenance.intoto.jsonl`: - github-release now exposes its resolved tag as a job output. - attestations `needs: github-release` (so the Release already exists), gains `contents: write`, gives the attest step an id, and uploads the bundle with `gh release upload --clobber` (idempotent on re-runs). Publishes stay fully isolated — cargo/PyPI/npm all run upstream of github-release, so a Sigstore hiccup here can never block or corrupt a publish; at worst the release just lacks the provenance asset. Signed-Releases climbs over the next releases as each tag carries the bundle.
710 lines
30 KiB
YAML
710 lines
30 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
|
|
- 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
|
|
- 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
|
|
- 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
|
|
|
|
- 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 install
|
|
|
|
- name: Build native module
|
|
working-directory: bindings/node
|
|
run: npx napi build --platform --release --target ${{ matrix.target }}
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: bindings-${{ matrix.target }}
|
|
path: bindings/node/wickra.*.node
|
|
if-no-files-found: error
|
|
|
|
node-publish:
|
|
name: Publish to npm
|
|
needs: node-build
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
# `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
|
|
|
|
- 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 install
|
|
|
|
- name: Download all platform binaries
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
path: bindings/node/artifacts
|
|
pattern: bindings-*
|
|
|
|
# The npm/<platform>/package.json templates are committed to the
|
|
# repo (see bindings/node/npm/), so `napi artifacts` only needs to
|
|
# drop the freshly built .node binary into the matching directory.
|
|
- name: Move binaries into platform package layout
|
|
working-directory: bindings/node
|
|
run: npx napi artifacts --dir artifacts
|
|
|
|
# Publish each platform package individually. Skip versions that are
|
|
# already on npm. A first-attempt 403 from npm's spam filter is
|
|
# tolerated for a single 30-second retry — that historically clears
|
|
# rate-limit-driven false positives. Anything that still fails after
|
|
# the retry is a *real* failure (the platform binary will be missing
|
|
# from `optionalDependencies` and Windows-style installs will break,
|
|
# exactly the regression that produced audit finding R20) — fail the
|
|
# job loudly so the release does not silently land in a half-published
|
|
# state. Previously this loop swallowed the second-attempt failure with
|
|
# a `::warning::` and `return 0`; that mask is removed.
|
|
- name: Publish platform packages (idempotent)
|
|
working-directory: bindings/node
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
run: |
|
|
set +e
|
|
version=$(node -p "require('./package.json').version")
|
|
fail=0
|
|
publish_dir() {
|
|
local dir=$1
|
|
local pkg=$(basename "$dir")
|
|
local pkgname="wickra-$pkg"
|
|
local existing
|
|
existing=$(npm view "$pkgname@$version" version 2>/dev/null)
|
|
if [ "$existing" = "$version" ]; then
|
|
echo "::notice::skip $pkgname@$version (already on npm)"
|
|
return 0
|
|
fi
|
|
echo "::group::publish $pkgname@$version"
|
|
# --ignore-scripts: a per-platform package must never run lifecycle
|
|
# scripts during publish (npm runs prepublishOnly/prepare/etc. from
|
|
# the package being published — a malicious or stray script would
|
|
# execute with the npm token in the environment).
|
|
(cd "$dir" && npm publish --access public --ignore-scripts --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
|
|
|
|
- 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 = 'PolyForm-Noncommercial-1.0.0';
|
|
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
|
"
|
|
|
|
- name: Pack wickra-wasm for release attachment
|
|
working-directory: bindings/wasm/pkg
|
|
run: npm pack
|
|
|
|
- name: Upload WASM tarball
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: wasm-tarball
|
|
path: bindings/wasm/pkg/wickra-wasm-*.tgz
|
|
|
|
- name: Publish wickra-wasm to npm (idempotent)
|
|
working-directory: bindings/wasm/pkg
|
|
run: |
|
|
out=$(npm publish --access public --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.
|
|
# --------------------------------------------------------------------------
|
|
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
|
|
# 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:
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve target tag
|
|
id: tag
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
|
tag="${{ github.ref_name }}"
|
|
else
|
|
# workflow_dispatch / non-tag push: attach to the latest v* tag.
|
|
tag=$(git tag --list 'v*' --sort=-v:refname | head -n1)
|
|
fi
|
|
if [ -z "$tag" ]; then
|
|
echo "::error::no v* tag found to attach assets to"
|
|
exit 1
|
|
fi
|
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::attaching assets to release $tag"
|
|
|
|
- name: Download all build artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
path: artifacts
|
|
|
|
- name: Stage release assets
|
|
run: |
|
|
set -e
|
|
mkdir -p release-assets
|
|
# Python wheels + sdist (5 wheel artifacts + 1 sdist artifact).
|
|
find artifacts -type f -name "*.whl" -exec cp {} release-assets/ \;
|
|
find artifacts -type f -name "*.tar.gz" -exec cp {} release-assets/ \;
|
|
# Native Node binaries (one per platform).
|
|
find artifacts -type f -name "*.node" -exec cp {} release-assets/ \;
|
|
# Node npm-pack tarballs (main + per-platform).
|
|
find artifacts -type f -name "wickra-*.tgz" -exec cp {} release-assets/ \;
|
|
# Cargo .crate files (one per workspace member).
|
|
find artifacts -type f -name "*.crate" -exec cp {} release-assets/ \;
|
|
# 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 GitHub Release with assets
|
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
with:
|
|
tag_name: ${{ steps.tag.outputs.tag }}
|
|
name: Wickra ${{ steps.tag.outputs.tag }}
|
|
files: release-assets/*
|
|
generate_release_notes: true
|
|
fail_on_unmatched_files: false
|
|
body: |
|
|
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries.
|
|
|
|
### Install
|
|
|
|
```bash
|
|
cargo add wickra
|
|
pip install wickra
|
|
npm install wickra
|
|
npm install wickra-wasm
|
|
```
|
|
|
|
### Attached assets
|
|
|
|
Pre-built artefacts for every supported platform — the same files that
|
|
were uploaded to crates.io, PyPI, and npm by this workflow run.
|
|
|
|
- `*.whl` / `wickra-*.tar.gz` — Python wheels + sdist (5 platforms, ABI3 ≥ 3.9)
|
|
- `wickra.*.node` — native Node bindings (linux-x64-gnu, darwin-x64,
|
|
darwin-arm64, win32-x64-msvc)
|
|
- `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM)
|
|
- `*.crate` — cargo source crates (wickra-core, wickra-data, wickra)
|
|
|
|
### Auto-generated changelog
|
|
|
|
See below; GitHub computes it from the commits since the previous tag.
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 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 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. A
|
|
# failure here still only costs the provenance asset; the release is already
|
|
# complete.
|
|
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 }}" |