opik-mcp

release

Purpose

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.

What it does now

The version number

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.

What a pull request or a merge runs

What a release run does

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.

Replaying a failed release

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.

The image and the chart

Legacy TypeScript package

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.

Installing a branch

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.

How it works

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.

Decisions

No ADR covers release; the reasons come from workflow comments and PRs.

Traps

Proven by

Log