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

build(workflows): publish portable wheels and recover releases (#82) · pythonnative/pythonnative@b3680df · GitHub

Commit b3680df

Browse files
authored
build(workflows): publish portable wheels and recover releases (#82)
1 parent df7455c commit b3680df

10 files changed

Lines changed: 592 additions & 50 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 87 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,62 +4,110 @@ on:
44
push:
55
branches: [main]
66
workflow_dispatch:
7+
inputs:
8+
tag:
9+
description: Existing release tag to recover (leave empty for a new release)
10+
type: string
11+
required: false
12+
13+
# Serialize the entire release, including manual recoveries and uploads.
14+
concurrency:
15+
group: release
16+
cancel-in-progress: false
17+
18+
permissions:
19+
contents: read
720

821
jobs:
922
release:
10-
if: github.repository == 'pythonnative/pythonnative'
11-
name: Semantic Release
12-
runs-on: ubuntu-latest
13-
concurrency:
14-
group: release
15-
cancel-in-progress: false
23+
if: github.repository == 'pythonnative/pythonnative' && github.ref == 'refs/heads/main'
24+
name: Select release
25+
runs-on: ubuntu-24.04
1626
permissions:
1727
contents: write
18-
id-token: write
19-
28+
outputs:
29+
tag: ${{ steps.select.outputs.tag }}
30+
commit: ${{ steps.select.outputs.commit }}
2031
steps:
21-
- name: Checkout
22-
uses: actions/checkout@v4
32+
- uses: actions/checkout@v4
2333
with:
2434
fetch-depth: 0
25-
26-
- name: Set up uv
27-
uses: astral-sh/setup-uv@v10.0.1
35+
- uses: astral-sh/setup-uv@v10.0.1
2836
with:
29-
enable-cache: true
3037
python-version: '3.13'
31-
32-
# TEMPORARY: inlined replacement for the
33-
# python-semantic-release/python-semantic-release@v9 action. That
34-
# action builds its Docker image at job time with GitPython
35-
# unpinned, and GitPython 3.1.60 (2026-08-25) removed
36-
# Actor.name_email_regex, which crashes every semantic-release
37-
# config load (python-semantic-release issue #1475). This step
38-
# mirrors the action: same git identity, same `version` command,
39-
# and a `released` output for the steps below. Restore the action
40-
# once the fix (python-semantic-release PR #1477) is released.
41-
- name: Python Semantic Release
42-
id: release
38+
- name: Create a version or select an existing release
39+
id: select
4340
env:
4441
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42+
RECOVERY_TAG: ${{ inputs.tag }}
4543
run: |
46-
git config user.name "github-actions"
47-
git config user.email "actions@github.com"
48-
uv tool install "python-semantic-release==9.21.2" --with "gitpython<3.1.60"
49-
tags_before=$(git tag | wc -l)
50-
semantic-release -v version
51-
tags_after=$(git tag | wc -l)
52-
if [ "$tags_after" -gt "$tags_before" ]; then
53-
echo "released=true" >> "$GITHUB_OUTPUT"
44+
if [ -n "$RECOVERY_TAG" ]; then
45+
# Only published release tags from main can be recovered.
46+
[[ "$RECOVERY_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
47+
commit=$(git rev-parse --verify "refs/tags/$RECOVERY_TAG^{commit}")
48+
git merge-base --is-ancestor "$commit" origin/main
49+
gh release view "$RECOVERY_TAG" --json isDraft --jq '.isDraft' | grep -qx false
50+
tag="$RECOVERY_TAG"
5451
else
55-
echo "released=false" >> "$GITHUB_OUTPUT"
52+
git config user.name "github-actions"
53+
git config user.email "actions@github.com"
54+
# GitPython 3.1.60 removed Actor.name_email_regex, which PSR
55+
# 9.21.2 still uses. Keep this constraint until PSR is upgraded.
56+
uv tool install "python-semantic-release==9.21.2" --with "gitpython<3.1.60"
57+
tags_before=$(git tag | wc -l)
58+
semantic-release -v version
59+
tags_after=$(git tag | wc -l)
60+
if [ "$tags_after" -eq "$tags_before" ]; then
61+
echo "No new release. To resume an upload, run this workflow with its existing tag."
62+
exit 0
63+
fi
64+
tag=$(git describe --tags --exact-match HEAD)
65+
commit=$(git rev-parse HEAD)
5666
fi
67+
# Check the tag against package metadata without importing/building it.
68+
git show "$commit:pyproject.toml" > "$RUNNER_TEMP/release-project.toml"
69+
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"]'
70+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
71+
echo "commit=$commit" >> "$GITHUB_OUTPUT"
72+
73+
distributions:
74+
name: Build release distributions
75+
needs: release
76+
if: needs.release.outputs.tag != ''
77+
uses: ./.github/workflows/wheels.yml
78+
with:
79+
source-ref: ${{ needs.release.outputs.commit }}
5780

58-
# The distributions come from `build_command` in
59-
# [tool.semantic_release], which runs `uv build` after PSR stamps the
60-
# new version, so there is no separate build step here.
81+
publish:
82+
name: Publish distributions
83+
needs: [release, distributions]
84+
runs-on: ubuntu-24.04
85+
permissions:
86+
contents: write
87+
id-token: write
88+
steps:
89+
- uses: actions/checkout@v4
90+
with:
91+
persist-credentials: false
92+
- uses: actions/download-artifact@v4
93+
with:
94+
name: release-distributions
95+
path: dist
96+
- uses: astral-sh/setup-uv@v10.0.1
97+
with:
98+
python-version: '3.13'
99+
- name: Preserve release assets for resumable publishing
100+
env:
101+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
102+
RELEASE_TAG: ${{ needs.release.outputs.tag }}
103+
run: uv run --no-project python scripts/release-assets.py "$RELEASE_TAG" dist
104+
- name: Validate the exact files to publish
105+
env:
106+
RELEASE_TAG: ${{ needs.release.outputs.tag }}
107+
run: |
108+
uv run --no-project --with packaging python scripts/check-distributions.py dist --version "${RELEASE_TAG#v}"
109+
uvx twine check --strict dist/*
61110
- name: Publish to PyPI
62-
if: steps.release.outputs.released == 'true'
63111
uses: pypa/gh-action-pypi-publish@release/v1
64112
with:
65113
skip-existing: true

‎.github/workflows/wheels.yml‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
name: Distributions
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
workflow_dispatch:
7+
inputs:
8+
source-ref:
9+
description: Commit or tag to build and test (no publishing)
10+
type: string
11+
required: true
12+
default: main
13+
workflow_call:
14+
inputs:
15+
source-ref:
16+
description: Immutable source commit selected by the release workflow
17+
type: string
18+
required: true
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
sdist:
25+
name: Source distribution
26+
runs-on: ubuntu-24.04
27+
outputs:
28+
filename: ${{ steps.build.outputs.filename }}
29+
version: ${{ steps.build.outputs.version }}
30+
steps:
31+
- uses: actions/checkout@v4
32+
with:
33+
ref: ${{ inputs.source-ref || github.sha }}
34+
persist-credentials: false
35+
- uses: astral-sh/setup-uv@v10.0.1
36+
with:
37+
python-version: '3.13'
38+
- name: Build source distribution
39+
id: build
40+
run: |
41+
uv build --sdist --python 3.13
42+
echo "filename=$(basename dist/*.tar.gz)" >> "$GITHUB_OUTPUT"
43+
python -c 'import tomllib; print("version=" + tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])' >> "$GITHUB_OUTPUT"
44+
- uses: actions/upload-artifact@v4
45+
with:
46+
name: distribution-sdist
47+
path: dist/*.tar.gz
48+
if-no-files-found: error
49+
50+
wheels:
51+
name: Wheels (${{ matrix.runner }})
52+
needs: sdist
53+
runs-on: ${{ matrix.runner }}
54+
strategy:
55+
fail-fast: false
56+
matrix:
57+
runner: [ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-14, windows-2022]
58+
steps:
59+
# Build policy comes from the workflow revision, source from the sdist.
60+
# Recovery uses fixed tooling with an existing tag's unmodified source.
61+
- uses: actions/checkout@v4
62+
with:
63+
persist-credentials: false
64+
- uses: actions/download-artifact@v4
65+
with:
66+
name: distribution-sdist
67+
path: dist
68+
- name: Build, repair, install, and test wheels
69+
uses: pypa/cibuildwheel@v4.2.1
70+
with:
71+
package-dir: dist/${{ needs.sdist.outputs.filename }}
72+
config-file: ${{ github.workspace }}/scripts/cibuildwheel.toml
73+
output-dir: wheelhouse
74+
- uses: actions/upload-artifact@v4
75+
with:
76+
name: distribution-${{ matrix.runner }}
77+
path: wheelhouse/*.whl
78+
if-no-files-found: error
79+
80+
validate:
81+
name: Validate release distributions
82+
needs: [sdist, wheels]
83+
runs-on: ubuntu-24.04
84+
steps:
85+
- uses: actions/checkout@v4
86+
with:
87+
persist-credentials: false
88+
- uses: actions/download-artifact@v4
89+
with:
90+
pattern: distribution-*
91+
merge-multiple: true
92+
path: dist
93+
- uses: astral-sh/setup-uv@v10.0.1
94+
with:
95+
python-version: '3.13'
96+
- name: Check metadata, platform tags, and complete wheel matrix
97+
env:
98+
RELEASE_VERSION: ${{ needs.sdist.outputs.version }}
99+
run: |
100+
uv run --no-project --with packaging python scripts/check-distributions.py dist --version "$RELEASE_VERSION"
101+
uvx twine check --strict dist/*
102+
- uses: actions/upload-artifact@v4
103+
with:
104+
name: release-distributions
105+
path: dist/*
106+
if-no-files-found: error

‎CONTRIBUTING.md‎

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -336,15 +336,47 @@ Co-authored-by: Name <email>
336336

337337
- 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/).
338338
- **Automated release pipeline** (on every merge to `main`):
339-
1. `python-semantic-release` scans Conventional Commit messages since the last tag.
340-
2. It determines the next SemVer bump: `feat` → **minor**, `fix`/`perf` → **patch**, `BREAKING CHANGE` → **major** (minor while version < 1.0).
341-
3. Version files are updated, `CHANGELOG.md` is generated, and a tagged release commit (`chore(release): vX.Y.Z`) is pushed.
342-
4. A GitHub Release is created with auto-generated release notes and the built sdist/wheel attached.
343-
5. When drafts are disabled, the package is also published to PyPI via Trusted Publishing.
344-
- **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.
345-
- 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.
346-
- Tag format: `v`-prefixed (e.g., `v0.4.0`).
347-
- 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`).
339+
1. `python-semantic-release` scans Conventional Commits, updates the version files and `uv.lock`, generates `CHANGELOG.md`, and pushes the release commit and tag.
340+
2. A published GitHub release is created with generated release notes.
341+
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.
342+
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.
343+
5. Validated distributions are attached to the GitHub release before PyPI uploads begin. PyPI uses Trusted Publishing; no API token is needed.
344+
- 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.
345+
- 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`.
346+
- 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.
347+
- Tag format: `v`-prefixed (for example, `v0.40.0`). Manual version bumps aren't needed.
348+
349+
### Recovering a failed publication
350+
351+
Version creation and package publication are separate jobs. A GitHub release can
352+
exist even when its PyPI upload failed. Rerunning version creation won't create
353+
another release for the same commits.
354+
355+
Once the workflow repair is merged, select **Actions → Release → Run workflow**,
356+
choose `main`, and enter the existing tag in the recovery field. With the GitHub
357+
CLI, the equivalent is:
358+
359+
```bash
360+
gh workflow run release.yml --ref main -f tag=v0.40.0
361+
```
362+
363+
Recovery verifies that the tag belongs to `main` and matches the package version,
364+
then runs the same build and validation jobs. It doesn't bump the version,
365+
rewrite the tag, or change the tagged source. Existing distribution assets are
366+
reused byte for byte; missing assets are uploaded before publishing to PyPI.
367+
Files already uploaded to PyPI are skipped, so a partial upload can resume.
368+
Release runs are serialized to prevent competing uploads.
369+
370+
For a build-only rehearsal, run **Distributions** with a `source-ref` of the tag
371+
or commit to check. This runs the full matrix without publishing anything:
372+
373+
```bash
374+
gh workflow run wheels.yml --ref main -f source-ref=v0.40.0
375+
```
376+
377+
A source-only install of `v0.40.0` on Windows still needs the export flags from
378+
`scripts/cibuildwheel.toml`; its tagged `setup.py` predates that fix. The recovered
379+
Windows wheels include those exports and install without a compiler.
348380

349381
### Branch naming (suggested)
350382

‎MANIFEST.in‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
global-exclude *.pyc *.pyo .DS_Store
2+
include scripts/cibuildwheel.toml scripts/check-distributions.py scripts/release-assets.py
23
recursive-exclude src/pythonnative/native .build/* .gradle/* .cxx/* build/* .swiftpm/* .kotlin/*
34
prune src/pythonnative/native/android/build
45
prune src/pythonnative/native/android/.gradle

‎pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,11 @@ target-version = ['py313']
153153
# PSR stamps the new version into pyproject.toml before running this, so
154154
# `--upgrade-package` re-syncs only this project's own version in the lock
155155
# (never dependency upgrades), and the staged uv.lock lands in the release
156-
# commit. This also replaces the workflow's separate `python -m build`.
156+
# commit. The release workflow builds distributions from that tagged commit
157+
# using the same cibuildwheel matrix as PR CI.
157158
build_command = """
158159
uv lock --upgrade-package pythonnative
159160
git add uv.lock
160-
uv build
161161
"""
162162
version_toml = ["pyproject.toml:project.version"]
163163
version_variables = ["src/pythonnative/__init__.py:__version__"]

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL