Release Process¶
This repo publishes two independently-versioned Python packages —
stocktrim-openapi-client (the generated API client, at the repo root) and
stocktrim-mcp-server (the MCP server, in stocktrim_mcp_server/) — using
release-please in manifest mode.
release-please owns versioning, changelogs, and tagging; it does not publish packages
itself.
The three workflows¶
(Not linked below: workflow files and the release-please config/manifest sit outside the
docs tree, and mkdocs-built links to them would 404 on the deployed site. Browse them
directly in the repo: .github/workflows/, release-please-config.json,
.release-please-manifest.json.)
release-please.yml— the only workflow that watches pushes tomain. Readsrelease-please-config.jsonand.release-please-manifest.jsonand either opens/updates one aggregated release PR covering both packages (separate-pull-requests: false), or — if that PR was just merged — creates aclient-v*/mcp-v*tag and a draft GitHub Release per changed package at the merge commit. It never pushes tomainitself.release-pr-prepare.yml— runs only on release-please's own PR branch (release-please--branches--main, matched by prefix). Resyncsuv.lockand keepsstocktrim_mcp_server/pyproject.toml'sstocktrim-openapi-client>=Xfloor (and the MCP server's own__version__, belt-and-braces alongsideextra-files) equal to the client version the PR proposes — see Inter-package pinning below. Commits land on the release PR branch, never onmain.publish.yml— the only workflow that builds and ships. Triggered exclusively byclient-v*/mcp-v*tag pushes (i.e. only after a release PR merges). Builds the package, publishes it to PyPI via OIDC Trusted Publishing, attaches the build artifacts (and, for the MCP server, the.mcpbbundle) to the still-draft release, then flips the release out of draft.
graph TD
A[Push to main] --> B[release-please.yml]
B -->|Release-worthy commits| C[Open/update aggregated release PR]
C --> D[release-pr-prepare.yml: sync uv.lock + MCP pin]
D -->|PR merged| E[release-please.yml: create tags + draft Releases]
E --> F[client-v* tag]
E --> G[mcp-v* tag]
F --> H[publish.yml: publish-client]
G --> I[publish.yml: publish-mcp]
Draft -> upload -> publish asset flow¶
release-please creates GitHub Releases as drafts ("draft": true in
release-please-config.json). Draft releases accept asset uploads; once published a
release becomes
immutable
and permanently rejects further uploads — this is exactly the failure mode issue #237
hit when python-semantic-release published the mcp-v0.16.0 release before the
.mcpb bundle existed. publish.yml's jobs always: build the artifact(s), publish to
PyPI, gh release upload <tag> <files> --clobber onto the still-draft release, then
gh release edit <tag> --draft=false. A release is never finalized before its assets
exist.
Unlike the old release.yml (which had build-mcpb create the mcp-v* release itself
via gh release create <tag> <file> --title ... --notes-file ..., because PSR had
already bumped the version but explicitly skipped vcs_release), release-please now
creates the release — with its notes — directly. publish-mcp only needs to attach
assets to the release release-please already made; there's no changelog-extraction step
anymore.
Inter-package pinning (issue #238)¶
stocktrim_mcp_server/pyproject.toml depends on stocktrim-openapi-client>=X.Y.Z — a
floor, not an exact pin, and not a capped range (>=X.Y.Z,<X+1). release-please has
no built-in concept of inter-package version pinning; release-pr-prepare.yml is the
glue that keeps it truthful, rewriting the floor to match the client version on every
release PR.
Floor-only was chosen over the alternatives for two reasons:
- A capped range (
>=X.Y.Z,<X+1) adds automation surface for marginal benefit here.release-pr-prepare.ymlalready re-derives the floor from scratch every release cycle, so the floor is never more than one cycle stale regardless of whether it's capped. A cap would need its own upkeep (bumping the ceiling alongside the floor) without meaningfully changing the failure mode it guards against, since a genuine breaking client change would need an MCP-side fix and release regardless of what the dependency metadata says. - An exact pin (
==X.Y.Z, what the oldUpdate MCP client dependencystep inrelease.ymlwrote when it ran) forces an MCP release on every client release, even when nothing MCP-relevant changed. Under release-please's path-based partitioning (see below) a client-only commit no longer touchesstocktrim_mcp_server/, so nothing would trigger that companion MCP release automatically — an exact pin would just make the previously-published MCP package's metadata stale immediately, which is worse than a floor being one release behind.
This matches the floor-only convention adopted in the sibling repos
(statuspro-openapi-client, frontapp-openapi-client) for the same class of bug (see
statuspro#63, frontapp#165).
Before this migration, the committed dependency had no constraint at all (issue
238) — the old workflow's pin-rewrite step only ran, and only landed on main, when¶
the client itself released in the same run; otherwise whatever was committed (no constraint) is what shipped. A correct committed default plus automation that fixes it every cycle, rather than conditionally, closes that gap.
Bump-semantics change (behavioral)¶
python-semantic-release partitioned commits by scope — (client)/(mcp) in the
commit message decided which package bumped. release-please partitions by path —
which files a commit touches decides which package(s) bump. A commit that touches both
stocktrim_mcp_server/ and the client root now bumps both packages, even without an
(mcp) scope; conversely a (client)-scoped commit that happens to touch nothing under
the client's tracked paths won't bump the client. Commit scopes remain useful for
changelog grouping/readability but are no longer load-bearing for version decisions.
Manual prerequisites¶
None — unlike a from-scratch migration, stocktrim-openapi-client and
stocktrim-mcp-server already have working PyPI Trusted Publishers from before this
migration, and the dougborg-release-please GitHub App (ID 4392719) was already
configured on this repo. No new secrets, variables, environments, or Trusted Publisher
registrations are required.
Troubleshooting¶
No release PR appears after merging to main: confirm the merged commit's
conventional-commit type is release-worthy (feat, fix, perf) and touches a
configured package path (. or stocktrim_mcp_server/). docs:/chore:/ci: commits
are recorded in the (hidden) changelog sections but don't bump a version on their own.
uv.lock or the MCP pin looks stale on the release PR: check the
release-pr-prepare.yml run for that PR — it only fires on release-please--*
branches. If it didn't run or failed, re-push to the PR branch to retrigger it
(synchronize event) or push a fix commit directly.
Publish fails at the PyPI step: verify the Trusted Publisher config on PyPI still
lists publish.yml and the correct job name (publish-client/publish-mcp) for the
tag that was pushed.
Release stuck in draft: publish.yml's asset-upload or PyPI-publish step failed
before reaching gh release edit --draft=false. Check the run logs for that tag; a
draft release can be safely re-run once the underlying failure is fixed, since draft
releases are still mutable.