diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index b24786d..49a5015 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -6,6 +6,8 @@ on: - 'socketdev/**' - 'pyproject.toml' - 'uv.lock' + # Included so a change to the check itself is exercised by its own PR. + - '.github/workflows/version-check.yml' permissions: contents: read @@ -32,15 +34,29 @@ jobs: PR_VERSION=$(grep -o "__version__.*" socketdev/version.py | awk '{print $3}' | tr -d '"' | tr -d "'") echo "PR_VERSION=$PR_VERSION" >> $GITHUB_ENV - # Get version from main branch - git checkout origin/main - MAIN_VERSION=$(grep -o "__version__.*" socketdev/version.py | awk '{print $3}' | tr -d '"' | tr -d "'") + # Get version from main branch. Read it straight out of the ref: + # `git checkout origin/main` leaves the working tree detached on main, + # so the uv.lock guard below ends up diffing main against itself and + # never fires. + MAIN_VERSION=$(git show origin/main:socketdev/version.py | grep -o "__version__.*" | awk '{print $3}' | tr -d '"' | tr -d "'") echo "MAIN_VERSION=$MAIN_VERSION" >> $GITHUB_ENV export PR_VERSION export MAIN_VERSION - # Compare against both main and latest published PyPI release. + # Only enforce a bump when the PR actually changes shipped content. + # This workflow sits in its own paths filter so edits to it are + # exercised, but a CI-only change ships nothing and must not be told + # to cut a release. The comparison still runs and reports either way. + if git diff --name-only origin/main...HEAD \ + | grep -qE '^(socketdev/|pyproject\.toml$|uv\.lock$)'; then + PACKAGE_CHANGED=true + else + PACKAGE_CHANGED=false + fi + export PACKAGE_CHANGED + + # Compare against the latest published PyPI release. python3 <<'PY' import json import os @@ -60,19 +76,47 @@ jobs: published_versions.append(parsed) pypi_ver = max(published_versions) if published_versions else version.parse("0.0.0") - required_floor = max(main_ver, pypi_ver) - if pr_ver <= required_floor: + enforced = os.environ["PACKAGE_CHANGED"] == "true" + + def reject(message): + print(message) + if enforced: + raise SystemExit(1) print( - f"❌ Version must be greater than main and PyPI! " - f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}" + "ℹ️ Not enforced: this PR changes no packaged files, " + "so it ships nothing that needs a new version." + ) + raise SystemExit(0) + + # The only hard requirement is that the version is ahead of what is + # actually released. Treating main's version as a second floor breaks + # the legitimate case where several PRs share one unreleased release: + # the first bumps main to the new version and the rest ride it without + # bumping again, which is what keeps them under a single changelog + # header. Main is therefore only a floor when this PR moves the + # version -- a change to it must go forwards, never backwards. + if pr_ver <= pypi_ver: + reject( + f"❌ Version {pr_ver} is already published on PyPI " + f"(latest release: {pypi_ver}). Bump it." + ) + + if pr_ver < main_ver: + reject( + f"❌ Version moves backwards: main is {main_ver}, PR is {pr_ver}." ) - raise SystemExit(1) - print( - f"✅ Version properly incremented. " - f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}" - ) + if pr_ver == main_ver: + print( + f"✅ Riding main's unreleased {pr_ver} " + f"(latest PyPI release: {pypi_ver})." + ) + else: + print( + f"✅ Version properly incremented. " + f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}" + ) PY - name: Require uv.lock update when pyproject changes