opi 0.13.2

Operations Interface — a project control center for your terminal
name: Release

# A version tag verifies the crate, builds the release binaries, publishes to
# crates.io and creates the GitHub release. The binaries are what install.sh
# downloads.
#
# Publishing uses crates.io trusted publishing: the job exchanges a GitHub OIDC
# token for a short-lived crates.io token, so no registry token is stored
# anywhere. It runs in the `crates-io` environment, whose required reviewer has
# to approve it — releasing stays a deliberate step rather than a side effect
# of pushing a tag. It waits for the binaries, because a publish cannot be
# undone and a version without binaries would break install.sh.
#
# Every archive carries a signed build provenance attestation (Sigstore, via
# actions/attest-build-provenance), next to the per-archive .sha256 that
# install.sh checks and a combined SHA256SUMS. The binaries are built with
# cargo auditable, which embeds the dependency list in the binary so that
# `cargo audit bin` can check a downloaded one. The release also carries a
# CycloneDX SBOM, attested against every archive (actions/attest-sbom).
#
# workflow_dispatch runs the verification and the builds without releasing, as
# a dry run before tagging.

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always

jobs:
  verify:
    name: Verify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt
      - run: cargo fmt --check
      - run: cargo clippy --all-targets --all-features -- -D warnings
      - run: cargo test --all-features
      - name: Verify package
        run: cargo package --locked
      - name: Tag matches crate version
        if: startsWith(github.ref, 'refs/tags/')
        run: |
          crate_version="$(cargo metadata --format-version 1 --no-deps \
            | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
          tag_version="${GITHUB_REF_NAME#v}"
          echo "crate=$crate_version tag=$tag_version"
          test "$crate_version" = "$tag_version"

  binaries:
    name: Binary (${{ matrix.target }})
    needs: verify
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read
      id-token: write
      attestations: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: aarch64-apple-darwin
            os: macos-latest
          - target: x86_64-apple-darwin
            os: macos-latest
          # Linux is built against musl so one binary runs everywhere rather
          # than against the glibc of whatever runner built it.
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: aarch64-unknown-linux-musl
            os: ubuntu-24.04-arm
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - name: Install musl tooling
        if: endsWith(matrix.target, '-musl')
        run: |
          sudo apt-get update
          sudo apt-get install -y musl-tools
      - name: Install cargo-auditable
        run: cargo install cargo-auditable --version 0.7.7 --locked
      - run: cargo auditable build --release --locked --target ${{ matrix.target }}
      - name: Archive
        run: |
          version="$(cargo metadata --format-version 1 --no-deps \
            | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
          name="opi-${version}-${{ matrix.target }}"
          mkdir -p "dist/${name}"
          cp "target/${{ matrix.target }}/release/opi" "dist/${name}/opi"
          cp README.md LICENSE "dist/${name}/"
          tar -czf "dist/${name}.tar.gz" -C dist "${name}"
          rm -rf "dist/${name}"
          cd dist
          if command -v sha256sum >/dev/null 2>&1; then
            sha256sum "${name}.tar.gz" > "${name}.tar.gz.sha256"
          else
            shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256"
          fi
      - name: Attest build provenance
        if: startsWith(github.ref, 'refs/tags/')
        uses: actions/attest-build-provenance@v3
        with:
          subject-path: dist/*.tar.gz
      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.target }}
          path: dist/*
          if-no-files-found: error

  publish:
    name: Publish to crates.io
    if: startsWith(github.ref, 'refs/tags/')
    needs: [verify, binaries]
    runs-on: ubuntu-latest
    timeout-minutes: 15
    environment: crates-io
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: rust-lang/crates-io-auth-action@v1
        id: auth
      - run: cargo publish --locked
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

  release:
    name: GitHub Release
    if: startsWith(github.ref, 'refs/tags/')
    needs: [verify, binaries, publish]
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
      attestations: write
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true
      - name: Combined checksums
        working-directory: dist
        run: cat ./*.tar.gz.sha256 > SHA256SUMS
      - name: SBOM
        run: |
          cargo install cargo-cyclonedx --version 0.5.9 --locked
          cargo cyclonedx --format json --override-filename "opi-${GITHUB_REF_NAME}.cdx"
          mv "opi-${GITHUB_REF_NAME}.cdx.json" dist/
      - name: Attest SBOM
        uses: actions/attest-sbom@v4
        with:
          subject-path: dist/*.tar.gz
          sbom-path: dist/opi-${{ github.ref_name }}.cdx.json
      - uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: dist/*