You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by hg...@apache.org on 2026/05/26 10:49:34 UTC

(iggy) branch master updated: ci(python): add interpreter version sync check (#3319)

This is an automated email from the ASF dual-hosted git repository.

hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new c5fde76ac ci(python): add interpreter version sync check (#3319)
c5fde76ac is described below

commit c5fde76acfd500bda68aac8ccfc38d0c528ed668
Author: WaterWhisperer <wa...@qq.com>
AuthorDate: Tue May 26 18:49:29 2026 +0800

    ci(python): add interpreter version sync check (#3319)
    
    Create a bash script that runs in CI and pre-commit
    hooks and checks that the python versions being used
    are all in sync.
---
 .github/workflows/_build_python_wheels.yml |   6 +-
 .github/workflows/_common.yml              |  19 ++
 .pre-commit-config.yaml                    |   8 +
 foreign/python/.devcontainer/Dockerfile    |   2 +-
 scripts/ci/sync-python-version.sh          | 413 +++++++++++++++++++++++++++++
 5 files changed, 444 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/_build_python_wheels.yml b/.github/workflows/_build_python_wheels.yml
index 3341888a5..c88b33753 100644
--- a/.github/workflows/_build_python_wheels.yml
+++ b/.github/workflows/_build_python_wheels.yml
@@ -85,7 +85,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v6
         with:
-          python-version: "3.11"
+          python-version: "3.10"
 
       - name: Setup Rust with cache
         uses: ./.github/actions/utils/setup-rust-with-cache
@@ -147,7 +147,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v6
         with:
-          python-version: "3.11"
+          python-version: "3.10"
 
       - name: Setup Rust with cache
         uses: ./.github/actions/utils/setup-rust-with-cache
@@ -209,7 +209,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v6
         with:
-          python-version: "3.11"
+          python-version: "3.10"
           architecture: x64
 
       - name: Build wheels
diff --git a/.github/workflows/_common.yml b/.github/workflows/_common.yml
index 55e118367..ef6a9c9d8 100644
--- a/.github/workflows/_common.yml
+++ b/.github/workflows/_common.yml
@@ -42,6 +42,15 @@ jobs:
       - name: Check Python SDK versions are synchronized
         run: ./scripts/ci/python-sdk-version-sync.sh --check
 
+  python-interpreter-versions:
+    name: Check Python interpreter versions sync
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Check Python interpreter versions are synchronized
+        run: ./scripts/ci/sync-python-version.sh --check
+
   version-consistency:
     name: Check version consistency
     runs-on: ubuntu-latest
@@ -193,6 +202,7 @@ jobs:
       [
         rust-versions,
         python-versions,
+        python-interpreter-versions,
         version-consistency,
         license-headers,
         third-party-licenses,
@@ -217,6 +227,7 @@ jobs:
           # Always-run checks
           RUST_VERSIONS="${{ needs.rust-versions.result }}"
           PYTHON_VERSIONS="${{ needs.python-versions.result }}"
+          PYTHON_INTERPRETER_VERSIONS="${{ needs.python-interpreter-versions.result }}"
           VERSION_CONSISTENCY="${{ needs.version-consistency.result }}"
           LICENSE_HEADERS="${{ needs.license-headers.result }}"
           THIRD_PARTY_LICENSES="${{ needs.third-party-licenses.result }}"
@@ -238,6 +249,14 @@ jobs:
             echo "| ⏭️ Python SDK Versions | $PYTHON_VERSIONS | Check skipped |" >> $GITHUB_STEP_SUMMARY
           fi
 
+          if [ "$PYTHON_INTERPRETER_VERSIONS" = "success" ]; then
+            echo "| ✅ Python Interpreter Versions | success | Python interpreter versions synchronized |" >> "$GITHUB_STEP_SUMMARY"
+          elif [ "$PYTHON_INTERPRETER_VERSIONS" = "failure" ]; then
+            echo "| ❌ Python Interpreter Versions | failure | Python interpreter version mismatch detected |" >> "$GITHUB_STEP_SUMMARY"
+          else
+            echo "| ⏭️ Python Interpreter Versions | $PYTHON_INTERPRETER_VERSIONS | Check skipped |" >> "$GITHUB_STEP_SUMMARY"
+          fi
+
           if [ "$VERSION_CONSISTENCY" = "success" ]; then
             echo "| ✅ Version Consistency | success | Group and workspace dep versions match |" >> $GITHUB_STEP_SUMMARY
           elif [ "$VERSION_CONSISTENCY" = "failure" ]; then
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8e7909a54..9d0b2dfbe 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -79,6 +79,14 @@ repos:
         files: ^foreign/python/(Cargo\.toml|pyproject\.toml)$
         pass_filenames: false
 
+      - id: python-interpreter-version-sync
+        name: python interpreter version sync
+        entry: ./scripts/ci/sync-python-version.sh
+        args: ["--fix"]
+        language: system
+        files: ^(scripts/ci/sync-python-version\.sh|foreign/python/(pyproject\.toml|pylock\.toml|Dockerfile\.test|README\.md|\.devcontainer/Dockerfile)|bdd/python/(pyproject\.toml|pylock\.toml)|examples/python/(pyproject\.toml|pylock\.toml)|\.github/actions/python-maturin/pre-merge/action\.yml|\.github/workflows/(_build_python_wheels|_test_examples|coverage-baseline)\.yml)$
+        pass_filenames: false
+
       - id: uv-lock-check
         name: uv lock check
         entry: ./scripts/ci/uv-lock-check.sh
diff --git a/foreign/python/.devcontainer/Dockerfile b/foreign/python/.devcontainer/Dockerfile
index a8d57920e..dccd31551 100644
--- a/foreign/python/.devcontainer/Dockerfile
+++ b/foreign/python/.devcontainer/Dockerfile
@@ -27,7 +27,7 @@
 # or stability of the code, it does indicate that the project has yet to be
 # fully endorsed by the ASF.
 
-FROM mcr.microsoft.com/devcontainers/python:1-3.12-bullseye
+FROM mcr.microsoft.com/devcontainers/python:1-3.10-bullseye
 
 ENV VIRTUAL_ENV=/opt/venv
 RUN python -m venv $VIRTUAL_ENV
diff --git a/scripts/ci/sync-python-version.sh b/scripts/ci/sync-python-version.sh
new file mode 100755
index 000000000..74e6e97e6
--- /dev/null
+++ b/scripts/ci/sync-python-version.sh
@@ -0,0 +1,413 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -euo pipefail
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Default mode
+MODE=""
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --check)
+            MODE="check"
+            shift
+            ;;
+        --fix)
+            MODE="fix"
+            shift
+            ;;
+        --help|-h)
+            echo "Usage: $0 [--check|--fix]"
+            echo ""
+            echo "Sync the Python interpreter version across SDK files, CI, and Dockerfiles"
+            echo ""
+            echo "Options:"
+            echo "  --check    Check if Python interpreter versions are synchronized"
+            echo "  --fix      Update files to use the Python interpreter version from foreign/python/pyproject.toml"
+            echo "  --help     Show this help message"
+            exit 0
+            ;;
+        *)
+            echo -e "${RED}Error: Unknown option $1${NC}"
+            echo "Use --help for usage information"
+            exit 1
+            ;;
+    esac
+done
+
+# Require mode to be specified
+if [ -z "$MODE" ]; then
+    echo -e "${RED}Error: Please specify either --check or --fix${NC}"
+    echo "Use --help for usage information"
+    exit 1
+fi
+
+# Get the repository root (two levels up from scripts/ci/)
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$REPO_ROOT"
+
+SOURCE_FILE="foreign/python/pyproject.toml"
+PYTHON_VERSION=$(sed -nE 's/^requires-python = ">=([0-9]+\.[0-9]+)".*/\1/p' "$SOURCE_FILE" | head -1)
+
+if [ -z "$PYTHON_VERSION" ]; then
+    echo -e "${RED}Error: Could not extract Python interpreter version from $SOURCE_FILE${NC}"
+    exit 1
+fi
+
+PYTHON_VERSION_REGEX=${PYTHON_VERSION//./\\.}
+PYTHON_VERSION_MAJOR=${PYTHON_VERSION%%.*}
+PYTHON_VERSION_MINOR=${PYTHON_VERSION#*.}
+PYTHON_VERSION_NUMBER=$((PYTHON_VERSION_MAJOR * 100 + PYTHON_VERSION_MINOR))
+FAILED=0
+TOTAL_CHECKS=0
+FIXED_CHECKS=0
+
+echo -e "Python interpreter version from $SOURCE_FILE: ${GREEN}$PYTHON_VERSION${NC}"
+echo ""
+
+ensure_line() {
+    local file="$1"
+    local current_pattern="$2"
+    local expected_pattern="$3"
+    local replacement="$4"
+    local description="$5"
+    local mismatched=0
+    local total=0
+
+    TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
+
+    if [ ! -f "$file" ]; then
+        echo -e "${RED}✗${NC} $file: file does not exist"
+        FAILED=1
+        return
+    fi
+
+    while IFS= read -r line; do
+        total=$((total + 1))
+        if [[ ! "$line" =~ $expected_pattern ]]; then
+            mismatched=1
+        fi
+    done < <(grep -E "$current_pattern" "$file" || true)
+
+    if [ "$total" -eq 0 ]; then
+        echo -e "${RED}✗${NC} $file: could not find $description"
+        FAILED=1
+        return
+    fi
+
+    if [ "$mismatched" -eq 0 ]; then
+        echo -e "${GREEN}✓${NC} $file: $description"
+        return
+    fi
+
+    if [ "$MODE" = "fix" ]; then
+        sed -i -E "s|$current_pattern|$replacement|" "$file"
+        FIXED_CHECKS=$((FIXED_CHECKS + 1))
+        echo -e "${GREEN}Fixed${NC} $file: $description"
+    else
+        echo -e "${RED}✗${NC} $file: $description is not $PYTHON_VERSION"
+        FAILED=1
+    fi
+}
+
+ensure_classifiers() {
+    local file="$1"
+    local versions=()
+    local version
+    local version_major
+    local version_minor
+    local version_number
+    local has_minimum=0
+    local mismatched=0
+
+    TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
+
+    if [ ! -f "$file" ]; then
+        echo -e "${RED}✗${NC} $file: file does not exist"
+        FAILED=1
+        return
+    fi
+
+    mapfile -t versions < <(sed -nE 's/^    "Programming Language :: Python :: ([0-9]+\.[0-9]+)",$/\1/p' "$file")
+
+    if [ "${#versions[@]}" -eq 0 ]; then
+        echo -e "${RED}✗${NC} $file: could not find Python version classifiers"
+        FAILED=1
+        return
+    fi
+
+    for version in "${versions[@]}"; do
+        if [ "$version" = "$PYTHON_VERSION" ]; then
+            has_minimum=1
+        fi
+
+        version_major=${version%%.*}
+        version_minor=${version#*.}
+        version_number=$((version_major * 100 + version_minor))
+
+        if [ "$version_number" -lt "$PYTHON_VERSION_NUMBER" ]; then
+            mismatched=1
+        fi
+    done
+
+    if [ "$has_minimum" -eq 0 ]; then
+        mismatched=1
+    fi
+
+    if [ "$mismatched" -eq 0 ]; then
+        echo -e "${GREEN}✓${NC} $file: Python version classifiers"
+        return
+    fi
+
+    if [ "$MODE" = "fix" ]; then
+        for version in "${versions[@]}"; do
+            version_major=${version%%.*}
+            version_minor=${version#*.}
+            version_number=$((version_major * 100 + version_minor))
+
+            if [ "$version_number" -lt "$PYTHON_VERSION_NUMBER" ]; then
+                sed -i -E "/^    \"Programming Language :: Python :: ${version//./\\.}\",$/d" "$file"
+            fi
+        done
+
+        if [ "$has_minimum" -eq 0 ]; then
+            sed -i -E "/^    \"Programming Language :: Python :: ${PYTHON_VERSION_MAJOR}\",$/a\\    \"Programming Language :: Python :: ${PYTHON_VERSION}\"," "$file"
+        fi
+
+        FIXED_CHECKS=$((FIXED_CHECKS + 1))
+        echo -e "${GREEN}Fixed${NC} $file: Python version classifiers"
+    else
+        echo -e "${RED}✗${NC} $file: Python version classifiers are not aligned with $PYTHON_VERSION"
+        FAILED=1
+    fi
+}
+
+ensure_lock_python_requirement() {
+    local file="$1"
+    local current_pattern="^(requires-python = \">=)[0-9]+\\.[0-9]+(\".*)$"
+    local expected_pattern="^requires-python = \">=${PYTHON_VERSION_REGEX}\".*$"
+    local replacement="\\1${PYTHON_VERSION}\\2"
+    local first_table_line
+    local top_level_end
+
+    TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
+
+    if [ ! -f "$file" ]; then
+        echo -e "${RED}✗${NC} $file: file does not exist"
+        FAILED=1
+        return
+    fi
+
+    first_table_line=$(grep -n -m1 '^\[' "$file" | cut -d: -f1 || true)
+    if [ -n "$first_table_line" ]; then
+        top_level_end=$((first_table_line - 1))
+    else
+        top_level_end=$(wc -l < "$file")
+    fi
+
+    if [ "$top_level_end" -lt 1 ]; then
+        echo -e "${RED}✗${NC} $file: could not find lock file Python requirement"
+        FAILED=1
+        return
+    fi
+
+    if sed -n "1,${top_level_end}p" "$file" | grep -Eq "$expected_pattern"; then
+        echo -e "${GREEN}✓${NC} $file: lock file Python requirement"
+        return
+    fi
+
+    if ! sed -n "1,${top_level_end}p" "$file" | grep -Eq "$current_pattern"; then
+        echo -e "${RED}✗${NC} $file: could not find lock file Python requirement"
+        FAILED=1
+        return
+    fi
+
+    if [ "$MODE" = "fix" ]; then
+        sed -i -E "1,${top_level_end}s|$current_pattern|$replacement|" "$file"
+        FIXED_CHECKS=$((FIXED_CHECKS + 1))
+        echo -e "${GREEN}Fixed${NC} $file: lock file Python requirement"
+    else
+        echo -e "${RED}✗${NC} $file: lock file Python requirement is not $PYTHON_VERSION"
+        FAILED=1
+    fi
+}
+
+ensure_wheel_interpreters() {
+    local file="$1"
+    local classifier_versions=()
+    local expected_interpreters=""
+    local current_interpreters
+    local version
+    local mismatched=0
+    local total=0
+
+    TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
+
+    if [ ! -f "$file" ]; then
+        echo -e "${RED}✗${NC} $file: file does not exist"
+        FAILED=1
+        return
+    fi
+
+    mapfile -t classifier_versions < <(sed -nE 's/^    "Programming Language :: Python :: ([0-9]+\.[0-9]+)",$/\1/p' "$SOURCE_FILE")
+
+    if [ "${#classifier_versions[@]}" -eq 0 ]; then
+        echo -e "${RED}✗${NC} $SOURCE_FILE: could not find Python version classifiers"
+        FAILED=1
+        return
+    fi
+
+    for version in "${classifier_versions[@]}"; do
+        expected_interpreters+=" python${version}"
+    done
+    expected_interpreters=${expected_interpreters# }
+
+    while IFS= read -r line; do
+        total=$((total + 1))
+        current_interpreters=$(echo "$line" | sed -E 's/^.*--interpreter[[:space:]]+//')
+        if [ "$current_interpreters" != "$expected_interpreters" ]; then
+            mismatched=1
+        fi
+    done < <(grep -- "--interpreter" "$file" || true)
+
+    if [ "$total" -eq 0 ]; then
+        echo -e "${RED}✗${NC} $file: could not find wheel interpreter list"
+        FAILED=1
+        return
+    fi
+
+    if [ "$mismatched" -eq 0 ]; then
+        echo -e "${GREEN}✓${NC} $file: wheel interpreter versions"
+        return
+    fi
+
+    if [ "$MODE" = "fix" ]; then
+        sed -i -E "s|(--interpreter ).*$|\\1${expected_interpreters}|" "$file"
+        FIXED_CHECKS=$((FIXED_CHECKS + 1))
+        echo -e "${GREEN}Fixed${NC} $file: wheel interpreter versions"
+    else
+        echo -e "${RED}✗${NC} $file: wheel interpreter versions are not $expected_interpreters"
+        FAILED=1
+    fi
+}
+
+ensure_classifiers "$SOURCE_FILE"
+
+ensure_line \
+    "foreign/python/Dockerfile.test" \
+    "^(FROM python:)[0-9]+\\.[0-9]+(-slim AS base)$" \
+    "^FROM python:${PYTHON_VERSION_REGEX}-slim AS base$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "Docker image Python version"
+
+ensure_line \
+    "foreign/python/.devcontainer/Dockerfile" \
+    "^(FROM mcr\\.microsoft\\.com/devcontainers/python:1-)[0-9]+\\.[0-9]+(-bullseye)$" \
+    "^FROM mcr\\.microsoft\\.com/devcontainers/python:1-${PYTHON_VERSION_REGEX}-bullseye$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "devcontainer Python image version"
+
+ensure_line \
+    "foreign/python/README.md" \
+    "^(- Python )[0-9]+\\.[0-9]+(\\+.*)$" \
+    "^- Python ${PYTHON_VERSION_REGEX}\\+.*$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "README Python requirement"
+
+ensure_line \
+    "bdd/python/pyproject.toml" \
+    "^(requires-python = \">=)[0-9]+\\.[0-9]+(\".*)$" \
+    "^requires-python = \">=${PYTHON_VERSION_REGEX}\".*$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "BDD Python requirement"
+
+ensure_line \
+    "examples/python/pyproject.toml" \
+    "^(requires-python = \">=)[0-9]+\\.[0-9]+(\".*)$" \
+    "^requires-python = \">=${PYTHON_VERSION_REGEX}\".*$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "examples Python requirement"
+
+ensure_line \
+    ".github/actions/python-maturin/pre-merge/action.yml" \
+    "^([[:space:]]*python-version: \")[0-9]+\\.[0-9]+(\".*)$" \
+    "^[[:space:]]*python-version: \"${PYTHON_VERSION_REGEX}\".*$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "maturin action Python version"
+
+ensure_line \
+    ".github/workflows/_test_examples.yml" \
+    "^([[:space:]]*python-version: \")[0-9]+\\.[0-9]+(\".*)$" \
+    "^[[:space:]]*python-version: \"${PYTHON_VERSION_REGEX}\".*$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "examples workflow Python version"
+
+ensure_line \
+    ".github/workflows/coverage-baseline.yml" \
+    "^([[:space:]]*python-version: \")[0-9]+\\.[0-9]+(\".*)$" \
+    "^[[:space:]]*python-version: \"${PYTHON_VERSION_REGEX}\".*$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "coverage workflow Python version"
+
+ensure_line \
+    ".github/workflows/_build_python_wheels.yml" \
+    "^([[:space:]]*python-version: \")[0-9]+\\.[0-9]+(\".*)$" \
+    "^[[:space:]]*python-version: \"${PYTHON_VERSION_REGEX}\".*$" \
+    "\\1${PYTHON_VERSION}\\2" \
+    "wheel workflow setup-python versions"
+
+ensure_wheel_interpreters ".github/workflows/_build_python_wheels.yml"
+
+PYLOCK_FILES=(
+    "foreign/python/pylock.toml"
+    "bdd/python/pylock.toml"
+    "examples/python/pylock.toml"
+)
+
+for pylock_file in "${PYLOCK_FILES[@]}"; do
+    if [ -f "$pylock_file" ]; then
+        ensure_lock_python_requirement "$pylock_file"
+    fi
+done
+
+echo ""
+echo "────────────────────────────────────────────────"
+
+if [ "$MODE" = "check" ]; then
+    if [ "$FAILED" -eq 0 ]; then
+        echo -e "${GREEN}✓ All $TOTAL_CHECKS Python interpreter version checks are synchronized with $PYTHON_VERSION${NC}"
+        exit 0
+    fi
+
+    echo -e "${RED}✗ Python interpreter version checks failed${NC}"
+    echo -e "${YELLOW}Run '$0 --fix' to fix supported drift points automatically${NC}"
+    exit 1
+fi
+
+if [ "$FIXED_CHECKS" -eq 0 ]; then
+    echo -e "${GREEN}✓ All $TOTAL_CHECKS Python interpreter version checks were already synchronized with $PYTHON_VERSION${NC}"
+else
+    echo -e "${GREEN}✓ Fixed $FIXED_CHECKS Python interpreter version check(s) to use $PYTHON_VERSION${NC}"
+fi