@@ -29,123 +29,7 @@ jobs:
2929 python -m pip install --upgrade pip
3030 pip install packaging
3131
32- # Get version from current PR
33- PR_VERSION=$(grep -E '^__version__[[:space:]]*=' socketsecurity/__init__.py | awk '{print $3}' | tr -d "\"'")
34- echo "PR_VERSION=$PR_VERSION" >> "$GITHUB_ENV"
35-
36- # Get version from main branch
37- MAIN_VERSION=$(git show origin/main:socketsecurity/__init__.py | grep -E '^__version__[[:space:]]*=' | awk '{print $3}' | tr -d "\"'")
38- echo "MAIN_VERSION=$MAIN_VERSION" >> "$GITHUB_ENV"
39-
40- export PR_VERSION
41- export MAIN_VERSION
42-
43- # CI and documentation changes do not require a package release.
44- if git diff --name-only origin/main...HEAD \
45- | grep -qE '^(socketsecurity/|pyproject\.toml$|uv\.lock$)'; then
46- PACKAGE_CHANGED=true
47- else
48- PACKAGE_CHANGED=false
49- fi
50- export PACKAGE_CHANGED
51-
52- # Compare against the latest published PyPI release.
53- python3 <<'PY'
54- import json
55- import os
56- import subprocess
57- import tomllib
58- import urllib.request
59- from packaging import version
60-
61- pr_ver = version.parse(os.environ["PR_VERSION"])
62- main_ver = version.parse(os.environ["MAIN_VERSION"])
63-
64- enforced = os.environ["PACKAGE_CHANGED"] == "true"
65-
66- def reject(message):
67- print(message)
68- if enforced:
69- raise SystemExit(1)
70- print(
71- "ℹ️ Not enforced: this PR changes no packaged files, "
72- "so it ships nothing that needs a new version."
73- )
74- raise SystemExit(0)
75-
76- with open("pyproject.toml", "rb") as fh:
77- pyproject_ver = version.parse(tomllib.load(fh)["project"]["version"])
78-
79- # The version is two hand-maintained literals with nothing deriving one
80- # from the other: pyproject.toml is what actually gets published, and
81- # socketsecurity/__init__.py is what the CLI reports as its User-Agent.
82- # Every comparison below reads only __init__.py, so bumping that alone
83- # would pass this job and then publish under the old number -- caught
84- # late, by twine rejecting an existing file, after the merge. Require
85- # the two to agree before comparing anything. (uv.lock carries a third
86- # copy, but uv derives it and `uv lock --locked` in python-tests
87- # already fails when it drifts.)
88- if pr_ver != pyproject_ver:
89- reject(
90- f"❌ Version mismatch inside the PR: pyproject.toml is "
91- f"{pyproject_ver}, socketsecurity/__init__.py is {pr_ver}. "
92- f"Bump both."
93- )
94-
95- with urllib.request.urlopen("https://pypi.org/pypi/socketsecurity/json") as response:
96- pypi_data = json.load(response)
97-
98- published_versions = []
99- for raw in pypi_data.get("releases", {}).keys():
100- parsed = version.parse(raw)
101- if not parsed.is_prerelease and not parsed.is_devrelease:
102- published_versions.append(parsed)
103-
104- pypi_ver = max(published_versions) if published_versions else version.parse("0.0.0")
105-
106- # The only hard requirement is that the version is ahead of what is
107- # actually released. Treating main's version as a second floor breaks
108- # the legitimate case where several PRs share one unreleased release:
109- # the first bumps main to the new version and the rest ride it without
110- # bumping again, which is what keeps them under a single changelog
111- # header. Main is therefore only a floor when this PR moves the
112- # version -- a change to it must go forwards, never backwards.
113- if pr_ver <= pypi_ver:
114- reject(
115- f"❌ Version {pr_ver} is already published on PyPI "
116- f"(latest release: {pypi_ver}). Bump it."
117- )
118-
119- # Releases are immutable: once a v<version> release is published, its tag can't
120- # be moved or reused, even when that release failed and nothing reached PyPI.
121- # PyPI alone misses that, so a burned tag would only surface when the release
122- # run fails after the merge.
123- tag = f"v{os.environ['PR_VERSION']}"
124- if subprocess.run(
125- ["git", "rev-parse", "--quiet", "--verify", f"refs/tags/{tag}"],
126- capture_output=True,
127- ).returncode == 0:
128- reject(
129- f"❌ Tag {tag} already exists, so {pr_ver} can never be released "
130- f"(latest PyPI release: {pypi_ver}). Bump to a version with no tag."
131- )
132-
133- if pr_ver < main_ver:
134- reject(
135- f"❌ Version moves backwards: main is {main_ver}, PR is {pr_ver}."
136- )
137-
138- if pr_ver == main_ver:
139- print(
140- f"✅ Riding main's unreleased {pr_ver} "
141- f"(latest PyPI release: {pypi_ver})."
142- )
143- else:
144- print(
145- f"✅ Version properly incremented. "
146- f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}"
147- )
148- PY
32+ python3 .hooks/check_version.py
14933
15034 - name : Require uv.lock update when pyproject changes
15135 run : |
0 commit comments