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.
| Releasing `1.2.3` for each product then produces the tags `cli@1.2.3` / `mcp@1.2.3` on release branches `release/cli/1.2.3` / `release/mcp/1.2.3` — no collisions. | ||
|
|
||
| :::caution | ||
| Declaring **multiple** `github` targets with **different** `tagPrefix` values in a *single* config is ambiguous: Craft uses the first prefix for read-path operations and logs a warning. Use a separate `.craft.yml` per product instead. |
There was a problem hiding this comment.
Isn't it better if we support this use case explicitly to manage everything from a single top-level file when it makes sense? Otherwise I fear we may have issues with the publish repo?
| releaseBranchPrefix: release/cli | ||
| targets: | ||
| - name: github | ||
| tagPrefix: "cli@" |
There was a problem hiding this comment.
I wonder if we we can find a way to unify these. I'd even go as far as saying we should support "workspaces" and associated patterns. Sounds like a natural next step of this evolution.
| // The prefix may itself contain slashes for monorepo per-product releases | ||
| // (e.g. "release/cli" → branches like "release/cli/1.2.3"). Compare the | ||
| // branch's leading segments against the prefix rather than only the first | ||
| // path segment, so slashed prefixes match exactly and fuzzily. | ||
| const prefixSegmentCount = prefix.split('/').length; | ||
| const branchSegments = withoutRemote.split('/'); | ||
| const branchPrefix = branchSegments | ||
| .slice(0, prefixSegmentCount) | ||
| .join('/'); |
There was a problem hiding this comment.
Why not just cut the first segment with .indexOf() and .slice() to avoid the full split/merge overhead. I'd say this is also more error prone? Slashes should not have any meaning for the prefix and we should treat that as an opaque string.
| /** | ||
| * Gets git tag prefix from configuration | ||
| * | ||
| * Returns the `tagPrefix` of the first `github` target. In a monorepo where |
There was a problem hiding this comment.
Why limit this to the first target and not allow indexing at all?
|
Parking this PR in favor of a broader, target-agnostic workspaces redesign (design doc landing shortly, tracked under #842). WhyThe core feedback here — @BYK's "why limit to the first target / allow indexing" (
PlanThe design will be delivered as a sequence of small PRs off
Closing as parked (converting to draft) so the review threads aren't lost. The salvaged fixes will land in PR A. |
## Summary The salvageable, non-controversial core of #844 — prefix-aware version-detection read paths — plus the two review fixes it accumulated. #844 itself is **parked** in favor of a broader, target-agnostic **workspaces** redesign (tracked in #842); this PR lands the parts that are correct regardless of that redesign so they aren't lost. Lets a single repo host independently-versioned products (e.g. `cli@1.2.3`, `mcp@2.0.0`) without cross-contaminating each other's latest-tag detection, changelog base, or CalVer scans. Builds on the existing `tagPrefix` write-side support (no new version library — uses craft's home-grown SemVer utils). ## Changes - **`getLatestTag(git, tagPrefix='')`** scopes `git describe` via `--match '<prefix>*'`; threaded through the `prepare` and `changelog` read paths. Backward compatible (empty prefix → previous behavior). - **`getGitTagPrefix()`** warns when multiple `github` targets declare differing `tagPrefix` values (ambiguous) and returns the first. No selector yet — that's deferred to the workspaces work. - **`getVersion`/`parseVersion`** extract the version from prefixed tags (`cli@1.2.3` → `1.2.3`), locked with tests. ### Review fixes folded in - **changelog (Bugbot Medium):** the tag-prefix lookup is now wrapped in try/catch (mirroring the `versioningPolicy` path) so an unreadable/invalid `.craft.yml` no longer aborts a standalone `craft changelog` run; it falls back to the latest tag overall. - **`findReleaseBranches` (review + Bugbot Low):** the release-branch prefix is treated as an **opaque string** — cut at the last `/` (branch = `<prefix>/<version>`) instead of segment arithmetic. A bare `release` run no longer wrongly claims another product's `release/cli/x` branches. ## Docs `github.md` / `configuration.md` document the current per-`.craft.yml`-per-product model and explicitly note that a first-class, target-agnostic `workspaces:` model is coming (#842) and will supersede it — so we're not doubling down on the per-file convention in docs. ## Testing - `pnpm test` — full suite green (1067 passed, 1 skipped). New/updated tests: opaque-prefix `findReleaseBranches` (incl. the "bare `release` doesn't claim `release/cli/x`" case), changelog try/catch fallback, prefixed version extraction, `getGitTagPrefix` conflict warning. - `tsc`, `pnpm lint`, and `prettier --check` on all changed source files clean. Docs build clean. ## Review Adversarial subagent review: code logic **solid** — the opaque-prefix rewrite's one behavioral divergence is the intended fix (not a regression), all edges (HEAD entries, leading/trailing slash, empty version, no-slash) handled correctly, try/catch correct, no half-implemented selector. Flagged a `prettier` miss on `config.ts` (from the cherry-picked base) which is fixed here. Parks #844; supersedes its multi-product portion. Part of #842. 🤖 Generated with [opencode](https://opencode.ai)


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