# Monthly bump of the uv pin in `.github/actions/uv_setup/action.yml`.
#
# We pin uv (rather than letting setup-uv resolve latest) because
# `releases.astral.sh` lags GitHub Releases on new uv versions, causing CI
# to flap on fresh-release days. This workflow keeps the pin fresh without
# exposing that race.
#
# Dependabot's `github-actions` ecosystem only updates `uses:` SHA pins, not
# the `UV_VERSION` env value the action passes to `astral-sh/setup-uv`, so we
# open the PR ourselves. Idempotent: if a PR for the target version already
# exists, the workflow exits without creating a duplicate.

name: "Bump uv pin"

on:
  schedule:
    - cron: "0 9 1 * *"
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: bump-uv-pin
  cancel-in-progress: false

jobs:
  bump:
    if: github.repository_owner == 'langchain-ai'
    name: "Open PR if uv has a newer release"
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Resolve current and latest uv versions
        id: versions
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          action_file=".github/actions/uv_setup/action.yml"
          current=$(grep -oE 'UV_VERSION: "[0-9]+\.[0-9]+\.[0-9]+"' "$action_file" \
            | sed -E 's/UV_VERSION: "([^"]+)"/\1/' | head -n1)
          latest=$(gh api repos/astral-sh/uv/releases/latest --jq .tag_name)
          semver='^[0-9]+\.[0-9]+\.[0-9]+$'
          if [[ ! "$current" =~ $semver ]]; then
            echo "::error::Could not parse current uv pin from $action_file (got '$current')"
            exit 1
          fi
          if [[ ! "$latest" =~ $semver ]]; then
            echo "::error::Unexpected uv tag from GitHub API (got '$latest')"
            exit 1
          fi
          echo "current=$current" >> "$GITHUB_OUTPUT"
          echo "latest=$latest" >> "$GITHUB_OUTPUT"
          echo "branch=chore/bump-uv-$latest" >> "$GITHUB_OUTPUT"
          echo "Current pin: $current"
          echo "Latest uv:   $latest"

      - name: Log if already up to date
        # The actual skip is implemented by the `if:` guards on every
        # subsequent step; this step only emits a log line so the run
        # history shows why no PR was opened.
        if: steps.versions.outputs.current == steps.versions.outputs.latest
        run: echo "uv pin already at ${{ steps.versions.outputs.latest }}; nothing to do."

      - name: Skip if PR already open for this version
        id: existing
        if: steps.versions.outputs.current != steps.versions.outputs.latest
        env:
          GH_TOKEN: ${{ github.token }}
          BRANCH: ${{ steps.versions.outputs.branch }}
        run: |
          set -euo pipefail
          count=$(gh pr list --head "$BRANCH" --state open --json number --jq 'length')
          echo "count=$count" >> "$GITHUB_OUTPUT"
          if [ "$count" -gt 0 ]; then
            echo "Open PR already exists for $BRANCH; skipping."
          fi

      - name: Wait for astral mirror to replicate
        id: mirror
        if: steps.versions.outputs.current != steps.versions.outputs.latest && steps.existing.outputs.count == '0'
        env:
          LATEST: ${{ steps.versions.outputs.latest }}
        run: |
          set -euo pipefail
          # The mirror can lag GitHub Releases. If it hasn't replicated yet,
          # defer the bump rather than landing a pin that races the mirror
          # on every CI run. We probe several arches because partial
          # replication (linux ready, macOS/aarch64 not) would still race
          # CI on other runners.
          assets=(
            "uv-x86_64-unknown-linux-gnu.tar.gz"
            "uv-aarch64-unknown-linux-gnu.tar.gz"
            "uv-x86_64-apple-darwin.tar.gz"
            "uv-aarch64-apple-darwin.tar.gz"
          )
          ready=true
          for asset in "${assets[@]}"; do
            url="https://releases.astral.sh/github/uv/releases/download/${LATEST}/${asset}"
            # `curl -sI` returns nothing on stderr at -s; capture exit code so a
            # permanently broken DNS/TLS path is surfaced instead of collapsing
            # to an opaque "000".
            set +e
            status=$(curl -sIo /dev/null -w '%{http_code}' --max-time 30 "$url" 2>/tmp/curl.err)
            curl_rc=$?
            set -e
            echo "Mirror HEAD $url -> HTTP $status (curl exit=$curl_rc)"
            if [ "$status" != "200" ]; then
              ready=false
              if [ "$curl_rc" -ne 0 ]; then
                echo "::warning::curl failed for $asset (exit=$curl_rc): $(cat /tmp/curl.err 2>/dev/null || true)"
              else
                echo "::warning::astral mirror has not replicated $asset for uv $LATEST yet (HTTP $status)."
              fi
            fi
          done
          if [ "$ready" = "true" ]; then
            echo "ready=true" >> "$GITHUB_OUTPUT"
          else
            echo "ready=false" >> "$GITHUB_OUTPUT"
            echo "::warning::Deferring uv bump to $LATEST until all probed arches are mirrored."
          fi

      - name: Open bump PR
        if: steps.versions.outputs.current != steps.versions.outputs.latest && steps.existing.outputs.count == '0' && steps.mirror.outputs.ready == 'true'
        env:
          GH_TOKEN: ${{ github.token }}
          CURRENT: ${{ steps.versions.outputs.current }}
          LATEST: ${{ steps.versions.outputs.latest }}
          BRANCH: ${{ steps.versions.outputs.branch }}
          DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
        run: |
          set -euo pipefail
          action_file=".github/actions/uv_setup/action.yml"

          # `grep -c` returns 1 on no-match and 2 on read errors. We want
          # "no match" surfaced as the explicit count-of-zero check below;
          # read errors must abort. Capture the exit code separately so
          # `set -e` doesn't swallow either case.
          set +e
          before=$(grep -cE "UV_VERSION: \"${CURRENT}\"" "$action_file")
          before_rc=$?
          set -e
          if [ "$before_rc" -gt 1 ]; then
            echo "::error::grep read error on $action_file (exit=$before_rc)"
            exit 1
          fi
          if [ "$before" -ne 1 ]; then
            echo "::error::Expected exactly 1 'UV_VERSION: \"$CURRENT\"' in $action_file, found $before"
            exit 1
          fi
          sed -i -E "s/UV_VERSION: \"${CURRENT}\"/UV_VERSION: \"${LATEST}\"/" "$action_file"
          set +e
          after=$(grep -cE "UV_VERSION: \"${LATEST}\"" "$action_file")
          after_rc=$?
          set -e
          if [ "$after_rc" -gt 1 ]; then
            echo "::error::grep read error on $action_file (exit=$after_rc)"
            exit 1
          fi
          if [ "$after" -ne 1 ]; then
            echo "::error::Expected exactly 1 'UV_VERSION: \"$LATEST\"' after sed, found $after"
            exit 1
          fi
          if git diff --quiet "$action_file"; then
            echo "No changes after sed; bailing out (current=$CURRENT, latest=$LATEST)."
            exit 1
          fi

          # Reuse-or-recreate orphan branch from a prior run that pushed
          # but failed before `gh pr create` (no open PR sits on it).
          # The delete can race a concurrent run (manual workflow_dispatch
          # firing while the cron is mid-flight, since concurrency group
          # does not cancel-in-progress); fall through with a warning so a
          # losing race does not kill an otherwise-clean job mid-state.
          if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
            echo "::warning::Branch $BRANCH exists on origin without an open PR; deleting before recreating."
            if ! git push origin --delete "$BRANCH"; then
              echo "::warning::Delete of $BRANCH failed (concurrent run, or branch already gone); the subsequent push will surface any real conflict."
            fi
          fi

          git config --local user.name "github-actions[bot]"
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git checkout -b "$BRANCH"
          git add "$action_file"
          git commit -m "chore(deps): bump uv to $LATEST"
          git push --set-upstream origin "$BRANCH"

          body_file="$(mktemp)"
          {
            printf 'Bumps the uv pin in `.github/actions/uv_setup/action.yml` from `%s` to [`%s`](https://github.com/astral-sh/uv/releases/tag/%s).\n\n' "$CURRENT" "$LATEST" "$LATEST"
            printf 'Opened automatically by `bump_uv_pin.yml`. Mirror availability on `releases.astral.sh` was verified before this PR was created, so CI should not race the fallback.\n'
          } > "$body_file"

          gh pr create \
            --head "$BRANCH" \
            --base "$DEFAULT_BRANCH" \
            --title "chore(deps): bump uv to $LATEST" \
            --body-file "$body_file"
