From 4747bb08dc875b86e0c8d832955e16c28086e6e2 Mon Sep 17 00:00:00 2001 From: Bryant Date: Sun, 12 Jul 2026 12:16:53 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(ci):=20Drive=20the=20?= =?UTF-8?q?wheel=20interpreter=20list=20from=20one=20env=20var?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AAASM-4446 added cp312/cp313/cp314 by hardcoding "3.12 3.13 3.14" in four places (each job's --interpreter) plus the two macOS setup-python lists, so adding or dropping a supported version means editing six sites and any missed site silently drifts. Introduce a single PYTHON_INTERPRETERS env var and consume it everywhere: the four maturin --interpreter args reference it directly, and the macOS jobs translate it to setup-python's newline-separated form in a small Expand step. Adding a version is now a one-line env edit; the drift guard added next pins that env to pyproject's declared support. Refs AAASM-4453 --- .github/workflows/release-python.yml | 71 +++++++++++++++++++--------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index ec4d5502..a6df4159 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -42,7 +42,18 @@ env: # before each maturin build so it lands at agent_assembly/bin/aasm # inside the wheel (matches runtime.py's WHEEL_BUNDLED_BIN search path). AASM_BINARY_RELEASE_REPO: ai-agent-assembly/agent-assembly + # Host/tooling interpreter for jobs that only need *a* Python (sbom, sdist, + # the sidecar-staging steps) — not the set wheels are built for. PYTHON_VERSION: '3.12' + # SINGLE SOURCE OF TRUTH for the CPython versions the release builds wheels + # for. Every platform wheel job feeds this to maturin's --interpreter; the + # macOS jobs also install exactly these via setup-python. Adding/removing a + # supported version is a one-line edit here — and the wheel-python-matrix + # drift guard (`.github/workflows/wheel-python-matrix.yml`) fails any PR where + # this list diverges from pyproject.toml's requires-python + Python + # classifiers, so this can never silently lag declared support again + # (AAASM-4446 / AAASM-4453). Space-separated: maturin accepts each X.Y tag. + PYTHON_INTERPRETERS: '3.12 3.13 3.14' # protoc binary version + per-arch SHA256 sums (cross-verified against # the GitHub release API's `digest:` field on the v32.1 release assets). # Bump in one place when upgrading protoc. @@ -278,7 +289,7 @@ jobs: # silently falls back to the native-core-less hatchling sdist # (AAASM-4446). The manylinux image ships all CPythons; maturin resolves # each X.Y tag to the matching /opt/python interpreter in-image. - args: --release --out dist --interpreter 3.12 3.13 3.14 + args: --release --out dist --interpreter ${{ env.PYTHON_INTERPRETERS }} manylinux: auto # The manylinux2014 image (CentOS 7-based) lacks protoc; aa-proto's # build.rs needs it via prost-build for proto3 syntax. The yum/dnf @@ -373,7 +384,7 @@ jobs: # See linux-x86_64 above: build one wheel per supported CPython so no # supported interpreter falls back to the native-core-less sdist # (AAASM-4446). manylinux ships all CPythons; maturin resolves each tag. - args: --release --out dist --interpreter 3.12 3.13 3.14 + args: --release --out dist --interpreter ${{ env.PYTHON_INTERPRETERS }} manylinux: auto # See linux-x86_64 above for rationale + security model. Same # SHA-verified protoc binary download. @@ -405,17 +416,25 @@ jobs: runs-on: macos-14 # Apple silicon runner steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - name: Expand interpreter list for setup-python + id: pythons + # macOS runners have no manylinux image bundling every CPython, so each + # interpreter the Build wheel step targets must be installed here first. + # setup-python wants a newline-separated list while PYTHON_INTERPRETERS + # (the single source of truth) is space-separated for maturin — translate + # here so adding a version stays a one-line env edit (AAASM-4453). + env: + PYTHON_INTERPRETERS: ${{ env.PYTHON_INTERPRETERS }} + run: | + set -euo pipefail + { + echo 'versions<<__EOF__' + printf '%s' "$PYTHON_INTERPRETERS" | tr ' ' '\n' + printf '\n__EOF__\n' + } >> "$GITHUB_OUTPUT" - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: - # macOS runners have no manylinux image bundling every CPython, so - # each interpreter the Build wheel step targets below must be installed - # here first — otherwise maturin finds only the runner's default python - # and the wheel ships for that single version, the AAASM-4446 gap. - # Keep this list in lockstep with the --interpreter list below. - python-version: | - 3.12 - 3.13 - 3.14 + python-version: ${{ steps.pythons.outputs.versions }} - name: Stage aasm sidecar binary env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -471,7 +490,7 @@ jobs: # supported interpreter falls back to the native-core-less sdist # (AAASM-4446). Each X.Y here is provided by the multi-version # setup-python step above (macOS has no manylinux image). - args: --release --out dist --interpreter 3.12 3.13 3.14 + args: --release --out dist --interpreter ${{ env.PYTHON_INTERPRETERS }} - name: Upload wheel artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: @@ -484,17 +503,25 @@ jobs: runs-on: macos-15-intel # Intel runner (macos-13 sunset 2025-09-19) steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - name: Expand interpreter list for setup-python + id: pythons + # macOS runners have no manylinux image bundling every CPython, so each + # interpreter the Build wheel step targets must be installed here first. + # setup-python wants a newline-separated list while PYTHON_INTERPRETERS + # (the single source of truth) is space-separated for maturin — translate + # here so adding a version stays a one-line env edit (AAASM-4453). + env: + PYTHON_INTERPRETERS: ${{ env.PYTHON_INTERPRETERS }} + run: | + set -euo pipefail + { + echo 'versions<<__EOF__' + printf '%s' "$PYTHON_INTERPRETERS" | tr ' ' '\n' + printf '\n__EOF__\n' + } >> "$GITHUB_OUTPUT" - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: - # macOS runners have no manylinux image bundling every CPython, so - # each interpreter the Build wheel step targets below must be installed - # here first — otherwise maturin finds only the runner's default python - # and the wheel ships for that single version, the AAASM-4446 gap. - # Keep this list in lockstep with the --interpreter list below. - python-version: | - 3.12 - 3.13 - 3.14 + python-version: ${{ steps.pythons.outputs.versions }} - name: Stage aasm sidecar binary env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -550,7 +577,7 @@ jobs: # supported interpreter falls back to the native-core-less sdist # (AAASM-4446). Each X.Y here is provided by the multi-version # setup-python step above (macOS has no manylinux image). - args: --release --out dist --interpreter 3.12 3.13 3.14 + args: --release --out dist --interpreter ${{ env.PYTHON_INTERPRETERS }} - name: Upload wheel artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: From 510e839d861f56ed29c5f3b2c74eac05d2f5d2ae Mon Sep 17 00:00:00 2001 From: Bryant Date: Sun, 12 Jul 2026 12:18:45 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=85=20(ci):=20Add=20wheel-python-matr?= =?UTF-8?q?ix=20drift-guard=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fail-fast check that the release wheel matrix matches declared Python support: the PYTHON_INTERPRETERS list in release-python.yml must equal the "Programming Language :: Python :: 3.x" classifiers in pyproject.toml, the requires-python floor must equal the lowest built version, and no wheel job may hardcode a literal --interpreter list bypassing the single source. This is the root-cause fix for AAASM-4446: the cp312-only-vs-declared-3.13 drift now fails a check instead of silently shipping a broken install. Refs AAASM-4453 --- .github/scripts/check-wheel-python-matrix.sh | 144 +++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100755 .github/scripts/check-wheel-python-matrix.sh diff --git a/.github/scripts/check-wheel-python-matrix.sh b/.github/scripts/check-wheel-python-matrix.sh new file mode 100755 index 00000000..112e85d5 --- /dev/null +++ b/.github/scripts/check-wheel-python-matrix.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# +# Drift guard: fail when the release wheel-build interpreter matrix diverges +# from the Python support pyproject.toml declares. +# +# AAASM-4446 shipped rc3 with cp312-only wheels while pyproject claimed +# 3.12/3.13/3.14, so a 3.13 install silently fell back to a native-core-less +# sdist and governance registration became a no-op. The root cause was a +# hand-maintained build matrix drifting from `requires-python` + the Python +# classifiers with nothing checking they agree. This script is that check. +# +# It compares three facts and fails (non-zero) on any mismatch: +# 1. The "Programming Language :: Python :: 3.x" classifiers in pyproject. +# 2. The PYTHON_INTERPRETERS list in release-python.yml (the versions every +# wheel job actually builds for — see that file's single-source comment). +# 3. The `requires-python` lower bound in pyproject. +# The built set must equal the classifier set, and the requires-python floor +# must equal the lowest built version. It also fails if any wheel job hardcodes +# a literal `--interpreter ` list, bypassing the single source. +# +# Run locally: +# bash .github/scripts/check-wheel-python-matrix.sh +# +# Run from CI: see .github/workflows/wheel-python-matrix.yml (runs on every PR +# that touches pyproject.toml or the release workflow, not only at release). +# +# Usage (CLI, paths optional — default to the repo's files; overridable so the +# fixture suite can point at temp copies): +# .github/scripts/check-wheel-python-matrix.sh [pyproject.toml] [release-python.yml] +# +# Owner: python-sdk release pipeline (AAASM-4453). + +# Print each "3." declared by a Python classifier, numerically sorted, +# deduped. The bare ":: 3" classifier (no minor) is intentionally excluded. +extract_classifier_versions() { + local pyproject="$1" + grep -oE 'Programming Language :: Python :: 3\.[0-9]+' "$pyproject" \ + | grep -oE '3\.[0-9]+' \ + | sort -t. -k2,2n -u +} + +# Print the lower-bound "3." from `requires-python = ">=3.X,<4.0"`. +extract_requires_python_floor() { + local pyproject="$1" + grep -E '^[[:space:]]*requires-python[[:space:]]*=' "$pyproject" \ + | grep -oE '>=[[:space:]]*3\.[0-9]+' \ + | grep -oE '3\.[0-9]+' \ + | head -n1 +} + +# Print each version in the workflow's PYTHON_INTERPRETERS env value, +# numerically sorted, deduped. +extract_built_interpreters() { + local workflow="$1" + grep -E '^[[:space:]]*PYTHON_INTERPRETERS[[:space:]]*:' "$workflow" \ + | head -n1 \ + | grep -oE "'[^']*'" \ + | tr -d "'" \ + | tr ' ' '\n' \ + | grep -E '^3\.[0-9]+$' \ + | sort -t. -k2,2n -u +} + +# check_wheel_python_matrix +# Returns 0 when the matrix matches declared support, non-zero otherwise. +check_wheel_python_matrix() { + local pyproject="${1:-pyproject.toml}" + local workflow="${2:-.github/workflows/release-python.yml}" + local rc=0 + + if [[ ! -f "$pyproject" ]]; then + echo "::error::pyproject not found: $pyproject" >&2 + return 1 + fi + if [[ ! -f "$workflow" ]]; then + echo "::error::release workflow not found: $workflow" >&2 + return 1 + fi + + local classifiers built floor + classifiers="$(extract_classifier_versions "$pyproject")" + built="$(extract_built_interpreters "$workflow")" + floor="$(extract_requires_python_floor "$pyproject")" + + # A parse miss must fail loud, never pass vacuously — an empty extraction + # would otherwise make two empty strings compare equal. + if [[ -z "$classifiers" ]]; then + echo "::error::no 'Programming Language :: Python :: 3.x' classifiers found in $pyproject" >&2 + return 1 + fi + if [[ -z "$built" ]]; then + echo "::error::no PYTHON_INTERPRETERS list found in $workflow" >&2 + return 1 + fi + if [[ -z "$floor" ]]; then + echo "::error::could not parse requires-python lower bound in $pyproject" >&2 + return 1 + fi + + # Core drift check: the versions wheels are built for must equal the + # versions pyproject advertises support for. + if [[ "$classifiers" != "$built" ]]; then + { + echo "::error::wheel build matrix diverges from pyproject Python classifiers" + printf 'pyproject classifiers : %s\n' "$(echo "$classifiers" | paste -sd' ' -)" + printf 'built (PYTHON_INTERPRETERS): %s\n' "$(echo "$built" | paste -sd' ' -)" + } >&2 + rc=1 + fi + + # A requires-python floor bump without dropping the matching classifier + + # wheel (or vice versa) is itself a support-claim drift — catch it. + local min_built + min_built="$(echo "$built" | head -n1)" + if [[ "$floor" != "$min_built" ]]; then + echo "::error::requires-python floor (>=$floor) != lowest built interpreter ($min_built)" >&2 + rc=1 + fi + + # No wheel job may hardcode a literal `--interpreter ` list: that + # would let one platform silently drift from the single source of truth, + # which is exactly the failure mode this guard exists to prevent. + local literal + literal="$(grep -nE -- '--interpreter[[:space:]]+[0-9]' "$workflow" || true)" + if [[ -n "$literal" ]]; then + { + echo "::error::a wheel job hardcodes --interpreter versions instead of \${{ env.PYTHON_INTERPRETERS }}:" + printf '%s\n' "$literal" + } >&2 + rc=1 + fi + + if [[ "$rc" -eq 0 ]]; then + echo "OK: wheel build matrix matches pyproject Python support ($(echo "$built" | paste -sd' ' -)); requires-python floor >=$floor" + fi + return "$rc" +} + +# When executed directly, run the check as a CLI. When sourced, define the +# functions only and let the caller (the fixture suite) drive them. +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + set -uo pipefail + check_wheel_python_matrix "${1:-}" "${2:-}" +fi From d3c2951e3efade53aa700f3cab183abe82836da5 Mon Sep 17 00:00:00 2001 From: Bryant Date: Sun, 12 Jul 2026 12:20:20 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=85=20(ci):=20Add=20fixture=20suite?= =?UTF-8?q?=20for=20the=20wheel-python-matrix=20drift=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercises check-wheel-python-matrix.sh against synthetic pyproject/workflow fixtures for the passing case, both drift directions, requires-python floor drift, a hardcoded --interpreter literal, and unparseable-input fail-loud — plus one run against the repo's real files proving the shipped config passes. 8 cases, mirrors the existing test_pep440_to_tag.sh style. Refs AAASM-4453 --- .../scripts/test_check-wheel-python-matrix.sh | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100755 .github/scripts/test_check-wheel-python-matrix.sh diff --git a/.github/scripts/test_check-wheel-python-matrix.sh b/.github/scripts/test_check-wheel-python-matrix.sh new file mode 100755 index 00000000..d3973f9a --- /dev/null +++ b/.github/scripts/test_check-wheel-python-matrix.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# +# Unit tests for `.github/scripts/check-wheel-python-matrix.sh`. +# +# Drives the drift guard that keeps the release wheel matrix in sync with the +# Python support pyproject.toml declares (AAASM-4446 / AAASM-4453). Sources the +# same file CI runs, then exercises it against synthetic pyproject/workflow +# fixtures for both the passing and the drifting cases — plus one run against +# the repo's real files to prove the shipped config actually passes. +# +# Run locally: +# bash .github/scripts/test_check-wheel-python-matrix.sh +# +# Run from CI: see .github/workflows/wheel-python-matrix.yml. +# +# Exits 0 when every case passes, 1 otherwise. +# +# Refs AAASM-4453. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +# shellcheck source=.github/scripts/check-wheel-python-matrix.sh +source "${SCRIPT_DIR}/check-wheel-python-matrix.sh" + +# Literal GitHub Actions env reference — kept in a single-quoted variable so +# bash never tries to expand `${{ ... }}`. The non-expansion is the point here. +# shellcheck disable=SC2016 +ENVREF='${{ env.PYTHON_INTERPRETERS }}' + +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +pass=0 +fail=0 +total=0 + +# assert_exit