Skip to content

feat(release): migrate to release-please manifest-mode release automation - #168

Merged
dougborg merged 6 commits into
mainfrom
feat/release-please-manifest
Jul 26, 2026
Merged

feat(release): migrate to release-please manifest-mode release automation#168
dougborg merged 6 commits into
mainfrom
feat/release-please-manifest

Conversation

@dougborg

Copy link
Copy Markdown
Owner

Summary

This is the pilot for a four-repo migration off python-semantic-release / JS
semantic-release and onto release-please in manifest mode. The design is filed
as dougborg/katana-openapi-client#1002 (§4 recommendation, §5.1–5.6 target topology,
§6 migration path). This repo was chosen as the pilot because its release pipeline
was already disabled and has never successfully run (#10) — there is nothing working
to break.

This PR does not merge or publish anything. No secrets, variables, rulesets, or
repo settings are touched. It opens a branch for review.

Before → after topology

Before (partially disabled): release.yml (push-triggered, currently
workflow_dispatch-only pending #10) ran release-client and release-mcp as two
independent python-semantic-release jobs that each pushed a version-bump commit
straight to main, release-mcp.yml was a second, overlapping tag-triggered MCP
publish surface, release-ts.yml ran JS semantic-release for the TS client, and
update-mcp-dependency.yml chased client releases with a follow-up commit + push to
main. Four workflows, three separate writers to main, all guarded off because the
required secrets were never configured.

After:

  • release-please.yml — the only workflow that watches pushes to main. Runs
    googleapis/release-please-action@v4 in manifest mode
    (release-please-config.json / .release-please-manifest.json, both at the repo
    root). Opens or updates one aggregated release PR covering all three packages
    (separate-pull-requests: false); once that PR merges, creates client-v*,
    mcp-v*, ts-v* tags and draft GitHub Releases atomically at the merge commit.
    Never pushes to main itself.
  • release-pr-prepare.yml — runs only on release-please's own PR branch
    (pull_request filtered to release-please--* heads). Resyncs uv.lock and keeps
    frontapp_mcp_server/pyproject.toml's frontapp-openapi-client>=X floor equal to
    the client version the PR proposes, pushing a commit to the PR branch — this is
    where the old sync-lockfile-equivalent work and update-mcp-dependency.yml's job
    go, but landing atomically with the version bump instead of as a follow-up push to
    main.
  • publish.yml — the only workflow that builds and ships. Triggered exclusively
    by client-v* / mcp-v* / ts-v* tag pushes, never by a main push. Four jobs:
    publish-client (PyPI), publish-mcp (PyPI) → publish-mcp-docker (GHCR,
    multi-arch), publish-ts (npm). Each builds its artifact, publishes to the
    registry via OIDC, then attaches the artifact to the still-draft release, then
    flips it to published.

What was deleted and why

Removed Why
.github/workflows/release.yml Replaced by release-please.yml + publish.yml; its two jobs each pushed a version-bump commit directly to main
.github/workflows/release-ts.yml JS semantic-release (version+tag only, npmPublish: false) folded into release-please + publish.yml's publish-ts
.github/workflows/release-mcp.yml Duplicated release.yml's MCP release surface (both could publish MCP); folded into publish.yml's publish-mcp/publish-mcp-docker
.github/workflows/update-mcp-dependency.yml Its job (bump MCP's client pin + uv.lock, push to main, re-trigger release.yml) is now release-pr-prepare.yml, running on the release PR branch instead of main
packages/frontapp-client/.releaserc.json + its @semantic-release/*/semantic-release devDependencies No longer needed; pnpm-lock.yaml regenerated (-2199 lines)
[tool.semantic_release] in pyproject.toml (root) and frontapp_mcp_server/pyproject.toml Versioning/changelog/tagging now owned by release-please config
python-semantic-release dev dependency, frontapp-version/frontapp-release script entry points (semantic_release.cli:main) Dead once PSR is gone
docs/MONOREPO_SEMANTIC_RELEASE.md Folded into rewritten docs/RELEASE.md; nav/toctree updated in mkdocs.yml and docs/index.md

Grepped the repo for stray references after removal (justfile/poe tasks, docs,
tests) — see "Validation" below.

Fixes #165 — the MCP→client pin floor

frontapp_mcp_server/pyproject.toml pinned frontapp-openapi-client>=0.51.0, a
version that has never been published (nothing here has ever published to
PyPI/npm at all). This is the same defect statuspro shipped and fixed in
dougborg/statuspro-openapi-client#63 — a constraint copy-pasted from a sibling repo
that automation never validated.

Chosen floor: >=0.1.0, seeded from .release-please-manifest.json /
pyproject.toml's currently-committed client version. Reasoning: release-please only
ever bumps a package's version forward from its seeded manifest value, so the
client's actual first published version can never be lower than 0.1.0 — the floor
can never overshoot. release-pr-prepare.yml then keeps this floor truthful on every
future release by rewriting it to match whatever version the client actually gets
bumped to on each release PR, so this doesn't regress into another stale pin.

Manual prerequisites before a real release (required, will otherwise fail)

No PyPI Trusted Publisher exists for frontapp-openapi-client or
frontapp-mcp-server, and no npm Trusted Publisher exists for frontapp-client.

publish.yml uses OIDC only (pypa/gh-action-pypi-publish, pnpm publish --provenance) — no tokens are introduced as a fallback. Until these are registered,
the first real release's publish-client/publish-mcp/publish-ts jobs will
fail
their registry-publish step with Non-user identities cannot create new projects (PyPI) or the npm equivalent. This is expected, not a bug in this PR.
Before merging the first release-please release PR, register:

  • PyPI Trusted Publisher — frontapp-openapi-client, workflow publish.yml, job
    publish-client
  • PyPI Trusted Publisher — frontapp-mcp-server, workflow publish.yml, job
    publish-mcp
  • npm Trusted Publisher — frontapp-client, workflow publish.yml, job publish-ts

publish.yml scopes each job to a GitHub Environment (pypi-client, pypi-mcp,
npm-ts) — the standard way to scope Trusted Publisher OIDC trust. These
environments don't need to exist for the workflow YAML to be valid; nothing in this
PR creates or touches them (no repo-settings changes were made).

The dougborg-release-please GitHub App (ID 4392719,
vars.RELEASE_PLEASE_APP_ID / secrets.RELEASE_PLEASE_APP_PRIVATE_KEY) was already
configured on this repo before this PR and is used as-is by
release-please.yml/release-pr-prepare.yml — nothing new was added or rotated.

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 the failure mode described in
katana-openapi-client#1002 §1.4 (an MCP .mcpb bundle permanently lost because the
release was already published when the upload was attempted). publish.yml's jobs
always: (1) build the artifact, (2) publish it to the registry, (3) gh release upload <tag> <artifact> --clobber onto the still-draft release, (4) gh release edit <tag> --draft=false. A release is never finalized before its assets
exist.

Bump-semantics change (behavioral)

python-semantic-release partitioned commits by scope(client)/(mcp)/(ts)
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 frontapp_mcp_server/ and the repo 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. Documented in docs/RELEASE.md and frontapp_mcp_server/docs/deployment.md.

Deviations from the proposal

  • No .mcpb bundle handling. §5.4's draft-release pseudocode includes an MCPB
    build step for the MCP job. I checked whether .mcpb building exists on main
    here (issue feat(mcp): package server as MCPB (.mcpb) for one-click Claude Desktop install #102 references it) — it does not; feat(mcp): package server as MCPB (.mcpb) for one-click Claude Desktop install #102 is an open, unmerged PR
    (feat/mcp-mcpb-bundle) still mixed with unrelated changes, not something on
    main to preserve. Adding net-new MCPB packaging isn't part of "rework the release
    flow" and is out of scope for this PR; publish-mcp has no MCPB step. When feat(mcp): package server as MCPB (.mcpb) for one-click Claude Desktop install #102
    lands, its build step slots into publish-mcp before the "attach assets" step,
    following the same draft→upload→publish pattern already established here.
  • No extra-files/jsonpath for the MCP→client pin. §5.3's pseudocode does the pin
    bump via both extra-files and a scripted fallback ("belt-and-braces… in case
    extra-files under-delivers", citing release-please-action#879's known
    unreliability for exactly this case — a TOML dependency-array edit). I skipped the
    extra-files half entirely and kept only the script (release-pr-prepare.yml's
    "Sync MCP -> client dependency floor" step, a tomllib read + sed rewrite). Given
    the documented unreliability, maintaining both a fragile jsonpath config and a
    script that has to reconcile with it seemed like more surface area for less
    reliability than the script alone. Worth revisiting if a cleaner extra-files
    pattern is found in a later repo of this migration.
  • This repo also has GHCR/Docker for the MCP server (publish-mcp-docker,
    multi-arch), which §2's repo-inventory table doesn't list for frontapp (only for
    katana/statuspro). It's real — release.yml's publish-mcp-docker job already
    builds and pushes to ghcr.io/dougborg/frontapp-mcp-server — so I carried it
    forward into publish.yml unchanged in behavior, just re-triggered off the
    mcp-v* tag instead of release.yml's completion.
  • Extra JSON-formatting/lint fallout: removing python-semantic-release and the
    JS semantic-release plugin stack required regenerating pnpm-lock.yaml (a large
    mechanical diff, -2199 lines) and uv.lock, and running prettier --write on the
    markdown docs I touched. Not called out in the proposal but necessary for uv sync
    / pnpm install --frozen-lockfile / poe format-check to stay green.
  • uv build workspace-output gotcha (not in the proposal, found during
    validation): uv build for frontapp_mcp_server writes to the workspace root's
    dist/, not frontapp_mcp_server/dist/, because it's a uv workspace member — this
    matches how the old release.yml already relied on it (its upload-artifact step
    used a bare dist/ path after cd frontapp_mcp_server && uv build). publish.yml's
    publish-mcp job uses uv build --package frontapp-mcp-server from the repo root
    and reads assets from the repo-root dist/, verified locally.

Validation

  • actionlint: clean on all workflows in .github/workflows/, before and after
    this change (pre-existing shellcheck findings were confined to the workflow files
    this PR deletes; zero findings on the three new/changed workflows).
  • release-please-config.json / .release-please-manifest.json: valid JSON;
    validated against the release-please JSON schemas
    (https://raw.githubusercontent.com/googleapis/release-please/main/schemas/{config,manifest}.json,
    the $schema referenced in the config) via jsonschema.validate — both pass.
    Package paths (., frontapp_mcp_server, packages/frontapp-client) confirmed
    against the real directory layout.
  • uv run pytest -n 4 -m 'not docs and not schema_validation and not integration':
    610 passed, 1 skipped — unchanged from the baseline (this PR touches release
    config/docs/tooling, not product code).
  • uv run poe lint / uv run poe format-check: clean (ruff, ty, yamllint,
    prettier).
  • Removed-config grep sweep: confirmed no remaining references to
    python-semantic-release, semantic_release, SEMANTIC_RELEASE_TOKEN,
    .releaserc, or MONOREPO_SEMANTIC_RELEASE.md outside this PR's own
    explanatory prose (poe tasks, justfile-equivalent [tool.poe.tasks], docs,
    tests, mkdocs.yml nav all checked).
  • uv build / uv build --package frontapp-mcp-server / pnpm run build +
    pnpm pack
    : all run locally to confirm publish.yml's build steps actually
    produce the artifacts/paths the workflow references.

What to verify on the first real run

  1. First release PR's diff — with no prior tags, release-please computes bump
    decisions from full commit history for each package. Expect a larger first bump
    and changelog than usual; review before merging.
  2. include-component-in-tag tag format — confirm it actually produces
    client-vX.Y.Z / mcp-vX.Y.Z / ts-vX.Y.Z (open question 1 in the design doc;
    config validates against schema but hasn't run against a live repo yet).
  3. release-pr-prepare.yml actually fires and commits on the real release PR
    (both on open and on every subsequent release-please force-push) — confirm the
    uv.lock/pin-sync commit lands before merging.
  4. Draft → publish sequencing for a real release — confirm each publish.yml job
    reaches gh release edit --draft=false only after its registry publish and asset
    upload succeed (it will currently stop short of that at the registry-publish step
    until the Trusted Publisher prerequisites above are configured).
  5. GHCR Docker tagging on a real mcp-v* release — unchanged logic, but now
    sourced from the tag name instead of a job output.

References

Design: dougborg/katana-openapi-client#1002. This repo: closes #10 (superseded by
this migration rather than restoring the old push triggers), fixes #165 (MCP→client
pin floor). #166 (MCP pytest asyncio_mode config bug) was already fixed on main
independently of this PR (commit 9a57510, merged before this branch was cut) — noted
here only because the task briefing called it out, not because this PR touches it.

🤖 Generated with Claude Code

dougborg added 3 commits July 26, 2026 08:20
…tion

Replace the per-package python-semantic-release + JS semantic-release
workflows (release.yml, release-ts.yml, release-mcp.yml,
update-mcp-dependency.yml) with release-please in manifest mode:

- release-please.yml: the only workflow watching pushes to main. Opens
  or updates ONE aggregated release PR (separate-pull-requests: false)
  covering client, mcp, and ts; once that PR merges, creates all tags
  and draft GitHub Releases atomically at the merge commit. Never
  pushes to main itself.
- release-pr-prepare.yml: runs only on the release PR branch. Resyncs
  uv.lock and the MCP server's frontapp-openapi-client>=X floor to the
  versions release-please just bumped, landing as a commit on the PR
  branch so it merges atomically with the version bump.
- publish.yml: the only workflow that builds and publishes. Triggered
  exclusively by client-v*/mcp-v*/ts-v* tag pushes. Builds, publishes
  to PyPI/npm via OIDC, attaches assets to the still-draft release,
  then publishes it - never finalizing a release before its assets
  exist, so nothing is lost to GitHub's immutable-release asset rule.

This removes every job that pushed directly to main during a release
(the root cause of the races this design is meant to eliminate) and
collapses the previously duplicated MCP release surfaces
(release.yml's release-mcp job + release-mcp.yml) into one.

Ref dougborg/katana-openapi-client#1002, closes #10.
…x client pin floor

- Remove [tool.semantic_release] from pyproject.toml (root) and
  frontapp_mcp_server/pyproject.toml - versioning/tagging/changelogs
  are now release-please's job (see release-please-config.json).
- Remove the python-semantic-release dev dependency and the
  frontapp-version/frontapp-release script entry points that backed
  it (semantic_release.cli:main).
- Remove packages/frontapp-client/.releaserc.json and the
  @semantic-release/* + semantic-release devDependencies it required;
  regenerate pnpm-lock.yaml accordingly.
- frontapp_mcp_server/pyproject.toml: set frontapp-openapi-client's
  floor to >=0.1.0 (the version seeded in .release-please-manifest.json
  and currently committed in pyproject.toml), replacing >=0.51.0, a
  version that has never been published. Fixes #165. release-please
  only ever bumps forward from the seeded version, so >=0.1.0 can
  never overshoot the client's actual first published version; the
  old >=0.51.0 floor was inherited from a sibling repo and never
  validated against what this repo would actually ship
  (dougborg/statuspro-openapi-client#63 was the same defect there).
  release-pr-prepare.yml keeps this floor truthful on every future
  release.
…-release guide

- docs/RELEASE.md: rewritten for the release-please manifest-mode flow
  (release PR -> release-pr-prepare glue -> tag -> publish.yml),
  including the manual PyPI/npm Trusted Publisher prerequisites and
  the path-based vs scope-based bump-decision change.
- Remove docs/MONOREPO_SEMANTIC_RELEASE.md; fold anything still
  relevant into docs/RELEASE.md. Update mkdocs.yml nav, docs/index.md
  toctree, docs/README.md's file listing, and the doc-tree diagram in
  docs/adr/0013-module-local-documentation.md accordingly.
- frontapp_mcp_server/docs/deployment.md: replace the
  semantic-release-specific mechanics with a pointer to
  docs/RELEASE.md, keeping the MCP-specific verification/testing
  sections.
- .github/workflows/README.md: describe the new
  release-please.yml / release-pr-prepare.yml / publish.yml topology
  in place of the old release.yml / release-ts.yml /
  update-mcp-dependency.yml.
- README.md, docs/CONTRIBUTING.md: update commit-standards guidance -
  bump type still comes from the commit type, but which package(s)
  bump now depends on which paths a commit touches, not its scope.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep OSS found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Matches the form already merged in stocktrim/statuspro
(309159260+dougborg-release-please[bot]@users.noreply.github.com).
Without the numeric id GitHub does not link the commit to the bot
account, so release-PR commits would show an unlinked author.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRhJ5dF3N3z7X63aRaDBcQ
@dougborg

Copy link
Copy Markdown
Owner Author

Reviewed. Two notes, one of which is a setup trap worth recording before anyone registers the publishers.

Trusted-publisher registration must specify the environment name

publish.yml scopes each publish job to a dedicated GitHub Environment — pypi-client, pypi-mcp, npm-ts. That is the right call (PyPI explicitly encourages a dedicated environment), but it changes what has to be entered when registering the publisher.

None of those environments exist in this repo yet — gh api repos/dougborg/frontapp-openapi-client/environments currently returns only github-pages. GitHub creates them implicitly on first workflow run, so that is not a blocker in itself. The blocker is that the OIDC claim includes the environment name, so the Trusted Publisher entry must list it or the publish will be rejected.

Concretely, when registering at https://pypi.org/manage/account/publishing/ :

Field frontapp-openapi-client frontapp-mcp-server
PyPI Project Name frontapp-openapi-client frontapp-mcp-server
Owner dougborg dougborg
Repository name frontapp-openapi-client frontapp-openapi-client
Workflow name publish.yml publish.yml
Environment name pypi-client pypi-mcp

And the npm Trusted Publisher for frontapp-client needs workflow publish.yml with environment npm-ts.

Note this differs from how statuspro-openapi-client was set up earlier today — there the publish jobs declare no environment:, so its pending publisher was registered with the environment field blank. Leaving the field blank here, or filling it in there, would break the claim match in either direction. Worth keeping straight when the other three repos migrate, since they will pick up this topology.

Both packages are new to PyPI, so these must be created as pending publishers (a trusted publisher cannot create a project that does not exist — that is the Non-user identities cannot create new projects failure statuspro hit).

Bot commit identity

Pushed a small fix: release-pr-prepare.yml used dougborg-release-please[bot]@users.noreply.github.com without the numeric id. GitHub only links a commit to the bot account when the address carries the account id, so release-PR commits would have shown an unlinked author. Now 309159260+dougborg-release-please[bot]@users.noreply.github.com, matching what is already merged in stocktrim and statuspro.

What I checked and am satisfied with

  • release-please.yml never pushes to main — it only opens/updates the release PR, or tags at the merge commit. The cascade class is gone by construction, not by guard.
  • release-pr-prepare.yml uses pull_request (not pull_request_target), is guarded on both the release-please-- branch prefix and head.repo.full_name == github.repository, and pushes only to the PR branch. It also converges — its own push re-triggers synchronize, and the second run finds nothing to commit and exits.
  • publish.yml is tag-only, permissions: {} at the top with per-job grants, and follows build → registry publish → attach assets to the draft → gh release edit --draft=false. That is the ordering immutable releases require, and it generalises the fix from stocktrim#237 and AirHound#122 to the case where release-please owns release creation.
  • draft: true is set at the top level of release-please-config.json, so every component release is created as a draft.
  • Manifest seeds all three packages at 0.1.0 and include-component-in-tag preserves the client-v* / mcp-v* / ts-v* tag shapes.

Deliberately unresolved

The first publish will still fail until the publishers above are registered. That is intended — the workflow is correct and inert rather than absent, and this repo has never published anything, so there is no regression risk in landing it before the registrations exist.

mkdocs runs in --strict mode and aborts on links whose targets are not
part of the docs tree. release-please-config.json and
.release-please-manifest.json live at the repo root, so linking them
failed the quality job:

  Aborted with 2 warnings in strict mode!

Name them in code spans instead. Verified with a local `mkdocs build
--strict`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRhJ5dF3N3z7X63aRaDBcQ
My previous edit tripped format-markdown-check, which gates the test jobs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRhJ5dF3N3z7X63aRaDBcQ
@dougborg
dougborg merged commit fcdaaf4 into main Jul 26, 2026
9 of 10 checks passed
dougborg added a commit to dougborg/stocktrim-openapi-client that referenced this pull request Jul 27, 2026
…tion (#240)

Migrates stocktrim-openapi-client off python-semantic-release and onto
release-please in manifest mode, mirroring the topology piloted in
dougborg/frontapp-openapi-client#168 and specified in
dougborg/katana-openapi-client#1002 (design doc). Fixes #238 (unconstrained
MCP->client dependency) in passing.


Claude-Session: https://claude.ai/code/session_01QRhJ5dF3N3z7X63aRaDBcQ

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants