fix(ci): stop running uv lock --check on PRs that can't touch the lockfile (#84675)
This commit is contained in:
parent
6aaa181f0e
commit
9eab7a4473
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
Loading…
Reference in New Issue