From ee34aa4cc03ee3b07a9f0af573f713153c4d6db8 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:43:20 +0000 Subject: [PATCH 1/2] fix(github): Skip prereleases and ignore floating tags in version lookup Prereleases no longer move floatingTags, so users pinned to a floating tag like v0 keep getting stable releases only. Previous-release lookups now skip tags without a version after the tag prefix. A floating tag such as v0 on the same commit as v0.53.0 sorted first in git describe and hid the release, so auto versioning restarted from 0.0.0. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- docs/src/content/docs/targets/github.md | 7 ++++ src/targets/__tests__/github.test.ts | 35 ++++++++++++++++++ src/targets/github.ts | 10 +++++- src/utils/__tests__/git.test.ts | 47 +++++++++++++++++++++++++ src/utils/git.ts | 21 +++++++---- 5 files changed, 113 insertions(+), 7 deletions(-) diff --git a/docs/src/content/docs/targets/github.md b/docs/src/content/docs/targets/github.md index 57cd98ca5..0ed118a90 100644 --- a/docs/src/content/docs/targets/github.md +++ b/docs/src/content/docs/targets/github.md @@ -58,6 +58,13 @@ targets: This is useful for users who want to pin to a major version while automatically receiving updates. +Prereleases (versions with a prerelease identifier, such as `2.16.0-alpha.0`) +do not move floating tags, so users pinned to `v2` only get stable releases. + +When Craft looks up the previous release, it skips tags that are not a full +version after the tag prefix, such as `v2`, `v2.15`, or `latest`. A floating +tag on the same commit as a release tag therefore does not hide that release. + ## Preview Releases If `previewReleases` is `true` (default), releases containing pre-release identifiers like `alpha`, `beta`, `rc`, etc. are marked as pre-releases on GitHub. See [Versioning](../../versioning/#preview-releases) for the identifiers Craft recognizes. diff --git a/src/targets/__tests__/github.test.ts b/src/targets/__tests__/github.test.ts index fdf2a9062..9ac3c02ba 100644 --- a/src/targets/__tests__/github.test.ts +++ b/src/targets/__tests__/github.test.ts @@ -278,6 +278,41 @@ describe('GitHubTarget', () => { }); }); + describe('updateFloatingTags', () => { + let updateRef: ReturnType; + + beforeEach(() => { + githubTarget = new GitHubTarget( + { name: 'github', floatingTags: ['v{major}', 'v{major}.{minor}'] }, + new NoneArtifactProvider(), + { owner: 'testOwner', repo: 'testRepo' }, + ); + updateRef = vi.fn().mockResolvedValue({}); + githubTarget.github.rest.git.updateRef = updateRef as any; + }); + + it('moves floating tags to a stable release', async () => { + await (githubTarget as any).updateFloatingTags('0.53.0', 'abc123'); + + expect(updateRef).toHaveBeenCalledTimes(2); + expect(updateRef).toHaveBeenCalledWith( + expect.objectContaining({ ref: 'tags/v0', sha: 'abc123', force: true }), + ); + expect(updateRef).toHaveBeenCalledWith( + expect.objectContaining({ ref: 'tags/v0.53', sha: 'abc123' }), + ); + }); + + it('leaves floating tags alone for a prerelease', async () => { + await (githubTarget as any).updateFloatingTags( + '0.54.0-alpha.0', + 'abc123', + ); + + expect(updateRef).not.toHaveBeenCalled(); + }); + }); + describe('getRelease', () => { it('returns release data on success', async () => { const mockRelease = { diff --git a/src/targets/github.ts b/src/targets/github.ts index 4c8decdd6..f8c4938fe 100644 --- a/src/targets/github.ts +++ b/src/targets/github.ts @@ -666,7 +666,8 @@ export class GitHubTarget extends BaseTarget { * Creates or updates floating tags for the release. * * Floating tags (like "v2") point to the latest release in a major version line. - * They are force-updated if they already exist. + * They are force-updated if they already exist. Prereleases leave them + * unchanged, so users pinned to a floating tag only get stable releases. * * @param version The version being released * @param revision Git commit SHA to point the tags to @@ -688,6 +689,13 @@ export class GitHubTarget extends BaseTarget { return; } + if (parsedVersion.pre) { + this.logger.info( + `Not updating floating tags because "${version}" is a prerelease.`, + ); + return; + } + for (const pattern of floatingTags) { const tag = this.resolveFloatingTag(pattern, parsedVersion); const tagRef = `refs/tags/${tag}`; diff --git a/src/utils/__tests__/git.test.ts b/src/utils/__tests__/git.test.ts index b8c067147..14f4876bf 100644 --- a/src/utils/__tests__/git.test.ts +++ b/src/utils/__tests__/git.test.ts @@ -155,6 +155,53 @@ describe('release base tags', () => { }); }); +describe('release base tags with floating tags', () => { + let dir: string; + let client: ReturnType; + + beforeAll(() => { + dir = mkdtempSync(join(tmpdir(), 'craft-floating-tags-')); + const git = (...args: string[]) => + execFileSync('git', ['-C', dir, ...args], { + env: { + ...process.env, + GIT_CONFIG_GLOBAL: '/dev/null', + GIT_CONFIG_NOSYSTEM: '1', + }, + }); + git('init', '--quiet'); + git('config', 'user.name', 'Craft Test'); + git('config', 'user.email', 'craft-test@example.com'); + for (const tags of [['v0.52.0'], ['v0.53.0', 'v0', 'latest'], []]) { + git( + '-c', + 'commit.gpgsign=false', + 'commit', + '--quiet', + '--allow-empty', + '-m', + tags.length ? `release: ${tags[0]}` : 'Unreleased change', + ); + for (const tag of tags) { + git('tag', tag); + } + } + client = createGitClient(dir); + }); + + afterAll(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + it('skips floating tags on the latest release commit', async () => { + // `git describe` alone picks `v0`, which sorts before `v0.53.0`. + expect(await getLatestTag(client, 'v')).toBe('v0.53.0'); + expect(await getLatestTag(client)).toBe('v0.53.0'); + expect(await getLatestStableTag(client, 'v')).toBe('v0.53.0'); + expect(await getChangelogBaseTag(client, '0.54.0', 'v')).toBe('v0.53.0'); + }); +}); + describe('getGitClient', () => { it('preserves Git identity environment variables for commits', async () => { const dir = mkdtempSync(join(tmpdir(), 'craft-git-identity-')); diff --git a/src/utils/git.ts b/src/utils/git.ts index 698f5d504..1aa77cbb8 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -72,7 +72,7 @@ export async function getLatestTag( git: SimpleGit, tagPrefix = '', ): Promise { - return describeLatestTag(git, tagPrefix, []); + return findLatestTag(git, tagPrefix, () => true); } /** @@ -87,7 +87,7 @@ export async function getLatestStableTag( git: SimpleGit, tagPrefix = '', ): Promise { - return findLatestTag(git, tagPrefix, tagVersion => !tagVersion?.pre); + return findLatestTag(git, tagPrefix, tagVersion => !tagVersion.pre); } /** @@ -109,7 +109,7 @@ export async function getChangelogBaseTag( git, tagPrefix, tagVersion => - !tagVersion?.pre || + !tagVersion.pre || (!!release?.pre && tagVersion.major === release.major && tagVersion.minor === release.minor && @@ -117,16 +117,25 @@ export async function getChangelogBaseTag( ); } -/** Returns the latest tag whose version `accept` accepts. */ +/** + * Returns the latest tag whose version `accept` accepts. + * + * Tags without a version after the prefix, such as floating tags (`v2`) or + * `latest`, are never release tags, so they are skipped. + */ async function findLatestTag( git: SimpleGit, tagPrefix: string, - accept: (tagVersion: SemVer | null) => boolean, + accept: (tagVersion: SemVer) => boolean, ): Promise { const skippedTags: string[] = []; for (;;) { const tag = await describeLatestTag(git, tagPrefix, skippedTags); - if (!tag || accept(parseVersion(tag.slice(tagPrefix.length)))) { + if (!tag) { + return tag; + } + const tagVersion = parseVersion(tag.slice(tagPrefix.length)); + if (tagVersion && accept(tagVersion)) { return tag; } skippedTags.push(tag); From 9c244110d6e6b963b572219a2859188740937523 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:01:36 +0000 Subject: [PATCH 2/2] docs(github): Clarify which tags the previous-release lookup ignores --- docs/src/content/docs/targets/github.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/targets/github.md b/docs/src/content/docs/targets/github.md index 0ed118a90..59b77efe3 100644 --- a/docs/src/content/docs/targets/github.md +++ b/docs/src/content/docs/targets/github.md @@ -61,9 +61,12 @@ This is useful for users who want to pin to a major version while automatically Prereleases (versions with a prerelease identifier, such as `2.16.0-alpha.0`) do not move floating tags, so users pinned to `v2` only get stable releases. -When Craft looks up the previous release, it skips tags that are not a full -version after the tag prefix, such as `v2`, `v2.15`, or `latest`. A floating -tag on the same commit as a release tag therefore does not hide that release. +When Craft looks up the previous release, it ignores tags that have no +`major.minor.patch` version after the tag prefix, such as `v2`, `v2.15`, or +`latest`. A floating tag on the same commit as a release tag therefore does not +hide that release. Prerelease tags still count as releases: a prerelease builds +on the previous prerelease of its version, and a stable release on the latest +stable tag (see [Changelog base](../../versioning/#changelog-base)). ## Preview Releases