fix(ci): stop running uv lock --check on PRs that can't touch the lockfile (#84675)

This commit is contained in:
brooklyn! 2026-08-12 13:30:01 -05:00 committed by GitHub
parent 6aaa181f0e
commit 9eab7a4473
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 58 additions and 6 deletions

View File

@ -33,6 +33,9 @@ outputs:
deps:
description: Check pyproject.toml dependency upper bounds.
value: ${{ steps.classify.outputs.deps }}
uv_lock:
description: Run `uv lock --check` (pyproject.toml / uv.lock changes only).
value: ${{ steps.classify.outputs.uv_lock }}
npm_lock:
description: Post/update the semantic package-lock.json diff PR comment.
value: ${{ steps.classify.outputs.npm_lock }}

View File

@ -46,6 +46,7 @@ jobs:
site: ${{ steps.classify.outputs.site }}
scan: ${{ steps.classify.outputs.scan }}
deps: ${{ steps.classify.outputs.deps }}
uv_lock: ${{ steps.classify.outputs.uv_lock }}
npm_lock: ${{ steps.classify.outputs.npm_lock }}
installer: ${{ steps.classify.outputs.installer }}
docker_meta: ${{ steps.classify.outputs.docker_meta }}
@ -143,6 +144,12 @@ jobs:
uv-lockfile:
name: Check uv.lock
needs: detect
# Gated: `uv lock --check` re-resolves the whole dependency graph against
# PyPI, so on every PR it spent a network round-trip — and, on a registry
# blip, a blocking red X — for diffs that cannot desync the lockfile
# (docs, frontend, prose). Only pyproject.toml / uv.lock can. A
# `.github/` change still forces it on via the classifier's fail-open.
if: needs.detect.outputs.uv_lock == 'true'
uses: ./.github/workflows/uv-lockfile-check.yml
infographic-check:

View File

@ -89,16 +89,46 @@ jobs:
# 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.
#
# Backoff rather than a flat 10s: three attempts inside ~20s all
# land in the same blip. 5/15/45s spans ~65s instead.
#
# Network failure and a real desync are also reported differently.
# uv says "Request failed after N retries" when it can't reach the
# registry, versus "lockfile needs to be updated" when the lock is
# genuinely stale. Only the second is the contributor's to fix, so
# an unreachable registry says so instead of sending them to
# `uv lock` with nothing to regenerate.
ok=false
net_fail=false
delays=(5 15 45)
for i in 1 2 3; do
if uv lock --check; then
if uv lock --check >lock-check.log 2>&1; then
ok=true
net_fail=false
cat lock-check.log
break
fi
cat lock-check.log
if grep -qiE "Request failed after|Failed to fetch|error sending request|connection (reset|closed)|timed out" lock-check.log; then
net_fail=true
else
# Deterministic failure (a real desync) — retrying just prints
# the same error twice more.
net_fail=false
break
fi
[ "$i" = 3 ] && break
echo "::warning::uv lock --check failed (attempt $i); retrying in 10s"
sleep 10
echo "::warning::uv lock --check could not reach the registry (attempt $i); retrying in ${delays[$((i-1))]}s"
sleep "${delays[$((i-1))]}"
done
if [ "$ok" != true ] && [ "$net_fail" = true ]; then
echo "::error title=uv.lock check could not reach PyPI::Registry unreachable after 3 attempts — infrastructure failure, not a stale lockfile. Re-run the job."
review_status='[{"source":"uv.lock check","results":[{"kind":"action_required","title":"uv.lock check could not reach PyPI","summary":"`uv lock --check` could not reach the package registry after 3 attempts. This is an infrastructure failure, not a stale lockfile.","how_to_fix":"Re-run the failed job. No change to `uv.lock` is needed."}]}]'
echo "review_status=${review_status}" >> "$GITHUB_OUTPUT"
echo "review_status=${review_status}" > review-status.json
exit 1
fi
if [ "$ok" != true ]; then
cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
## ❌ uv.lock is out of sync with pyproject.toml

View File

@ -18,6 +18,9 @@ Lanes:
* ``site`` Docusaurus + generated skill docs.
* ``scan`` supply-chain scan (Python files, .pth, setup hooks).
* ``deps`` pyproject.toml dependency bounds check.
* ``uv_lock`` ``uv lock --check``. Re-resolves the whole graph against
PyPI, so a diff that touches neither ``pyproject.toml`` nor ``uv.lock``
must not run it.
* ``npm_lock`` semantic package-lock.json diff PR comment.
* ``installer`` PowerShell installer tests (Windows runner).
* ``mcp_catalog`` bundled MCP catalog / installer review.
@ -132,6 +135,7 @@ def classify(files: list[str]) -> dict[str, bool]:
"site": any(f.startswith(_SITE) for f in files),
"scan": any(_is_scan(f) for f in files),
"deps": any(f == "pyproject.toml" for f in files),
"uv_lock": any(f in ("pyproject.toml", "uv.lock") for f in files),
"npm_lock": any(f.split("/")[-1] == "package-lock.json" for f in files),
"installer": any(_is_installer(f) for f in files),
"mcp_catalog": any(_is_mcp_catalog(f) for f in files),
@ -145,6 +149,7 @@ def classify(files: list[str]) -> dict[str, bool]:
ret["site"] = True
ret["scan"] = True
ret["deps"] = True
ret["uv_lock"] = True
ret["npm_lock"] = True
ret["installer"] = True
ret["ci_review"] = True

View File

@ -29,6 +29,7 @@ DEFAULT = {
"site": True,
"scan": True,
"deps": True,
"uv_lock": True,
"npm_lock": True,
"installer": True,
"mcp_catalog": False,
@ -36,7 +37,7 @@ DEFAULT = {
}
def _lanes(python=False, frontend=False, site=False, scan=False, deps=False, npm_lock=False, installer=False, mcp_catalog=False, docker_meta=False, ci_review=False, python_prod=None) -> dict[str, bool]:
def _lanes(python=False, frontend=False, site=False, scan=False, deps=False, uv_lock=False, npm_lock=False, installer=False, mcp_catalog=False, docker_meta=False, ci_review=False, python_prod=None) -> dict[str, bool]:
# python_prod tracks python except for tests-only diffs; default it to
# python so the majority of cases don't need to spell it out.
return {
@ -47,6 +48,7 @@ def _lanes(python=False, frontend=False, site=False, scan=False, deps=False, npm
"site": site,
"scan": scan,
"deps": deps,
"uv_lock": uv_lock,
"npm_lock": npm_lock,
"installer": installer,
"mcp_catalog": mcp_catalog,
@ -57,14 +59,19 @@ def _lanes(python=False, frontend=False, site=False, scan=False, deps=False, npm
CASES = {
"docs-only → nothing heavy": (["README.md", "docs/guide.md"], _lanes()),
"python source → python": (["run_agent.py"], _lanes(python=True, scan=True)),
"dep manifest → python": (["pyproject.toml"], _lanes(python=True, scan=True, deps=True)),
"uv.lock → python": (["uv.lock"], _lanes(python=True)),
"dep manifest → python": (["pyproject.toml"], _lanes(python=True, scan=True, deps=True, uv_lock=True)),
"uv.lock → python": (["uv.lock"], _lanes(python=True, uv_lock=True)),
"ts package → frontend": (["apps/desktop/src/app.tsx"], _lanes(frontend=True)),
"ui-tui → frontend": (["ui-tui/src/entry.ts"], _lanes(frontend=True)),
# Lockfile bump shifts every TS package's tree, but not the Python suite.
"root lockfile → frontend, not python": (["package-lock.json"], _lanes(frontend=True, npm_lock=True)),
"nested lockfile → npm_lock": (["website/package-lock.json"], _lanes(site=True, npm_lock=True)),
"website → site": (["website/docs/intro.md"], _lanes(site=True)),
# uv lock --check re-resolves against PyPI, so it must stay off for any
# diff that can't desync the lockfile — a registry blip on a docs PR
# otherwise shows up as a blocking "uv.lock out of sync" red X.
"docs → no uv_lock": (["website/docs/user-guide/profiles.md"], _lanes(site=True)),
"frontend → no uv_lock": (["apps/desktop/src/store/profile.ts"], _lanes(frontend=True)),
# SKILL.md reads like docs, but the skill-doc tests read skills/, so a
# skill edit must still run Python.
"skill md → python + site": (["skills/github/SKILL.md"], _lanes(python=True, site=True)),