noyalib-wasm 0.0.15

WebAssembly (wasm-bindgen) bindings for the noyalib YAML library — browser-ready parse / serialise / lossless edit
Documentation
# SPDX-FileCopyrightText: 2026 Noyalib
# SPDX-License-Identifier: MIT OR Apache-2.0

name: Release

# Satellite release workflow — patterns match the parent
# `sebastienrousseau/noyalib`'s `release.yml`:
#   validate → artifacts (SLSA L3 + sigstore + SBOM) → cross-verify
#   → github-release → crates-io → npm-publish
#
# Consumed on `v*.*.*` tag pushes and via manual dispatch (with
# dry-run gate). Strict-lockstep versioning (ADR-0005) requires
# the tag version match this crate's Cargo.toml version AND the
# `noyalib` dep's exact-match pin — both are verified in `validate`.

on:
  push:
    tags: ['v[0-9]+.[0-9]+.[0-9]+*']
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Dry run (skip publish + npm)'
        type: boolean
        default: true

# Least-privilege default: top-level workflow only reads. Jobs
# that need write scope get it locally in their `permissions:` block.
permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: '0'

jobs:
  # ── Quality gate ────────────────────────────────────────────────────
  validate:
    name: Validate
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0

      - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88
        with:
          toolchain: stable
          components: rustfmt, clippy

      # CACHE-POISONING GUARD: release-time gate uses an isolated
      # CARGO_TARGET_DIR so stale artefacts from a PR build can't
      # mask a release regression.
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          workspaces: ". -> target-release-validate"
          key: release-validate

      - name: Quality checks
        env:
          CARGO_TARGET_DIR: ${{ github.workspace }}/target-release-validate
        run: |
          cargo fmt --all -- --check
          cargo clippy --all-targets --all-features -- -D warnings
          cargo test --all-features --locked
          cargo doc --no-deps --all-features --locked

      - name: Verify tag matches Cargo.toml version
        if: startsWith(github.ref, 'refs/tags/v')
        run: |
          TAG_VERSION="${GITHUB_REF_NAME#v}"
          CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then
            echo "::error::Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)"
            exit 1
          fi
          # Strict-lockstep (ADR-0005): the `noyalib` dep must be
          # pinned to the exact same version as this crate.
          NOYALIB_PIN=$(grep -E '^noyalib\s*=' Cargo.toml | head -1 | sed 's/.*"=\?\([0-9.]*\)".*/\1/')
          if [ "$NOYALIB_PIN" != "$CARGO_VERSION" ]; then
            echo "::error::noyalib dep pin ($NOYALIB_PIN) is out of lockstep with noyalib-wasm ($CARGO_VERSION)"
            exit 1
          fi
          echo "Version verified: tag=$TAG_VERSION, crate=$CARGO_VERSION, noyalib dep pin=$NOYALIB_PIN"

  # ── Build checksums, SBOM, SLSA L3 provenance, sigstore signatures ──
  artifacts:
    name: Build artifacts
    needs: validate
    runs-on: ubuntu-latest
    # `id-token: write` — Fulcio keyless signing exchanges the
    #   OIDC token for an ephemeral cert that signs each .crate.
    # `attestations: write` — SLSA L3 build-provenance uploaded
    #   into the workflow's attestation store.
    # `contents: read` — checkout / source access only.
    permissions:
      contents: read
      id-token: write
      attestations: write
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

      - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88
        with:
          toolchain: stable

      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

      - name: Package crate
        run: cargo package --all-features --allow-dirty

      - name: Generate SBOM
        run: |
          cargo tree --edges normal --prefix depth --format '{p} {l}' > SBOM.txt

      - name: Generate checksums
        run: |
          set -euo pipefail
          cd target/package
          : > SHA256SUMS.txt
          : > SHA512SUMS.txt
          for f in *.crate; do
            sha256sum "$f" >> SHA256SUMS.txt
            sha512sum "$f" >> SHA512SUMS.txt
          done
          ls *.crate > CRATE_FILES.txt

      # ── SLSA L3 build provenance ───────────────────────────────────
      # Records the exact source commit, builder identity, and
      # invocation parameters into the public Rekor transparency
      # log. Verify later via `gh attestation verify`.
      - name: Attest build provenance (SLSA L3)
        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
        with:
          subject-path: |
            target/package/*.crate
            SBOM.txt

      # ── Keyless sigstore signing ──────────────────────────────────
      # `cosign sign-blob` without `--key` triggers Fulcio keyless
      # signing: the workflow's OIDC token is exchanged for an
      # ephemeral certificate bound to this repo + ref + workflow;
      # the artefact is signed, and signature + cert are recorded
      # in Rekor. Verifiers use `cosign verify-blob --bundle` per
      # SECURITY.md.
      - name: Install cosign
        uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
      - name: Sign release artefacts (keyless)
        env:
          COSIGN_YES: "true"
        run: |
          set -euo pipefail
          cd target/package
          for f in *.crate; do
            cosign sign-blob --bundle "${f}.bundle" "$f"
          done
          cd -
          cosign sign-blob --bundle "SBOM.txt.bundle" SBOM.txt

      - name: Upload release artifacts
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: release-artifacts
          path: |
            target/package/*.crate
            target/package/*.bundle
            target/package/SHA256SUMS.txt
            target/package/SHA512SUMS.txt
            SBOM.txt
            SBOM.txt.bundle
          retention-days: 5

  # ── Cross-platform verification ─────────────────────────────────────
  # noyalib-wasm is a wasm-bindgen crate; end-to-end coverage is via
  # `wasm-bindgen-test` under a headless browser on ubuntu-latest
  # (native `cargo test` on macOS/Windows carries no additional
  # information for a wasm-target crate).
  cross-verify:
    name: Verify (ubuntu-latest)
    needs: validate
    runs-on: ubuntu-latest
    permissions:
      contents: read
    env:
      CARGO_TARGET_DIR: ${{ github.workspace }}/target-release-verify-ubuntu
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88
        with:
          toolchain: stable
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          workspaces: ". -> target-release-verify-ubuntu"
          key: release-verify-ubuntu
      - run: cargo test --all-features --locked

  # ── GitHub Release ──────────────────────────────────────────────────
  github-release:
    name: GitHub Release
    needs: [artifacts, cross-verify]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    # `contents: write` — required to create the GitHub Release.
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0

      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: release-artifacts
          path: artifacts

      - name: Generate release notes
        run: |
          TAG="${GITHUB_REF_NAME}"
          PREV_TAG=$(git tag --sort=-version:refname | grep -v "$TAG" | head -1 || echo "")
          if [ -n "$PREV_TAG" ]; then
            RANGE="${PREV_TAG}..${TAG}"
          else
            RANGE="${TAG}"
          fi
          echo "## What's Changed" > RELEASE_NOTES.md
          echo "" >> RELEASE_NOTES.md
          git log "$RANGE" --pretty=format:"- %s (%h)" --no-merges >> RELEASE_NOTES.md
          echo "" >> RELEASE_NOTES.md
          echo "" >> RELEASE_NOTES.md
          echo "## Checksums" >> RELEASE_NOTES.md
          echo '```' >> RELEASE_NOTES.md
          cat artifacts/target/package/SHA256SUMS.txt >> RELEASE_NOTES.md
          echo '```' >> RELEASE_NOTES.md

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release create "$GITHUB_REF_NAME" \
            --title "noyalib-wasm $GITHUB_REF_NAME" \
            --notes-file RELEASE_NOTES.md \
            artifacts/target/package/*.crate \
            artifacts/target/package/*.bundle \
            artifacts/target/package/SHA256SUMS.txt \
            artifacts/target/package/SHA512SUMS.txt \
            artifacts/SBOM.txt \
            artifacts/SBOM.txt.bundle

  # ── Publish to crates.io ────────────────────────────────────────────
  crates-io:
    name: Publish to crates.io
    needs: [github-release]
    if: startsWith(github.ref, 'refs/tags/v') && github.event.inputs.dry_run != 'true'
    runs-on: ubuntu-latest
    environment: crates-io
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88
        with:
          toolchain: stable
      - name: Publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -euxo pipefail
          cargo publish --all-features --locked

  # ── Publish to npm (wasm-pack) ──────────────────────────────────────
  npm-publish:
    name: Publish @sebastienrousseau/noyalib-wasm to npm
    needs: [github-release]
    if: startsWith(github.ref, 'refs/tags/v') && github.event.inputs.dry_run != 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      # `id-token: write` — needed for npm Trusted Publishing OIDC
      # exchange + `--provenance` attestation.
      id-token: write
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          ref: ${{ github.ref }}

      - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88
        with:
          toolchain: stable
          targets:   wasm32-unknown-unknown

      - uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
        with:
          version: 'latest'

      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'

      - name: Build (wasm-pack)
        run: |
          wasm-pack build --release --target bundler --scope sebastienrousseau

      - name: Publish (Trusted Publishing + provenance)
        run: |
          cd pkg
          npm publish --provenance --access public