linguo 1.2.0

Cross-platform, multi-language runtime, package, and project manager
# Cuts a release: bumps the version in Cargo.toml, pushes a semver tag,
# builds binaries for every supported platform, and publishes a GitHub
# release whose notes are generated from the commit messages since the
# previous release.
#
# Trigger it from the Actions tab (workflow_dispatch) and pick which semver
# component to bump.

name: Release

on:
  workflow_dispatch:
    inputs:
      bump:
        description: Semver component to bump
        type: choice
        options: [patch, minor, major]
        default: patch

permissions:
  contents: write

concurrency: release

jobs:
  tag:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: dtolnay/rust-toolchain@stable
      - name: Compute next version
        id: version
        run: |
          prev="$(git tag --list 'v*' --sort=-v:refname | head -1)"
          prev="${prev:-v0.0.0}"
          IFS=. read -r major minor patch <<< "${prev#v}"
          case "${{ inputs.bump }}" in
            major) major=$((major + 1)); minor=0; patch=0 ;;
            minor) minor=$((minor + 1)); patch=0 ;;
            patch) patch=$((patch + 1)) ;;
          esac
          echo "version=${major}.${minor}.${patch}" >> "$GITHUB_OUTPUT"
      - name: Bump Cargo.toml, commit, and push tag
        run: |
          version="${{ steps.version.outputs.version }}"
          sed -i "s/^version = \".*\"/version = \"${version}\"/" Cargo.toml
          cargo update --workspace  # sync Cargo.lock to the new version
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          # Cargo.toml may already carry the target version (e.g. the very
          # first release); tag HEAD directly in that case.
          if ! git diff --quiet; then
            git commit -am "Release v${version}"
          fi
          git tag "v${version}"
          git push origin main "v${version}"

  build:
    needs: tag
    strategy:
      # Each lane already runs as its own parallel job; don't let one broken
      # lane cancel the others mid-build (failed lanes can be re-run alone,
      # since the tag exists by this point).
      fail-fast: false
      matrix:
        include:
          - target: aarch64-apple-darwin
            runner: macos-14
          # Intel macOS runners are retired; cross-compile on arm64
          # (Apple's toolchain targets both architectures natively).
          - target: x86_64-apple-darwin
            runner: macos-14
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-24.04-arm
          # musl builds are fully static (rustls, no OpenSSL) and serve
          # Alpine and friends.
          - target: x86_64-unknown-linux-musl
            runner: ubuntu-latest
          - target: aarch64-unknown-linux-musl
            runner: ubuntu-24.04-arm
          - target: x86_64-pc-windows-msvc
            runner: windows-latest
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: v${{ needs.tag.outputs.version }}
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - name: Install musl toolchain
        if: contains(matrix.target, 'musl')
        run: sudo apt-get update && sudo apt-get install -y musl-tools
      - run: cargo build --release --target ${{ matrix.target }}
      - name: Package
        shell: bash
        run: |
          name="linguo-v${{ needs.tag.outputs.version }}-${{ matrix.target }}"
          mkdir "$name"
          cp README.md LICENSE "$name/"
          case "${{ matrix.target }}" in
            *windows*)
              cp "target/${{ matrix.target }}/release/linguo.exe" "$name/"
              7z a "$name.zip" "$name" > /dev/null
              sha256sum "$name.zip" > "$name.zip.sha256"
              ;;
            *)
              cp "target/${{ matrix.target }}/release/linguo" "$name/"
              tar czf "$name.tar.gz" "$name"
              shasum -a 256 "$name.tar.gz" > "$name.tar.gz.sha256"
              ;;
          esac
      - uses: taiki-e/install-action@v2
        if: endsWith(matrix.target, '-linux-gnu')
        with:
          tool: cargo-deb,cargo-generate-rpm
      - name: Package deb and rpm
        if: endsWith(matrix.target, '-linux-gnu')
        shell: bash
        run: |
          name="linguo-v${{ needs.tag.outputs.version }}-${{ matrix.target }}"
          cargo deb --no-build --target ${{ matrix.target }} --output "$name.deb"
          cargo generate-rpm --target ${{ matrix.target }} --output "$name.rpm"
          sha256sum "$name.deb" > "$name.deb.sha256"
          sha256sum "$name.rpm" > "$name.rpm.sha256"
      - name: Package MSI
        if: contains(matrix.target, 'windows')
        shell: bash
        run: |
          cargo install cargo-wix --locked
          name="linguo-v${{ needs.tag.outputs.version }}-${{ matrix.target }}"
          cargo wix --no-build --target ${{ matrix.target }} --nocapture --output "$name.msi"
          sha256sum "$name.msi" > "$name.msi.sha256"
      - uses: actions/upload-artifact@v4
        with:
          name: linguo-${{ matrix.target }}
          path: |
            linguo-v*.tar.gz*
            linguo-v*.zip*
            linguo-v*.deb*
            linguo-v*.rpm*
            linguo-v*.msi*

  publish-crates:
    needs: [tag, build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: v${{ needs.tag.outputs.version }}
      - uses: dtolnay/rust-toolchain@stable
      # Set a CARGO_REGISTRY_TOKEN repo secret (a crates.io API token with
      # publish scope) and each release also publishes to crates.io; the
      # job self-skips while the secret is absent.
      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
            echo "no CARGO_REGISTRY_TOKEN secret configured; skipping crates.io publish"
            exit 0
          fi
          cargo publish

  release:
    needs: [tag, build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: v${{ needs.tag.outputs.version }}
      - uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true
      - name: Generate release notes from commit messages
        run: |
          version="v${{ needs.tag.outputs.version }}"
          if prev="$(git describe --tags --abbrev=0 --match 'v*' "${version}^" 2>/dev/null)"; then
            range="${prev}..${version}"
          else
            prev=""
            range="$version"
          fi
          {
            echo "## Changes"
            echo
            git log --no-merges --format='- %s (%h)' "$range" | grep -v '^- Release v' || true
            if [ -n "$prev" ]; then
              echo
              echo "**Full changelog:** https://github.com/${{ github.repository }}/compare/${prev}...${version}"
            fi
          } > notes.md
          cat notes.md
      - name: Create GitHub release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          version="v${{ needs.tag.outputs.version }}"
          gh release create "$version" dist/* --title "linguo $version" --notes-file notes.md
      - name: Generate and attach Homebrew formula
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          version="${{ needs.tag.outputs.version }}"
          ./packaging/homebrew/generate.sh "$version" dist > linguo.rb
          gh release upload "v${version}" linguo.rb
      # The HOMEBREW_TAP_DEPLOY_KEY secret holds an SSH deploy key with write
      # access to BoxingOctopusCreative/homebrew-tap; the step self-skips if
      # the secret is absent.
      - name: Push formula to the Homebrew tap
        env:
          DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
        run: |
          if [ -z "$DEPLOY_KEY" ]; then
            echo "no HOMEBREW_TAP_DEPLOY_KEY secret configured; skipping tap update"
            exit 0
          fi
          version="${{ needs.tag.outputs.version }}"
          mkdir -p ~/.ssh
          printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/tap_deploy_key
          chmod 600 ~/.ssh/tap_deploy_key
          ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
          export GIT_SSH_COMMAND="ssh -i ~/.ssh/tap_deploy_key -o IdentitiesOnly=yes"
          git clone git@github.com:BoxingOctopusCreative/homebrew-tap.git tap
          mkdir -p tap/Formula
          cp linguo.rb tap/Formula/linguo.rb
          cd tap
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add Formula/linguo.rb
          git commit -m "linguo ${version}"
          git push