vik 0.1.2

Vik is an issue-driven coding workflow automation tool.
name: Release

on:
  push:
    tags:
      - "v*.*.*"

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  validate:
    name: Validate release tag
    runs-on: ubuntu-latest
    outputs:
      package: ${{ steps.release.outputs.package }}
      prerelease: ${{ steps.release.outputs.prerelease }}
      tag: ${{ steps.release.outputs.tag }}
      version: ${{ steps.release.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Parse and validate tag
        id: release
        shell: bash
        run: |
          set -euo pipefail

          tag="${GITHUB_REF_NAME}"
          if [[ ! "${tag}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?)$ ]]; then
            echo "::error::Tag '${tag}' must match vX.Y.Z, vX.Y.Z-alpha.N, or vX.Y.Z-beta.N"
            exit 1
          fi

          version="${BASH_REMATCH[1]}"
          manifest_version="$(sed -nE 's/^version = "([^"]+)"/\1/p' Cargo.toml | head -n1)"
          package="$(sed -nE 's/^name = "([^"]+)"/\1/p' Cargo.toml | head -n1)"

          if [[ -z "${package}" ]]; then
            echo "::error::Could not parse package name from Cargo.toml"
            exit 1
          fi

          if [[ "${version}" != "${manifest_version}" ]]; then
            echo "::error::Tag version ${version} does not match Cargo.toml version ${manifest_version}"
            exit 1
          fi

          prerelease=false
          if [[ "${version}" == *-* ]]; then
            prerelease=true
          fi

          echo "package=${package}" >> "${GITHUB_OUTPUT}"
          echo "prerelease=${prerelease}" >> "${GITHUB_OUTPUT}"
          echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
          echo "version=${version}" >> "${GITHUB_OUTPUT}"

  build:
    name: Build ${{ matrix.label }}
    needs: validate
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - label: linux-x64
            os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            binary: vik
            artifact: vik-linux-x64
          - label: linux-arm64
            os: ubuntu-24.04-arm
            target: aarch64-unknown-linux-gnu
            binary: vik
            artifact: vik-linux-arm64
          - label: macos-arm64
            os: macos-latest
            target: aarch64-apple-darwin
            binary: vik
            artifact: vik-macos-arm64
          - label: windows-x64
            os: windows-latest
            target: x86_64-pc-windows-msvc
            binary: vik.exe
            artifact: vik-windows-x64.exe
    steps:
      - name: Checkout
        uses: actions/checkout@v6

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

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Build release binary
        run: cargo build --release --locked --target ${{ matrix.target }}

      - name: Stage release binary
        shell: bash
        run: |
          set -euo pipefail

          mkdir -p dist
          cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "dist/${{ matrix.artifact }}"

          if [[ "${RUNNER_OS}" != "Windows" ]]; then
            chmod +x "dist/${{ matrix.artifact }}"
          fi

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

  publish-crate:
    name: Publish crate
    needs:
      - validate
      - build
    runs-on: ubuntu-latest
    env:
      CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      PACKAGE: ${{ needs.validate.outputs.package }}
      VERSION: ${{ needs.validate.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6

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

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Publish to crates.io
        shell: bash
        run: |
          set -euo pipefail

          if [[ -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then
            echo "::error::CARGO_REGISTRY_TOKEN secret is required to publish to crates.io"
            exit 1
          fi

          if curl -fsS \
            -H "User-Agent: forehalo/vik release workflow" \
            "https://crates.io/api/v1/crates/${PACKAGE}/${VERSION}" >/dev/null; then
            echo "${PACKAGE} ${VERSION} is already published on crates.io; skipping."
            exit 0
          fi

          cargo publish --locked

  github-release:
    name: Create GitHub release
    needs:
      - validate
      - build
      - publish-crate
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Download release binaries
        uses: actions/download-artifact@v8
        with:
          path: dist
          pattern: vik-*
          merge-multiple: true

      - name: Stage installer
        shell: bash
        run: |
          set -euo pipefail

          sed "s/__VIK_RELEASE_TAG__/${{ needs.validate.outputs.tag }}/g" \
            scripts/install.sh > dist/install.sh
          chmod +x dist/install.sh

      - name: Generate checksums
        shell: bash
        run: |
          set -euo pipefail

          cd dist
          find . -maxdepth 1 -type f ! -name '*-sha256.txt' -print0 \
            | sort -z \
            | xargs -0 sha256sum > "vik-${{ needs.validate.outputs.version }}-sha256.txt"
          cat "vik-${{ needs.validate.outputs.version }}-sha256.txt"

      - name: Generate release body
        id: release_body
        shell: bash
        run: |
          set -euo pipefail

          tag="${{ needs.validate.outputs.tag }}"
          version="${{ needs.validate.outputs.version }}"
          notes_path="${RUNNER_TEMP}/release-body.md"

          cat > "${notes_path}" <<EOF
          ## Install

          \`\`\`sh
          curl -fsSL https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/install.sh | sh -
          \`\`\`

          Or install from crates.io:

          \`\`\`sh
          cargo install vik --version ${version} --locked
          \`\`\`

          ## Binaries

          - Linux x64: \`vik-linux-x64\`
          - Linux arm64: \`vik-linux-arm64\`
          - macOS arm64: \`vik-macos-arm64\`
          - Windows x64: \`vik-windows-x64.exe\`

          Verify downloads with \`vik-${version}-sha256.txt\`.
          EOF

          previous_tag="$(git describe --tags --match 'v[0-9]*.[0-9]*.[0-9]*' --abbrev=0 "${tag}^{commit}^" 2>/dev/null || true)"

          {
            echo
            echo "<details>"
            echo "<summary>Diff log</summary>"
            echo

            if [[ -n "${previous_tag}" ]]; then
              echo "Full diff: https://github.com/${GITHUB_REPOSITORY}/compare/${previous_tag}...${tag}"
              echo
              git log --no-merges --pretty=format:'- %h %s' "${previous_tag}..${tag}"
            else
              echo "Initial release diff:"
              echo
              git log --no-merges --pretty=format:'- %h %s' "${tag}"
            fi

            echo
            echo "</details>"
          } >> "${notes_path}"

          echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"

      - name: Create GitHub release
        uses: softprops/action-gh-release@v3
        with:
          tag_name: ${{ needs.validate.outputs.tag }}
          name: ${{ needs.validate.outputs.version }}
          prerelease: ${{ needs.validate.outputs.prerelease == 'true' }}
          files: dist/*
          body_path: ${{ steps.release_body.outputs.path }}