diff --git a/.github/workflows/uv-lock-automation.yaml b/.github/workflows/uv-lock-automation.yaml index 52c3364386..af4961add2 100644 --- a/.github/workflows/uv-lock-automation.yaml +++ b/.github/workflows/uv-lock-automation.yaml @@ -13,6 +13,14 @@ on: required: false default: "" # Run for all dirs specified in docker/scripts/uv-lock-gen/uv-lock.sh +# Serialize per branch. Pushing twice in quick succession used to start +# overlapping runs that raced each other's auto-commit, and the loser failed the +# whole job with "failed to push some refs". Only the newest run's lockfiles are +# of any interest, so cancel whatever is already in flight for the same branch. +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: update_uv_lock: name: Update UV lock in all directories diff --git a/docker/scripts/uv-lock-gen/uv-lock.sh b/docker/scripts/uv-lock-gen/uv-lock.sh index 7180b9fd56..213027e9d1 100755 --- a/docker/scripts/uv-lock-gen/uv-lock.sh +++ b/docker/scripts/uv-lock-gen/uv-lock.sh @@ -1,85 +1,39 @@ #!/bin/bash set -o pipefail -# Extract local path dependencies from [tool.uv.sources] in a pyproject.toml. -# Returns resolved pyproject.toml paths for each local dependency. -get_local_dep_pyprojects() { - local dir="$1" - local file_path="$dir/pyproject.toml" - - grep -A1 'path\s*=' "$file_path" 2>/dev/null \ - | grep -oP 'path\s*=\s*"\K[^"]+' \ - | while read -r rel_path; do - # Resolve relative to the service directory - local dep_pyproject - if [[ "$dir" == "." ]]; then - dep_pyproject="$rel_path/pyproject.toml" - else - dep_pyproject="$dir/$rel_path/pyproject.toml" - fi - # Normalize the path - dep_pyproject=$(realpath --relative-to=. "$dep_pyproject" 2>/dev/null || echo "$dep_pyproject") - if [[ -f "$dep_pyproject" ]]; then - echo "$dep_pyproject" - fi - done -} - -# Check if a directory's own pyproject.toml or any of its local -# path dependencies' pyproject.toml files have changed vs origin/main. -has_dependency_changes() { - local dir="$1" - local file_path="$dir/pyproject.toml" - - # Check direct changes - if ! git diff --quiet origin/main -- "$file_path"; then - echo "[$dir] Changes detected in '$file_path'" - return 0 - fi - - # Check transitive local dependency changes - local dep_pyprojects - dep_pyprojects=$(get_local_dep_pyprojects "$dir") - for dep in $dep_pyprojects; do - if ! git diff --quiet origin/main -- "$dep"; then - echo "[$dir] Changes detected in transitive dependency '$dep'" - return 0 - fi - done - - return 1 -} - # Function to update the lockfile in a directory +# +# `uv lock --check` is the authoritative staleness test: it exits non-zero only +# when uv.lock is missing or no longer agrees with pyproject.toml, transitive +# local path dependencies included. Gating on it means a PR that already carries +# correct lockfiles is a genuine no-op, rather than a re-resolve that rewrites +# the tree and leaves the auto-commit step with nothing useful to push. +# +# `uv lock` (not `uv sync`) is what we want here: the workflow only ever commits +# uv.lock, so building a virtualenv and installing every package is wasted work +# and the single largest source of failures in this job. update_lockfile() { - dir="$1" - file_path="$dir/pyproject.toml" + local dir="$1" + local file_path="$dir/pyproject.toml" if [[ ! -f "$file_path" ]]; then echo "[$dir] No pyproject.toml found in '$dir'" return 0 fi - echo "[$dir] Checking '$file_path' and its dependencies for changes against origin/main..." - if has_dependency_changes "$dir"; then - echo "[$dir] Updating uv.lock for '$dir' ..." + # Run in a subshell so the directory change is scoped to this invocation + ( + cd "$dir" || exit 1 - # Move to the directory if it's not root - if [[ "$dir" != "." ]]; then - cd "$dir" || return 1 + echo "[$dir] Checking whether uv.lock is up to date..." + if uv lock --check >/dev/null 2>&1; then + echo "[$dir] uv.lock is already up to date, nothing to do" + exit 0 fi - # Use uv to generate lock file - echo "[$dir] Updating uv.lock..." - uv sync 2>&1 | sed "s|^|[$dir] |" || return 1 - - # Go back to root if moved to a subdirectory - if [[ "$dir" != "." ]]; then - cd - || return 1 - fi - else - echo "[$dir] No changes detected in '$file_path' or its dependencies" - fi + echo "[$dir] uv.lock is out of date, regenerating..." + uv lock 2>&1 | sed "s|^|[$dir] |" + ) } # https://unix.stackexchange.com/a/124148 @@ -115,9 +69,6 @@ if [ "$#" -gt 0 ]; then directories=("$@") fi -# To compare against main branch -git fetch origin main - # Array to store the job PIDs and directories pids=() dirs=()