gwm-cli 1.6.0

git worktree manager — TUI + CLI, native libgit2, per-repo bootstrap
Documentation
name: pre-release

on:
  push:
    tags:
      # Pre-release SemVer identifiers must follow `-rc.N`, `-alpha.N`, `-beta.N`.
      # Tighter than `*-rc*` so we don't fire on tags like `v0.2.0-rc` or
      # `v0.2.0-rc-anything`.
      - "v*.*.*-rc.*"
      - "v*.*.*-alpha.*"
      - "v*.*.*-beta.*"
  workflow_dispatch:
    inputs:
      tag:
        description: "rc/alpha/beta tag to (re)build (e.g. v0.2.0-rc.1)"
        required: true

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always

concurrency:
  group: pre-release-${{ github.ref }}
  cancel-in-progress: false

jobs:
  build:
    name: build (${{ matrix.target }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            archive: tar.gz
          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            archive: tar.gz
            cross: true
          - os: macos-latest
            target: x86_64-apple-darwin
            archive: tar.gz
          - os: macos-latest
            target: aarch64-apple-darwin
            archive: tar.gz
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            archive: zip
    steps:
      - name: resolve tag
        id: tag
        shell: bash
        # For workflow_dispatch, prefer the input over github.ref_name (which is
        # the dispatching branch). For tag-driven runs, github.ref_name is the
        # tag itself. The resolved value drives BOTH the checkout ref (so the
        # build matches the tagged commit) and the archive STAGE naming.
        run: |
          if [ -n "${{ inputs.tag }}" ]; then
            echo "name=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
          else
            echo "name=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/checkout@v7
        with:
          ref: ${{ steps.tag.outputs.name }}
          persist-credentials: false

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

      - name: install cross (linux aarch64)
        if: matrix.cross == true
        run: cargo install cross --locked

      - name: build (cargo)
        if: matrix.cross != true
        run: cargo build --release --target ${{ matrix.target }} --locked

      - name: build (cross)
        if: matrix.cross == true
        run: cross build --release --target ${{ matrix.target }} --locked

      - name: package (unix)
        if: matrix.os != 'windows-latest'
        shell: bash
        run: |
          BIN_NAME="gwm"
          STAGE="${BIN_NAME}-${{ steps.tag.outputs.name }}-${{ matrix.target }}"
          mkdir -p "dist/${STAGE}"
          cp "target/${{ matrix.target }}/release/${BIN_NAME}" "dist/${STAGE}/"
          cp README.md LICENSE.md CHANGELOG.md "dist/${STAGE}/"
          tar -C dist -czf "dist/${STAGE}.tar.gz" "${STAGE}"
          shasum -a 256 "dist/${STAGE}.tar.gz" > "dist/${STAGE}.tar.gz.sha256"

      - name: package (windows)
        if: matrix.os == 'windows-latest'
        shell: pwsh
        # Set-Content -Encoding ascii ensures the sidecar is plain ASCII (no
        # UTF-16 BOM that PowerShell's `Out-File` would add by default) so the
        # checksum file is verifiable by `sha256sum -c` on other platforms.
        run: |
          $BIN_NAME = "gwm.exe"
          $STAGE = "gwm-${{ steps.tag.outputs.name }}-${{ matrix.target }}"
          New-Item -ItemType Directory -Force -Path "dist/$STAGE" | Out-Null
          Copy-Item "target/${{ matrix.target }}/release/$BIN_NAME" "dist/$STAGE/"
          Copy-Item README.md, LICENSE.md, CHANGELOG.md "dist/$STAGE/"
          Compress-Archive -Path "dist/$STAGE" -DestinationPath "dist/$STAGE.zip"
          $hash = (Get-FileHash -Algorithm SHA256 "dist/$STAGE.zip").Hash.ToLower()
          Set-Content -Path "dist/$STAGE.zip.sha256" -Encoding ascii -Value "$hash  $STAGE.zip"

      - name: upload artifacts
        uses: actions/upload-artifact@v7
        with:
          name: gwm-${{ matrix.target }}
          path: |
            dist/*.tar.gz
            dist/*.tar.gz.sha256
            dist/*.zip
            dist/*.zip.sha256
          if-no-files-found: error

  release:
    name: github pre-release
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: resolve tag
        id: tag
        shell: bash
        run: |
          if [ -n "${{ inputs.tag }}" ]; then
            echo "name=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
          else
            echo "name=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/checkout@v7
        with:
          ref: ${{ steps.tag.outputs.name }}
          persist-credentials: false

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

      # Release notes come from the per-rc file under `changelogs/pre-releases/`,
      # not the top-level `CHANGELOG.md` (which is only the in-progress
      # index — RC entries land in `changelogs/pre-releases/X.Y.Z-rc.N.md`
      # when the RC is cut). Sourcing from the index would publish an
      # empty body, as observed for v0.6.0-rc.1 before this fix.
      - name: resolve changelog path
        id: changelog
        shell: bash
        run: |
          TAG="${{ steps.tag.outputs.name }}"
          VERSION="${TAG#v}"
          CHANGELOG_PATH="changelogs/pre-releases/${VERSION}.md"
          if [ ! -f "${CHANGELOG_PATH}" ]; then
            echo "::error::Expected ${CHANGELOG_PATH} to exist for tag ${TAG} — release notes would otherwise fall back to the empty CHANGELOG.md index."
            exit 1
          fi
          echo "path=${CHANGELOG_PATH}" >> "$GITHUB_OUTPUT"

      - name: check unreleased changelog against previous rc
        shell: bash
        run: ./.github/scripts/check-rc-changelog-dupes.sh "${{ steps.tag.outputs.name }}"

      - name: publish pre-release
        uses: softprops/action-gh-release@v3
        with:
          tag_name: ${{ steps.tag.outputs.name }}
          name: ${{ steps.tag.outputs.name }}
          body_path: ${{ steps.changelog.outputs.path }}
          files: |
            dist/*.tar.gz
            dist/*.tar.gz.sha256
            dist/*.zip
            dist/*.zip.sha256
          draft: false
          prerelease: true