-
-
Notifications
You must be signed in to change notification settings - Fork 20
feat: support prefixed tags for monorepo multi-product releases #844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,44 @@ targets: | |
|
|
||
| This is useful for users who want to pin to a major version while automatically receiving updates. | ||
|
|
||
| ## Monorepo: independently-versioned products | ||
|
|
||
| `tagPrefix` lets a single repository host several independently-versioned products by namespacing their git tags — for example `cli@1.2.3` and `mcp@2.0.0`. Craft honors the prefix on both the **write** side (the tag it creates) and the **read** side (latest-tag detection, changelog base, and CalVer scans are all scoped to the prefix), so the products don't cross-contaminate each other's version history. | ||
|
|
||
| The intended layout is **one `.craft.yml` per product**, each with a single `github` target declaring its own `tagPrefix` and a matching `releaseBranchPrefix` (so release branches don't collide): | ||
|
|
||
| ```yaml | ||
| # .craft.yml for the CLI product | ||
| github: | ||
| owner: getsentry | ||
| repo: toolkit | ||
| releaseBranchPrefix: release/cli | ||
| targets: | ||
| - name: github | ||
| tagPrefix: "cli@" | ||
| ``` | ||
|
|
||
| ```yaml | ||
| # .craft.yml for the MCP product | ||
| github: | ||
| owner: getsentry | ||
| repo: toolkit | ||
| releaseBranchPrefix: release/mcp | ||
| targets: | ||
| - name: github | ||
| tagPrefix: "mcp@" | ||
| ``` | ||
|
|
||
| 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. | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ::: | ||
|
|
||
| :::note[Known limitation: the GitHub "Latest" badge is repo-wide] | ||
| GitHub tracks a single "Latest" release **per repository**, not per tag prefix. Craft decides whether to mark a release as latest by comparing its version against the repository's current latest release ([`isLatestRelease`](https://github.com/getsentry/craft/blob/master/src/targets/github.ts)), which is not prefix-scoped. In a monorepo this means the "Latest" badge can move between products (e.g. publishing `cli@9.0.0` may take the badge from `mcp@3.0.0`, and publishing `cli@1.2.3` while `mcp@3.0.0` is latest won't earn the badge). Tags, changelogs, release branches, and version detection remain correctly per-product — only GitHub's single "Latest" pointer is shared. | ||
| ::: | ||
|
|
||
| ## Preview Releases | ||
|
|
||
| If `previewReleases` is `true` (default), releases containing pre-release identifiers like `alpha`, `beta`, `rc`, etc. are marked as pre-releases on GitHub. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import { Argv, CommandBuilder } from 'yargs'; | ||
|
|
||
| import { logger } from '../logger'; | ||
| import { findConfigFile, getVersioningPolicy } from '../config'; | ||
| import { findConfigFile, getGitTagPrefix, getVersioningPolicy } from '../config'; | ||
| import { getGitClient, getLatestTag } from '../utils/git'; | ||
| import { | ||
| generateChangesetFromGit, | ||
|
|
@@ -55,7 +55,12 @@ export async function changelogMain(argv: ChangelogOptions): Promise<void> { | |
| // Determine base revision for changelog generation | ||
| let since = argv.since; | ||
| if (!since) { | ||
| since = await getLatestTag(git); | ||
| // Scope the latest-tag lookup to the configured tag prefix (if any) so | ||
| // monorepos with interleaved product tags (e.g. `cli@`, `mcp@`) resolve | ||
| // 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changelog lacks config error guardMedium Severity
Reviewed by Cursor Bugbot for commit 8aa8586. Configure here. |
||
| if (since) { | ||
| logger.debug(`Using latest tag as base revision: ${since}`); | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -328,11 +328,33 @@ export async function getGlobalGitHubConfig( | |
|
|
||
| /** | ||
| * Gets git tag prefix from configuration | ||
| * | ||
| * Returns the `tagPrefix` of the first `github` target. In a monorepo where | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
| * multiple products are released from separate `.craft.yml` files, each config | ||
| * has a single `github` target with its own prefix (e.g. `cli@`, `mcp@`), so | ||
| * this resolves unambiguously per release run. If a single config declares | ||
| * multiple `github` targets with *differing* prefixes, the configuration is | ||
| * ambiguous: the first prefix is returned and a warning is emitted. | ||
| */ | ||
| export function getGitTagPrefix(): string { | ||
| const targets = getConfiguration().targets || []; | ||
| const githubTarget = targets.find(target => target.name === 'github'); | ||
| return (githubTarget?.tagPrefix as string | undefined) || ''; | ||
| const githubTargets = targets.filter(target => target.name === 'github'); | ||
| const firstPrefix = | ||
| (githubTargets[0]?.tagPrefix as string | undefined) || ''; | ||
|
|
||
| const hasConflictingPrefix = githubTargets.some( | ||
| target => ((target.tagPrefix as string | undefined) || '') !== firstPrefix, | ||
| ); | ||
| if (hasConflictingPrefix) { | ||
| logger.warn( | ||
| 'Multiple "github" targets with different "tagPrefix" values found. ' + | ||
| `Using "${firstPrefix}". For independently-versioned products in a ` + | ||
| 'monorepo, use a separate .craft.yml per product, each with a single ' + | ||
| '"github" target and its own "tagPrefix".', | ||
| ); | ||
| } | ||
|
|
||
| return firstPrefix; | ||
| } | ||
|
|
||
| /** | ||
|
|
||


There was a problem hiding this comment.
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.