ci(sdk-python): harden py-version detection — max-over-releases + numeric-local-only

Compare against the max numeric version in PyPI `releases` rather than
`info.version` (latest-uploaded, not highest). Apply strict dotted-numeric
validation to the LOCAL version only; non-numeric published versions
(prereleases) are filtered out instead of aborting the script. Add curl
--max-time/--retry hardening and red-green test coverage for both cases.
This commit is contained in:
Jordan Ritter
2026-05-28 15:46:07 -07:00
committed by Jordan Ritter
parent fcc5f537e3
commit 8b758ac806
2 changed files with 104 additions and 13 deletions
@@ -6,8 +6,9 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
SCRIPT="${HERE}/../detect-py-version-changes.sh"
TMP="$(mktemp -d)"
SRV_PID=""
STDERR_LOG="$TMP/stderr.log"
cleanup() { [ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null || true; rm -rf "$TMP"; }
trap cleanup EXIT
trap cleanup EXIT INT TERM
# Preflight: tomllib requires py3.11+
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,11) else 1)' \
@@ -39,21 +40,59 @@ httpd.serve_forever()
PY
SRV_PID=$!
for _ in $(seq 1 50); do [ -s "$PORTFILE" ] && break; sleep 0.1; done
if [ ! -s "$PORTFILE" ]; then
echo "FAIL: fixture server failed to bind/write PORTFILE within 5s" >&2
exit 1
fi
PORT="$(cat "$PORTFILE")"
}
stop_server() { kill "$SRV_PID" 2>/dev/null || true; wait "$SRV_PID" 2>/dev/null || true; SRV_PID=""; }
serve_published() { mkdir -p "$WWW/pypi/copilotkit"; printf '{"info":{"version":"%s"}}' "$1" > "$WWW/pypi/copilotkit/json"; }
serve_published() {
# Realistic PyPI-shaped response: info.version AND a releases dict (single
# released version). Real PyPI always returns `releases`; the bare-info shape
# was a test-only shortcut that the max-over-releases logic doesn't match.
mkdir -p "$WWW/pypi/copilotkit"
printf '{"info":{"version":"%s"},"releases":{"%s":[]}}' "$1" "$1" > "$WWW/pypi/copilotkit/json"
}
# Serve a fuller PyPI-shaped response: info.version + a releases dict whose keys
# are the version strings. $1 = info.version, remaining args = release keys.
serve_published_with_releases() {
mkdir -p "$WWW/pypi/copilotkit"
local info="$1"; shift
local rels="" k
for k in "$@"; do
[ -z "$rels" ] && rels="\"$k\":[]" || rels="$rels,\"$k\":[]"
done
printf '{"info":{"version":"%s"},"releases":{%s}}' "$info" "$rels" > "$WWW/pypi/copilotkit/json"
}
run() {
PYPROJECT_PATH="$TMP/pkg/pyproject.toml" PYPI_BASE_URL="http://127.0.0.1:${PORT}" \
"$SCRIPT" 2>/dev/null | tail -n1
"$SCRIPT" 2>"$STDERR_LOG" | tail -n1
}
fail() {
echo "FAIL: $1" >&2
if [ -s "$STDERR_LOG" ]; then
echo "--- captured stderr ---" >&2
cat "$STDERR_LOG" >&2
echo "--- end stderr ---" >&2
fi
exit 1
}
fail() { echo "FAIL: $1" >&2; exit 1; }
# Case A: published == local (0.2.0) -> should_publish=false (no-op)
# Also assert GITHUB_OUTPUT emission: the script must append should_publish=,
# name=, and version= lines when GITHUB_OUTPUT is set.
rm -rf "$WWW"; serve_published "0.2.0"; start_server
OUT="$(run)"; echo "A: $OUT"; [ "$OUT" = "false copilotkit 0.2.0" ] || fail "no-op: got '$OUT'"
GHO="$TMP/gho.txt"; : > "$GHO"
OUT="$(GITHUB_OUTPUT="$GHO" PYPROJECT_PATH="$TMP/pkg/pyproject.toml" PYPI_BASE_URL="http://127.0.0.1:${PORT}" \
"$SCRIPT" 2>"$STDERR_LOG" | tail -n1)"
echo "A: $OUT"; [ "$OUT" = "false copilotkit 0.2.0" ] || fail "no-op: got '$OUT'"
grep -q '^should_publish=false$' "$GHO" || fail "GITHUB_OUTPUT missing should_publish=false (got: $(cat "$GHO"))"
grep -q '^name=copilotkit$' "$GHO" || fail "GITHUB_OUTPUT missing name=copilotkit (got: $(cat "$GHO"))"
grep -q '^version=0.2.0$' "$GHO" || fail "GITHUB_OUTPUT missing version=0.2.0 (got: $(cat "$GHO"))"
stop_server
# Case B: published < local (0.1.91 < 0.2.0) -> should_publish=true (exactly one pkg)
@@ -66,4 +105,30 @@ rm -rf "$WWW"; mkdir -p "$WWW"; start_server
OUT="$(run)"; echo "C: $OUT"; [ "$OUT" = "true copilotkit 0.2.0" ] || fail "new-pkg: got '$OUT'"
stop_server
# Case D: info.version is LOWER than the true max in releases. PyPI's info.version
# is the LATEST-UPLOADED, not the highest — an out-of-order patch upload to an
# old line can produce this state. The script must compute the max over the
# numeric-parseable releases keys, not trust info.version.
# releases = {0.1.0, 0.2.0}, info.version=0.1.0, local=0.2.0 -> 0.2.0==0.2.0 -> false.
rm -rf "$WWW"; serve_published_with_releases "0.1.0" "0.1.0" "0.2.0"; start_server
OUT="$(run)"; echo "D: $OUT"; [ "$OUT" = "false copilotkit 0.2.0" ] || fail "max-over-releases: got '$OUT'"
stop_server
# Case E: releases contains a non-numeric prerelease key alongside numeric. The
# script must ignore non-numeric published keys (not abort on them) and compare
# against the numeric max. info.version is the prerelease (rc1); local is 0.2.1.
# numeric max published = 0.2.0 < 0.2.1 -> should_publish=true.
rm -rf "$WWW"
mkdir -p "$TMP/pkg2"
cat > "$TMP/pkg2/pyproject.toml" <<'TOML'
[tool.poetry]
name = "copilotkit"
version = "0.2.1"
TOML
serve_published_with_releases "0.2.1rc1" "0.2.0" "0.2.1rc1"; start_server
OUT="$(PYPROJECT_PATH="$TMP/pkg2/pyproject.toml" PYPI_BASE_URL="http://127.0.0.1:${PORT}" \
"$SCRIPT" 2>"$STDERR_LOG" | tail -n1)"
echo "E: $OUT"; [ "$OUT" = "true copilotkit 0.2.1" ] || fail "non-numeric-released-ignored: got '$OUT'"
stop_server
echo "ALL PASS"
+34 -8
View File
@@ -30,12 +30,36 @@ echo "Local: ${NAME}==${VERSION}" >&2
# Fetch published version, distinguishing 404 (new package) from other failures.
RESP="$(mktemp)"; trap 'rm -f "$RESP"' EXIT
CODE="$(curl -sS -o "$RESP" -w '%{http_code}' "${PYPI_BASE_URL}/pypi/${NAME}/json" 2>/dev/null || echo "000")"
CODE="$(curl -sS --max-time 30 --retry 3 --retry-connrefused -o "$RESP" -w '%{http_code}' "${PYPI_BASE_URL}/pypi/${NAME}/json" 2>/dev/null || echo "000")"
case "$CODE" in
200)
PUBLISHED="$(python3 -c 'import sys,json; print(json.load(open(sys.argv[1]))["info"]["version"])' "$RESP")" \
|| { echo "ERROR: bad JSON from PyPI" >&2; exit 1; }
echo "Published: ${NAME}==${PUBLISHED}" >&2 ;;
# Compute the MAX numeric-parseable version from the `releases` dict (the
# complete set of released versions). `info.version` is the LATEST-UPLOADED,
# not necessarily the highest — out-of-order patch uploads to an old line
# can produce info.version < max(releases). Non-numeric keys (prereleases
# like "0.2.0rc1", dev/post tags) are filtered out, not aborted on. If no
# numeric keys exist, treat as empty (same as 404 -> NEW).
PUBLISHED="$(python3 - "$RESP" <<'PY'
import sys, json, re
with open(sys.argv[1]) as f:
data = json.load(f)
releases = data.get("releases") or {}
numeric = []
for k in releases.keys():
if re.fullmatch(r"\d+(\.\d+)*", k):
numeric.append(k)
if not numeric:
print("")
else:
best = max(numeric, key=lambda v: tuple(int(x) for x in v.split(".")))
print(best)
PY
)" || { echo "ERROR: bad JSON from PyPI" >&2; exit 1; }
if [ -z "$PUBLISHED" ]; then
echo "Published: ${NAME} has no numeric releases — treating as NEW" >&2
else
echo "Published: ${NAME}==${PUBLISHED}" >&2
fi ;;
404)
PUBLISHED=""; echo "Not found on PyPI — treating as NEW" >&2 ;;
*)
@@ -46,14 +70,16 @@ if [ -z "$PUBLISHED" ]; then
SHOULD_PUBLISH="true"
else
# Plain X.Y.Z numeric-tuple comparison (no third-party deps). The stable lane
# only ships dotted-numeric versions; refuse anything non-numeric loudly.
# only ships dotted-numeric LOCAL versions; refuse non-numeric LOCAL loudly.
# PUBLISHED is already guaranteed numeric (filtered above when computing max).
SHOULD_PUBLISH="$(python3 - "$VERSION" "$PUBLISHED" <<'PY'
import sys, re
def parse(v):
def parse_local(v):
if not re.fullmatch(r"\d+(\.\d+)*", v):
sys.exit(f"non-numeric version not supported on stable lane: {v!r}")
sys.exit(f"non-numeric local version not supported on stable lane: {v!r}")
return tuple(int(x) for x in v.split("."))
local, pub = parse(sys.argv[1]), parse(sys.argv[2])
local = parse_local(sys.argv[1])
pub = tuple(int(x) for x in sys.argv[2].split("."))
print("true" if local > pub else "false")
PY
)" || exit 1