fix(tier-check): strip package-name prefixes from monorepo release tags (#463)
Version detection stripped only a leading `v`, so the monorepo tags Cargo workspaces publish (`rmcp-v3.0.1`, `rmcp-macros-v3.0.1`) parsed to NaN and scored `stable_release: fail`. rust-sdk was reported as having no stable release despite a non-prerelease `rmcp-v3.0.1`. Match the version at the end of the tag instead, so any package-name prefix is ignored while bare (`1.2.3`), `v`-prefixed and prerelease (`v2.0.0-rc1`) tags parse as they did before. Tags carrying no recognizable version are returned unchanged rather than dropped, so they still surface in the check output. Fixes #425
This commit is contained in:
parent
48561a1aec
commit
6b6e41a86d
2 changed files with 133 additions and 1 deletions
119
src/tier-check/checks/release.test.ts
Normal file
119
src/tier-check/checks/release.test.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { checkStableRelease } from './release';
|
||||
|
||||
type Release = {
|
||||
tag_name: string;
|
||||
draft?: boolean;
|
||||
prerelease?: boolean;
|
||||
};
|
||||
|
||||
/** An Octokit stub that returns a fixed release list. */
|
||||
function octokitWith(releases: Release[]): Octokit {
|
||||
return {
|
||||
repos: {
|
||||
listReleases: async () => ({
|
||||
data: releases.map((r) => ({
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
...r
|
||||
}))
|
||||
})
|
||||
}
|
||||
} as unknown as Octokit;
|
||||
}
|
||||
|
||||
async function check(releases: Release[]) {
|
||||
return checkStableRelease(
|
||||
octokitWith(releases),
|
||||
'modelcontextprotocol',
|
||||
'sdk'
|
||||
);
|
||||
}
|
||||
|
||||
describe('checkStableRelease', () => {
|
||||
it('accepts a bare semver tag', async () => {
|
||||
expect(await check([{ tag_name: '1.2.3' }])).toMatchObject({
|
||||
status: 'pass',
|
||||
version: '1.2.3',
|
||||
is_stable: true
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts a v-prefixed tag', async () => {
|
||||
expect(await check([{ tag_name: 'v1.2.3' }])).toMatchObject({
|
||||
status: 'pass',
|
||||
version: '1.2.3',
|
||||
is_stable: true
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/modelcontextprotocol/conformance/issues/425
|
||||
it('accepts a monorepo tag carrying a package-name prefix', async () => {
|
||||
expect(await check([{ tag_name: 'rmcp-v3.0.1' }])).toMatchObject({
|
||||
status: 'pass',
|
||||
version: '3.0.1',
|
||||
is_stable: true
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts a monorepo tag whose package name contains a hyphen', async () => {
|
||||
expect(await check([{ tag_name: 'rmcp-macros-v3.0.1' }])).toMatchObject({
|
||||
status: 'pass',
|
||||
version: '3.0.1',
|
||||
is_stable: true
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a pre-1.0 release', async () => {
|
||||
expect(await check([{ tag_name: 'rmcp-v0.9.0' }])).toMatchObject({
|
||||
status: 'fail',
|
||||
version: '0.9.0',
|
||||
is_stable: false
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a prerelease named in the tag', async () => {
|
||||
expect(await check([{ tag_name: 'rmcp-v3.0.1-alpha.1' }])).toMatchObject({
|
||||
status: 'fail',
|
||||
version: '3.0.1-alpha.1',
|
||||
is_stable: false,
|
||||
is_prerelease: true
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a release GitHub flags as a prerelease', async () => {
|
||||
expect(
|
||||
await check([{ tag_name: 'rmcp-v3.0.1', prerelease: true }])
|
||||
).toMatchObject({
|
||||
status: 'fail',
|
||||
is_stable: false,
|
||||
is_prerelease: true
|
||||
});
|
||||
});
|
||||
|
||||
it('skips drafts and scores the first published release', async () => {
|
||||
expect(
|
||||
await check([
|
||||
{ tag_name: 'rmcp-v4.0.0', draft: true },
|
||||
{ tag_name: 'rmcp-v3.0.1' }
|
||||
])
|
||||
).toMatchObject({ status: 'pass', version: '3.0.1' });
|
||||
});
|
||||
|
||||
it('reports an unparseable tag unchanged rather than dropping it', async () => {
|
||||
expect(await check([{ tag_name: 'nightly' }])).toMatchObject({
|
||||
status: 'fail',
|
||||
version: 'nightly',
|
||||
is_stable: false
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when the repo has no releases', async () => {
|
||||
expect(await check([])).toMatchObject({
|
||||
status: 'fail',
|
||||
version: null,
|
||||
is_stable: false
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -33,7 +33,7 @@ export async function checkStableRelease(
|
|||
};
|
||||
}
|
||||
|
||||
const version = latest.tag_name.replace(/^v/, '');
|
||||
const version = versionFromTag(latest.tag_name);
|
||||
const isPrerelease =
|
||||
latest.prerelease ||
|
||||
/-(alpha|beta|rc|dev|preview|snapshot)/i.test(version);
|
||||
|
|
@ -57,3 +57,16 @@ export async function checkStableRelease(
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the semantic version from a release tag.
|
||||
*
|
||||
* Handles bare tags (`1.2.3`), `v`-prefixed tags (`v1.2.3`), and the
|
||||
* package-prefixed tags monorepos publish (`rmcp-v3.0.1`,
|
||||
* `rmcp-macros-v3.0.1`). Tags that carry no recognizable version are
|
||||
* returned unchanged so they still surface in the check output.
|
||||
*/
|
||||
function versionFromTag(tag: string): string {
|
||||
const match = tag.match(/(?:^|-)v?(\d+(?:\.\d+)*(?:[-+].*)?)$/);
|
||||
return match ? match[1] : tag;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue