name: uv.lock check

# Verify uv.lock is in sync with pyproject.toml.  Blocking check — PRs
# that modify pyproject.toml without regenerating uv.lock (or vice versa)
# must not merge, because the Docker build's `uv sync --frozen` step will
# fail on a stale lockfile and we'd rather catch it here than in the
# docker workflow on main.
#
# ─────────────────────────────────────────────────────────────────────────
# IMPORTANT: this check runs against the MERGED state, not just your branch
# ─────────────────────────────────────────────────────────────────────────
#
# For `pull_request` events, GitHub checks out `refs/pull/<N>/merge` by
# default — a synthetic commit that merges your PR branch into the CURRENT
# state of `main`.  That means the pyproject.toml evaluated here is
# `main's pyproject.toml + your PR's changes to pyproject.toml`, not just
# what's on your branch.
#
# Failure mode this creates: if `main` has advanced since you branched
# (e.g. someone merged a PR that added a dep to pyproject.toml + its
# corresponding uv.lock entries), your branch's uv.lock is missing those
# new entries.  `uv lock --check` resolves against the merged pyproject
# and sees a lockfile that doesn't cover all the current deps → fails
# with "The lockfile at uv.lock needs to be updated."
#
# This can be confusing: `uv lock --check` passes locally (your branch
# is internally consistent) but fails in CI (merged state isn't).
#
# Fix is to sync your branch with main and regenerate the lockfile:
#
#     git fetch origin main
#     git rebase origin/main      # or merge, whatever the repo prefers
#     uv lock                     # regenerates uv.lock against new pyproject.toml
#     git add uv.lock
#     git commit -m "chore: refresh uv.lock after rebase onto main"
#     git push --force-with-lease # if you rebased
#
# If you also changed pyproject.toml in your PR, `uv lock` handles that
# at the same time — one regeneration covers both your changes and the
# drift from main.
#
# This is the correct behavior!  The check is protecting main's Docker
# build: a post-merge build would see the same merged state and fail
# the same way.  Better to catch it here than after merge.

on:
  workflow_call:
    outputs:
      review_status:
        description: "JSON review status for the review-status aggregator"
        value: ${{ jobs.check.outputs.review_status }}

permissions:
  contents: read

concurrency:
  group: uv-lockfile-check-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  check:
    name: uv lock --check
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      review_status: ${{ steps.verify.outputs.review_status }}
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Install uv
        uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0
        with:
          # Pinned: unpinned setup-uv fetches a 'latest' manifest from
          # raw.githubusercontent.com every job; transient fetch failures
          # fail the job (2026-07-28 incident). Keep in sync with tests.yml.
          version: "0.9.28"

      # `uv lock --check` re-resolves the project from pyproject.toml and
      # compares the result to uv.lock, exiting non-zero if they disagree.
      # No network writes, no file modifications.
      #
      # On PRs this runs against the merge commit (see comment at the top
      # of this file) — failures often mean "your branch is behind main,
      # rebase and regenerate uv.lock."
      - name: Verify uv.lock is up-to-date
        id: verify
        run: |
          # uv lock --check re-resolves against PyPI (network). Retry so a
          # registry blip doesn't read as "lockfile stale". A genuinely stale
          # lockfile fails all attempts (deterministic), costing only seconds.
          ok=false
          for i in 1 2 3; do
            if uv lock --check; then
              ok=true
              break
            fi
            [ "$i" = 3 ] && break
            echo "::warning::uv lock --check failed (attempt $i); retrying in 10s"
            sleep 10
          done
          if [ "$ok" != true ]; then
            cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
          ## ❌ uv.lock is out of sync with pyproject.toml

          **If this is a PR:** this check runs against the merged state
          (your branch + current `main`), not just your branch.  If
          `uv lock --check` passes locally, your branch is likely behind
          `main` — recent changes to `pyproject.toml` on `main` aren't
          reflected in your branch's `uv.lock` yet.

          To fix, sync with main and regenerate the lockfile:

          ```bash
          git fetch origin main
          git rebase origin/main   # or `git merge origin/main`
          uv lock                  # regenerate against new pyproject.toml
          git add uv.lock
          git commit -m "chore: refresh uv.lock after syncing with main"
          git push --force-with-lease  # drop --force-with-lease if you merged
          ```

          **If you only changed pyproject.toml:** run `uv lock` locally
          and commit the result.

          This check is blocking because the Docker image build uses
          `uv sync --frozen --extra all`, which rejects stale lockfiles
          — catching it here avoids a ~15 min failed docker run
          on `main` post-merge.
          EOF
            echo "::error title=uv.lock out of sync::Run \`uv lock\` locally and commit the result. If on a PR, sync with main first."
            review_status='[{"source":"uv.lock check","results":[{"kind":"action_required","title":"uv.lock out of sync","summary":"uv.lock is out of sync with pyproject.toml.","how_to_fix":"Run `uv lock` locally and commit the result. If on a PR, sync with main first:\n```\ngit fetch origin main\ngit rebase origin/main\nuv lock\ngit add uv.lock\ngit commit -m \"chore: refresh uv.lock\"\n```\n"}]}]'
            echo "review_status=${review_status}" >> "$GITHUB_OUTPUT"
            exit 1
          fi
          review_status='[]'
          echo "review_status=${review_status}" >> "$GITHUB_OUTPUT"
