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.

mcp-registry is the exception: bump-version does not wait for it, so when it fails version.txt may already name the next version, and a new dispatch would release that one. Re-run the failed mcp-registry job inside the same run instead.

The MCP Registry entry

server.json is the entry for io.github.comet-ml/opik-mcp in the official MCP Registry, which galleries and registry-aware clients install from. It lists the PyPI package run with uvx, the hosted server as a remote, and the env vars the server reads. Its version is the placeholder 0.0.0, as in Chart.yaml.

mcp-registry in release.yaml stamps the release version into it and publishes it after pypi: the registry accepts a PyPI package only when that version’s PyPI description has the README’s mcp-name line. It logs in with GitHub OIDC, which proves the io.github.comet-ml namespace, and retries for a few minutes while PyPI’s JSON catches up, logging in again on each attempt. A version already in the registry as this PyPI package, deleted ones included, is left alone, so a replay is a no-op; the same number held by another package, such as an old npm entry, fails the job. github-release does not wait for it. CI’s server-json job validates the file on every pull request with the same pinned mcp-publisher.

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. It publishes to npm only; the registry entry comes from the Python release (Traps). 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, server-json, 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>
                                  -> mcp-registry => io.github.comet-ml/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