Skip to content

chore(release): bump to v0.20.0 (#296) #46

chore(release): bump to v0.20.0 (#296)

chore(release): bump to v0.20.0 (#296) #46

Workflow file for this run

name: Release
# Releases publish to registries + write SLSA attestations. Cancelling
# mid-publish leaves external state inconsistent. Group for
# serialization (one release per tag), never cancel.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
on:
push:
tags:
- "v*"
permissions:
contents: write
id-token: write
attestations: write
env:
CARGO_TERM_COLOR: always
jobs:
# ── Version consistency check ──────────────────────────────────────────
check-versions:
name: Verify version consistency
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Check tag matches Cargo.toml and package.json
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
VSIX_VERSION=$(node -p "require('./vscode-spar/package.json').version")
echo "tag=$TAG_VERSION cargo=$CARGO_VERSION vsix=$VSIX_VERSION"
if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then
echo "::error::Tag ($TAG_VERSION) does not match Cargo.toml ($CARGO_VERSION)"
exit 1
fi
if [ "$TAG_VERSION" != "$VSIX_VERSION" ]; then
echo "::error::Tag ($TAG_VERSION) does not match package.json ($VSIX_VERSION)"
exit 1
fi
echo "All versions match: $TAG_VERSION"
# ── Cross-platform binary builds ──────────────────────────────────────
build-binaries:
name: Build ${{ matrix.target }}
needs: [check-versions]
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
cross: true
- target: x86_64-apple-darwin
os: macos-14
archive: tar.gz
- target: aarch64-apple-darwin
os: macos-latest
archive: tar.gz
- target: x86_64-pc-windows-msvc
os: windows-latest
archive: zip
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
- name: Install cross
if: matrix.cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build (native)
if: ${{ !matrix.cross }}
run: cargo build --release --target ${{ matrix.target }} -p spar
- name: Build (cross)
if: matrix.cross
run: cross build --release --target ${{ matrix.target }} -p spar
- name: Strip binary (Unix)
if: runner.os != 'Windows'
env:
TARGET: ${{ matrix.target }}
run: strip "target/${TARGET}/release/spar" 2>/dev/null || true
- name: Package (tar.gz)
if: matrix.archive == 'tar.gz'
env:
TARGET: ${{ matrix.target }}
run: |
VERSION="${GITHUB_REF#refs/tags/}"
ARCHIVE="spar-${VERSION}-${TARGET}.tar.gz"
mkdir -p staging
cp "target/${TARGET}/release/spar" staging/
tar -czf "$ARCHIVE" -C staging .
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"
- name: Package (zip)
if: matrix.archive == 'zip'
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
VERSION="${GITHUB_REF#refs/tags/}"
ARCHIVE="spar-${VERSION}-${TARGET}.zip"
mkdir -p staging
cp "target/${TARGET}/release/spar.exe" staging/
cd staging && 7z a "../$ARCHIVE" . && cd ..
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"
- uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: ${{ env.ARCHIVE }}
# ── Compliance report (HTML export via rivet) ─────────────────────────
build-compliance:
name: Build compliance report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate compliance report
id: report
uses: pulseengine/rivet/.github/actions/compliance@main
with:
theme: dark
rivet-version: v0.1.0
# Emit artifacts.yaml (generic-yaml) into the bundle so the website's
# fetch-reports can auto-generate data/spar/{stats,artifacts}.json.
include-data-formats: true
- uses: actions/upload-artifact@v4
with:
name: compliance-report
path: ${{ steps.report.outputs.archive-path }}
# ── Test evidence bundle ──────────────────────────────────────────────
build-test-evidence:
name: Build test evidence
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- name: Install tools
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest,cargo-llvm-cov
- name: Run tests with JUnit XML
run: |
mkdir -p test-evidence/test-results
cargo nextest run --workspace --profile ci
cp target/nextest/ci/junit.xml test-evidence/test-results/junit.xml
- name: Generate coverage
run: |
mkdir -p test-evidence/coverage
cargo llvm-cov --workspace --lcov --output-path test-evidence/coverage/lcov.info
cargo llvm-cov report > test-evidence/coverage/summary.txt
- name: Run spar analyze on test models
run: |
mkdir -p test-evidence/validation
set +e
cargo run --release -- analyze --root Analysis_Pkg::Full_System.Impl \
test-data/analysis_test.aadl > test-evidence/validation/analyze-output.txt 2>&1
rc=$?
set -e
echo "exit_code=${rc}" >> test-evidence/validation/analyze-output.txt
- name: Generate metadata
run: |
TAG="${GITHUB_REF#refs/tags/}"
jq -n \
--arg tag "${TAG}" \
--arg commit "${GITHUB_SHA}" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg rust_version "$(rustc --version)" \
--arg os "$(uname -srm)" \
'{tag: $tag, commit: $commit, timestamp: $timestamp, rust_version: $rust_version, os: $os}' \
> test-evidence/metadata.json
- name: Package
run: |
VERSION="${GITHUB_REF#refs/tags/}"
tar czf "spar-${VERSION}-test-evidence.tar.gz" test-evidence/
- uses: actions/upload-artifact@v4
with:
name: test-evidence
path: spar-*-test-evidence.tar.gz
# ── Browser-ready spar-wasm bundle (jco transpile) ───────────────────
# Builds spar-wasm for wasm32-wasip2 and transpiles the component to a
# browser-loadable bundle (spar_wasm.js + spar_wasm.core*.wasm) that
# downstream consumers (rivet's dashboard/exports) embed without a local
# toolchain. The MILP solver (good_lp/HiGHS) is excluded from this build
# via spar-wasm's default-features = false (issue #259).
build-wasm:
name: Build spar-wasm browser bundle
needs: [check-versions]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-wasip2
- uses: Swatinem/rust-cache@v2
with:
key: release-wasm32-wasip2
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Build spar-wasm (wasm32-wasip2)
run: cargo build --release --target wasm32-wasip2 -p spar-wasm
- name: Transpile to browser bundle (jco)
run: |
set -euo pipefail
mkdir -p dist/wasm
# Pinned jco version for a deterministic, supply-chain-stable bundle.
npx --yes @bytecodealliance/jco@1.4.0 transpile \
--instantiation async \
target/wasm32-wasip2/release/spar_wasm.wasm \
-o dist/wasm/
ls -la dist/wasm/
- name: Package browser bundle
env:
GH_REF: ${{ github.ref }}
run: |
set -euo pipefail
VERSION="${GH_REF#refs/tags/}"
# spar_wasm.js + spar_wasm.core*.wasm + .d.ts + interfaces/
tar -C dist/wasm -czf "spar-wasm-browser-${VERSION}.tar.gz" .
ls -la "spar-wasm-browser-${VERSION}.tar.gz"
- uses: actions/upload-artifact@v4
with:
name: wasm-bundle
path: spar-wasm-browser-*.tar.gz
# ── VS Code Extension (per-platform) ─────────────────────────────────
build-vsix:
name: Build VS Code Extension (${{ matrix.target }})
needs: [build-binaries]
runs-on: ubuntu-latest
strategy:
matrix:
include:
- target: darwin-arm64
rust-target: aarch64-apple-darwin
binary: spar
- target: darwin-x64
rust-target: x86_64-apple-darwin
binary: spar
- target: linux-x64
rust-target: x86_64-unknown-linux-gnu
binary: spar
- target: linux-arm64
rust-target: aarch64-unknown-linux-gnu
binary: spar
- target: win32-x64
rust-target: x86_64-pc-windows-msvc
binary: spar.exe
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Download binary for ${{ matrix.rust-target }}
uses: actions/download-artifact@v4
with:
name: binary-${{ matrix.rust-target }}
path: binary-artifact
- name: Extract and place binary
run: |
mkdir -p vscode-spar/bin
cd binary-artifact
if ls *.tar.gz 1>/dev/null 2>&1; then
tar -xzf *.tar.gz
elif ls *.zip 1>/dev/null 2>&1; then
unzip *.zip
fi
cp ${{ matrix.binary }} ../vscode-spar/bin/${{ matrix.binary }}
chmod +x ../vscode-spar/bin/${{ matrix.binary }} 2>/dev/null || true
- name: Install and compile extension
working-directory: vscode-spar
run: npm install && npm run compile
- name: Package platform VSIX
working-directory: vscode-spar
run: npx @vscode/vsce package --target ${{ matrix.target }} --no-dependencies
- uses: actions/upload-artifact@v4
with:
name: vsix-${{ matrix.target }}
path: vscode-spar/*.vsix
# ── Publish to VS Code Marketplace ──────────────────────────────────
publish-vsix:
name: Publish to Marketplace
needs: [build-vsix, create-release]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: vsix-*
path: vsix
merge-multiple: true
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Publish to VS Code Marketplace
run: |
if [ -n "$VSCE_PAT" ]; then
npx @vscode/vsce publish --packagePath vsix/*.vsix
else
echo "VSCE_PAT not set, skipping marketplace publish"
fi
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
# ── SBOM (Software Bill of Materials) ─────────────────────────────────
build-sbom:
name: Generate SBOM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install cargo-cyclonedx
uses: taiki-e/install-action@v2
with:
tool: cargo-cyclonedx
- name: Generate CycloneDX SBOM
run: |
set -euo pipefail
# cargo-cyclonedx does not proxy cargo's `-p` package-selection
# flag, so we point `--manifest-path` at the CLI crate directly
# (mirrors the synth reference). The generated SBOM lands next
# to that Cargo.toml.
cargo cyclonedx \
--manifest-path crates/spar-cli/Cargo.toml \
--format json \
--spec-version 1.5
mkdir -p sbom-out
SBOM_SRC="crates/spar-cli/spar.cdx.json"
if [ ! -f "$SBOM_SRC" ]; then
SBOM_SRC="$(find crates/spar-cli -maxdepth 2 -name '*.cdx.json' | head -1)"
fi
test -n "$SBOM_SRC" && test -f "$SBOM_SRC"
# Tag-version stamping (`spar-<bare>.cdx.json`) happens at
# release-assets flatten time in the create-release job.
cp "$SBOM_SRC" sbom-out/spar.cdx.json
ls -la sbom-out/
- uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom-out/spar.cdx.json
# ── Create GitHub Release ─────────────────────────────────────────────
#
# Follows the pulseengine standard release-artifact flow (synth is the
# reference; see pulseengine/synth/.github/workflows/release.yml).
# Per-tag invariant set of assets:
#
# spar-vX.Y.Z-<triple>.{tar.gz|zip} one per build target
# spar-X.Y.Z.cdx.json CycloneDX SBOM (toolchain)
# SHA256SUMS.txt single sums file (no per-file sidecars)
# SHA256SUMS.txt.sig detached cosign signature
# SHA256SUMS.txt.pem Fulcio cert
# SHA256SUMS.txt.cosign.bundle verifier-friendly cosign bundle
# build-env.txt rustc/cargo/cosign/runner versions
#
# Plus, by historical contract for this repo: the platform VSIXes and
# the compliance-report archive, both folded into SHA256SUMS.txt.
create-release:
name: Create GitHub Release
needs: [build-binaries, build-compliance, build-test-evidence, build-vsix, build-sbom, build-wasm]
runs-on: ubuntu-latest
# Restated at job-level: actions/attest-build-provenance + cosign
# keyless OIDC need both `id-token: write` and `attestations: write`.
# Workflow-level permissions grant the same trio — restated here to
# keep the job locally self-explanatory.
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Flatten release assets
env:
GH_REF: ${{ github.ref }}
run: |
set -euo pipefail
mkdir -p release-assets
# tar.gz / zip / vsix flow through unchanged.
find artifacts -type f \
\( -name "*.tar.gz" -o -name "*.zip" -o -name "*.vsix" \) \
-exec cp {} release-assets/ \;
# Version-stamp the SBOM per the release-asset naming
# standard: <tool>-X.Y.Z.cdx.json (bare version, no `v`).
VERSION="${GH_REF#refs/tags/}"
BARE="${VERSION#v}"
SBOM_SRC="$(find artifacts -type f -name '*.cdx.json' | head -1)"
if [ -n "$SBOM_SRC" ]; then
cp "$SBOM_SRC" "release-assets/spar-${BARE}.cdx.json"
else
echo "::error::No CycloneDX SBOM found in downloaded artifacts."
exit 1
fi
ls -la release-assets/
# Generate SHA256SUMS *before* attest/sign so its content is
# stable and the cosign signature pins it. All non-signature
# assets get checksummed (SLSA attestation jsonl, .sig/.pem/.bundle
# are added after this step and are not in the sums file).
- name: Generate SHA256 checksums
run: |
set -euo pipefail
cd release-assets
sha256sum ./* > SHA256SUMS.txt
cat SHA256SUMS.txt
# ── SLSA build provenance (GitHub-native) ──────────────────────────
# actions/attest-build-provenance@v2 generates an in-toto SLSA v1
# provenance statement for every binary archive, signs it keyless
# via Sigstore (Fulcio cert bound to this workflow's OIDC identity),
# and records it in the Rekor transparency log. Consumers verify
# with `gh attestation verify <file> --repo pulseengine/spar`.
# GitHub-native attestation (not the standalone SLSA generator)
# keeps the workflow self-contained. spar ships both .tar.gz
# (Unix) and .zip (Windows), so both globs are attested.
- name: Generate SLSA build provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: |
release-assets/*.tar.gz
release-assets/*.zip
# ── Sigstore keyless signing (cosign) ──────────────────────────────
# Signs SHA256SUMS.txt so a consumer can verify the checksum file
# itself was produced by this workflow (closes the gap where an
# attacker who can replace a release asset could also replace the
# plain checksum file). Mirrors the pulseengine/synth and
# pulseengine/witness cosign sign-blob pattern. The .cosign.bundle
# is the verifier-friendly artifact; .sig + .pem are the detached
# signature and Fulcio certificate. Verify with:
# cosign verify-blob \
# --certificate-identity-regexp \
# 'https://github.com/pulseengine/spar/.github/workflows/release.yml@.*' \
# --certificate-oidc-issuer \
# 'https://token.actions.githubusercontent.com' \
# --bundle SHA256SUMS.txt.cosign.bundle \
# SHA256SUMS.txt
- name: Install cosign
uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: Sign SHA256SUMS with cosign (keyless OIDC)
run: |
set -euo pipefail
cd release-assets
cosign sign-blob \
--yes \
--bundle SHA256SUMS.txt.cosign.bundle \
--output-signature SHA256SUMS.txt.sig \
--output-certificate SHA256SUMS.txt.pem \
SHA256SUMS.txt
echo "::notice::SHA256SUMS signed via Sigstore keyless flow."
ls -la ./*
- name: Capture build environment
run: |
set -euo pipefail
{
echo "rustc: $(rustc --version)"
echo "cargo: $(cargo --version)"
echo "cosign: $(cosign version 2>&1 | head -1)"
echo "runner: $(uname -srm)"
} > release-assets/build-env.txt
cat release-assets/build-env.txt
- name: Create or update GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
VERSION="$GH_REF_NAME"
# Idempotent: re-running the workflow for an existing release
# uploads/overwrites assets rather than failing. --clobber lets
# a re-run replace assets a previous partial run left behind.
if gh release view "$VERSION" >/dev/null 2>&1; then
echo "::notice::Release $VERSION exists; uploading assets"
gh release upload "$VERSION" --clobber release-assets/*
else
echo "::notice::Creating Release $VERSION with assets"
gh release create "$VERSION" \
--title "spar $VERSION" \
--generate-notes \
release-assets/*
fi