Skip to content

feat: support prefixed tags for monorepo multi-product releases - #844

Draft
BYK wants to merge 1 commit into
masterfrom
feat/prefixed-tags
Draft

feat: support prefixed tags for monorepo multi-product releases#844
BYK wants to merge 1 commit into
masterfrom
feat/prefixed-tags

Conversation

@BYK

@BYK BYK commented Jul 16, 2026

Copy link
Copy Markdown
Member

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 the cloudflare target in #843).

The write path already honored github.tagPrefix (setting tagPrefix: "cli@" already produces cli@1.2.3). This PR fills the gaps on the read paths.

Changes

  • getLatestTag(git, tagPrefix) (src/utils/git.ts) — scopes git describe --tags --abbrev=0 with --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).
  • Thread the prefix through read pathsprepare.ts auto-version base (:762) and changelog/oldVersion base (:870) now pass getGitTagPrefix(); the changelog command (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 multiple github targets with differing tagPrefix values (ambiguous), returning the first. The intended layout is one .craft.yml per product.
  • findReleaseBranches (src/utils/git.ts) — handles slashed release-branch prefixes (release/clirelease/cli/1.2.3) so the failed-checkout error hints work with the per-product releaseBranchPrefix collision fix. Verified regression-free for single-segment prefixes.

Already prefix-aware, no code change (now covered by tests): CalVer scan (calver.ts), versionToTag, and getVersion/parseVersion (which correctly extract 1.2.3 from cli@1.2.3, mcp@2.0.0-dev.1, sentry-cli@10.20.30, etc.).

Monorepo model

One .craft.yml per product, each with its own tagPrefix + slashed releaseBranchPrefix:

# CLI
releaseBranchPrefix: release/cli
targets:
  - name: github
    tagPrefix: "cli@"
# MCP
releaseBranchPrefix: release/mcp
targets:
  - name: github
    tagPrefix: "mcp@"

→ tags cli@1.2.3 / mcp@1.2.3 on branches release/cli/1.2.3 / release/mcp/1.2.3. No collisions.

Known limitation (documented)

GitHub tracks a single repo-wide "Latest" release; isLatestRelease is not prefix-scoped, so the "Latest" badge can move between products. Tags, changelogs, release branches, and version detection remain correctly per-product. Documented in github.md.

Testing

  • pnpm test — full suite green (1055 passed). New/updated tests: getLatestTag with/without prefix + --match assertion; slashed-prefix findReleaseBranches (incl. the versionless-branch edge); getVersion/parseVersion on prefixed tags; getGitTagPrefix single/same/conflicting/mixed-undefined cases.
  • pnpm lint / tsc --noEmit clean. Docs build clean.

Review

Adversarial subagent review: no CRITICAL/MAJOR bugs in changed code; the findReleaseBranches refactor 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

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.
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://craft.sentry.dev/pr-preview/pr-844/

Built to branch gh-pages at 2026-07-16 13:00 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread src/commands/changelog.ts
// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8aa8586. Configure here.

Comment thread src/utils/git.ts
// 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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?

Comment on lines +185 to +188
releaseBranchPrefix: release/cli
targets:
- name: github
tagPrefix: "cli@"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment thread src/utils/git.ts
Comment on lines +251 to +259
// 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('/');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment thread src/config.ts
/**
* Gets git tag prefix from configuration
*
* Returns the `tagPrefix` of the first `github` target. In a monorepo where

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why limit this to the first target and not allow indexing at all?

@BYK

BYK commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Parking this PR in favor of a broader, target-agnostic workspaces redesign (design doc landing shortly, tracked under #842).

Why

The core feedback here — @BYK's "why limit to the first target / allow indexing" (config.ts) and "support managing everything from a single top-level file / workspaces" (github.md, configuration.md) — points at a bigger design than this PR's per-.craft.yml-per-product approach. After digging into the release + publish_repo/publish-issue pipeline, the conclusion is:

  • The current model disambiguates products only by version + subdirectory path. path is a proxy for physical layout, not release identity — it breaks down for a single-file multi-product repo (issue-title collision at action.yml, shared publish-state key at publishState.ts, whole-config craft targets checklist).
  • A first-class, target-agnostic top-level workspaces: concept + an explicit --workspace selector is the better long-term primitive. It also addresses Add workspaces support for dotnet/nuget #649 (dotnet/nuget) and Resume publishing a Rust crates workspace #394 (crates resume).

Plan

The design will be delivered as a sequence of small PRs off master:

  • PR A — salvage the non-controversial fixes from this PR (they're correct regardless of the redesign):
    1. Wrap getGitTagPrefix() in a try/catch in changelog.ts so an invalid .craft.yml doesn't abort a standalone craft changelog run (Bugbot Medium).
    2. Treat the release-branch prefix as an opaque string in findReleaseBranches (cut at the last /), per @BYK's review + Bugbot Low.
    3. Keep the prefix-aware read paths (getLatestTag --match, tests, docs).
  • PR B–Eworkspaces: schema + resolver + selector → prepare/publish threading → action layer + (additive) getsentry/publish accept-side → docs.

Closing as parked (converting to draft) so the review threads aren't lost. The salvaged fixes will land in PR A.

@BYK
BYK marked this pull request as draft July 21, 2026 18:53
BYK added a commit that referenced this pull request Jul 21, 2026
## 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant