# 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"