name: Release

permissions: {}

on:
  push:
    tags: ["v*.*.*"]

env:
  CARGO_TERM_COLOR: always

jobs:
  validate:
    name: validate release
    runs-on: ubuntu-22.04
    timeout-minutes: 20
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
        with:
          toolchain: stable
      - name: Match tag to the workspace version
        shell: bash
        run: |
          version=$(cargo metadata --no-deps --format-version 1 | jq -er '.packages[] | select(.name == "nanocodex") | .version')
          test "$GITHUB_REF_NAME" = "v$version"
          grep -Fq "## [$version]" CHANGELOG.md
          grep -Fq '<!-- generated by git-cliff -->' CHANGELOG.md
          for crate in nanocodex-core nanocodex-macros nanocodex-observability nanocodex-service nanocodex-tools nanocodex-mcp nanocodex; do
            grep -Fq "## [$version]" "crates/$crate/CHANGELOG.md"
            grep -Fq '<!-- generated by git-cliff -->' "crates/$crate/CHANGELOG.md"
          done
          cargo metadata --no-deps --format-version 1 | jq -e --arg version "$version" \
            '[.packages[].dependencies[] | select(.source == null and (.name | startswith("nanocodex"))) | .req] | all(. == ("^" + $version))' >/dev/null
      - name: Validate installers and crate packages
        shell: bash
        run: |
          bash -n install scripts/changelog.sh scripts/check-docs.sh scripts/publish-crates.sh
          for crate in nanocodex-core nanocodex-macros nanocodex-observability nanocodex-service nanocodex-tools nanocodex-mcp nanocodex; do
            cargo package --locked --no-verify --config .cargo/release.toml --package "$crate"
          done
          ./scripts/check-docs.sh

  prepare-release:
    name: prepare draft release
    needs: validate
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    permissions:
      contents: write
      pull-requests: read
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
        with:
          persist-credentials: false
          fetch-depth: 0
      - name: Find previous stable release
        id: release_info
        shell: bash
        run: |
          previous=$(git tag -l 'v*.*.*' --sort=-v:refname | awk -v current="$GITHUB_REF_NAME" '$0 != current { print; exit }')
          printf 'previous=%s\n' "$previous" >> "$GITHUB_OUTPUT"
      - name: Build contributor changelog
        id: changelog
        uses: mikepenz/release-changelog-builder-action@c9bcd8238b6f41e05561348339429d360b1c0247 # v6.2.3
        with:
          configuration: ./.github/changelog.json
          fromTag: ${{ steps.release_info.outputs.previous || '' }}
          toTag: ${{ github.ref_name }}
        env:
          GITHUB_TOKEN: ${{ github.token }}
      - name: Create or refresh draft release
        env:
          CHANGELOG: ${{ steps.changelog.outputs.changelog }}
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          notes_file=$(mktemp)
          cp .github/RELEASE_TEMPLATE.md "$notes_file"
          printf '\n%s\n' "$CHANGELOG" >> "$notes_file"

          if is_draft=$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq .isDraft 2>/dev/null); then
            if [[ "$is_draft" != true ]]; then
              echo "::error::release $GITHUB_REF_NAME is already published"
              exit 1
            fi
            gh release edit "$GITHUB_REF_NAME" \
              --title "Nanocodex $GITHUB_REF_NAME" \
              --notes-file "$notes_file"
          else
            gh release create "$GITHUB_REF_NAME" \
              --draft \
              --verify-tag \
              --title "Nanocodex $GITHUB_REF_NAME" \
              --notes-file "$notes_file"
          fi

  build:
    name: build ${{ matrix.target }}
    needs: prepare-release
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
            artifact: nanocodex-x86_64-unknown-linux-gnu
          - os: ubuntu-22.04
            target: aarch64-unknown-linux-gnu
            artifact: nanocodex-aarch64-unknown-linux-gnu
            apt: gcc-aarch64-linux-gnu
            linker: aarch64-linux-gnu-gcc
          - os: macos-14
            target: aarch64-apple-darwin
            artifact: nanocodex-aarch64-apple-darwin
          - os: macos-15-intel
            target: x86_64-apple-darwin
            artifact: nanocodex-x86_64-apple-darwin
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            artifact: nanocodex-x86_64-pc-windows-msvc.exe
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
        with:
          toolchain: stable
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
        with:
          cache-on-failure: true
      - name: Install cross-linker
        if: matrix.apt != ''
        shell: bash
        run: |
          sudo apt-get update
          sudo apt-get install --yes ${{ matrix.apt }}
      - name: Build release binary
        shell: bash
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.linker }}
        run: cargo build --locked --profile maxperf --package nanocodex-bin --target ${{ matrix.target }}
      - name: Stage Unix artifact
        if: runner.os != 'Windows'
        shell: bash
        run: |
          mkdir dist
          cp "target/${{ matrix.target }}/maxperf/nanocodex" "dist/${{ matrix.artifact }}"
      - name: Stage Windows artifact
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Path dist
          Copy-Item "target/${{ matrix.target }}/maxperf/nanocodex.exe" "dist/${{ matrix.artifact }}"
      - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: ${{ matrix.artifact }}
          path: dist/*
          if-no-files-found: error

  publish-crates:
    name: publish crates.io packages
    needs: build
    runs-on: ubuntu-22.04
    timeout-minutes: 30
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
        with:
          toolchain: stable
      - name: Publish workspace crates in dependency order
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          test -n "$CARGO_REGISTRY_TOKEN"
          ./scripts/publish-crates.sh

  finalize-release:
    name: attach GitHub release assets
    needs: [build, publish-crates]
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          path: dist
          merge-multiple: true
      - name: Generate checksums
        shell: bash
        run: |
          cd dist
          sha256sum nanocodex-* > SHA256SUMS
      - name: Upload binary assets to draft release
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          is_draft=$(gh release view "$GITHUB_REF_NAME" \
            --repo "$GITHUB_REPOSITORY" \
            --json isDraft \
            --jq .isDraft)
          test "$is_draft" = true
          gh release upload "$GITHUB_REF_NAME" dist/* \
            --repo "$GITHUB_REPOSITORY" \
            --clobber
