noya-cli 0.0.15

Command-line tools for noyalib (noyafmt, noyavalidate)
Documentation
# SPDX-FileCopyrightText: 2026 Noyalib
# SPDX-License-Identifier: MIT OR Apache-2.0

name: Release

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

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: '0'

jobs:
  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
      - 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 + noyalib pin
        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 ($TAG_VERSION) != Cargo.toml ($CARGO_VERSION)"
            exit 1
          fi
          NOYALIB_PIN=$(grep -E '^noyalib\s*=' Cargo.toml | head -1 | sed 's/.*"=\?\([0-9.]*\)".*/\1/')
          if [ "$NOYALIB_PIN" != "$CARGO_VERSION" ]; then
            echo "::error::noyalib pin ($NOYALIB_PIN) out of lockstep with noya-cli ($CARGO_VERSION)"
            exit 1
          fi

  artifacts:
    name: Build artifacts
    needs: validate
    runs-on: ubuntu-latest
    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
      - name: Attest build provenance (SLSA L3)
        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
        with:
          subject-path: |
            target/package/*.crate
            SBOM.txt
      - 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-verify:
    name: Verify (${{ matrix.os }})
    needs: validate
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    env:
      CARGO_TARGET_DIR: ${{ github.workspace }}/target-release-verify-${{ matrix.os }}
    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-${{ matrix.os }}"
          key: release-verify-${{ matrix.os }}
      - run: cargo test --all-features --locked

  github-release:
    name: GitHub Release
    needs: [artifacts, cross-verify]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    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 "noya-cli $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

  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: cargo publish --all-features --locked