jjpr 0.12.7

Manage stacked pull requests in Jujutsu repositories
Documentation
name: Release

on:
  push:
    branches: [main]

env:
  CARGO_TERM_COLOR: always

permissions:
  contents: write

jobs:
  check-version:
    name: Check for version bump
    runs-on: ubuntu-latest
    outputs:
      should_release: ${{ steps.check.outputs.should_release }}
      version: ${{ steps.check.outputs.version }}
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0
          fetch-tags: true

      - name: Check if version tag exists
        id: check
        run: |
          VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          if git tag -l "v$VERSION" | grep -q "v$VERSION"; then
            echo "Tag v$VERSION already exists, skipping release"
            echo "should_release=false" >> "$GITHUB_OUTPUT"
          else
            echo "Tag v$VERSION does not exist, proceeding with release"
            echo "should_release=true" >> "$GITHUB_OUTPUT"
          fi

  ci:
    name: CI checks
    needs: check-version
    if: needs.check-version.outputs.should_release == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

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

      - uses: Swatinem/rust-cache@v2

      - name: cargo check
        run: cargo check --locked

      - name: cargo test
        run: cargo test --locked

      - name: cargo clippy
        run: cargo clippy --locked --tests -- -D warnings

      - uses: taiki-e/install-action@cargo-deny

      - name: cargo deny check licenses
        run: cargo deny check licenses

      - name: Generate distribution assets
        run: cargo run --locked --example generate_assets

      - name: Upload distribution assets
        uses: actions/upload-artifact@v6
        with:
          name: dist-assets
          path: target/assets/
          retention-days: 1

  build:
    name: Build ${{ matrix.target }}
    needs: ci
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        include:
          - target: x86_64-apple-darwin
            runner: macos-14
            archive: jjpr-x86_64-apple-darwin.tar.gz
            is_macos: true
          - target: aarch64-apple-darwin
            runner: macos-14
            archive: jjpr-aarch64-apple-darwin.tar.gz
            is_macos: true
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-latest
            archive: jjpr-x86_64-unknown-linux-gnu.tar.gz
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-latest
            archive: jjpr-aarch64-unknown-linux-gnu.tar.gz
          - target: x86_64-pc-windows-msvc
            runner: windows-latest
            archive: jjpr-x86_64-pc-windows-msvc.zip
    steps:
      - uses: actions/checkout@v5

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install cross
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        uses: taiki-e/install-action@cross

      - name: Build binary
        shell: bash
        run: |
          if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
            cross build --release --locked --target ${{ matrix.target }}
          else
            cargo build --release --locked --target ${{ matrix.target }}
          fi

      - name: Import Apple certificate
        if: matrix.is_macos && env.APPLE_CERT != ''
        env:
          APPLE_CERT: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
          APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
        run: |
          KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
          KEYCHAIN_PASSWORD="$(openssl rand -hex 16)"

          echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
          echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"

          echo "$APPLE_CERT" | base64 --decode > "$RUNNER_TEMP/certificate.p12"

          security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security import "$RUNNER_TEMP/certificate.p12" -P "$APPLE_CERT_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
          security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security list-keychain -d user -s "$KEYCHAIN_PATH"

          rm "$RUNNER_TEMP/certificate.p12"

      - name: Sign binary
        if: matrix.is_macos && env.APPLE_CERT != ''
        env:
          APPLE_CERT: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
        run: |
          BINARY="target/${{ matrix.target }}/release/jjpr"
          IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | head -1 | sed 's/.*"\(.*\)".*/\1/')
          echo "Signing with identity: $IDENTITY"
          codesign --force --options=runtime --sign "$IDENTITY" --timestamp "$BINARY"
          codesign -v --verify --deep --strict "$BINARY"

      - name: Notarize binary
        if: matrix.is_macos && env.APPLE_CERT != ''
        env:
          APPLE_CERT: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        run: |
          BINARY="target/${{ matrix.target }}/release/jjpr"
          /usr/bin/ditto -c -k "$BINARY" "$RUNNER_TEMP/jjpr.zip"
          xcrun notarytool submit "$RUNNER_TEMP/jjpr.zip" \
            --apple-id "$APPLE_ID" \
            --password "$APPLE_ID_PASSWORD" \
            --team-id "$APPLE_TEAM_ID" \
            --wait
          rm "$RUNNER_TEMP/jjpr.zip"

      - name: Download distribution assets
        uses: actions/download-artifact@v7
        with:
          name: dist-assets
          path: dist-assets

      - name: Package binary (unix)
        if: runner.os != 'Windows'
        run: |
          mkdir -p staging/completions
          cp target/${{ matrix.target }}/release/jjpr staging/
          cp dist-assets/jjpr.1 staging/
          cp dist-assets/completions/* staging/completions/
          cp README.md LICENSE-MIT LICENSE-APACHE staging/
          cd staging
          tar czf ../${{ matrix.archive }} jjpr jjpr.1 completions/ README.md LICENSE-MIT LICENSE-APACHE

      - name: Package binary (windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          Compress-Archive -Path "target/${{ matrix.target }}/release/jjpr.exe" -DestinationPath "${{ matrix.archive }}"

      - name: Upload artifact
        uses: actions/upload-artifact@v6
        with:
          name: ${{ matrix.archive }}
          path: ${{ matrix.archive }}
          retention-days: 1

      - name: Cleanup keychain
        if: always() && matrix.is_macos && env.KEYCHAIN_PATH != ''
        run: security delete-keychain "$KEYCHAIN_PATH"

  release:
    name: Create release and publish
    needs: [check-version, build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - uses: dtolnay/rust-toolchain@stable

      - name: Publish to crates.io
        run: cargo publish || echo "::warning::Publish failed (version may already exist)"
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

      - name: Download all artifacts
        uses: actions/download-artifact@v7
        with:
          path: artifacts
          merge-multiple: true

      - name: Create git tag
        run: |
          git tag "v${{ needs.check-version.outputs.version }}"
          git push origin "v${{ needs.check-version.outputs.version }}"

      - name: Generate release notes
        run: |
          cargo install git-cliff
          git cliff --latest --strip all > RELEASE_NOTES.md

      - name: Generate checksums
        run: |
          cd artifacts
          sha256sum *.tar.gz *.zip > SHA256SUMS.txt
          cat SHA256SUMS.txt

      - name: Create GitHub release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: v${{ needs.check-version.outputs.version }}
          name: v${{ needs.check-version.outputs.version }}
          body_path: RELEASE_NOTES.md
          files: |
            artifacts/*.tar.gz
            artifacts/*.zip
            artifacts/SHA256SUMS.txt

      - name: Trigger Homebrew tap update
        if: env.TAP_TOKEN != ''
        env:
          TAP_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
        uses: peter-evans/repository-dispatch@v4
        with:
          token: ${{ secrets.TAP_GITHUB_TOKEN }}
          repository: michaeldhopkins/homebrew-tap
          event-type: update-formula
          client-payload: '{"formula": "jjpr", "repo": "michaeldhopkins/jjpr", "version": "${{ needs.check-version.outputs.version }}"}'