Skip to main content

upgrade-python

workflow to rebuild all packages against a newer python version

Source facts

Repository
getsentry/pypi
Last source activity
October 16, 2026 at 22:52
Detected SKILL.md language
English
Stars
23
Forks
5

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
upgrade-python
description
workflow to rebuild all packages against a newer python version
Adds support for a new Python version by building all packages, identifying failures, and marking them with `python_versions` restrictions. ## Usage ``` /upgrade-python <full-version> ``` Example: `/upgrade-python 3.14.3` The argument is the full Python version (e.g., `3.14.3`). Derive the major.minor (`3.14`) and cpython tag (`cp314`) from it. ## Step 1: Modify files for single-version build mode Edit these files to build ONLY the new Python version (this speeds up CI dramatically): ### `build.py` Change `PYTHONS` to only the new version: ```python PYTHONS = ((3, 14),) # temporarily building only new version ``` ### `validate.py` Same change to `PYTHONS`: ```python PYTHONS = ((3, 14),) # temporarily building only new version ``` ### `docker/install-pythons` Change `VERSIONS` to only the new full version: ```python VERSIONS = ("3.14.3",) # temporarily building only new version ``` ### `docker/Dockerfile` In the `ENV` `PATH` entry, keep only the new cpython path (the base image already provides python3.11 on PATH): ```dockerfile PATH=/venv/bin:/opt/python/cp314-cp314/bin:$PATH \ ``` ### `.github/workflows/build.yml` The production workflow uses pre-built `:latest` images. During a Python upgrade, the Dockerfile has changes that aren't in the production image yet, so you must temporarily add an `image` job that builds and pushes the modified image, and wire the `linux` job to use it. **Add a temporary `image` job** before the `linux` job: ```yaml image: strategy: matrix: include: - {arch: amd64, os: ubuntu-latest} - {arch: arm64, os: ubuntu-24.04-arm} runs-on: ${{ matrix.os }} permissions: packages: write steps: - uses: actions/checkout@v3 - run: docker login --username '${{ github.actor }}' --password-stdin ghcr.io <<< '${{ secrets.GITHUB_TOKEN }}' - run: | cp pyproject.toml uv.lock docker/ docker buildx build \ --cache-from ghcr.io/getsentry/pypi-manylinux-${{ matrix.arch }}-ci:latest \ --cache-to type=inline \ --platform linux/${{ matrix.arch }} \ --tag ghcr.io/getsentry/pypi-manylinux-${{ matrix.arch }}-ci:${{ github.sha }} \ ${{ github.ref == 'refs/heads/main' && format('--tag ghcr.io/getsentry/pypi-manylinux-{0}-ci:latest', matrix.arch) || '' }} \ --push \ docker ``` **Modify the `linux` job** to depend on `image` and use the SHA-tagged image: ```yaml linux: needs: [image] ... container: ghcr.io/getsentry/pypi-manylinux-${{ matrix.arch }}-ci:${{ github.sha }} ``` **macOS PATH entries** — keep only the new cpython PATH entry: ```yaml - run: | echo "$PWD/pythons/cp314-cp314/bin" >> "$GITHUB_PATH" echo "$PWD/.venv/bin" >> "$GITHUB_PATH" ``` **Add `--upgrade-python` flag** to both linux and macos build commands: ```yaml - run: python3 -um build --pypi-url https://pypi.devinfra.sentry.io --upgrade-python ``` ## Step 2: Commit and push Commit all changes with a message like "build: single-version mode for Python 3.14 upgrade" and push. ## Step 3: Wait for CI, then download and parse logs Wait for CI to complete (it will likely fail — that's expected). Use `gh run watch` to avoid rate limits if polling. Download logs from each build job using the GitHub CLI: ```bash # Get the workflow run gh run list --branch <current-branch> --limit 1 # Get job IDs from the run gh run view <run-id> --json jobs --jq '.jobs[] | {name: .name, id: .databaseId, conclusion: .conclusion}' # Download logs for each job gh api repos/getsentry/pypi/actions/jobs/<job-id>/logs > job-<name>.log ``` Parse the logs to identify: - **Succeeded packages**: lines matching `=== <name>==<version>@<python>` that are NOT followed by `!!! FAILED:` or `!!! SKIPPED` - **Failed packages**: lines matching `!!! FAILED: <name>==<version>: <error message>` - **Skipped packages**: lines matching `!!! SKIPPED (newer version already failed): <name>==<version>` — these are older versions that were auto-skipped because a newer version of the same package already failed A package is considered failed if it failed or was skipped on ANY platform (linux-amd64, linux-arm64, macos). ## Step 4: Update `packages.ini` In one pass: 1. **Remove** all packages that **succeeded on all platforms** — delete their entire section (`[name==version]` header + all config lines). The `format-packages-ini` pre-commit hook uses `configparser` which strips `#` comments, so commenting out doesn't work. Just delete succeeded sections outright. They don't need to rebuild. 2. **Add `python_versions = <MAJOR.MINOR`** to each **failed** package's section. Do NOT add `#` comments above — `configparser` strips them and can cause the `python_versions` line to be lost during formatting. 3. **Do NOT modify** packages that already have a `python_versions` restriction that is stricter than or equal to the new version (e.g., if a package already has `python_versions = <3.13`, leave it alone). ## Step 5: Write detailed failure summary to `PYTHON-MAJOR-MINOR-UPGRADE.md` After parsing logs, create a file named `PYTHON-MAJOR.MINOR-UPGRADE.md` (e.g., `PYTHON-3.14-UPGRADE.md`) in the repo root with a detailed summary of all packages that failed to build. This serves as a reference for fixing build issues. The file should contain: - A header with the Python version and date - A table or list of every failed package with: - Package name and version - Which platform(s) it failed on (linux-amd64, linux-arm64, macos, or all) - The error message / root cause extracted from the logs - A category for the failure (e.g., "Cython incompatibility", "pyo3 version too old", "missing C API", "setuptools/distutils issue", etc.) - A summary section grouping failures by category with counts, so we can prioritize which categories to tackle first Example structure: ```markdown # Python 3.14 Upgrade — Build Failures ## Summary by category | Category | Count | Packages | |----------|-------|----------| | pyo3 too old | 5 | pkg1, pkg2, ... | | Cython incompatibility | 3 | pkg3, pkg4, ... | ## Detailed failures ### pkg1==1.2.3 - **Platforms**: all - **Category**: pyo3 too old - **Error**: pyo3 0.22.2 only supports up to Python 3.13 ``` ## Step 6: Commit, push, repeat Commit with a message like "mark python 3.14 build failures in packages.ini" and push. Wait for CI again. If there are still failures, repeat steps 3-6 until CI is green. ## Step 7: Restore deleted packages and verify After CI is green, restore all previously-succeeded packages that were deleted in step 4. These packages don't need to rebuild (their existing wheels are fine), but they must remain in `packages.ini` so future builds include them. Use a script to find sections present in the pre-deletion commit but missing from the current file: ```python import configparser, subprocess old_content = subprocess.check_output(['git', 'show', '<pre-deletion-commit>:packages.ini']).decode() old = configparser.RawConfigParser(strict=False) old.read_string(old_content) with open('packages.ini') as f: cur = configparser.RawConfigParser(strict=False) cur.read_string(f.read()) missing = set(old.sections()) - set(cur.sections()) # Exclude any packages intentionally not restored # Append missing sections to packages.ini with open('packages.ini', 'a') as f: for section in sorted(missing): f.write(f'\n[{section}]\n') for k, v in old[section].items(): v = v.strip() if '\n' in v: f.write(f'{k} =\n') for part in v.split('\n'): if part.strip(): f.write(f' {part.strip()}\n') else: f.write(f'{k} = {v}\n') ``` Then run `python3 -m format_ini packages.ini` to sort and format. The formatter handles ordering automatically. Commit and push. Verify CI passes — all packages should either download pre-built wheels or build successfully. ## Step 8: Revert single-version mode After all packages are restored and CI passes, revert the single-version mode changes from step 1 to build all Python versions again: 1. **`build.py`**: Change `PYTHONS` back to all versions (e.g., `((3, 11), (3, 12), (3, 13), (3, 14))`) 2. **`validate.py`**: Same change to `PYTHONS` 3. **`docker/install-pythons`**: Change `VERSIONS` back to all versions (e.g., `("3.11.14", "3.12.12", "3.13.12", "3.14.3")`) 4. **`docker/Dockerfile`**: Restore all cpython paths in `PATH` env var 5. **`.github/workflows/build.yml`**: - Remove the temporary `image` job entirely - Remove `needs: [image]` from the `linux` job - Change the `linux` container back to `:latest` tag (from `:${{ github.sha }}`) - Remove `--upgrade-python` flag from both linux and macos build commands - Restore all cpython PATH entries in the macos job Commit with a message like "revert single-version mode, build all python versions" and push. Verify CI passes with all Python versions building. ## Important notes - The `--upgrade-python` flag in `build.py` enables continue-on-failure mode with a 10-minute timeout per package. Without it, builds fail on first error, except on Python 3.14, where `build.py` and `validate.py` print `!!! WARNING (py3.14)` and continue. - The `=== name==version@python`, `!!! FAILED: name==version: error`, and `!!! SKIPPED (newer version already failed): name==version` log lines are the markers used to parse results. - In `--upgrade-python` mode, packages are sorted newest-version-first within each name. If the newest version fails, all older versions are automatically skipped. - Do NOT try to comment out sections with `#` — the `format-packages-ini` pre-commit hook uses Python's `configparser` which strips all `#` comments. Instead, delete succeeded sections entirely. - The `format-packages-ini` hook also sorts and reformats `packages.ini`, so ordering is handled automatically.
View on GitHub