Skip to content

Commit d27c73a

Browse files
dmealingclaude
andcommitted
chore(python): uv.lock still resolved 1.0.0, and nothing could see it
The 1.0.1 cut bumped `server/python/pyproject.toml` but not `uv.lock`, which went on stating `version = "1.0.0"` for the project itself. Found by running the python lane locally and getting a dirty tree afterwards. It stayed invisible because no lane runs `uv` with `--locked` or `--frozen`: plain `uv run` REWRITES a disagreeing lockfile in place, so every lane passes, the committed file keeps saying something untrue, and the only symptom is a surprise modified file for whoever next runs the suite. It also is not the first time — the 1.0.0 cut needed its own follow-up commit (`e5f39e302 chore(release): uv.lock resolves metaobjects at 1.0.0`), which is what a step that is easy to skip and impossible to detect looks like on its second occurrence. Regenerates the lock and adds `scripts/check-uv-lock-version.sh` to the gates lane beside the pom and bun version-parity guards, so the third occurrence fails instead of drifting. Toolchain-free (awk + grep, no uv) so it runs in `gates` rather than needing the python lane, and the package entry is matched inside its own `[[package]]` block so a similarly-named dependency cannot be read instead. Verified by restoring the stale lock: exit 1, naming both versions and the `uv lock` command that fixes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent e075dab commit d27c73a

3 files changed

Lines changed: 79 additions & 1 deletion

File tree

scripts/check-uv-lock-version.sh

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Guard: server/python/uv.lock's own `metaobjects` entry must carry the SAME version
4+
# as server/python/pyproject.toml.
5+
#
6+
# Why this exists: nothing in this repo runs `uv` with `--locked` or `--frozen` — every
7+
# lane uses plain `uv run`, which silently REWRITES the lockfile in place when it
8+
# disagrees with the project. So a release that bumps pyproject.toml and forgets the
9+
# lock leaves a committed file stating the previous version, every lane stays green,
10+
# and the drift is invisible until someone happens to run the suite and notices a dirty
11+
# tree. That is not hypothetical and it is not once: the 1.0.0 cut needed a dedicated
12+
# follow-up commit (`chore(release): uv.lock resolves metaobjects at 1.0.0`), and the
13+
# 1.0.1 cut skipped the step again — pyproject at 1.0.1, lock still saying 1.0.0.
14+
#
15+
# It matters beyond tidiness because the lock is what a `--locked` install would
16+
# resolve: the moment any consumer or CI adds that flag, a stale lock fails the install
17+
# rather than quietly self-healing.
18+
#
19+
# Toolchain-free on purpose (grep + sed, no uv), so it runs in the `gates` lane beside
20+
# the pom and bun version-parity guards rather than needing the python lane.
21+
#
22+
# Usage: scripts/check-uv-lock-version.sh # exit 1 + diagnostic on drift
23+
#
24+
set -uo pipefail
25+
26+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
27+
PYPROJECT="$ROOT/server/python/pyproject.toml"
28+
LOCK="$ROOT/server/python/uv.lock"
29+
30+
for f in "$PYPROJECT" "$LOCK"; do
31+
[ -f "$f" ] || { echo "check-uv-lock-version: missing $f" >&2; exit 1; }
32+
done
33+
34+
# `version = "..."` under [project]; the first such line in the file is the project's.
35+
project_version="$(grep -m1 -oE '^version[[:space:]]*=[[:space:]]*"[^"]+"' "$PYPROJECT" \
36+
| sed -E 's/.*"([^"]+)".*/\1/')"
37+
if [ -z "$project_version" ]; then
38+
echo "check-uv-lock-version: could not read the version from $PYPROJECT" >&2
39+
exit 1
40+
fi
41+
42+
# The lock's own entry for this project — `name = "metaobjects"` followed by its
43+
# version. Anchored to the package block so a DEPENDENCY that happens to be named
44+
# similarly cannot be read instead.
45+
lock_version="$(awk '
46+
/^name = "metaobjects"$/ { inpkg = 1; next }
47+
inpkg && /^version = / { gsub(/^version = "|"$/, ""); print; exit }
48+
inpkg && /^\[\[package\]\]$/ { inpkg = 0 }
49+
' "$LOCK")"
50+
if [ -z "$lock_version" ]; then
51+
echo "check-uv-lock-version: could not find the metaobjects package entry in $LOCK" >&2
52+
exit 1
53+
fi
54+
55+
if [ "$project_version" != "$lock_version" ]; then
56+
cat >&2 <<MSG
57+
check-uv-lock-version: server/python/uv.lock is STALE.
58+
59+
pyproject.toml : $project_version
60+
uv.lock : $lock_version
61+
62+
The lock still resolves the previous version. Nothing runs uv with --locked, so every
63+
lane will keep passing while the committed lock says something untrue — and a plain
64+
\`uv run\` rewrites it, so whoever next runs the suite gets a surprise dirty tree.
65+
66+
Fix: (cd server/python && uv lock) then commit server/python/uv.lock
67+
MSG
68+
exit 1
69+
fi
70+
71+
echo "check-uv-lock-version: uv.lock agrees with pyproject.toml ($project_version)"

scripts/ci-local.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ gate_publish_set() { node scripts/publish-set.mjs --check && node scripts/test-p
173173
# checks names. Offline; one manifest.
174174
gate_script_name_hooks() { node scripts/check-script-name-hooks.mjs; }
175175

176+
# ── release hygiene: uv.lock must agree with pyproject.toml ──────────────────
177+
# Every lane runs plain `uv run`, which REWRITES a disagreeing lockfile in place, so a
178+
# release that bumps pyproject and forgets the lock stays green forever. Twice now: the
179+
# 1.0.0 cut needed a follow-up commit, and 1.0.1 skipped the step again.
180+
gate_uv_lock_version() { scripts/check-uv-lock-version.sh; }
181+
176182
# ── the lane selector decides what CI runs, so nothing may run it untested ────
177183
# Both selector tests existed-but-unwired at some point: test-ci-affected-ports.sh was
178184
# referenced by no lane at all, so the mapping that decides which ports get tested was
@@ -709,6 +715,7 @@ fi
709715
if want gates; then step "leak-scan (security)" gate_leak_scan; fi
710716
if want gates; then step "pom-version parity" gate_pom_versions; fi
711717
if want gates; then step "bun-version parity" gate_bun_version; fi
718+
if want gates; then step "uv.lock version parity" gate_uv_lock_version; fi
712719
if want gates; then step "publish-intent parity" gate_publish_intent; fi
713720
if want gates; then step "publish-set parity" gate_publish_set; fi
714721
if want gates; then step "no committed pre-release version" gate_no_prerelease_versions; fi

server/python/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)