Releases
Public distribution via PyPI (ADR 0021)
Kendra Orchestrator SDK is public: the repository is public and releases publish to
PyPI via Trusted Publishing (OIDC — no stored
API token). pip install korchestrator[dspy] works for anyone, no GitHub credential needed.
See Installation. This supersedes the private-distribution pass in
ADR 0020;
the decision to go public and enable PyPI is recorded in
ADR 0021.
How a release is decided
- Every change intended for the release is merged into
dev, green on the full CI matrix, then promoted tostagingand verified there. Releases are cut fromstaging, never fromdev. - The version bump follows Versioning's SemVer rules, applied to the diff since the last release. If the compatibility surface changed, an ADR justifying it already exists.
- A release PR (
chore/release-vX.Y.Z) fromstagingintomaincontains exactly two kinds of change: the version bump insrc/korchestrator/version.py, and the CHANGELOG edit moving[Unreleased]into a dated section. Nothing else.scripts/cut_release.py prepareautomates both edits and opens this PR. - Every entry under
Changed/Deprecated/Removedin that CHANGELOG section has a matching Migration section with a before/after example. - Merge into
main, then tag: an annotated (or signed, with--sign)vX.Y.Ztag on the merge commit, matchingversion.pyexactly (release.ymlfails the build if it doesn't).scripts/cut_release.py tagautomates the tag creation and push. - After the tag push and the release workflow completes, merge
mainback intostaginganddevso all three branches converge again — the promotion invariant in.claude/rules/branching-and-promotion.mdexpects this "at rest" state between releases.
This merge-back must be a fast-forward, not a merge commit. A normal PR merge (base=staging,
head=main) creates a new commit on staging, which makes main an ancestor of staging —
backwards from the invariant (dev ancestor-of staging ancestor-of main). Right after a
release, main is already a fast-forward descendant of staging (the release PR's only parent
was staging), so push it directly instead of merging a PR:
git push origin main:staging
git push origin main:dev
Verify before moving on:
git fetch origin
git merge-base --is-ancestor origin/dev origin/staging \
&& git merge-base --is-ancestor origin/staging origin/main \
&& echo OK
One-time setup: PyPI Trusted Publisher
Before the first publish, and only then, the PyPI account that owns the korchestrator
project must register this repository as a trusted publisher. This is an identity/ownership action
on the PyPI account itself — no CI job, agent, or token can do it; it takes about five minutes:
- Sign in at pypi.org with the account that will own the
korchestratorproject (create the account first if it doesn't exist yet — no need to reserve the name manually, a pending publisher does that automatically on first publish). - Go to Account settings → Publishing (directly: pypi.org/manage/account/publishing/).
- Add a pending publisher with exactly:
- PyPI project name:
korchestrator - Owner:
Kendralabs - Repository name:
korch-sdk - Workflow name:
release.yml - Environment name:
pypi - In the GitHub repository, create a Environment named
pypi(Settings → Environments) if it doesn't already exist. Optionally add required reviewers for an auditable approval gate before every publish — recommended given a PyPI publish is irreversible (see below). - The first successful run of the
publishjob inrelease.yml(from the exact repo, workflow file, and environment name above) creates thekorchestratorproject on PyPI automatically and converts the pending publisher into an active one. No further setup is needed for subsequent releases.
Nothing else in this repository, CI, or an agent's local environment can perform this step — it requires the account owner's own PyPI login (and, if enabled, 2FA).
What the release workflow does today
On a vX.Y.Z tag push, or a manual workflow_dispatch run against an existing tag
(.github/workflows/release.yml):
build— validates the tag matchessrc/korchestrator/version.py, builds the wheel and sdist (python -m build), installs the built wheel (not the source tree) into a fresh virtualenv outside the repo and confirmskorchestrator.__version__matches the tag, confirms the sdist itself builds a wheel, generates an SBOM (cyclonedx-bom, CycloneDX JSON), generatesSHA256SUMSover the wheel/sdist/SBOM, and attests build provenance (actions/attest-build-provenance).publish— uploads the wheel and sdist to PyPI via Trusted Publishing (OIDC;pypa/gh-action-pypi-publish) — no long-lived API token is stored anywhere. Gated on thepypiGitHub Environment.github-release— publishes a GitHub Release for the tag with the wheel, sdist, SBOM, andSHA256SUMSattached, and notes extracted directly from the taggedCHANGELOG.mdsection.verify-published— installs the just-published version from the real public PyPI index (pip install korchestrator==X.Y.Z, no git ref, no credential), retrying briefly for index-propagation lag, and confirmskorchestrator.__version__matches. This is the check that provespip install korchestratoractually works for a stranger with no relationship to this repository.deploy-docs— redeploys documentation (docs.yml) so published docs stay in sync with the release. The koe-proxy VPS deploy documented inDOCS_DEPLOYMENT.mdremains the primary published documentation URL; GitHub Pages (now free, since the repo is public) is a secondary target the same workflow already produces.
Releases are immutable
Once published, a version is never overwritten, re-tagged, or deleted:
- A defective release is fixed forward with a new patch version —
0.2.1supersedes0.2.0;0.2.0stays on the release list. - Yank a release on PyPI (
pip installstill resolves an exact pin, but the version is hidden frompip install korchestratorwith no version pinned) or mark the GitHub Release as a pre-release/draft — only if it's actively harmful (a security defect, data loss, a wholly broken artifact). This is not deletion; existing pins keep working. - Tags never move. Consumers and anyone who downloaded a release asset assume a tag is permanent.
- PyPI additionally refuses to let the same version number ever be re-uploaded, even after a yank — this is a platform rule, not just a project convention.
Next
- Installation — how to
pip installa release. - Versioning — how the bump itself is decided.
- Migration — what changes between versions and how it's communicated.