fix(release): always bump minor for releases

This commit is contained in:
morluto 2026-10-09 02:15:30 +08:00
parent 7005166f58
commit be783efaba
7 changed files with 87 additions and 21 deletions

View File

@ -61,7 +61,8 @@ limits, security/privacy/containment implications, and meaningful proof gaps. --
- Breaking changes or migration steps:
<!-- For ! / BREAKING CHANGE: show a previously valid call/configuration,
its new behavior and migration, and why compatibility/deprecation cannot
preserve it. Compatible fixes/additions should use patch/minor semantics.
preserve it. Every release increments minor, including breaking changes;
keep breaking markers and migration notes so callers can assess compatibility.
See docs/releasing.md. -->
- Real Hopper/Ghidra, browser, or OS coverage:
- Package or release metadata impact:

View File

@ -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/`).
<!-- BEGIN:turborepo-agent-rules -->

View File

@ -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.

View File

@ -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 },

View File

@ -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)

View File

@ -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<ReturnType<typeof fixture>>;
@ -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,

View File

@ -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": [