Skip to content

Commit d4cb8ee

Browse files
authored
fix(release): bump marketplace catalogs for coding-tutor removal (#951)
1 parent 3437de3 commit d4cb8ee

5 files changed

Lines changed: 123 additions & 15 deletions

File tree

.github/release-please-config.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
".claude-plugin": {
6868
"release-type": "simple",
6969
"package-name": "marketplace",
70+
"release-as": "1.0.3",
7071
"extra-files": [
7172
{
7273
"type": "json",
@@ -78,6 +79,7 @@
7879
".cursor-plugin": {
7980
"release-type": "simple",
8081
"package-name": "cursor-marketplace",
82+
"release-as": "1.0.2",
8183
"extra-files": [
8284
{
8385
"type": "json",

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ jobs:
3737

3838
steps:
3939
- uses: actions/checkout@v6
40+
# Full history so release:validate can read the base-branch (origin/main)
41+
# release-please manifest when checking release-as pins for staleness.
42+
with:
43+
fetch-depth: 0
4044

4145
- name: Setup Bun
4246
uses: oven-sh/setup-bun@v2

scripts/release/validate.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,39 @@
11
#!/usr/bin/env bun
2+
import { execFileSync } from "node:child_process"
23
import path from "path"
34
import { validateReleasePleaseConfig } from "../../src/release/config"
45
import { getCompoundEngineeringCounts, syncReleaseMetadata } from "../../src/release/metadata"
56
import { readJson } from "../../src/utils/files"
67

78
type ReleasePleaseManifest = Record<string, string>
89

10+
const MANIFEST_RELATIVE_PATH = ".github/.release-please-manifest.json"
11+
12+
// The release-as staleness check must compare a pin against the version already
13+
// released on the base branch (main), NOT the working tree: a release-please PR
14+
// bumps the working-tree manifest to the proposed version, which would make a
15+
// legitimate pin look stale and block the very release it exists to create.
16+
// Returns {} when origin/main is unreachable (e.g. a shallow checkout that did
17+
// not fetch it) so the staleness check no-ops rather than risk a false block.
18+
function readReleasedManifest(): ReleasePleaseManifest {
19+
try {
20+
const raw = execFileSync("git", ["show", `origin/main:${MANIFEST_RELATIVE_PATH}`], {
21+
encoding: "utf8",
22+
stdio: ["ignore", "pipe", "ignore"],
23+
})
24+
return JSON.parse(raw) as ReleasePleaseManifest
25+
} catch {
26+
return {}
27+
}
28+
}
29+
930
const releasePleaseConfig = await readJson<{ packages: Record<string, unknown> }>(
1031
path.join(process.cwd(), ".github", "release-please-config.json"),
1132
)
1233
const manifest = await readJson<ReleasePleaseManifest>(
13-
path.join(process.cwd(), ".github", ".release-please-manifest.json"),
34+
path.join(process.cwd(), ...MANIFEST_RELATIVE_PATH.split("/")),
1435
)
15-
const configErrors = validateReleasePleaseConfig(releasePleaseConfig)
36+
const configErrors = validateReleasePleaseConfig(releasePleaseConfig, readReleasedManifest())
1637
const counts = await getCompoundEngineeringCounts(process.cwd())
1738
const result = await syncReleaseMetadata({
1839
write: false,

src/release/config.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,56 @@ type ReleasePleaseConfig = {
1010
packages: Record<string, ReleasePleasePackageConfig>
1111
}
1212

13-
export function validateReleasePleaseConfig(config: ReleasePleaseConfig): string[] {
13+
// Maps a release component path (e.g. ".claude-plugin") to its last-released
14+
// version. Callers pass the manifest as it exists on the base branch (main),
15+
// NOT the working tree -- on a release-please PR the working-tree manifest is
16+
// already bumped to the proposed version, which would make a legitimate pin
17+
// look stale. See validateReleasePleaseConfig and scripts/release/validate.ts.
18+
type ReleasePleaseManifest = Record<string, string>
19+
20+
// Compares two plain "x.y.z" versions. Returns a negative number when `a` is
21+
// lower than `b`, 0 when equal, positive when higher. Any pre-release suffix is
22+
// ignored -- release-owned versions in this repo are plain semver.
23+
function compareReleaseVersions(a: string, b: string): number {
24+
const parse = (version: string) =>
25+
version
26+
.split("-")[0]
27+
.split(".")
28+
.map((part) => Number.parseInt(part, 10) || 0)
29+
const left = parse(a)
30+
const right = parse(b)
31+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
32+
const diff = (left[index] ?? 0) - (right[index] ?? 0)
33+
if (diff !== 0) return diff
34+
}
35+
return 0
36+
}
37+
38+
export function validateReleasePleaseConfig(
39+
config: ReleasePleaseConfig,
40+
manifest: ReleasePleaseManifest = {},
41+
): string[] {
1442
const errors: string[] = []
1543

1644
for (const [packagePath, packageConfig] of Object.entries(config.packages)) {
1745
const releaseAs = packageConfig["release-as"]
1846
if (releaseAs) {
19-
errors.push(
20-
`Package "${packagePath}" uses temporary release-as pin "${releaseAs}". Remove release-as after the pinned release ships so future releases can bump normally.`,
21-
)
47+
// A release-as pin is only legitimate as a one-shot forward override: it
48+
// must be strictly ahead of the released version so it drives exactly one
49+
// release. Once that release ships, the released version catches up to the
50+
// pin, and this check then fails on the next PR -- forcing cleanup. This
51+
// is what bit the repo in #674: a pin left behind at-or-below the released
52+
// version silently re-pins (freezes) every subsequent release.
53+
//
54+
// `released` is the base-branch (main) version. If it is unknown (e.g. the
55+
// base manifest could not be read), we cannot prove the pin is stale, so
56+
// we allow it rather than risk blocking a legitimate release.
57+
const released = manifest[packagePath]
58+
if (released !== undefined && compareReleaseVersions(releaseAs, released) <= 0) {
59+
errors.push(
60+
`Package "${packagePath}" uses a stale release-as pin "${releaseAs}" that is not ahead of the released version "${released}". Remove release-as after the pinned release ships so future releases can bump normally.`,
61+
)
62+
}
2263
}
2364

2465
const changelogPath = packageConfig["changelog-path"]

tests/release-config.test.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,62 @@ describe("release-please config validation", () => {
3737
expect(errors).toEqual([])
3838
})
3939

40-
test("rejects checked-in release-as pins", () => {
40+
// The `manifest` argument is the released (base-branch) version, so an
41+
// at-or-below pin means the release already shipped -- the pin is stale.
42+
test("rejects a stale release-as pin that is not ahead of the released version", () => {
43+
const errors = validateReleasePleaseConfig(
44+
{
45+
packages: {
46+
".claude-plugin": { "release-as": "1.0.2" },
47+
".cursor-plugin": { "release-as": "1.0.0" },
48+
},
49+
},
50+
{
51+
".claude-plugin": "1.0.2",
52+
".cursor-plugin": "1.0.1",
53+
},
54+
)
55+
56+
expect(errors).toHaveLength(2)
57+
expect(errors[0]).toContain('Package ".claude-plugin"')
58+
expect(errors[0]).toContain("stale")
59+
expect(errors[0]).toContain("1.0.2")
60+
expect(errors[1]).toContain('Package ".cursor-plugin"')
61+
expect(errors[1]).toContain("1.0.0")
62+
})
63+
64+
// A forward pin is ahead of the released version. This is also the state of an
65+
// in-flight release-please PR when compared against the base manifest (the
66+
// release has not shipped yet), so it must pass.
67+
test("allows a forward release-as pin that is ahead of the released version", () => {
68+
const errors = validateReleasePleaseConfig(
69+
{
70+
packages: {
71+
".claude-plugin": { "release-as": "1.0.3" },
72+
".cursor-plugin": { "release-as": "1.0.2" },
73+
},
74+
},
75+
{
76+
".claude-plugin": "1.0.2",
77+
".cursor-plugin": "1.0.1",
78+
},
79+
)
80+
81+
expect(errors).toEqual([])
82+
})
83+
84+
// No released baseline (e.g. the base manifest could not be read) means
85+
// staleness is unprovable, so the pin is allowed rather than risk blocking a
86+
// legitimate release.
87+
test("allows a release-as pin when the released version is unknown", () => {
4188
const errors = validateReleasePleaseConfig({
4289
packages: {
4390
".": {
4491
"release-as": "3.0.2",
4592
},
46-
"plugins/compound-engineering": {
47-
"release-as": "3.0.2",
48-
},
4993
},
5094
})
5195

52-
expect(errors).toHaveLength(2)
53-
expect(errors[0]).toContain('Package "."')
54-
expect(errors[0]).toContain("release-as")
55-
expect(errors[1]).toContain('Package "plugins/compound-engineering"')
56-
expect(errors[1]).toContain("3.0.2")
96+
expect(errors).toEqual([])
5797
})
5898
})

0 commit comments

Comments
 (0)