How a commit on main becomes a PyPI version, an image and a Helm chart. It
answers what a merge builds, what a release publishes and in what order, where
the version comes from, and how to finish a release that failed halfway.
version.txt holds the next unreleased version, x.y.z. bump-version in
release.yaml increments its patch after a release; edit it by hand to choose
another.
There are two version stamps, and both read $VERSION. make version
generates the git-ignored _version.py, which the running server reports
(analytics/identity.py). Hatch computes the distribution’s version
separately, from get_version() in scripts/_build_version.py
([tool.hatch.version]), so that uv build and uv lock work on a checkout
where make version never ran. Each falls back to <version.txt>.dev0 when
$VERSION is unset, so a release has to put it in the environment for both —
env: on the step, not a per-command assignment
(test_the_release_build_gives_uv_build_the_version).
| Build | __version__ and the dist |
Set by |
|---|---|---|
| Local, PR, branch, manual CI run | x.y.z.dev0 |
make version default; pyver in ci.yaml |
Image from a push to main |
x.y.z |
pyver in ci.yaml |
| PyPI wheel from a release | x.y.z |
VERSION on the pypi job’s build step |
The pypi job checks each built file carries the released version before
publishing, because a stamp that silently fell back cannot be unpublished.
A main image is stamped plain x.y.z because a release promotes that digest
unchanged, so its stamp is what production reports.
build-image pushes nothing on a pull request and logs in to no registry,
so fork PRs work. On any other event it pushes sha-<12-char commit>, and
on main also :main.main are not cancelled in progress, since each builds an image a
release may promote. A queued run can still be dropped; see Traps. PR runs
cancel older ones.addopts in pyproject.toml keeps the hermetic marker out of make check.release.yaml runs only by hand. validate pins one commit for every later
job: the tagged commit if the tag <version> (no v) exists, else main’s
HEAD. create-git-tag fails on an existing tag unless
reuse_existing_tag is true, and on a missing tag when it is.
promote-image waits about ten minutes (the deadline in its step) for the
sha-<commit> image and retags it :<version> and :latest; if the image
never appears it fails with a message naming the missing tag. It never checks
the tests, so check CI on that commit first. publish-chart sets --version
and --app-version to the release version. pypi uses Trusted Publishing; the only secret read is
GITHUB_TOKEN. Release permissions, pypi approvals and the hosted rollout
live outside this repo.
If a job after create-git-tag fails, re-run with reuse_existing_tag: true.
Never delete the tag. validate then releases the tagged commit even if main
has moved. Every publish step can run twice (PyPI via skip-existing), and
bump-version runs only after all of them succeed.
python -m opik_mcp under tini, so main() runs in
hosted mode as in stdio (runtime).1000 so Kubernetes runAsNonRoot can check it.make docker-run binds loopback: on Linux -p 8080:8080 binds everywhere.Chart.yaml version 0.0.0 is a lint placeholder, replaced at release.The TypeScript server lives at the tag legacy-typescript-final (#203). To
publish, tag a branch off it npm-v<version> and dispatch
legacy-ts-deploy.yml from that tag. Delete the workflow after 2026-11-15.
make install-branch registers this worktree with Claude Code at local scope
as opik-<ticket>. URL and key come from one source (resolve_credentials):
the environment when OPIK_URL is set, else ~/.opik.config; the workspace
is the exception (Traps). An environment key is registered as ${OPIK_API_KEY};
a config-file key is stored, with a note. Telemetry is off in that server.
merge to main
ci.yaml: version -> build-image => opik-mcp:sha-<commit>, :main
python-checks, hermetic, helm-lint, skills-pack (in parallel)
manual dispatch of release.yaml
validate -> create-git-tag -> promote-image => :<version>, :latest
-> publish-chart => charts/opik-mcp:<version>
-> pypi => opik-mcp <version>
all three succeed -> github-release -> bump-version (commit to main)
To change stamps or image tags, start in ci.yaml; release steps, in
release.yaml.
No ADR covers release; the reasons come from workflow comments and PRs.
version.txt is the same between releases (#177).main images are stamped x.y.z: .dev0 in production fell outside
analytics cohorts that filter dev builds (#179).ci.yaml: a separate workflow needs a paths: filter,
which can leave a required check pending (#175).evals/ stay out, since the
editable install cannot show what a wheel holds (#176).--locked installs in CI (OPIK-8486); only Dockerfile has it.cancel-in-progress: false
(docs).
A merge between two others can end with no sha- image, and a release
pinned to it fails in promote-image.main stamps .dev0 (pyver is plain only for push),
queues behind the push run, and re-pushes sha-<commit> and :main. A
later release promotes that .dev0 image. Do not dispatch CI by hand on
main. If it happened, re-run the push-triggered run for that commit (a
re-run keeps event_name as push, so the stamp is plain) or push a new
commit, then release. Re-running an older push run also moves :main back.build-image needs only version, so a commit with red tests still gets a
promotable image.templates/deployment.yaml uses only .Values.image.tag (default main)
and ignores appVersion, so a published chart runs :main unless the tag
is overridden.OPIK_URL, an environment OPIK_WORKSPACE overrides the config
file’s (test_config_file_supplies_what_the_environment_does_not).tests/hermetic/test_wheel_contents.py skips silently when uv is not on PATH.tests/hermetic/test_wheel_contents.py.tests/repo/test_install_branch.py.helm-lint and build-image in ci.yaml..github/workflows/; a broken release step shows up
only in a release.$VERSION to make version only, so hatch fell back and published 0.2.37.dev0 as a release. Step-level env:, a check on the built filenames, and a guard test (#239).make install-branch runs a worktree as a local MCP server, to try a branch in a real host (#202).evals/ excluded from the wheel, with a test that builds one, so eval fixtures do not ship (#176).main images stamped x.y.z instead of .dev0, since the release promotes them unchanged (#179).reuse_existing_tag, PyPI skip-existing (#178).version.txt; CI stops tagging every merge (#177).