From be783efaba62dc76c5d8d42229869a32afdb302a Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:15:30 +0800 Subject: [PATCH] fix(release): always bump minor for releases --- .github/pull_request_template.md | 3 +- AGENTS.md | 2 +- docs/releasing.md | 30 ++++---- release-please-config.json | 1 + scripts/lib/release-checkpoint.mjs | 2 + .../filesystem/releaseCheckpoint.test.ts | 69 +++++++++++++++++-- .../conformance/releaseConfiguration.test.ts | 1 + 7 files changed, 87 insertions(+), 21 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b4abd478d..bfd3275ad 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -61,7 +61,8 @@ limits, security/privacy/containment implications, and meaningful proof gaps. -- - Breaking changes or migration steps: - Real Hopper/Ghidra, browser, or OS coverage: - Package or release metadata impact: diff --git a/AGENTS.md b/AGENTS.md index d60f2b07f..b0e98f54d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,7 +102,7 @@ Development requires Node.js 24.18.0 and npm 11.16.0 (`.nvmrc` and `packageManag ## Commit & Pull Request Guidelines -Use Conventional Commit subjects because Release Please derives versions and changelogs from them. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`). +Use Conventional Commit subjects because Release Please derives changelogs from them. Release Please uses `always-bump-minor`: every release increments minor, including breaking changes. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`). diff --git a/docs/releasing.md b/docs/releasing.md index 26dc8ee20..9d40b5e37 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -10,10 +10,10 @@ accepting changes without changing the candidate being tested. ## 1. Select the source -Choose the next version from the unreleased Conventional Commits, including +Use the next minor version proposed by Release Please, including releases with breaking changes. Record the full source SHA and create `release/VERSION` at -that commit. The version is a maintainer choice; Release Please's proposed -version must agree before publication. +that commit. The selected version and Release Please's proposed version must +agree before publication. Use the automatic main proposal to review the suggested version and notes. Keep that proposal unmerged: select the application commit from main, create @@ -28,19 +28,21 @@ registry metadata or changelog disagrees with that version before creating a tag ### Compatibility and version selection -REA uses the default SemVer strategy: compatible fixes increment patch; -compatible capabilities increment minor; incompatible public behavior increments -major. Adding tools, internal refactoring and correcting behavior outside the -supported contract do not by themselves require a major release. +REA uses Release Please's `always-bump-minor` strategy: every release increments +minor and resets patch, including releases with breaking changes. For example, +the next release after 6.0.0 is 6.1.0. Version numbers use the SemVer format, +but a minor increment does not promise backward compatibility. Review the +changelog's breaking-change section and migration notes before upgrading. The supported public surface includes documented CLI commands/options, MCP tool names and input/result contracts, saved evidence formats and supported runtime requirements. Before adding `!` or a `BREAKING CHANGE` footer, a PR must identify a previously valid call or configuration that will fail or change meaning, explain why compatibility cannot be preserved, and give its migration. Prefer -optional additions, compatibility adapters and a documented deprecation period; -group deliberate removals into a planned major release. Maintainers review this -impact before merging; the bot parses markers and cannot infer compatibility. +optional additions, compatibility adapters and a documented deprecation period. +Keep breaking markers so the bot includes migration notes without incrementing +major. Maintainers review this impact before merging; the bot parses markers +and cannot infer compatibility. For example, adding an optional inspection tool is a minor change. Rejecting a relative MCP path previously accepted by the public contract is breaking; @@ -90,9 +92,11 @@ and the PR titles in GitHub's default merge messages. The report preserves the original merge subject alongside the extracted Conventional Commit title. This matters after merging a side-branch release back into a newer main: Release Please's chronological history cutoff can omit unreleased mainline -commits. Unreleased breaking markers require a major increment and references -in the new release's breaking-change migration section. Other omitted entries -are reported for review. Historical notes from an older release cannot satisfy +commits. Under `always-bump-minor`, unreleased breaking markers allow a minor +increment and still require references in the new release's breaking-change +migration section. Checkpoints using the default strategy retain the major +increment requirement. Other omitted entries are reported for review. +Historical notes from an older release cannot satisfy the new release's migration check. This audits declared markers and references; it does not prove API compatibility or the quality of a migration explanation. diff --git a/release-please-config.json b/release-please-config.json index 4560b53fa..f68b92ec2 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,6 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "release-type": "node", + "versioning": "always-bump-minor", "include-v-in-tag": false, "changelog-sections": [ { "type": "feat", "section": "Features", "hidden": false }, diff --git a/scripts/lib/release-checkpoint.mjs b/scripts/lib/release-checkpoint.mjs index 1d1950d5f..a8c8831df 100644 --- a/scripts/lib/release-checkpoint.mjs +++ b/scripts/lib/release-checkpoint.mjs @@ -5,6 +5,7 @@ const shaSchema = z.string().regex(/^[a-f0-9]{40}$/u); const versionSchema = z.object({ version: z.string() }); const manifestSchema = z.object({ ".": z.string() }); const sectionsSchema = z.object({ + versioning: z.string().optional(), "changelog-sections": z.array( z.object({ type: z.string(), hidden: z.boolean().optional() }), ), @@ -297,6 +298,7 @@ export async function inspectReleaseCheckpoint(git, options) { ).filter((commit) => commit.breaking || visibleTypes.has(commit.type)); const breaking = commits.filter((commit) => commit.breaking); if ( + configuration.versioning !== "always-bump-minor" && breaking.length > 0 && major(baselineVersion) > 0 && major(expectedVersion) <= major(baselineVersion) diff --git a/tests/boundary/filesystem/releaseCheckpoint.test.ts b/tests/boundary/filesystem/releaseCheckpoint.test.ts index 00fff5087..8f720a22e 100644 --- a/tests/boundary/filesystem/releaseCheckpoint.test.ts +++ b/tests/boundary/filesystem/releaseCheckpoint.test.ts @@ -39,7 +39,12 @@ async function git( return result.stdout.trim(); } -async function metadata(directory: string, version: string, notes: string) { +async function metadata( + directory: string, + version: string, + notes: string, + versioning?: string, +) { await mkdir(join(directory, "docs"), { recursive: true }); await mkdir(join(directory, "src"), { recursive: true }); const files = { @@ -51,6 +56,7 @@ async function metadata(directory: string, version: string, notes: string) { packages: [{ identifier: "rea-agents", version }], }, "release-please-config.json": { + ...(versioning === undefined ? {} : { versioning }), "changelog-sections": [ { type: "feat", hidden: false }, { type: "fix", hidden: false }, @@ -74,15 +80,15 @@ async function metadata(directory: string, version: string, notes: string) { async function fixture( subject = "fix(contracts)!: require absolute paths (#948)", footer = "", - options: { mergePullRequest?: boolean } = {}, + options: { mergePullRequest?: boolean; versioning?: string } = {}, ) { const directory = await createTestTempDirectory("rea-release-ancestry-"); await git(directory, ["init", "--initial-branch=main"]); - await metadata(directory, "4.0.0", "Initial release"); + await metadata(directory, "4.0.0", "Initial release", options.versioning); await git(directory, ["add", "."]); await git(directory, ["commit", "-m", "chore: seed"], "2026-01-01T00:00:00Z"); await git(directory, ["switch", "-c", "release/5.0.0"]); - await metadata(directory, "5.0.0", "Previous release"); + await metadata(directory, "5.0.0", "Previous release", options.versioning); await git(directory, ["add", "."]); await git( directory, @@ -130,7 +136,13 @@ async function fixture( "chore: sync published baseline", ]); const sourceSha = await git(directory, ["rev-parse", "HEAD"]); - return { directory, sourceSha, changedSha, releasedSha }; + return { + directory, + sourceSha, + changedSha, + releasedSha, + versioning: options.versioning, + }; } type Fixture = Awaited>; @@ -140,7 +152,7 @@ async function candidate( version: string, notes = "### ⚠ BREAKING CHANGES\n\n* MCP callers must use absolute paths; CLI paths remain compatible. See #948.", ) { - await metadata(f.directory, version, notes); + await metadata(f.directory, version, notes, f.versioning); await git(f.directory, ["add", "."]); await git( f.directory, @@ -238,6 +250,51 @@ it("rejects a requested minor that omits the breaking ancestry", async () => { }); }); +it.each([ + { subject: "fix(contracts)!: require absolute paths (#948)", footer: "" }, + { + subject: "fix(contracts): change path inputs (#948)", + footer: "BREAKING CHANGE: relative MCP paths are rejected", + }, +])( + "accepts a breaking minor under always-bump-minor: $subject", + async ({ subject, footer }) => { + const f = await fixture(subject, footer, { + versioning: "always-bump-minor", + }); + await expect( + verify(f, "5.1.0", "prepare", "source"), + ).resolves.toMatchObject({ + stderr: "", + }); + const candidateSha = await candidate(f, "5.1.0"); + const report = reportSchema.parse( + JSON.parse((await verify(f, "5.1.0")).stdout), + ); + expect(report.expectedVersion).toBe("5.1.0"); + expect(report.missingNotes).toEqual([]); + expect(report.commits).toContainEqual({ + sha: f.changedSha, + subject, + breaking: true, + }); + await expect( + verify({ ...f, sourceSha: candidateSha }, "5.1.0", "publish", "source"), + ).resolves.toMatchObject({ stderr: "" }); + }, +); + +it("still requires migration notes for breaking minor releases", async () => { + const f = await fixture(undefined, "", { versioning: "always-bump-minor" }); + await candidate(f, "5.1.0", "### Bug Fixes\n\nSee #948."); + await expect(verify(f, "5.1.0")).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "migration notes omit unreleased breaking changes", + ), + }); +}); + it("rejects a minor when a GitHub merge commit carries its bang marker in the PR title", async () => { const f = await fixture("fix(contracts)!: require absolute paths", "", { mergePullRequest: true, diff --git a/tests/conformance/releaseConfiguration.test.ts b/tests/conformance/releaseConfiguration.test.ts index ddcadac61..1c5b09abe 100644 --- a/tests/conformance/releaseConfiguration.test.ts +++ b/tests/conformance/releaseConfiguration.test.ts @@ -26,6 +26,7 @@ describe("release configuration", () => { const configuration = await readJson("release-please-config.json"); expect(configuration).toMatchObject({ + versioning: "always-bump-minor", packages: { ".": { "extra-files": [