FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

build(workflows): publish portable wheels and recover releases by owenthcarey · Pull Request #82 · pythonnative/pythonnative · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .in  (1) .md  (1) .py  (4) .toml  (2) .yml  (2) All 5 file types selected
Only manifest files
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
126 changes: 87 additions & 39 deletions .github/workflows/release.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,110 @@ on:
push:
branches: [main]
workflow_dispatch:
inputs:
tag:
description: Existing release tag to recover (leave empty for a new release)
type: string
required: false

# Serialize the entire release, including manual recoveries and uploads.
concurrency:
group: release
cancel-in-progress: false

permissions:
contents: read

jobs:
release:
if: github.repository == 'pythonnative/pythonnative'
name: Semantic Release
runs-on: ubuntu-latest
concurrency:
group: release
cancel-in-progress: false
if: github.repository == 'pythonnative/pythonnative' && github.ref == 'refs/heads/main'
name: Select release
runs-on: ubuntu-24.04
permissions:
contents: write
id-token: write

outputs:
tag: ${{ steps.select.outputs.tag }}
commit: ${{ steps.select.outputs.commit }}
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up uv
uses: astral-sh/setup-uv@v10.0.1
- uses: astral-sh/setup-uv@v10.0.1
with:
enable-cache: true
python-version: '3.13'

# TEMPORARY: inlined replacement for the
# python-semantic-release/python-semantic-release@v9 action. That
# action builds its Docker image at job time with GitPython
# unpinned, and GitPython 3.1.60 (2026-08-25) removed
# Actor.name_email_regex, which crashes every semantic-release
# config load (python-semantic-release issue #1475). This step
# mirrors the action: same git identity, same `version` command,
# and a `released` output for the steps below. Restore the action
# once the fix (python-semantic-release PR #1477) is released.
- name: Python Semantic Release
id: release
- name: Create a version or select an existing release
id: select
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RECOVERY_TAG: ${{ inputs.tag }}
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
uv tool install "python-semantic-release==9.21.2" --with "gitpython<3.1.60"
tags_before=$(git tag | wc -l)
semantic-release -v version
tags_after=$(git tag | wc -l)
if [ "$tags_after" -gt "$tags_before" ]; then
echo "released=true" >> "$GITHUB_OUTPUT"
if [ -n "$RECOVERY_TAG" ]; then
# Only published release tags from main can be recovered.
[[ "$RECOVERY_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
commit=$(git rev-parse --verify "refs/tags/$RECOVERY_TAG^{commit}")
git merge-base --is-ancestor "$commit" origin/main
gh release view "$RECOVERY_TAG" --json isDraft --jq '.isDraft' | grep -qx false
tag="$RECOVERY_TAG"
else
echo "released=false" >> "$GITHUB_OUTPUT"
git config user.name "github-actions"
git config user.email "actions@github.com"
# GitPython 3.1.60 removed Actor.name_email_regex, which PSR
# 9.21.2 still uses. Keep this constraint until PSR is upgraded.
uv tool install "python-semantic-release==9.21.2" --with "gitpython<3.1.60"
tags_before=$(git tag | wc -l)
semantic-release -v version
tags_after=$(git tag | wc -l)
if [ "$tags_after" -eq "$tags_before" ]; then
echo "No new release. To resume an upload, run this workflow with its existing tag."
exit 0
fi
tag=$(git describe --tags --exact-match HEAD)
commit=$(git rev-parse HEAD)
fi
# Check the tag against package metadata without importing/building it.
git show "$commit:pyproject.toml" > "$RUNNER_TEMP/release-project.toml"
RELEASE_TAG="$tag" python -c 'import os, pathlib, tomllib; data = tomllib.loads((pathlib.Path(os.environ["RUNNER_TEMP"]) / "release-project.toml").read_text()); assert os.environ["RELEASE_TAG"] == "v" + data["project"]["version"]'
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "commit=$commit" >> "$GITHUB_OUTPUT"

distributions:
name: Build release distributions
needs: release
if: needs.release.outputs.tag != ''
uses: ./.github/workflows/wheels.yml
with:
source-ref: ${{ needs.release.outputs.commit }}

# The distributions come from `build_command` in
# [tool.semantic_release], which runs `uv build` after PSR stamps the
# new version, so there is no separate build step here.
publish:
name: Publish distributions
needs: [release, distributions]
runs-on: ubuntu-24.04
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: release-distributions
path: dist
- uses: astral-sh/setup-uv@v10.0.1
with:
python-version: '3.13'
- name: Preserve release assets for resumable publishing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: uv run --no-project python scripts/release-assets.py "$RELEASE_TAG" dist
- name: Validate the exact files to publish
env:
RELEASE_TAG: ${{ needs.release.outputs.tag }}
run: |
uv run --no-project --with packaging python scripts/check-distributions.py dist --version "${RELEASE_TAG#v}"
uvx twine check --strict dist/*
- name: Publish to PyPI
if: steps.release.outputs.released == 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
106 changes: 106 additions & 0 deletions .github/workflows/wheels.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Distributions

on:
pull_request:
branches: [main]
workflow_dispatch:
inputs:
source-ref:
description: Commit or tag to build and test (no publishing)
type: string
required: true
default: main
workflow_call:
inputs:
source-ref:
description: Immutable source commit selected by the release workflow
type: string
required: true

permissions:
contents: read

jobs:
sdist:
name: Source distribution
runs-on: ubuntu-24.04
outputs:
filename: ${{ steps.build.outputs.filename }}
version: ${{ steps.build.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.source-ref || github.sha }}
persist-credentials: false
- uses: astral-sh/setup-uv@v10.0.1
with:
python-version: '3.13'
- name: Build source distribution
id: build
run: |
uv build --sdist --python 3.13
echo "filename=$(basename dist/*.tar.gz)" >> "$GITHUB_OUTPUT"
python -c 'import tomllib; print("version=" + tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])' >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: distribution-sdist
path: dist/*.tar.gz
if-no-files-found: error

wheels:
name: Wheels (${{ matrix.runner }})
needs: sdist
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
runner: [ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-14, windows-2022]
steps:
# Build policy comes from the workflow revision, source from the sdist.
# Recovery uses fixed tooling with an existing tag's unmodified source.
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: distribution-sdist
path: dist
- name: Build, repair, install, and test wheels
uses: pypa/cibuildwheel@v4.2.1
with:
package-dir: dist/${{ needs.sdist.outputs.filename }}
config-file: ${{ github.workspace }}/scripts/cibuildwheel.toml
output-dir: wheelhouse
- uses: actions/upload-artifact@v4
with:
name: distribution-${{ matrix.runner }}
path: wheelhouse/*.whl
if-no-files-found: error

validate:
name: Validate release distributions
needs: [sdist, wheels]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
pattern: distribution-*
merge-multiple: true
path: dist
- uses: astral-sh/setup-uv@v10.0.1
with:
python-version: '3.13'
- name: Check metadata, platform tags, and complete wheel matrix
env:
RELEASE_VERSION: ${{ needs.sdist.outputs.version }}
run: |
uv run --no-project --with packaging python scripts/check-distributions.py dist --version "$RELEASE_VERSION"
uvx twine check --strict dist/*
- uses: actions/upload-artifact@v4
with:
name: release-distributions
path: dist/*
if-no-files-found: error
50 changes: 41 additions & 9 deletions CONTRIBUTING.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -336,15 +336,47 @@ Co-authored-by: Name <email>

- The version is tracked in `pyproject.toml` (`project.version`) and mirrored in `src/pythonnative/__init__.py` as `__version__`. Both files are updated automatically by [python-semantic-release](https://python-semantic-release.readthedocs.io/).
- **Automated release pipeline** (on every merge to `main`):
1. `python-semantic-release` scans Conventional Commit messages since the last tag.
2. It determines the next SemVer bump: `feat` → **minor**, `fix`/`perf` → **patch**, `BREAKING CHANGE` → **major** (minor while version < 1.0).
3. Version files are updated, `CHANGELOG.md` is generated, and a tagged release commit (`chore(release): vX.Y.Z`) is pushed.
4. A GitHub Release is created with auto-generated release notes and the built sdist/wheel attached.
5. When drafts are disabled, the package is also published to PyPI via Trusted Publishing.
- **Draft / published toggle**: the `DRAFT_RELEASE` variable at the top of `.github/workflows/release.yml` controls release mode. Set to `"true"` (the default) for draft GitHub Releases with PyPI publishing skipped; flip to `"false"` to publish releases and upload to PyPI immediately.
- Commit types that trigger a release: `feat` (minor), `fix` and `perf` (patch), `BREAKING CHANGE` (major). All other types (`build`, `chore`, `ci`, `docs`, `refactor`, `revert`, `style`, `test`) are recorded in the changelog but do **not** trigger a release on their own.
- Tag format: `v`-prefixed (e.g., `v0.4.0`).
- Manual version bumps are no longer needed: just merge PRs with valid Conventional Commit titles. For ad-hoc runs, use the workflow's **Run workflow** button (`workflow_dispatch`).
1. `python-semantic-release` scans Conventional Commits, updates the version files and `uv.lock`, generates `CHANGELOG.md`, and pushes the release commit and tag.
2. A published GitHub release is created with generated release notes.
3. The shared `Distributions` workflow builds a source archive from that exact commit, then uses [cibuildwheel](https://cibuildwheel.pypa.io/) to build all wheels from the archive.
4. Each wheel is repaired where needed, installed in an isolated environment, and tested with the Yoga layout suite and CLI. The complete artifact set must also pass platform, version, resource, and metadata checks.
5. Validated distributions are attached to the GitHub release before PyPI uploads begin. PyPI uses Trusted Publishing; no API token is needed.
- The same distribution builds and checks run on PRs. The wheel matrix covers CPython 3.13 and 3.14 on Linux x86-64 and ARM64 (glibc 2.28 or newer), macOS Intel and Apple Silicon (macOS 11 or newer), and Windows x64. These are development-host wheels; mobile apps compile the bundled Yoga source in their native builds.
- Build policy lives in `scripts/cibuildwheel.toml`, separately from the tagged package source, so fixed tooling can rebuild an older release without changing its code or version. Windows compiler/linker flags also support the original `v0.40.0` source; newer source distributions include the export settings in `setup.py`.
- Commit types that trigger a release: `feat` (minor), `fix` and `perf` (patch), and `BREAKING CHANGE` (major, or minor before 1.0). Other types, including `build` and `ci`, don't trigger a release on their own. Use a `build` or `ci` title for a publishing-only repair that should recover the existing version.
- Tag format: `v`-prefixed (for example, `v0.40.0`). Manual version bumps aren't needed.

### Recovering a failed publication

Version creation and package publication are separate jobs. A GitHub release can
exist even when its PyPI upload failed. Rerunning version creation won't create
another release for the same commits.

Once the workflow repair is merged, select **Actions → Release → Run workflow**,
choose `main`, and enter the existing tag in the recovery field. With the GitHub
CLI, the equivalent is:

```bash
gh workflow run release.yml --ref main -f tag=v0.40.0
```

Recovery verifies that the tag belongs to `main` and matches the package version,
then runs the same build and validation jobs. It doesn't bump the version,
rewrite the tag, or change the tagged source. Existing distribution assets are
reused byte for byte; missing assets are uploaded before publishing to PyPI.
Files already uploaded to PyPI are skipped, so a partial upload can resume.
Release runs are serialized to prevent competing uploads.

For a build-only rehearsal, run **Distributions** with a `source-ref` of the tag
or commit to check. This runs the full matrix without publishing anything:

```bash
gh workflow run wheels.yml --ref main -f source-ref=v0.40.0
```

A source-only install of `v0.40.0` on Windows still needs the export flags from
`scripts/cibuildwheel.toml`; its tagged `setup.py` predates that fix. The recovered
Windows wheels include those exports and install without a compiler.

### Branch naming (suggested)

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
global-exclude *.pyc *.pyo .DS_Store
include scripts/cibuildwheel.toml scripts/check-distributions.py scripts/release-assets.py
recursive-exclude src/pythonnative/native .build/* .gradle/* .cxx/* build/* .swiftpm/* .kotlin/*
prune src/pythonnative/native/android/build
prune src/pythonnative/native/android/.gradle
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@ target-version = ['py313']
# PSR stamps the new version into pyproject.toml before running this, so
# `--upgrade-package` re-syncs only this project's own version in the lock
# (never dependency upgrades), and the staged uv.lock lands in the release
# commit. This also replaces the workflow's separate `python -m build`.
# commit. The release workflow builds distributions from that tagged commit
# using the same cibuildwheel matrix as PR CI.
build_command = """
uv lock --upgrade-package pythonnative
git add uv.lock
uv build
"""
version_toml = ["pyproject.toml:project.version"]
version_variables = ["src/pythonnative/__init__.py:__version__"]
Expand Down
Loading
Loading

Back | FazBrowse Home | New Git URL