Releases¶
NSX uses Release Please to manage version bumps and changelog entries for the Python package. The release workflow creates the published tag only after fresh CI has passed for the exact release landing commit.
Release Flow¶
Releases are automated through the release.yml workflow. As a contributor you
take two actions — everything else runs in CI:
- Merge your change to
main. Release Please opens or updates a release PR that accumulates the version bump and changelog. The workflow then regeneratesuv.lockon that release PR branch itself (see uv.lock Sync) and dispatches CI for it. - Merge the release PR. That triggers exact-commit CI. After it succeeds, the workflow creates an immutable annotated tag, then publishes the GitHub release assets and package to PyPI.
gitGraph
commit id: "feature"
branch release-please
commit id: "version + changelog"
commit id: "uv.lock sync" type: HIGHLIGHT
checkout main
merge release-please
commit id: "exact landing-commit CI"
tag: "neuralspotx-v0.6.3" (annotated)
The workflow verifies that the new tag peels to the exact release landing
commit that passed CI. It then builds and checks the Python distributions,
smoke-tests the installed wheel, attaches the artifacts and a matching
SHA256SUMS manifest to the GitHub release, and publishes the packages to
PyPI. There is no follow-up PR: uv.lock is already in sync by the time the
release PR merges (see uv.lock Sync).
Because Release Please is configured with skip-github-release: true, the
custom publication path also owns Release Please's lifecycle bookkeeping.
After the annotated tag and GitHub release are verified against the exact
landing commit, the workflow finds that commit's single merged release PR and
changes its label from autorelease: pending to autorelease: tagged. This
step fails closed if the tag, release, merge commit, PR identity, or labels do
not match. Without that transition, Release Please treats the already-published
PR as an outstanding untagged release and refuses to create the next release
PR.
The same reconciliation also runs after a successful manual rebuild. This repairs the lifecycle state when initial tag creation succeeded but artifact or release publication failed before labels could be finalized. Historical manual rebuilds remain compatible with lightweight tags and releases that predate Release Please lifecycle labels; those have no pending state to reconcile. The finalizer requests pull-request write permission because GitHub authorizes label mutations on pull requests through the pull-request permission domain, even though the REST route uses the issues labels API.
Version Source of Truth¶
The package version in pyproject.toml is the version source of truth for the
Python package at release time.
For new releases, the release workflow validates that:
- the release tag is exactly
neuralspotx-v<version> - the tag version exactly matches
pyproject.toml - the tag is annotated and its peeled target is the CI-reviewed release landing commit
The published neuralspotx-v0.7.9 tag is the final documented lightweight-tag
exception. Existing historical tags remain available and are never moved,
deleted, or replaced.
Example:
pyproject.toml:0.2.0- release tag:
neuralspotx-v0.2.0
If those do not match, the release build fails.
Manual Rebuilds¶
release.yml also supports workflow_dispatch with an optional tag input.
Leaving tag empty intentionally runs only Release Please release-PR
generation/update; it does not publish a release. Publication occurs after the
release PR is merged and the resulting main push passes exact-commit CI.
This is intended only for rebuilding an existing tagged release, for example when:
- artifact upload failed
- the workflow logic changed and you need to regenerate release artifacts
This manual path does not create a new version, move a tag, or create a release
PR. It first runs fresh CI on the existing tag's peeled commit, then rebuilds
artifacts for an existing release tag such as neuralspotx-v0.6.3 or the
legacy form v0.6.3. PyPI uses skip-existing for this retry-only path so a
successful prior upload does not make an asset retry fail on duplicates.
The workflow explicitly overrides GitHub Actions' skipped-ancestor propagation:
Release Please is intentionally skipped for a tagged rebuild, while
release-context and exact-commit-ci use always() plus explicit result
checks so the validated rebuild chain still runs.
For a tagged rebuild, exact-commit CI dispatches directly on the existing
immutable tag. New releases still use a temporary branch because their tag does
not exist until exact-commit CI passes.
GitHub release notes are regenerated from the exact tagged checkout's
CHANGELOG.md, so a retry can recreate a missing GitHub Release without
depending on outputs from the intentionally skipped Release Please job.
Distribution builds use the tagged commit timestamp as SOURCE_DATE_EPOCH.
The PEP 517 backend toolchain is pinned in pyproject.toml so generated
metadata does not change when setuptools, wheel, or packaging releases move.
The workflow builds the wheel and normalized sdist twice and requires identical
SHA-256 manifests before publication. For a tagged retry of a version that
already exists on PyPI, rebuilt hashes are compared with PyPI's immutable file
hashes. Any differing legacy build is replaced with the hash-verified PyPI bytes
before GitHub release assets are updated, keeping both publication channels
byte-identical without moving the tag or attempting to overwrite PyPI.
If the tag exists but the version has not reached PyPI yet, a PyPI 404 is
treated as "no canonical files to reconcile" and the verified deterministic
rebuild proceeds to the normal publication jobs. Other API, network, filename,
or hash mismatches still fail closed.
PyPI Publishing¶
PyPI publishing runs in the same release.yml workflow as Release Please, the
artifact build, and the GitHub release asset upload.
This is intentional. PyPI trusted publishing must stay in the same workflow file as the release job, because PyPI does not support delegating the publish step to a reusable workflow.
The publish job uses GitHub OIDC trusted publishing against the repository's configured PyPI project. It runs when Release Please creates a root release in that workflow, or when a manual rebuild targets an existing release tag.
Before either GitHub or PyPI receives an artifact, the release workflow:
- runs fresh CI for the exact release commit
- runs
twine checkagainst both the wheel and source distribution - installs the wheel into an isolated environment
- creates an app without network bootstrap
- creates and validates a module scaffold
These checks exercise packaged templates from the installed distribution. A command working from a source checkout is not sufficient evidence that its templates or other data files were included in the wheel.
uv.lock Sync¶
Historically, uv.lock drifted out of sync with pyproject.toml for one
release cycle: Release Please bumped the version on main, but uv.lock
(which embeds the editable neuralspotx package's own version) was only
refreshed by a second, post-release PR against automation/update-uv-lock.
That meant every release briefly landed on main with a stale lockfile, and
required a human to notice and merge the follow-up PR.
The workflow now closes that gap before the release PR merges. After
Release Please creates or updates its release PR, the sync-release-lock job:
- validates that the reported PR is this repository's own open,
release-please--branches--main--components--neuralspotxbranch (never a fork or an unrelated PR) and resolves its exact head commit via the GitHub API, - checks out that exact commit and runs
uv lock— the canonical dependency-resolution command, not a hand-rolled regex edit — so the editable package version and any other lock metadata stay coherent, - commits and pushes only
uv.lockback to that same branch with thegithub-actions[bot]identity, using a plain, non-force push, if and only if the lockfile actually changed. Ifuv.lockwas already in sync (for example, on a second Release Please update to the same PR), the job is a no-op.
Because this push uses the job's GITHUB_TOKEN rather than a personal access
token, it does not itself trigger another workflow run — the same reason the
release PR's initial creation doesn't. The dispatch-release-pr-ci job runs
immediately afterward, re-confirms the branch still points at the commit the
sync job just produced (or confirmed unchanged), and only then dispatches CI
for it. This guarantees the release PR's required status check always
reflects the final, lock-synced commit, never the pre-sync Release Please
commit, and it removes the old per-release refresh-uv-lock /
dispatch-lock-refresh-ci jobs and their automation/update-uv-lock PR
entirely — there is nothing left to review or merge after the fact.
If a direct push to the release branch is ever rejected (for example, by
branch protection), the job falls back to committing the single uv.lock
file through the GitHub Contents API instead, after re-confirming the branch
has not moved since it was validated. Either way, there is still only one
release PR: Release Please's own.
The whole workflow (Release Please, the lock sync, and any manual rebuild)
also runs under a single concurrency group with queue: max, so overlapping
triggers queue up (FIFO) instead of racing each other on the same release
branch or tag. queue: max matters as much as cancel-in-progress: false
here: the default queue: single behavior cancels an already-pending run
the moment a second run queues behind it — which would have been able to
silently drop a release landing on main while exact-commit-ci was still
polling for a prior run.
Because the release PR can now carry a second commit (the lock sync, on top
of Release Please's version bump), whether a given push to main actually
lands a new release is derived from tag existence — does
neuralspotx-v<version> already exist? — rather than by diffing
pyproject.toml between HEAD and its parent commit. That check is
independent of how many commits the merged release PR contains or which
GitHub merge method (merge commit, squash, or rebase) is used to land it.
Contributor Guidance¶
- Do not create ad hoc release tags outside the Release Please flow.
- Do not move or delete a published release tag; a reused tag fails closed.
- Do not hand-edit version numbers unless you are intentionally repairing the release metadata.
- Do not hand-edit
uv.lock's editableneuralspotxversion line either; thesync-release-lockjob keeps it current on the release PR branch by runninguv lock. - If a tagged release needs to be retried, use the manual rebuild path for the existing tag.
- Keep release notes and changelog generation owned by Release Please.
- Treat distribution smoke-test failures as packaging regressions; do not bypass the check or publish the affected artifact manually.