patchcov 0.4.1

Patch coverage for git diffs: attribute lcov, llvm-cov, Cobertura, Go and JaCoCo reports to a diff and gate on it.
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  merge_group:

env:
  CARGO_TERM_COLOR: always

# Every job sets `timeout-minutes`: the default is 6 hours, and a hung test would otherwise hold a
# required check until the merge queue gives up at 60 minutes. The limits are several times the
# slowest run seen over the last 100 runs (113 s, Windows test); the jobs that run tests get 15
# (so does Coverage, which can build twice when it has to recompute a baseline), the build-only
# ones 10.
jobs:
  test:
    name: Test (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo test --locked --all-targets
      - run: cargo test --locked --doc

  lint:
    name: Lint
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - uses: Swatinem/rust-cache@v2
      - run: cargo fmt --check
      - run: cargo clippy --locked --all-targets -- -D warnings
      - run: cargo run --locked -- lint-markers

  msrv:
    name: MSRV (1.88)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@1.88.0
      - uses: Swatinem/rust-cache@v2
      - run: cargo test --locked --all-targets
      - run: cargo test --locked --doc

  docs:
    name: Docs
    runs-on: ubuntu-latest
    timeout-minutes: 10
    env:
      RUSTDOCFLAGS: -D warnings
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo doc --locked --no-deps --document-private-items

  # patchcov measures its own pull requests with the action it ships. Coverage is required
  # by the main ruleset, so a red PR run blocks a merge. The action installs the Rust toolchain,
  # the cache and cargo-llvm-cov itself. It publishes the baseline on a push to main, from
  # this workflow (the action looks for `ci.yml`), and comments and gates on pull requests.
  # On a merge_group run it would only repeat the instrumented tests, so the job is skipped
  # there; the skipped job satisfies the queue's required check. Only `@v1` is
  # tagged; `v1` defaults to an old patchcov, so `version` asks for the current release.
  coverage:
    name: Coverage
    if: github.event_name != 'merge_group'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: read
      actions: read # find and download the baseline artifact of the merge base's run
      pull-requests: write # post the coverage comment
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0 # full history so the merge base resolves
      - uses: action-works/patchcov-action@v1
        with:
          version: latest
          fail-under-patch: 90
          fail-under-lines: 95
          # A fork's token is read-only, so the comment would fail; the gates still apply.
          comment: ${{ github.event.pull_request.head.repo.fork != true }}

  # Exercise the PR's own CLI as well as the released action above. Advisory and PR-only:
  # no baseline recompute or publish, so this report has no deltas/indirect changes.
  coverage-pr-build:
    name: Coverage (PR build)
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - uses: Swatinem/rust-cache@v2
        with:
          key: coverage-pr-build
      - uses: taiki-e/install-action@cargo-llvm-cov
      - name: Build the PR's patchcov
        run: cargo build --locked --bin patchcov
      - name: Measure the PR head
        run: |
          cargo llvm-cov --locked --all-features --workspace --no-report
          cargo llvm-cov report --lcov --output-path coverage-pr-head.lcov
      - name: Render with the PR's patchcov
        id: diff
        env:
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
          COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          base=$(git merge-base "$BASE_SHA" "$HEAD_SHA")
          echo "base=$base" >> "$GITHUB_OUTPUT"
          ./target/debug/patchcov diff --report coverage-pr-head.lcov \
            --base-ref "$base" --head-ref "$HEAD_SHA" --collapse-ranges \
            --base-sha "$base" --head-sha "$HEAD_SHA" \
            --commit-url "$COMMIT_URL" --run-url "$RUN_URL" > coverage-pr-build.md
          {
            echo '## PR build'
            echo
            echo "Measured and rendered by this PR's own patchcov build. No baseline deltas; see the release Coverage report for those."
            echo
            cat coverage-pr-build.md
          } > coverage-pr-comment.md
      - name: Upload PR-build coverage
        id: coverage-artifact
        uses: actions/upload-artifact@v7
        with:
          name: coverage-pr-build
          if-no-files-found: error
          path: |
            coverage-pr-head.lcov
            coverage-pr-comment.md
      # The archived report has the run link; the upload URL exists only after upload.
      - name: Link PR-build artifact and publish run summary
        env:
          ARTIFACT_URL: ${{ steps.coverage-artifact.outputs.artifact-url }}
        run: |
          printf '\n<sub>📦 [PR-build coverage report (LCOV and Markdown)](%s)</sub>\n' "$ARTIFACT_URL" >> coverage-pr-comment.md
          cat coverage-pr-comment.md >> "$GITHUB_STEP_SUMMARY"
      - name: Comment PR-build coverage
        if: github.event.pull_request.head.repo.fork != true
        uses: marocchino/sticky-pull-request-comment@v3
        with:
          header: coverage-pr-build
          path: coverage-pr-comment.md
      # Render/upload/comment before gating so low coverage still leaves a report.
      - name: Enforce coverage gates with the PR's patchcov
        env:
          BASE_SHA: ${{ steps.diff.outputs.base }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        run: |
          ./target/debug/patchcov diff --report coverage-pr-head.lcov \
            --base-ref "$BASE_SHA" --head-ref "$HEAD_SHA" \
            --fail-under-patch 90 --fail-under-lines 95 -o json > /dev/null