name: release
run-name: >-
  ${{ case(
      github.event_name == 'pull_request',
        format('verify PR #{0}', github.event.pull_request.number),
      github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'),
        format('release {0}', github.ref_name),
      format('verify {0}', github.ref_name)
    ) }}

on:
  pull_request:
  push:
    branches:
      - master
    tags:
      - v*

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  verify:
    name: verify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: install rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: rust cache
        uses: Swatinem/rust-cache@v2

      - name: cargo fmt --check
        run: cargo fmt --check

      - name: cargo clippy
        run: cargo clippy --all-targets --all-features -- -D clippy::all

      - name: cargo test
        run: cargo test

  create-release:
    name: create draft release
    needs: verify
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v6

      - name: create github release
        uses: taiki-e/create-gh-release-action@v1
        with:
          token: ${{ github.token }}
          changelog: CHANGELOG.md
          draft: true
          branch: master

  matrix:
    name: build matrix
    needs: verify
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    permissions:
      contents: read
    outputs:
      include: ${{ steps.gen.outputs.include }}
    steps:
      - uses: actions/checkout@v6
        with:
          sparse-checkout: npm/targets.json
          sparse-checkout-cone-mode: false
          persist-credentials: false

      - id: gen
        # Single source of truth: npm/targets.json. Each entry maps to the
        # fields upload-assets consumes. `vm`-typed targets are filtered out
        # because they need a target-OS VM that taiki-e/upload-rust-binary-action
        # can't reach; if any are reintroduced they need a dedicated job.
        # Missing `experimental` coerces to false so `continue-on-error`
        # evaluates cleanly.
        # NOTE: `target: .rust` (the Rust triple) is mandatory — both
        # taiki-e and npm/scripts/build-packages.ts derive the release-asset
        # filename `runner-<tag>-<rust>.tar.gz` from this exact field. Don't
        # swap it for `.pkg` (npm package suffix) without updating both
        # consumers in lockstep.
        run: |
          include=$(jq -c '[.targets[] | select(.build != "vm") | {
            target: .rust,
            runner: .runner,
            "build-tool": .build,
            experimental: (.experimental // false)
          }]' npm/targets.json)
          echo "include=${include}" >> "${GITHUB_OUTPUT}"

  upload-assets:
    name: build+upload ${{ matrix.target }}
    needs: [matrix, create-release]
    runs-on: ${{ matrix.runner }}
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include: ${{ fromJSON(needs.matrix.outputs.include) }}
    continue-on-error: ${{ matrix.experimental }}
    env:
      RELEASE_TAG: ${{ github.ref_name }}
    steps:
      - uses: actions/checkout@v6

      # Rust toolchain has to land BEFORE setup-cross-toolchain-action: the
      # latter calls rustup against whatever's active. Tier-2 paths use
      # stable; tier-3 paths float on `nightly` because pinning to a snapshot
      # rots — and these targets are `experimental: true`, so a transient
      # nightly regression is absorbed by `continue-on-error` rather than
      # blocking the release.
      - name: install stable rust
        if: matrix.build-tool == 'cargo-cross-toolchain'
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: install nightly rust (tier-3 build-std)
        if: matrix.build-tool == 'cargo-build-std'
        uses: dtolnay/rust-toolchain@nightly
        with:
          components: rust-src

      # Tier-2 BSD targets where Rust ships prebuilt std but `cross` has no
      # working docker image. setup-cross-toolchain-action installs the
      # matching cross C toolchain (e.g. x86_64-unknown-netbsd-gcc) and
      # exports CARGO_TARGET_*_LINKER so host `cargo` builds work. Tier-3
      # `cargo-build-std` reuses the same C toolchain step but goes the
      # nightly + -Z build-std route to construct std locally.
      - name: setup cross toolchain
        if: contains(fromJSON('["cargo-cross-toolchain", "cargo-build-std"]'), matrix.build-tool)
        uses: taiki-e/setup-cross-toolchain-action@v1
        with:
          target: ${{ matrix.target }}

      - name: build and upload binaries (taiki-e)
        if: contains(fromJSON('["cargo", "cross", "cargo-cross-toolchain"]'), matrix.build-tool)
        uses: taiki-e/upload-rust-binary-action@v1
        with:
          token: ${{ github.token }}
          bin: runner,run
          target: ${{ matrix.target }}
          # Map our extended enum onto values upload-rust-binary-action accepts.
          # `cargo-cross-toolchain` builds with host cargo (the toolchain step
          # above already configured it), so `cargo` is the right value here.
          build-tool: ${{ case(matrix.build-tool == 'cargo-cross-toolchain', 'cargo', matrix.build-tool) }}
          archive: runner-$tag-$target
          tar: all
          zip: none
          checksum: sha256
          include: README.md,LICENSE
          locked: true

      # Tier-3 path: upload-rust-binary-action can't inject `-Z build-std`,
      # so we build, archive, and upload manually. The CARGO_UNSTABLE_BUILD_STD
      # env honors the same shape as `-Z build-std=...` on the cargo CLI.
      - name: build (cargo + -Z build-std)
        if: matrix.build-tool == 'cargo-build-std'
        env:
          CARGO_UNSTABLE_BUILD_STD: std,panic_abort
        run: |
          cargo +nightly build \
            --release --locked \
            --bin runner --bin run \
            --target "${{ matrix.target }}"

      - name: package and upload (cargo-build-std)
        if: matrix.build-tool == 'cargo-build-std'
        env:
          GH_TOKEN: ${{ github.token }}
          TARGET: ${{ matrix.target }}
          BIN_DIR: ${{ github.workspace }}/target/${{ matrix.target }}/release
        run: bash "${GITHUB_WORKSPACE}/.github/scripts/build/package-release-asset.sh"

  build-npm-dist:
    name: build npm dist
    needs: [upload-assets]
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    # Need write so `gh release download` can see the still-draft release;
    # drafts are invisible to read-only tokens, which surfaces as the
    # misleading "release not found" error we hit on v0.6.0.
    permissions:
      contents: write
    env:
      RELEASE_TAG: ${{ github.ref_name }}
    steps:
      - uses: actions/checkout@v6
        with: { persist-credentials: false }

      - uses: actions/setup-node@v6
        with: { node-version: latest }

      - name: download release archives
        env: { GH_TOKEN: "${{ github.token }}" }
        run: bash "${GITHUB_WORKSPACE}/.github/scripts/build/download-release-archives.sh"

      - name: verify checksums
        working-directory: npm/downloads
        run: bash "${GITHUB_WORKSPACE}/.github/scripts/build/verify-checksum.sh"

      - name: build npm packages
        run: bash "${GITHUB_WORKSPACE}/.github/scripts/build/build-npm-packages.sh"

      - name: smoke-test packaged linux-x64-gnu binary
        # Sanity-check that the binary we just packaged actually runs
        # before we upload it for the publish job. linux-x64-gnu is the
        # only target executable on ubuntu-latest; cross-target binaries
        # are exercised by the publish job's install-time checks.
        run: |
          set -euo pipefail
          bin="${GITHUB_WORKSPACE}/npm/dist/linux-x64-gnu/bin/runner"
          if [[ ! -x "${bin}" ]]; then
            echo "error: ${bin} missing or not executable" >&2
            exit 1
          fi
          "${bin}" --version

      - name: upload npm/dist artifact
        uses: actions/upload-artifact@v7
        with:
          name: npm-dist
          path: npm/dist/
          retention-days: 14
          if-no-files-found: error

  # Flip the draft release that create-release produced to published once
  # binaries and the npm-dist artifact are in place. This event drives the
  # downstream npm-release.yml workflow. Idempotent — `gh release edit
  # --draft=false` on an already-published release is a no-op.
  publish-release:
    name: publish github release
    needs: [build-npm-dist]
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    env:
      GH_TOKEN: ${{ github.token }}
      RELEASE_TAG: ${{ github.ref_name }}
    steps:
      - run: gh release edit "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --draft=false
