name: Release

on:
  workflow_dispatch:
    inputs:
      bump:
        description: Semver bump type
        type: choice
        required: true
        default: patch
        options:
          - patch
          - minor
          - major

permissions:
  contents: write
  id-token: write

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

env:
  CARGO_TERM_COLOR: always

jobs:
  prepare:
    name: Prepare release
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.bump.outputs.version }}
    steps:
      - name: Check RELEASE_TOKEN is set
        run: |
          if [ -z "${{ secrets.RELEASE_TOKEN }}" ]; then
            echo "::error::RELEASE_TOKEN secret is not set. This workflow requires a PAT that can push release commits and tags to main." >&2
            exit 1
          fi

      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
          ref: main
          token: ${{ secrets.RELEASE_TOKEN }}

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

      - name: Install Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org

      - name: Configure git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

      - name: Bump versions, commit, and tag
        id: bump
        shell: bash
        run: |
          set -euo pipefail
          npm version "${{ inputs.bump }}" --no-git-tag-version
          version="$(node -p "require('./package.json').version")"
          npm version "$version" --workspace eslint-plugin-playwright-ast-coverage --no-git-tag-version
          node -e "
            const fs = require('node:fs');
            const version = process.argv[1];
            const cargo = fs.readFileSync('Cargo.toml', 'utf8')
              .replace(/^version = \".*\"$/m, \`version = \"\${version}\"\`);
            fs.writeFileSync('Cargo.toml', cargo);
          " "$version"
          cargo update -w
          git add Cargo.toml Cargo.lock package.json package-lock.json packages/eslint-plugin-playwright-ast-coverage/package.json
          git commit -m "chore(release): v$version"
          git tag -a "v$version" -m "v$version"
          echo "version=$version" >> "$GITHUB_OUTPUT"

      - name: Push release commit and tag
        run: git push origin HEAD:main --follow-tags

  validate:
    name: Validate
    needs: prepare
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: v${{ needs.prepare.outputs.version }}

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt

      - name: Install Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org

      - name: Cache Rust build artifacts
        uses: Swatinem/rust-cache@v2
        with:
          prefix-key: v1-rust-release
          cache-bin: "false"

      - name: Install npm dependencies
        run: npm ci --ignore-scripts

      - name: Verify versions
        shell: bash
        run: |
          set -euo pipefail
          tag="${{ needs.prepare.outputs.version }}"
          cargo_version="$(cargo metadata --no-deps --format-version 1 | node -e "let d=''; process.stdin.on('data', c => d += c); process.stdin.on('end', () => console.log(JSON.parse(d).packages[0].version));")"
          npm_version="$(node -p "require('./package.json').version")"
          plugin_version="$(node -p "require('./packages/eslint-plugin-playwright-ast-coverage/package.json').version")"
          test "$tag" = "$cargo_version"
          test "$tag" = "$npm_version"
          test "$tag" = "$plugin_version"

      - name: Check formatting
        run: cargo fmt --all -- --check

      - name: Check JavaScript formatting
        run: npm run fmt:js:check

      - name: Run Clippy
        run: cargo clippy --all-targets --all-features -- -D warnings

      - name: Run Oxlint
        run: npm run lint:js

      - name: Run Rust tests
        run: cargo test --all-features

      - name: Run npm tests
        run: npm test

      - name: Check Cargo package
        run: cargo publish --dry-run --locked

      - name: Check npm package
        run: npm pack --dry-run

      - name: Check ESLint plugin package
        run: npm pack --dry-run --workspace eslint-plugin-playwright-ast-coverage

  build:
    name: Build ${{ matrix.target }}
    needs:
      - prepare
      - validate
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-15-intel
            target: x86_64-apple-darwin
            artifact: playwright-ast-coverage-v${{ needs.prepare.outputs.version }}-x86_64-apple-darwin
            binary: playwright-ast-coverage
          - os: macos-15
            target: aarch64-apple-darwin
            artifact: playwright-ast-coverage-v${{ needs.prepare.outputs.version }}-aarch64-apple-darwin
            binary: playwright-ast-coverage
          - os: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
            artifact: playwright-ast-coverage-v${{ needs.prepare.outputs.version }}-x86_64-unknown-linux-gnu
            binary: playwright-ast-coverage
          - os: ubuntu-22.04-arm
            target: aarch64-unknown-linux-gnu
            artifact: playwright-ast-coverage-v${{ needs.prepare.outputs.version }}-aarch64-unknown-linux-gnu
            binary: playwright-ast-coverage
          - os: windows-2025
            target: x86_64-pc-windows-msvc
            artifact: playwright-ast-coverage-v${{ needs.prepare.outputs.version }}-x86_64-pc-windows-msvc.exe
            binary: playwright-ast-coverage.exe
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: v${{ needs.prepare.outputs.version }}

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

      - name: Cache Rust build artifacts
        uses: Swatinem/rust-cache@v2
        with:
          prefix-key: v1-rust-release
          cache-bin: "false"

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

      - name: Verify binary
        shell: bash
        run: target/${{ matrix.target }}/release/${{ matrix.binary }} --version

      - name: Stage asset
        shell: bash
        run: |
          set -euo pipefail
          cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "${{ matrix.artifact }}"

      - name: Generate checksum (Unix)
        if: runner.os != 'Windows'
        shell: bash
        run: shasum -a 256 "${{ matrix.artifact }}" > "${{ matrix.artifact }}.sha256"

      - name: Generate checksum (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $artifact = "${{ matrix.artifact }}"
          $hash = (Get-FileHash -Algorithm SHA256 -Path $artifact).Hash.ToLowerInvariant()
          "$hash  $artifact" | Set-Content -Encoding ascii -NoNewline "$artifact.sha256"

      - name: Upload asset artifact
        uses: actions/upload-artifact@v7
        with:
          name: ${{ matrix.artifact }}
          path: |
            ${{ matrix.artifact }}
            ${{ matrix.artifact }}.sha256
          if-no-files-found: error

  publish:
    name: Publish
    needs:
      - prepare
      - validate
      - build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: v${{ needs.prepare.outputs.version }}

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

      - name: Install Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org

      - name: Publish Cargo crate
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          version="${{ needs.prepare.outputs.version }}"
          if curl --fail --silent --output /dev/null "https://crates.io/api/v1/crates/playwright-ast-coverage/$version"; then
            echo "playwright-ast-coverage $version is already published on crates.io; skipping"
          else
            cargo publish --locked
          fi

      - name: Download release assets
        uses: actions/download-artifact@v8
        with:
          path: dist
          merge-multiple: true

      - name: Create GitHub release
        env:
          GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          tag="v${{ needs.prepare.outputs.version }}"
          if gh release view "$tag" >/dev/null 2>&1; then
            gh release upload "$tag" dist/* --clobber
          else
            gh release create "$tag" dist/* --title "$tag" --generate-notes
          fi

      - name: Smoke test npm tarball install
        shell: bash
        env:
          PLAYWRIGHT_AST_COVERAGE_RELEASE_BASE_URL: file://${{ github.workspace }}/dist
        run: |
          set -euo pipefail
          npm pack
          mkdir npm-smoke
          cd npm-smoke
          npm init -y
          npm install "../playwright-ast-coverage-${{ needs.prepare.outputs.version }}.tgz"
          npx playwright-ast-coverage --version

      - name: Publish npm package
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          if [ -n "${NPM_TOKEN:-}" ]; then
            export NODE_AUTH_TOKEN="$NPM_TOKEN"
          fi
          version="${{ needs.prepare.outputs.version }}"
          if npm view "playwright-ast-coverage@$version" version >/dev/null 2>&1; then
            echo "playwright-ast-coverage $version is already published on npm; skipping"
          else
            npm publish --provenance --access public
          fi

      - name: Publish ESLint plugin package
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          if [ -n "${NPM_TOKEN:-}" ]; then
            export NODE_AUTH_TOKEN="$NPM_TOKEN"
          fi
          version="${{ needs.prepare.outputs.version }}"
          if npm view "eslint-plugin-playwright-ast-coverage@$version" version >/dev/null 2>&1; then
            echo "eslint-plugin-playwright-ast-coverage $version is already published on npm; skipping"
          else
            npm publish --provenance --access public --workspace eslint-plugin-playwright-ast-coverage
          fi
