jj-ryu 0.0.1-alpha.11

Stacked PRs for Jujutsu with GitHub/GitLab support
Documentation
name: Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: "Version to release (e.g., 0.1.0, 0.2.0-beta.1)"
        required: true
        type: string

permissions:
  contents: write
  id-token: write

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

env:
  CARGO_INCREMENTAL: 0
  CARGO_TERM_COLOR: always

jobs:
  validate:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Validate version
        id: version
        run: |
          VERSION="${{ inputs.version }}"
          VERSION="${VERSION#v}"
          if ! npx --yes semver "$VERSION" > /dev/null 2>&1; then
            echo "::error::Invalid semver: $VERSION"
            exit 1
          fi
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

      - name: Check tag doesn't exist
        run: |
          if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
            echo "::error::Tag v${{ steps.version.outputs.version }} already exists"
            exit 1
          fi

      - name: Check on main branch
        run: |
          if [ "${{ github.ref_name }}" != "main" ]; then
            echo "::error::Releases must be triggered from main branch"
            exit 1
          fi

  test:
    needs: validate
    uses: ./.github/workflows/test.yml

  prepare:
    needs: [validate, test]
    runs-on: ubuntu-latest
    outputs:
      version: ${{ needs.validate.outputs.version }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Update Cargo.toml version
        run: |
          sed -i 's/^version = ".*"/version = "${{ needs.validate.outputs.version }}"/' Cargo.toml

      - name: Update Cargo.lock
        run: cargo check --quiet

      - name: Update npm package versions
        run: node npm/scripts/update-versions.mjs ${{ needs.validate.outputs.version }}

      - name: Commit and tag
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Cargo.toml Cargo.lock npm/
          git commit -m "release: v${{ needs.validate.outputs.version }}"
          git tag "v${{ needs.validate.outputs.version }}"
          git push origin main
          git push origin "v${{ needs.validate.outputs.version }}"

  build:
    needs: prepare
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-latest
            target: x86_64-apple-darwin
            platform: darwin-x64
          - os: macos-latest
            target: aarch64-apple-darwin
            platform: darwin-arm64
          - os: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
            platform: linux-x64
          - os: ubuntu-22.04
            target: aarch64-unknown-linux-gnu
            platform: linux-arm64
            cross: true
          - os: ubuntu-22.04
            target: x86_64-unknown-linux-musl
            platform: linux-x64-musl
            cross: true
          - os: ubuntu-22.04
            target: aarch64-unknown-linux-musl
            platform: linux-arm64-musl
            cross: true
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            platform: win32-x64
          - os: windows-latest
            target: aarch64-pc-windows-msvc
            platform: win32-arm64

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4
        with:
          ref: v${{ needs.prepare.outputs.version }}

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

      - name: Install cross
        if: matrix.cross
        uses: taiki-e/install-action@cross

      - name: Build (cross)
        if: matrix.cross
        run: cross build --release --target ${{ matrix.target }}
        env:
          RUSTFLAGS: "-C strip=symbols"

      - name: Build (native)
        if: "!matrix.cross"
        run: cargo build --release --target ${{ matrix.target }}
        env:
          RUSTFLAGS: "-C strip=symbols"

      - name: Prepare artifact (Unix)
        if: runner.os != 'Windows'
        run: |
          mkdir -p dist
          cp target/${{ matrix.target }}/release/ryu dist/
          chmod +x dist/ryu

      - name: Sign binary (macOS)
        if: runner.os == 'macOS'
        run: codesign --force --sign - dist/ryu

      - name: Prepare artifact (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Force -Path dist
          Copy-Item "target/${{ matrix.target }}/release/ryu.exe" -Destination "dist/"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.platform }}
          path: dist/
          if-no-files-found: error

  release:
    needs: [prepare, build]
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          ref: v${{ needs.prepare.outputs.version }}

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Create release archives
        run: |
          mkdir -p release
          for platform in artifacts/*/; do
            name=$(basename "$platform")
            if [[ "$name" == win32-* ]]; then
              (cd "$platform" && zip -r "../../release/ryu-${name}.zip" .)
            else
              (cd "$platform" && tar -czvf "../../release/ryu-${name}.tar.gz" .)
            fi
          done

      - name: Generate checksums
        run: |
          cd release
          sha256sum * > checksums-sha256.txt
          cat checksums-sha256.txt

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: v${{ needs.prepare.outputs.version }}
          files: release/*
          generate_release_notes: true

  publish-npm:
    needs: [prepare, build]
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          ref: v${{ needs.prepare.outputs.version }}

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          registry-url: "https://registry.npmjs.org"

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Prepare platform packages
        run: |
          VERSION=${{ needs.prepare.outputs.version }}
          for platform in darwin-arm64 darwin-x64 linux-arm64 linux-x64 linux-arm64-musl linux-x64-musl win32-arm64 win32-x64; do
            mkdir -p npm/$platform
            if [[ "$platform" == win32-* ]]; then
              cp artifacts/$platform/ryu.exe npm/$platform/
            else
              cp artifacts/$platform/ryu npm/$platform/
              chmod +x npm/$platform/ryu
            fi
            node npm/scripts/generate-platform-package.mjs $platform $VERSION
          done

      - name: Publish platform packages
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          VERSION=${{ needs.prepare.outputs.version }}
          # Skip win32 packages for now (npm spam detection issue)
          for platform in darwin-arm64 darwin-x64 linux-arm64 linux-x64 linux-arm64-musl linux-x64-musl; do
            echo "Publishing jj-ryu-$platform..."
            npm publish ./npm/$platform --access public --provenance || {
              if npm view "jj-ryu-$platform@$VERSION" > /dev/null 2>&1; then
                echo "Version already published, skipping"
              else
                exit 1
              fi
            }
          done

      - name: Publish main package
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          VERSION=${{ needs.prepare.outputs.version }}
          echo "Publishing jj-ryu..."
          npm publish ./npm/jj-ryu --access public --provenance || {
            if npm view "jj-ryu@$VERSION" > /dev/null 2>&1; then
              echo "Version already published, skipping"
            else
              exit 1
            fi
          }

  publish-crates:
    needs: [prepare, build]
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          ref: v${{ needs.prepare.outputs.version }}

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set +e
          OUTPUT=$(cargo publish 2>&1)
          EXIT_CODE=$?
          set -e
          if [ $EXIT_CODE -eq 0 ]; then
            echo "Published successfully"
          elif echo "$OUTPUT" | grep -q "already exists"; then
            echo "Version already published, skipping"
          else
            echo "$OUTPUT"
            exit $EXIT_CODE
          fi

  homebrew:
    needs: [prepare, release]
    runs-on: ubuntu-latest

    steps:
      - name: Get tarball SHA256
        id: sha
        run: |
          sha=$(curl -sL https://github.com/dmmulroy/jj-ryu/archive/refs/tags/v${{ needs.prepare.outputs.version }}.tar.gz | shasum -a 256 | cut -d' ' -f1)
          echo "sha256=$sha" >> $GITHUB_OUTPUT

      - name: Checkout homebrew-tap
        uses: actions/checkout@v4
        with:
          repository: dmmulroy/homebrew-tap
          token: ${{ secrets.TAP_GITHUB_TOKEN }}

      - name: Update formula
        run: |
          sed -i 's|url ".*"|url "https://github.com/dmmulroy/jj-ryu/archive/refs/tags/v${{ needs.prepare.outputs.version }}.tar.gz"|' Formula/jj-ryu.rb
          sed -i 's|sha256 ".*"|sha256 "${{ steps.sha.outputs.sha256 }}"|' Formula/jj-ryu.rb

      - name: Create PR
        uses: peter-evans/create-pull-request@v7
        with:
          token: ${{ secrets.TAP_GITHUB_TOKEN }}
          commit-message: "Update jj-ryu to v${{ needs.prepare.outputs.version }}"
          title: "Update jj-ryu to v${{ needs.prepare.outputs.version }}"
          body: "Automated update from [jj-ryu release](https://github.com/dmmulroy/jj-ryu/releases/tag/v${{ needs.prepare.outputs.version }})"
          branch: update-jj-ryu-${{ needs.prepare.outputs.version }}
          base: main

  summary:
    needs: [prepare, release, publish-npm, publish-crates, homebrew]
    runs-on: ubuntu-latest
    if: always()

    steps:
      - name: Summary
        run: |
          echo "## Release v${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| Component | Status |" >> $GITHUB_STEP_SUMMARY
          echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY
          echo "| GitHub Release | ${{ needs.release.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| npm | ${{ needs.publish-npm.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| crates.io | ${{ needs.publish-crates.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| Homebrew | ${{ needs.homebrew.result }} |" >> $GITHUB_STEP_SUMMARY