diff --git a/.github/scripts/validate-version-alignment.sh b/.github/scripts/validate-version-alignment.sh new file mode 100755 index 0000000..1ba3ac3 --- /dev/null +++ b/.github/scripts/validate-version-alignment.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Validates that the Python SDK's version declarations match the most +# recent released section of CHANGELOG.md. Patterned on the same +# script in axonflow-enterprise and axonflow-sdk-go. +# +# Why: the release workflow sed-rewrites pyproject.toml + axonflow/ +# _version.py at publish time but never commits the bump back to main, +# so the repo version silently lags the registry version between +# releases. This gate enforces the invariant on every PR: +# +# pyproject.toml::version +# == axonflow/_version.py::__version__ +# == most recent `## [X.Y.Z]` section in CHANGELOG.md +# +# When it's time to release, a single release-prep PR renames +# [Unreleased] → [X.Y.Z] - DATE AND bumps both manifest files in the +# same commit so this gate always sees them together. +# +# Run locally: +# ./.github/scripts/validate-version-alignment.sh + +set -euo pipefail + +ERRORS=0 + +# Latest RELEASED version = first `## [x.y.z]` line that isn't +# [Unreleased] (which starts with a letter, not a digit). +# +# `{ grep || true; }` is deliberate: under `set -euo pipefail`, a +# failing grep (no match) aborts the whole command substitution +# before we reach the -z check, killing the script silently. The +# wrapper lets the `-z` check produce the real user-facing error. +LATEST_VERSION=$({ grep -m1 -E '^## \[[0-9]' CHANGELOG.md || true; } | sed 's/## \[\(.*\)\].*/\1/' | sed 's/^v//') + +if [ -z "${LATEST_VERSION:-}" ]; then + echo "❌ Could not extract a released version (## [X.Y.Z]) from CHANGELOG.md" + exit 1 +fi + +echo "📋 Latest CHANGELOG version: $LATEST_VERSION" +echo "" + +# Check pyproject.toml::version +echo "📦 Checking pyproject.toml..." +PYPROJECT_VER=$(grep -m1 -E '^version = "' pyproject.toml | sed 's/version = "\(.*\)"/\1/' || true) +if [ -z "${PYPROJECT_VER:-}" ]; then + echo " ❌ pyproject.toml — could not read version" + ERRORS=$((ERRORS + 1)) +elif [ "$PYPROJECT_VER" != "$LATEST_VERSION" ]; then + echo " ❌ pyproject.toml — version is \"$PYPROJECT_VER\", expected \"$LATEST_VERSION\"" + ERRORS=$((ERRORS + 1)) +else + echo " ✅ pyproject.toml — $PYPROJECT_VER" +fi + +# Check axonflow/_version.py::__version__ +echo "🔧 Checking axonflow/_version.py..." +VERSION_PY=$(grep -m1 -E '^__version__ = "' axonflow/_version.py | sed 's/__version__ = "\(.*\)"/\1/' || true) +if [ -z "${VERSION_PY:-}" ]; then + echo " ❌ axonflow/_version.py — could not read __version__" + ERRORS=$((ERRORS + 1)) +elif [ "$VERSION_PY" != "$LATEST_VERSION" ]; then + echo " ❌ axonflow/_version.py — __version__ is \"$VERSION_PY\", expected \"$LATEST_VERSION\"" + ERRORS=$((ERRORS + 1)) +else + echo " ✅ axonflow/_version.py — $VERSION_PY" +fi + +echo "" + +if [ "$ERRORS" -gt 0 ]; then + echo "❌ Found $ERRORS version misalignment(s)." + echo "" + echo "Fix: bump the stale file(s) to match CHANGELOG v$LATEST_VERSION." + echo "Or, if CHANGELOG is behind a tag you already pushed, add the" + echo "missing '## [X.Y.Z] - YYYY-MM-DD' section." + exit 1 +fi + +echo "✅ All version constants match CHANGELOG v$LATEST_VERSION." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23849b0..68c4420 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,12 +62,28 @@ jobs: - name: Update version in source run: | + set -e VERSION=${{ steps.version.outputs.VERSION }} + # The real __version__ single source of truth lives in + # axonflow/_version.py; axonflow/__init__.py only re-exports + # via `from axonflow._version import __version__`. Targeting + # __init__.py was a silent no-op and shipped wheels with a + # runtime __version__ that lagged the PyPI metadata version. sed -i "s/^version = .*/version = \"${VERSION}\"/" pyproject.toml - sed -i "s/^__version__ = .*/__version__ = \"${VERSION}\"/" axonflow/__init__.py + sed -i "s/^__version__ = .*/__version__ = \"${VERSION}\"/" axonflow/_version.py echo "Updated version to ${VERSION}" - grep version pyproject.toml | head -1 - grep __version__ axonflow/__init__.py + grep "^version" pyproject.toml | head -1 + grep "^__version__" axonflow/_version.py + # Fail fast if either sed missed its target — don't let a + # silent no-op ship a wheel with mismatched runtime version. + if ! grep -q "^version = \"${VERSION}\"" pyproject.toml; then + echo "::error::sed did not bump pyproject.toml to ${VERSION}" + exit 1 + fi + if ! grep -q "^__version__ = \"${VERSION}\"" axonflow/_version.py; then + echo "::error::sed did not bump axonflow/_version.py to ${VERSION}" + exit 1 + fi - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/validate-version-alignment.yml b/.github/workflows/validate-version-alignment.yml new file mode 100644 index 0000000..1c786c1 --- /dev/null +++ b/.github/workflows/validate-version-alignment.yml @@ -0,0 +1,44 @@ +name: Version Alignment Check + +# Blocks merges that would leave pyproject.toml / axonflow/_version.py +# out of sync with CHANGELOG.md's most recent released section. The +# invariant on main: +# pyproject.toml::version +# == axonflow/_version.py::__version__ +# == first ## [X.Y.Z] section in CHANGELOG +# +# When it's time to ship a new release, a single release-prep PR +# renames [Unreleased] → [X.Y.Z] - YYYY-MM-DD AND bumps both manifest +# files in the same commit, so this gate always sees them together. +# +# See .github/scripts/validate-version-alignment.sh for the script. + +on: + pull_request: + branches: [main] + paths: + - 'CHANGELOG.md' + - 'pyproject.toml' + - 'axonflow/_version.py' + - '.github/scripts/validate-version-alignment.sh' + - '.github/workflows/validate-version-alignment.yml' + push: + branches: [main] + paths: + - 'CHANGELOG.md' + - 'pyproject.toml' + - 'axonflow/_version.py' + - '.github/scripts/validate-version-alignment.sh' + - '.github/workflows/validate-version-alignment.yml' + +permissions: + contents: read + +jobs: + validate-versions: + name: Validate Version Alignment + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check version alignment + run: ./.github/scripts/validate-version-alignment.sh diff --git a/axonflow/_version.py b/axonflow/_version.py index d5e5d68..70fa39b 100644 --- a/axonflow/_version.py +++ b/axonflow/_version.py @@ -1,3 +1,3 @@ """Single source of truth for the AxonFlow SDK version.""" -__version__ = "6.6.0" +__version__ = "6.6.1" diff --git a/pyproject.toml b/pyproject.toml index f1d51d3..b9dbd89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "axonflow" -version = "6.6.0" +version = "6.6.1" description = "AxonFlow Python SDK - Enterprise AI Governance in 3 Lines of Code" readme = "README.md" license = {text = "MIT"}