feat: support prefixed tags for monorepo multi-product releases#844
Conversation
Enable two (or more) independently-versioned products to live in one repository (e.g. `cli@1.2.3`, `mcp@2.0.0`) without cross-contaminating each other's latest-tag/changelog-base detection or release branches. The write path already honored `github.tagPrefix`; this fills the gaps on the read paths: - `getLatestTag(git, tagPrefix)` now scopes `git describe` with `--match '<prefix>*'` so the latest tag is resolved per-product. - Thread the configured prefix through the auto-version base and changelog base in `prepare.ts`, and through the `changelog` command (guarded by a config-file check). - `getGitTagPrefix()` now warns when a single config declares multiple `github` targets with differing prefixes (ambiguous), returning the first. The intended layout is one `.craft.yml` per product. - `findReleaseBranches` handles slashed release-branch prefixes (`release/cli` -> `release/cli/1.2.3`) so the error-hint suggestions work for the per-product `releaseBranchPrefix` collision fix. CalVer and `versionToTag` were already prefix-aware; `getVersion`/ `parseVersion` already extract the version out of a prefixed tag — both are now covered by explicit tests. Docs: document the monorepo pattern (per-product `.craft.yml` with its own `tagPrefix` + slashed `releaseBranchPrefix`) and note the known limitation that GitHub's single per-repo "Latest" badge is not prefix-scoped.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8aa8586. Configure here.
| // the correct base. Only read the prefix when a config file is present; | ||
| // the changelog command can run standalone without one. | ||
| const tagPrefix = findConfigFile() ? getGitTagPrefix() : ''; | ||
| since = await getLatestTag(git, tagPrefix); |
There was a problem hiding this comment.
Changelog lacks config error guard
Medium Severity
getGitTagPrefix() runs whenever a config file exists, but unlike the versioningPolicy path below it is not wrapped in try/catch. An unreadable or invalid .craft.yml now aborts craft changelog before generating output, including standalone runs that previously only needed git history.
Reviewed by Cursor Bugbot for commit 8aa8586. Configure here.
| // Only consider branches that actually have a version part after the | ||
| // prefix (i.e. more segments than the prefix), mirroring the original | ||
| // single-segment behavior. | ||
| if (!branchPrefix || branchSegments.length <= prefixSegmentCount) { |
There was a problem hiding this comment.
Fuzzy matches confuse monorepo products
Low Severity
For slashed prefixes, Levenshtein distance is computed on the full prefix including the shared release/ segment. Short product names then fall within distance 3 of each other, so failed checkouts suggest other products' branches as typos instead of only near-miss prefixes.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8aa8586. Configure here.


Summary
Enables several independently-versioned products to live in one repository (e.g.
cli@1.2.3,mcp@2.0.0) without cross-contaminating each other's latest-tag/changelog-base detection or release branches. Part of the getsentry/toolkit monorepo work (#842) — the second of two PRs (after thecloudflaretarget in #843).The write path already honored
github.tagPrefix(settingtagPrefix: "cli@"already producescli@1.2.3). This PR fills the gaps on the read paths.Changes
getLatestTag(git, tagPrefix)(src/utils/git.ts) — scopesgit describe --tags --abbrev=0with--match '<prefix>*'so the latest tag is resolved per-product instead of picking whatever is newest across all interleaved tags. Backward compatible (empty prefix → current behavior).prepare.tsauto-version base (:762) and changelog/oldVersion base (:870) now passgetGitTagPrefix(); thechangelogcommand (changelog.ts) does too, guarded by a config-file check so it still runs standalone.getGitTagPrefix()(src/config.ts) — now warns when a single config declares multiplegithubtargets with differingtagPrefixvalues (ambiguous), returning the first. The intended layout is one.craft.ymlper product.findReleaseBranches(src/utils/git.ts) — handles slashed release-branch prefixes (release/cli→release/cli/1.2.3) so the failed-checkout error hints work with the per-productreleaseBranchPrefixcollision fix. Verified regression-free for single-segment prefixes.Already prefix-aware, no code change (now covered by tests): CalVer scan (
calver.ts),versionToTag, andgetVersion/parseVersion(which correctly extract1.2.3fromcli@1.2.3,mcp@2.0.0-dev.1,sentry-cli@10.20.30, etc.).Monorepo model
One
.craft.ymlper product, each with its owntagPrefix+ slashedreleaseBranchPrefix:→ tags
cli@1.2.3/mcp@1.2.3on branchesrelease/cli/1.2.3/release/mcp/1.2.3. No collisions.Known limitation (documented)
GitHub tracks a single repo-wide "Latest" release;
isLatestReleaseis not prefix-scoped, so the "Latest" badge can move between products. Tags, changelogs, release branches, and version detection remain correctly per-product. Documented ingithub.md.Testing
pnpm test— full suite green (1055 passed). New/updated tests:getLatestTagwith/without prefix +--matchassertion; slashed-prefixfindReleaseBranches(incl. the versionless-branch edge);getVersion/parseVersionon prefixed tags;getGitTagPrefixsingle/same/conflicting/mixed-undefined cases.pnpm lint/tsc --noEmitclean. Docs build clean.Review
Adversarial subagent review: no CRITICAL/MAJOR bugs in changed code; the
findReleaseBranchesrefactor verified regression-free case-by-case. Findings M1 (repo-wide "Latest" limitation) and M3 (mixed-prefix test) were addressed in this PR.🤖 Generated with opencode