lumen 2.32.0

lumen is a command-line tool that uses AI to generate commit messages, summarise git diffs or past commits, and more.
# Release workflow: builds macOS + Linux binaries, creates a GitHub release,
# publishes to crates.io, and updates the Homebrew tap.
#
# Trigger manually from the Actions tab (or `gh workflow run release.yml`):
#   - version: the new version to release (e.g. 2.31.0)
#   - dry_run: build everything and upload artifacts, but do NOT push a tag,
#     create a release, or publish anywhere. Use this to test the pipeline.
#
# One-time setup required for the publish steps (all publish jobs run in the
# "release" environment — restricted to main — so add secrets as environment
# secrets on that environment, not repo secrets):
#   - crates.io: either add a CARGO_REGISTRY_TOKEN environment secret, or
#     configure Trusted Publishing on crates.io (Settings -> Trusted
#     Publishing) for repository jnsahaj/lumen, workflow file release.yml,
#     environment "release".
#   - Homebrew: add a TAP_GITHUB_TOKEN environment secret — a fine-grained
#     PAT with "Contents: read and write" on jnsahaj/homebrew-lumen. If
#     missing, the Homebrew step is skipped (everything else still works).
#
# Supply-chain hardening applied here (keep these invariants when editing):
#   - Every action is pinned to a full commit SHA (mutable tags can be
#     repointed by a compromised action maintainer — tj-actions, March 2025).
#   - No build caches: release binaries are always built from pristine
#     sources so a poisoned cache entry can never reach a shipped artifact.
#   - GITHUB_TOKEN starts from zero permissions; each job requests only what
#     it needs, and checkouts don't persist credentials unless the job pushes.
#   - Untrusted input (the version string) is validated first and only ever
#     reaches shell via env vars, never ${{ }} interpolation in run scripts.
#   - Each tarball gets a signed build provenance attestation; verify with:
#     gh attestation verify lumen-<target>.tar.gz --repo jnsahaj/lumen

name: Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: "Version to release (e.g. 2.31.0)"
        required: true
        type: string
      dry_run:
        description: "Dry run: build only, publish nothing"
        type: boolean
        default: false

concurrency:
  group: release
  cancel-in-progress: false

permissions: {}

env:
  VERSION: ${{ inputs.version }}

jobs:
  # Validate the version, bump Cargo.toml/Cargo.lock, and (unless dry_run)
  # commit + tag + push. Builds consume the bumped manifests as an artifact so
  # dry runs exercise the exact same path as real releases.
  prepare:
    name: Prepare v${{ inputs.version }}
    runs-on: ubuntu-latest
    permissions:
      contents: write # push the release commit and tag
    steps:
      # persist-credentials is needed here: this job pushes the release
      # commit and tag.
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # zizmor: ignore[artipacked,ref-version-mismatch] # v6.0.3
        with:
          persist-credentials: true

      - name: Ensure release is dispatched from main
        run: |
          if [ "$GITHUB_REF" != "refs/heads/main" ]; then
            echo "::error::Releases must be dispatched from main (got $GITHUB_REF)"
            exit 1
          fi

      - name: Validate version
        run: |
          if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
            echo "::error::Invalid version '$VERSION'. Expected X.Y.Z"
            exit 1
          fi
          current=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
          if [ "$VERSION" = "$current" ]; then
            echo "::error::Version $VERSION is already the current version"
            exit 1
          fi
          if git ls-remote --exit-code origin "refs/tags/v$VERSION" >/dev/null 2>&1; then
            echo "::error::Tag v$VERSION already exists"
            exit 1
          fi

      - name: Bump version in Cargo.toml and Cargo.lock
        run: |
          sed -i "0,/^version = /s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
          cargo update --workspace
          grep -m1 '^version = ' Cargo.toml
          grep -A1 '^name = "lumen"$' Cargo.lock

      - name: Upload bumped manifests
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: release-manifests
          path: |
            Cargo.toml
            Cargo.lock
          if-no-files-found: error

      - name: Commit, tag, and push
        if: ${{ !inputs.dry_run }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add Cargo.toml Cargo.lock
          git commit -m "chore: bump version to $VERSION"
          git tag -a "v$VERSION" -m "Release v$VERSION"
          git push origin HEAD:main "v$VERSION"

  build:
    name: Build ${{ matrix.target }}
    needs: prepare
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-apple-darwin
            os: macos-15-intel
          - target: aarch64-apple-darwin
            os: macos-15
          # Build on the oldest supported Ubuntu so the glibc requirement
          # stays low enough for older distros.
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-22.04
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-22.04-arm
          - target: x86_64-unknown-linux-musl
            os: ubuntu-22.04
          - target: aarch64-unknown-linux-musl
            os: ubuntu-22.04-arm
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read # checkout only
      id-token: write # sign build provenance attestations (Sigstore OIDC)
      attestations: write # store the attestations
    env:
      TARGET: ${{ matrix.target }}
      CC_x86_64_unknown_linux_musl: musl-gcc
      CC_aarch64_unknown_linux_musl: musl-gcc
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          persist-credentials: false

      - name: Use bumped manifests
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          name: release-manifests

      # ports.ubuntu.com (the only apt mirror for ARM runners) has flapping
      # outages on port 80; use https and retry hard so a release doesn't
      # die on an apt blip.
      - name: Install musl toolchain
        if: ${{ contains(matrix.target, 'musl') }}
        run: |
          sudo sed -i 's|http://ports.ubuntu.com|https://ports.ubuntu.com|g' \
            /etc/apt/sources.list /etc/apt/sources.list.d/*.list \
            /etc/apt/sources.list.d/*.sources 2>/dev/null || true
          for attempt in 1 2 3 4 5; do
            sudo apt-get update -o Acquire::Retries=3 \
              && sudo apt-get install -y -o Acquire::Retries=3 musl-tools \
              && break
            echo "apt attempt $attempt failed; retrying in 45s"
            sleep 45
          done
          command -v musl-gcc

      # The runner's preinstalled rustup is used directly rather than a
      # third-party toolchain action — one less supply-chain dependency.
      - name: Install Rust
        run: |
          rustup toolchain install stable --profile minimal --no-self-update
          rustup default stable
          rustup target add "$TARGET"

      - name: Build
        run: cargo build --release --locked --target "$TARGET"

      - name: Smoke test
        run: |
          out=$("./target/$TARGET/release/lumen" --version)
          echo "$out"
          echo "$out" | grep -qF "$VERSION" || {
            echo "::error::Binary reports wrong version"
            exit 1
          }

      - name: Package
        run: |
          tarball="lumen-$TARGET.tar.gz"
          tar -czf "$tarball" -C "target/$TARGET/release" lumen
          if command -v sha256sum >/dev/null; then
            sha256sum "$tarball" > "$tarball.sha256"
          else
            shasum -a 256 "$tarball" > "$tarball.sha256"
          fi
          cat "$tarball.sha256"

      - name: Attest build provenance
        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
        with:
          subject-path: lumen-${{ matrix.target }}.tar.gz

      - name: Upload artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: lumen-${{ matrix.target }}
          path: |
            lumen-${{ matrix.target }}.tar.gz
            lumen-${{ matrix.target }}.tar.gz.sha256
          if-no-files-found: error

  release:
    name: Create GitHub release
    if: ${{ !inputs.dry_run }}
    needs: [prepare, build]
    runs-on: ubuntu-latest
    environment: release
    permissions:
      contents: write # create the GitHub release and upload assets
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          pattern: lumen-*
          path: dist
          merge-multiple: true

      - name: Create release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release create "v$VERSION" \
            --repo "$GITHUB_REPOSITORY" \
            --title "v$VERSION" \
            --generate-notes \
            --verify-tag \
            dist/*

  publish-crate:
    name: Publish to crates.io
    if: ${{ !inputs.dry_run }}
    needs: [prepare, build]
    runs-on: ubuntu-latest
    environment: release
    permissions:
      contents: read # checkout only
      id-token: write # for crates.io Trusted Publishing
    env:
      HAS_CLASSIC_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN != '' }}
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          ref: v${{ inputs.version }}
          persist-credentials: false

      - name: Install Rust
        run: |
          rustup toolchain install stable --profile minimal --no-self-update
          rustup default stable

      # Prefer a classic token if configured; otherwise use Trusted Publishing.
      - name: Authenticate with crates.io (Trusted Publishing)
        if: ${{ env.HAS_CLASSIC_TOKEN != 'true' }}
        id: auth
        uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1
      - name: Publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || steps.auth.outputs.token }}
        run: cargo publish --locked

  homebrew:
    name: Update Homebrew tap
    if: ${{ !inputs.dry_run }}
    needs: [prepare, release]
    runs-on: ubuntu-latest
    environment: release
    permissions: {}
    env:
      HAS_TAP_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN != '' }}
    steps:
      - name: Skip notice
        if: ${{ env.HAS_TAP_TOKEN != 'true' }}
        run: echo "::warning::TAP_GITHUB_TOKEN not set — skipping Homebrew tap update"

      # persist-credentials is needed here: this job pushes the updated
      # formula to the tap repo (using the fine-grained tap token only).
      - name: Checkout tap
        if: ${{ env.HAS_TAP_TOKEN == 'true' }}
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # zizmor: ignore[artipacked,ref-version-mismatch] # v6.0.3
        with:
          repository: jnsahaj/homebrew-lumen
          token: ${{ secrets.TAP_GITHUB_TOKEN }}
          persist-credentials: true

      - name: Download checksums
        if: ${{ env.HAS_TAP_TOKEN == 'true' }}
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          pattern: lumen-*
          path: dist
          merge-multiple: true

      - name: Update formula
        if: ${{ env.HAS_TAP_TOKEN == 'true' }}
        run: |
          sha() { awk '{print $1}' "dist/lumen-$1.tar.gz.sha256"; }
          url() { echo "https://github.com/jnsahaj/lumen/releases/download/v$VERSION/lumen-$1.tar.gz"; }

          cat > Formula/lumen.rb << EOF
          class Lumen < Formula
            desc "lumen is a command-line tool that can show pretty diff, generate commit messages with AI, summarise diffs / commits, and more without requiring an API key."
            homepage "https://github.com/jnsahaj/lumen"
            version "$VERSION"

            on_macos do
              on_intel do
                url "$(url x86_64-apple-darwin)"
                sha256 "$(sha x86_64-apple-darwin)"
              end

              on_arm do
                url "$(url aarch64-apple-darwin)"
                sha256 "$(sha aarch64-apple-darwin)"
              end
            end

            on_linux do
              on_intel do
                url "$(url x86_64-unknown-linux-gnu)"
                sha256 "$(sha x86_64-unknown-linux-gnu)"
              end

              on_arm do
                url "$(url aarch64-unknown-linux-gnu)"
                sha256 "$(sha aarch64-unknown-linux-gnu)"
              end
            end

            def install
              bin.install "lumen"
            end
          end
          EOF

          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add Formula/lumen.rb
          git commit -m "chore: bump lumen to $VERSION"
          git push