diff --git a/scripts/analyze-release-scope.ts b/scripts/analyze-release-scope.ts index f770707..19838fe 100644 --- a/scripts/analyze-release-scope.ts +++ b/scripts/analyze-release-scope.ts @@ -31,18 +31,19 @@ export function latestTag(root = process.cwd()): string | null { type Level = "major" | "minor" | "patch"; const LEVEL_RANK: Record = { patch: 1, minor: 2, major: 3 }; -// "chore(scope)" covers routine product refreshes that still ship (e.g. -// chore(catalog): sync models.json); "chore" without a scope stays inert. +// Releasable types only. "chore" (including chore(scope) like chore(release): +// sync manifests or chore(catalog)) is bookkeeping and must never cut a +// release — otherwise the post-release manifest-sync merge re-releases and +// loops forever. const TYPE_LEVEL: Record = { fix: "patch", perf: "patch", feat: "minor" }; const subjectLevel = (commit: string): Level | null => { const firstLine = commit.split("\n")[0] ?? ""; - const m = /^(?:(?:fix|perf|feat)|chore\([^)]*\))(?:\([^)]*\))?!?:/.exec(firstLine); + const m = /^(?:fix|perf|feat)(?:\([^)]*\))?!?:/.exec(firstLine); if (!m) return null; if (m[0].includes("!")) return "major"; const body = commit.split("\n").slice(1).join("\n"); const type = m[0].replace(/\(.*$/, "").replace(/!$/, "").replace(/:$/, ""); - if (type === "chore") return "patch"; return /BREAKING[- ]CHANGE:/.test(body) ? "major" : (TYPE_LEVEL[type] ?? null); }; diff --git a/tests/unit/analyze-release-scope.test.ts b/tests/unit/analyze-release-scope.test.ts index 309f64d..5177713 100644 --- a/tests/unit/analyze-release-scope.test.ts +++ b/tests/unit/analyze-release-scope.test.ts @@ -109,26 +109,15 @@ describe("analyzeReleaseScope", () => { } }); - test("chore(scope) commits that only touch non-product files yield no release", () => { + test("chore(scope) commits never yield a release even when they touch product files", () => { const r = repo(); r.tag("v0.6.0"); try { - r.commit("chore(catalog): tweak CI", { ".github/workflows/catalog-sync.yml": "cron: 0 *\n" }); + r.commit("chore(catalog): sync command-code@1.40.1", { "models.json": "[]\n" }); + r.commit("chore(release): sync manifests to v0.7.5", { "package.json": "{}" }); expect(analyzeReleaseScope(r.root)).toEqual({ level: null }); } finally { r.cleanup(); } }); - - test("chore(scope) with product-file change still releases at patch", () => { - const r = repo(); - r.tag("v0.6.0"); - try { - r.commit("chore(catalog): refresh", { "models.json": "[]\n" }); - // prod file touched + chore(scope): treat as the scope's default patch release - expect(analyzeReleaseScope(r.root).level).toBe("patch"); - } finally { - r.cleanup(); - } - }); });