Skip to content

Commit 94d4d67

Browse files
committed
fix(release): only fix/feat/perf commits can cut a release
The chore(scope) release gating added for catalog syncs made the post-release 'chore(release): sync manifests' merge releasable (it touches package.json/manifest.json), so every release spawned the next one in an infinite loop (0.7.5..0.7.36+). Restore fix/feat/perf-only subject matching: manifest-sync and catalog-sync chore commits are bookkeeping and never release.
1 parent 17e1654 commit 94d4d67

2 files changed

Lines changed: 8 additions & 18 deletions

File tree

scripts/analyze-release-scope.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,19 @@ export function latestTag(root = process.cwd()): string | null {
3131

3232
type Level = "major" | "minor" | "patch";
3333
const LEVEL_RANK: Record<Level, number> = { patch: 1, minor: 2, major: 3 };
34-
// "chore(scope)" covers routine product refreshes that still ship (e.g.
35-
// chore(catalog): sync models.json); "chore" without a scope stays inert.
34+
// Releasable types only. "chore" (including chore(scope) like chore(release):
35+
// sync manifests or chore(catalog)) is bookkeeping and must never cut a
36+
// release — otherwise the post-release manifest-sync merge re-releases and
37+
// loops forever.
3638
const TYPE_LEVEL: Record<string, Level> = { fix: "patch", perf: "patch", feat: "minor" };
3739

3840
const subjectLevel = (commit: string): Level | null => {
3941
const firstLine = commit.split("\n")[0] ?? "";
40-
const m = /^(?:(?:fix|perf|feat)|chore\([^)]*\))(?:\([^)]*\))?!?:/.exec(firstLine);
42+
const m = /^(?:fix|perf|feat)(?:\([^)]*\))?!?:/.exec(firstLine);
4143
if (!m) return null;
4244
if (m[0].includes("!")) return "major";
4345
const body = commit.split("\n").slice(1).join("\n");
4446
const type = m[0].replace(/\(.*$/, "").replace(/!$/, "").replace(/:$/, "");
45-
if (type === "chore") return "patch";
4647
return /BREAKING[- ]CHANGE:/.test(body) ? "major" : (TYPE_LEVEL[type] ?? null);
4748
};
4849

tests/unit/analyze-release-scope.test.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,26 +109,15 @@ describe("analyzeReleaseScope", () => {
109109
}
110110
});
111111

112-
test("chore(scope) commits that only touch non-product files yield no release", () => {
112+
test("chore(scope) commits never yield a release even when they touch product files", () => {
113113
const r = repo();
114114
r.tag("v0.6.0");
115115
try {
116-
r.commit("chore(catalog): tweak CI", { ".github/workflows/catalog-sync.yml": "cron: 0 *\n" });
116+
r.commit("chore(catalog): sync command-code@1.40.1", { "models.json": "[]\n" });
117+
r.commit("chore(release): sync manifests to v0.7.5", { "package.json": "{}" });
117118
expect(analyzeReleaseScope(r.root)).toEqual({ level: null });
118119
} finally {
119120
r.cleanup();
120121
}
121122
});
122-
123-
test("chore(scope) with product-file change still releases at patch", () => {
124-
const r = repo();
125-
r.tag("v0.6.0");
126-
try {
127-
r.commit("chore(catalog): refresh", { "models.json": "[]\n" });
128-
// prod file touched + chore(scope): treat as the scope's default patch release
129-
expect(analyzeReleaseScope(r.root).level).toBe("patch");
130-
} finally {
131-
r.cleanup();
132-
}
133-
});
134123
});

0 commit comments

Comments
 (0)