Skip to main content

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/:

PackageDistribution namePythonNotes
packages/candidate_selectioncandidate-selection3.13OEM-agnostic candidate scoring
packages/ai_coreai_core3.13Shared 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):

CommitExampleBump
fix:fix(candidate-selection): clamp negative CDFpatch
feat:feat(candidate-selection): add dealer fallbackminor
feat!: / BREAKING CHANGE:breaking API changeminor (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

FileRole
release-please-config.jsonDeclares each publishable package, its release-type (python), tag component, and bump policy
.release-please-manifest.jsonSource of truth for the last released version per package; release-please reads and writes this
.github/workflows/release-please.ymlRuns release-please on push to main, then the gated per-package publish jobs
.github/workflows/publish-package.ymlReusable workflow_call that runs pants publish <target> (build + Nexus upload); called by release-please
packages/<pkg>/BUILDpython_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.mdGenerated 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 the main tip when automation was introduced. ai_core 0.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, run pants publish packages/ai_core:dist locally once with the Nexus TWINE_* credentials set.

Adding another publishable package

  1. Give it a pyproject.toml with [project].version under packages/<pkg>/.
  2. Add a block under packages in release-please-config.json (release-type: python, a unique component).
  3. Add "packages/<pkg>": "<current version>" to .release-please-manifest.json and tag its anchor commit <pkg>-v<version>.
  4. Add the package to the Pants graph (a resolve in pants.toml + BUILD files) including a python_distribution(name="dist", …, generate_setup=False, repositories=[<nexus url>]) target.
  5. Add a publish job to release-please.yml gated on that package's --release_created output that calls publish-package.yml with target: 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

SecretUsed by
NEXUS_USERNAME, NEXUS_PASSWORDtwine upload to the private index (already configured)
RELEASE_PLEASE_TOKENGitHub 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.