chore(release): bump to v0.20.0 (#296) #46
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |