Releases
Publishable Python libraries live under packages/. Each is uploaded as a
wheel + sdist to the private Nexus index
https://nexus.sulzer-us.com/repository/pypi-private/:
| Package | Distribution name | Python | Notes |
|---|---|---|---|
packages/candidate_selection | candidate-selection | 3.13 | OEM-agnostic candidate scoring |
packages/ai_core | ai_core | 3.13 | Shared platform library; also consumed by OEM projects as an editable path dependency for in-repo builds |
Any package under packages/ with its own pyproject.toml and [project].version
is a release candidate. OEM projects under projects/ ship as Docker images, not
PyPI artifacts (see Dagster Execution).
Versions are independent per package (separate-pull-requests: true) — one
release PR each, so ai_core and candidate-selection move on their own
numbers; they are not locked together.
Versioning is automated from Conventional Commits
We do not hand-edit version numbers. release-please
runs on every push to main, reads the Conventional Commit
history scoped to each package's path, and maintains a standing "release
PR" that bumps the version and changelog. Merging that PR tags the release and
triggers the Nexus upload.
Bump rules (package is pre-1.0, so breaking changes bump the minor):
| Commit | Example | Bump |
|---|---|---|
fix: | fix(candidate-selection): clamp negative CDF | patch |
feat: | feat(candidate-selection): add dealer fallback | minor |
feat!: / BREAKING CHANGE: | breaking API change | minor (pre-1.0) |
chore: / docs: / refactor: | — | no release |
Path scoping is the reason release-please was chosen over commitizen: in this
monorepo only commits that touch files under a package's directory count
toward its version. A feat(mercedes): ... commit never bumps
candidate-selection, and a fix(ai_core): ... commit bumps only ai_core.
Flow
Each package has its own publish job (publish-candidate-selection,
publish-ai-core) in the same workflow run as release-please, gated on that
package's --release_created output. Each job calls the reusable
publish-package.yml workflow, which runs pants publish <pkg>:dist — Pants
builds the sdist + wheel via the package's [build-system] (hatchling, PEP 517,
so the version comes from pyproject.toml) and twine-uploads to Nexus. Keeping
publish in the same run sidesteps the GitHub gotcha that workflows triggered by
the default GITHUB_TOKEN do not themselves trigger further workflows.
Files that drive it
| File | Role |
|---|---|
release-please-config.json | Declares each publishable package, its release-type (python), tag component, and bump policy |
.release-please-manifest.json | Source of truth for the last released version per package; release-please reads and writes this |
.github/workflows/release-please.yml | Runs release-please on push to main, then the gated per-package publish jobs |
.github/workflows/publish-package.yml | Reusable workflow_call that runs pants publish <target> (build + Nexus upload); called by release-please |
packages/<pkg>/BUILD | python_distribution(name="dist", …) — the publishable target Pants builds and uploads |
packages/<pkg>/pyproject.toml | [project].version — written by the release PR, never edited by hand |
packages/<pkg>/CHANGELOG.md | Generated and appended by release-please |
release-please-config.json:
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"separate-pull-requests": true,
"bump-minor-pre-major": true,
"packages": {
"packages/candidate_selection": {
"release-type": "python",
"package-name": "candidate-selection",
"component": "candidate_selection",
"changelog-path": "CHANGELOG.md"
},
"packages/ai_core": {
"release-type": "python",
"package-name": "ai_core",
"component": "ai_core",
"changelog-path": "CHANGELOG.md"
}
}
}
Each package also carries an extra-files entry pinning $.project.version in
its pyproject.toml so the version string is rewritten deterministically.
.release-please-manifest.json:
{
"packages/candidate_selection": "0.2.6",
"packages/ai_core": "0.1.0"
}
Tags and the version anchor
Tags are namespaced per package: candidate_selection-v0.3.0, ai_core-v0.2.0.
release-please finds the last release by matching this tag (and the manifest
entry), then only considers commits after it. Each package's current version is
anchored by a GitHub release on a main commit, so the first release PR
proposes the correct next bump rather than rescanning all history:
candidate_selection-v0.2.6— anchored at the commit that cut 0.2.6 (already on Nexus from the previous manual workflow).ai_core-v0.1.0— anchored at themaintip when automation was introduced.ai_core0.1.0 was never published to Nexus, so the first automated upload is the next version bump (e.g. 0.1.1); 0.1.0 itself is treated as the baseline, not republished. To seed 0.1.0 into Nexus, runpants publish packages/ai_core:distlocally once with the NexusTWINE_*credentials set.
Adding another publishable package
- Give it a
pyproject.tomlwith[project].versionunderpackages/<pkg>/. - Add a block under
packagesinrelease-please-config.json(release-type: python, a uniquecomponent). - Add
"packages/<pkg>": "<current version>"to.release-please-manifest.jsonand tag its anchor commit<pkg>-v<version>. - Add the package to the Pants graph (a resolve in
pants.toml+ BUILD files) including apython_distribution(name="dist", …, generate_setup=False, repositories=[<nexus url>])target. - Add a
publishjob torelease-please.ymlgated on that package's--release_createdoutput that callspublish-package.ymlwithtarget: packages/<pkg>:dist.
separate-pull-requests: true keeps one release PR per package, so versions
move independently.
Manual / emergency publish
There is no manual publish workflow — releasing goes through the automated
release-please path. For an out-of-band republish or to seed a version, run
pants publish packages/<pkg>:dist locally with the Nexus TWINE_USERNAME /
TWINE_PASSWORD env set; it publishes the version currently in the package's
pyproject.toml.
Secrets
| Secret | Used by |
|---|---|
NEXUS_USERNAME, NEXUS_PASSWORD | twine upload to the private index (already configured) |
RELEASE_PLEASE_TOKEN | GitHub App / PAT so the release PR triggers normal CI; the default GITHUB_TOKEN opens the PR but does not trigger its test runs |
The Nexus index itself is declared in pants.toml under [python-repos] and
mirrored in each OEM's uv config — the same host that serves cosy-encryption
to the Mercedes and Stellantis resolves.