ci: release workflow fixes (node + python)#2043
Conversation
Two release-blocker fixes that surfaced when tagging v0.23.1rc0: 1. node-release.yml: napi-rs/cli v3 (which we adopted in #2034) renamed the cross-compile flag from `--zig` to `--cross-compile` (or `-x`). The build steps still passed `--zig`, so every linux-musl / linux-aarch64 / armv7 entry failed with Unknown Syntax Error: Unsupported option name ("--zig"). Replace `--zig` with `--cross-compile` everywhere (build commands + the `contains(...)` gate on the zig setup step). 2. python-release.yml: drop 3.14t from the windows-11-arm matrix entry. actions/python-versions currently ships a broken `python-3.14.4-win32-arm64-freethreaded` package (0-byte python.exe, pip install fails with `ModuleNotFoundError: encodings`). Keep the regular 3.14 arm64 build; 3.14t arm64 windows can return when the upstream package is fixed.
The pre-existing matrix had `interpreter: "3.14 3.14t"` on most
entries, which made maturin build a single wheel per cell intended to
serve both interpreters. With the project's `abi3` cargo feature
enabled by default that wheel was abi3-compiled — fine for 3.14, but
3.14t can't load abi3 extensions (no limited API on free-threaded
Python), so the published cp314t wheels would fail to import.
Split into two flavors via a discriminator:
- All existing entries set `interpreter: "3.14"` only (so the abi3
wheel they produce serves 3.10–3.14 cleanly).
- New `flavor: ft` include: entries for the platforms we want 3.14t
wheels on (linux x86_64/aarch64 manylinux+musllinux, macos x86_64/
aarch64, windows x86_64). Each passes
`extra-build-args: --no-default-features --features pyo3/extension-module`
which drops the abi3 cargo feature so maturin produces a proper
cp314t-tagged, non-abi3 wheel that actually loads on free-threaded
Python.
- Base matrix gets `flavor: [abi3]` so the `flavor: ft` includes
create new matrix cells instead of merging with the abi3 combos.
- Artifact name suffixed with `${{ matrix.flavor || 'abi3' }}` so the
abi3 and ft wheels for the same os/target/manylinux don't collide
on upload-artifact.
- Skipped 3.14t for windows-11-arm because the upstream 3.14t arm64
package is currently broken (0-byte python.exe). Skipped for the
niche linux targets (i686, armv7, ppc64le, s390x) too — easy to add
later.
Job name now includes the interpreter for log readability.
The 3.14t matrix cells only differ from their abi3 siblings on three
fields: `flavor`, `interpreter`, and `extra-build-args`. The latter
two are fully determined by the first, so derive them in the maturin
invocation:
args: >-
--release --out dist
--interpreter ${{ matrix.flavor == 'ft' && '3.14t' || matrix.interpreter || '3.14' }}
${{ matrix.flavor == 'ft' && '--no-default-features --features pyo3/extension-module' || '' }}
Each 3.14t cell is now a single-line YAML flow entry — the only thing
the matrix says about it is `flavor: ft`. Drops ~30 lines of
repetition without changing behaviour.
napi-rs/cli v3 renamed `napi universal` -> `napi universalize` and dropped the `--dir` flag — it now expects per-target .node files in the standard `npm/<triple>/` layout that `napi artifacts` produces, not in a flat directory. Rather than insert a `napi artifacts` step + reorganize, just call `lipo` directly. The two darwin .node files we already download into ./artifacts have known names; combining them into the universal binary is one line. That's exactly what `napi universal` was wrapping internally.
The crate is built with `abi3-py310`, so 3.10 is the actual minimum the wheels can load on. Update the PyPI metadata accordingly: - requires-python: ">=3.9" -> ">=3.10" - drop the "Python :: 3.9" trove classifier - add the "Python :: 3.14" trove classifier This stops pip from offering the wheel to 3.9 users (where it would fail to import) and makes the PyPI page accurate.
`yarn artifacts` (napi-cli v3 `napi artifacts`) writes each per-target .node file into `npm/<triple>/`. The repo has hand-written templates for most triples but darwin-universal was added to `napi.targets` without one, so the publish step failed with: ENOENT: no such file or directory, open '.../bindings/node/npm/darwin-universal/tokenizers.darwin-universal.node' Add `napi create-npm-dirs` before `yarn artifacts` — it scans `napi.targets` and creates any missing dirs (with a generated package.json) from scratch. Idempotent for triples that already have hand-written templates.
Two issues stacked when v0.23.1rc0 hit the publish job: 1. `napi prepublish` POSTs to /repos/.../releases to create a GitHub Release and got 403 — the workflow's default GITHUB_TOKEN has only `contents: read`, but creating a release needs `contents: write`. Bump the publish-job permission. 2. `napi prepublish` then runs `npm publish` for each per-platform sub-package; modern npm refuses to auto-route a `0.23.1-rc0` version to `latest` and demands an explicit `--tag`. Rather than wire up `--tag rc` in every sub-publish (and risk an rc landing on `latest` by accident), gate the entire publish job on `!contains(github.ref, 'rc')`. Matches the existing pattern in rust-release.yml. rc tags still run the build + universal-macOS matrix end-to-end — they just skip the registry push, so they're useful for verifying the cross-compile / lipo / artifact-collection flow before a real release.
There was a problem hiding this comment.
Pull request overview
Updates the Node and Python release GitHub Actions workflows to match upstream CLI/tooling changes and adjust release behavior, plus aligns Python packaging metadata with the project’s abi3 baseline.
Changes:
- Node release workflow: update napi-rs CLI flags, switch universal macOS build to direct
lipo, addcreate-npm-dirs, and skip publishing onrctags while allowing GitHub Release creation on stable tags. - Python release workflow: restructure the build matrix to add free-threaded (3.14t) wheel variants via a
flavordiscriminator and drop the broken Windows ARM 3.14t entry. - Python packaging metadata: raise
requires-pythonto>=3.10and update trove classifiers (drop 3.9, add 3.14).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
bindings/python/pyproject.toml |
Raises minimum supported Python version and updates trove classifiers. |
.github/workflows/python-release.yml |
Adds flavor-based matrix entries for 3.14t wheels and adjusts build args / artifact naming. |
.github/workflows/node-release.yml |
Updates cross-compilation flag usage, changes universal macOS combination strategy, and adjusts publish gating/permissions and packaging steps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -188,6 +203,15 @@ jobs: | |||
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |||
| with: | |||
| path: ./bindings/node/artifacts | |||
There was a problem hiding this comment.
actions/download-artifact without name will extract each artifact into its own subdirectory under ./bindings/node/artifacts. yarn artifacts / napi artifacts typically expects the .node files to be directly in the artifacts directory, so this layout can cause it to miss the binaries and fail at publish time. Consider setting merge-multiple: true (or downloading with an explicit pattern and merging) so the .node files land in a flat ./bindings/node/artifacts/ directory.
| path: ./bindings/node/artifacts | |
| path: ./bindings/node/artifacts | |
| merge-multiple: true |
| args: >- | ||
| --release --out dist | ||
| --interpreter ${{ matrix.flavor == 'ft' && '3.14t' || matrix.interpreter || '3.14' }} | ||
| ${{ matrix.flavor == 'ft' && '--no-default-features --features pyo3/extension-module' || '' }} |
There was a problem hiding this comment.
For flavor: ft builds you set --interpreter to 3.14t, but actions/setup-python still installs the default 3.14 on macOS/Linux unless matrix.python-install is overridden. This will likely make maturin fail to find a 3.14t interpreter on at least the macOS flavor: ft matrix cells. Add python-install: "3.14t" (and any required python-architecture) to the flavor: ft include entries that run on host Python (macOS, and any non-container Linux builds).
| args: >- | ||
| --release --out dist | ||
| --interpreter ${{ matrix.flavor == 'ft' && '3.14t' || matrix.interpreter || '3.14' }} | ||
| ${{ matrix.flavor == 'ft' && '--no-default-features --features pyo3/extension-module' || '' }} |
There was a problem hiding this comment.
The free-threaded path disables default features but then enables --features pyo3/extension-module directly. Since this repo already defines a crate feature ext-module = ["pyo3/extension-module"] in bindings/python/Cargo.toml, using --features ext-module would be less coupled to the dependency name and easier to maintain if the feature wiring changes.
| ${{ matrix.flavor == 'ft' && '--no-default-features --features pyo3/extension-module' || '' }} | |
| ${{ matrix.flavor == 'ft' && '--no-default-features --features ext-module' || '' }} |
|
|
||
| build: | ||
| name: build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }}) | ||
| name: build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }} - ${{ matrix.interpreter || '3.14' }}) |
There was a problem hiding this comment.
The job name reports matrix.interpreter || '3.14', but for the new flavor: ft matrix cells matrix.interpreter is unset, so these jobs will still display 3.14 even though the build is targeting 3.14t via the args expression. Consider deriving the displayed interpreter from matrix.flavor (or setting interpreter: "3.14t" on the flavor: ft include entries) to avoid confusion when debugging CI.
| name: build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }} - ${{ matrix.interpreter || '3.14' }}) | |
| name: build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }} - ${{ matrix.flavor == 'ft' && '3.14t' || matrix.interpreter || '3.14' }}) |
…dule alias, name from flavor Three review fixes folded into one commit: 1. macOS `flavor: ft` cells were missing `python-install: "3.14t"`; setup-python defaulted to 3.14, so maturin's `--interpreter 3.14t` would have failed to find an interpreter. Linux `manylinux`/ `musllinux` ft cells run inside the maturin docker image (which ships 3.14t), so they don't need it. Windows ft already had it. 2. Switch the dropped-feature flag from `pyo3/extension-module` to the project-level `ext-module` feature alias defined in `bindings/python/Cargo.toml`. Less coupled to the dependency name — if the feature wiring changes (e.g. pyo3 renames the feature, or we add wrapper logic to ext-module), the workflow doesn't have to move with it. 3. Job-name `interpreter` segment now derives from `flavor`, so 3.14t cells display as `3.14t` instead of the misleading `3.14` they got from the unset `matrix.interpreter`.
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
Picks up the release-pipeline fixes that surfaced while tagging `v0.23.1rc0`. The version-bump commit on `v0.23-release` (PR #2032) is intentionally not included here — that one stays release-specific.
What's in here
node-release.yml
python-release.yml
bindings/python/pyproject.toml
Not in this PR