diff --git a/actions/release-please/README.md b/actions/release-please/README.md index 593f622..f30e8e1 100644 --- a/actions/release-please/README.md +++ b/actions/release-please/README.md @@ -99,17 +99,25 @@ resolve to `false`: - `draft-pull-request` - `prerelease` -`include-v-in-tag` must resolve to `true` for every package. +`bump-minor-pre-major` and `include-v-in-tag` must resolve to `true` for every +package. + +Together these settings give every repository the same versioning while it is +below 1.0.0: a fix bumps the patch version, and a feature or a breaking change +bumps the minor version. Moving to 1.0.0 is a deliberate `Release-As` decision, +never a side effect of a breaking change. Package overrides take precedence over top-level settings. Validation checks each package's effective value, so an override can satisfy the policy even when the top-level value differs. Omitted settings use release-please's native -defaults: `false` for the settings above and `true` for `include-v-in-tag`. +defaults: `false` for every setting above except `include-v-in-tag`, which +defaults to `true`. Because `bump-minor-pre-major` defaults to `false`, it must +be set explicitly. Explicit values must be booleans; `null` and strings are rejected. A top-level, nonempty `signoff` is required. Other boolean settings are checked for their types, without forcing a shared -value. Settings such as `bump-minor-pre-major`, tag formatting, `release-type` +value. Settings such as tag formatting, `release-type` and plugins remain repository-specific. `versioning` must be a string when provided, and its effective value must not be `always-bump-patch`, which would override the required feature-bump behavior. diff --git a/actions/release-please/lib/index.mjs b/actions/release-please/lib/index.mjs index ac8f8c7..6407196 100644 --- a/actions/release-please/lib/index.mjs +++ b/actions/release-please/lib/index.mjs @@ -7,12 +7,12 @@ export const CONFIG_FILE = ".github/release-please-config.json"; // Policy changes must not alter upstream's behavior for omitted settings. const releaseRules = Object.freeze({ + "bump-minor-pre-major": { required: true, fallback: false }, "bump-patch-for-minor-pre-major": { required: false, fallback: false }, draft: { required: false, fallback: false }, "draft-pull-request": { required: false, fallback: false }, prerelease: { required: false, fallback: false }, "include-v-in-tag": { required: true, fallback: true }, - "bump-minor-pre-major": {}, "include-component-in-tag": {}, "separate-pull-requests": {}, }); diff --git a/tests/node/release-please/cli.test.mjs b/tests/node/release-please/cli.test.mjs index 7c94702..036bec8 100644 --- a/tests/node/release-please/cli.test.mjs +++ b/tests/node/release-please/cli.test.mjs @@ -11,6 +11,7 @@ function config(overrides = {}) { return { signoff: "Release contributor", "initial-version": "0.0.1", + "bump-minor-pre-major": true, packages: { ".": {} }, ...overrides, }; @@ -49,8 +50,10 @@ test("CLI rejects forbidden root and package policy flags", (t) => { rejected(run(t, config({ [flag]: true })), new RegExp(flag)); rejected(run(t, config({ packages: { ".": { [flag]: true } } })), new RegExp(flag)); } - rejected(run(t, config({ "include-v-in-tag": false })), /include-v-in-tag/); - rejected(run(t, config({ packages: { ".": { "include-v-in-tag": false } } })), /include-v-in-tag/); + for (const flag of ["bump-minor-pre-major", "include-v-in-tag"]) { + rejected(run(t, config({ [flag]: false })), new RegExp(flag)); + rejected(run(t, config({ packages: { ".": { [flag]: false } } })), new RegExp(flag)); + } }); test("disabling initial-version validation preserves policy and signoff validation", (t) => { @@ -73,7 +76,7 @@ test("CLI accepts native defaults and package release-type variation", (t) => { }); test("initial-version opt-out accepts missing initial versions when mandatory guards pass", (t) => { - const result = run(t, { signoff: "Release contributor", packages: { ".": {} } }, "false"); + const result = run(t, { signoff: "Release contributor", "bump-minor-pre-major": true, packages: { ".": {} } }, "false"); assert.equal(result.status, 0, result.stdout + result.stderr); assert.match(result.stdout, /not checked/); }); diff --git a/tests/node/release-please/index.test.mjs b/tests/node/release-please/index.test.mjs index 4552a67..15e39ac 100644 --- a/tests/node/release-please/index.test.mjs +++ b/tests/node/release-please/index.test.mjs @@ -135,6 +135,7 @@ test("the repository's own configuration passes", () => { }); const defaults = { + "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": false, draft: false, "draft-pull-request": false, @@ -143,17 +144,24 @@ const defaults = { }; test("omitted policy settings use compatible native defaults", () => { - validateReleaseDefaults({ packages: { ".": {} } }); + validateReleaseDefaults({ "bump-minor-pre-major": true, packages: { ".": {} } }); +}); + +test("omitted bump-minor-pre-major is rejected because the native default releases 1.0.0 on a breaking change", () => { + const error = failure(() => validateReleaseDefaults({ packages: { ".": {} } })); + assert.ok(error.message.includes("bump-minor-pre-major: true")); }); test("effective policy settings inherit root or native defaults", () => { validateReleaseDefaults({ ...defaults, packages: { ".": {}, other: { ...defaults } } }); for (const [key, expected] of Object.entries(defaults)) { validateReleaseDefaults({ + ...defaults, [key]: !expected, packages: { ".": { [key]: expected }, other: { [key]: expected } }, }); const error = failure(() => validateReleaseDefaults({ + ...defaults, [key]: !expected, packages: { compliant: { [key]: expected }, inherited: {} }, })); @@ -179,30 +187,32 @@ test("explicit package null and strings are not rescued by root policy defaults" for (const [key, expected] of Object.entries(defaults)) { for (const value of [null, "false", 0, undefined, [], {}]) { const error = failure(() => validateReleaseDefaults({ + ...defaults, [key]: expected, packages: { malformed: { [key]: value } }, })); assert.match(error.message, /Package 'malformed'/); assert.match(error.message, /to a boolean/); } - validateReleaseDefaults({ [key]: expected, packages: { ".": { [key]: expected } } }); + validateReleaseDefaults({ ...defaults, [key]: expected, packages: { ".": { [key]: expected } } }); } }); test("strategy booleans may vary but must actually be booleans", () => { - for (const key of ["bump-minor-pre-major", "include-component-in-tag", "separate-pull-requests"]) { + for (const key of ["include-component-in-tag", "separate-pull-requests"]) { for (const value of [true, false]) { - validateReleaseDefaults({ [key]: value, packages: { ".": {}, opposite: { [key]: !value } } }); + validateReleaseDefaults({ ...defaults, [key]: value, packages: { ".": {}, opposite: { [key]: !value } } }); } for (const value of [null, "true", 1]) { - failure(() => validateReleaseDefaults({ [key]: value, packages: { ".": {} } })); - failure(() => validateReleaseDefaults({ packages: { ".": { [key]: value } } })); + failure(() => validateReleaseDefaults({ ...defaults, [key]: value, packages: { ".": {} } })); + failure(() => validateReleaseDefaults({ ...defaults, packages: { ".": { [key]: value } } })); } } }); test("release strategies and unknown extra settings remain caller-owned", () => { validateReleaseDefaults({ + "bump-minor-pre-major": true, "release-type": "simple", "tag-separator": "@", plugins: [{ type: "cargo-workspace", "merge": false }], @@ -244,14 +254,16 @@ test("the repository's own configuration satisfies mandatory release validation" }); test("always-bump-patch versioning is rejected at its effective package value", () => { - failure(() => validateReleaseDefaults({ versioning: "always-bump-patch", packages: { ".": {} } })); - failure(() => validateReleaseDefaults({ packages: { ".": { versioning: "always-bump-patch" } } })); + failure(() => validateReleaseDefaults({ "bump-minor-pre-major": true, versioning: "always-bump-patch", packages: { ".": {} } })); + failure(() => validateReleaseDefaults({ "bump-minor-pre-major": true, packages: { ".": { versioning: "always-bump-patch" } } })); const error = failure(() => validateReleaseDefaults({ + "bump-minor-pre-major": true, versioning: "always-bump-patch", packages: { compliant: { versioning: "default" }, inherited: {} }, })); assert.match(error.message, /Package 'inherited'.*versioning: always-bump-patch/); validateReleaseDefaults({ + "bump-minor-pre-major": true, versioning: "always-bump-patch", packages: { ".": { versioning: "default" } }, }); @@ -259,8 +271,8 @@ test("always-bump-patch versioning is rejected at its effective package value", test("versioning preserves valid strings and own-property overrides", () => { for (const versioning of ["default", "always-bump-minor", "always-bump-major", "service-pack", "prerelease", "custom-strategy", ""]) { - validateReleaseDefaults({ versioning, packages: { ".": {} } }); - validateReleaseDefaults({ versioning: "always-bump-patch", packages: { ".": { versioning } } }); + validateReleaseDefaults({ "bump-minor-pre-major": true, versioning, packages: { ".": {} } }); + validateReleaseDefaults({ "bump-minor-pre-major": true, versioning: "always-bump-patch", packages: { ".": { versioning } } }); } for (const versioning of [null, false, 1, undefined, [], {}]) { failure(() => validateReleaseDefaults({ versioning, packages: { ".": { versioning: "default" } } })); diff --git a/tests/node/release-please/runtime-guard.test.mjs b/tests/node/release-please/runtime-guard.test.mjs index 64cd9b8..04b4604 100644 --- a/tests/node/release-please/runtime-guard.test.mjs +++ b/tests/node/release-please/runtime-guard.test.mjs @@ -19,6 +19,7 @@ test("upstream release execution loads the config response guard", () => { const snapshot = JSON.stringify({ signoff: "Release contributor", "initial-version": "0.0.1", + "bump-minor-pre-major": true, packages: { ".": {} }, }); const jsonResponse = (value) => new Response(JSON.stringify(value), { @@ -76,9 +77,9 @@ test("unrelated blobs, repositories, and API hosts pass through", async () => { }); test("preload policy always checks signoff and supports initial-version opt-out", () => { - assert.throws(() => makeGuard({}, { snapshot: JSON.stringify({ signoff: "", packages: { ".": {} } }), requireInitialVersion: false }), /signoff/); - assert.throws(() => makeGuard({}, { snapshot: JSON.stringify({ signoff: "Release contributor", packages: { ".": {} } }) }), /initial-version/); - assert.doesNotThrow(() => makeGuard({}, { snapshot: JSON.stringify({ signoff: "Release contributor", packages: { ".": {} } }), requireInitialVersion: false })); + assert.throws(() => makeGuard({}, { snapshot: JSON.stringify({ signoff: "", "bump-minor-pre-major": true, packages: { ".": {} } }), requireInitialVersion: false }), /signoff/); + assert.throws(() => makeGuard({}, { snapshot: JSON.stringify({ signoff: "Release contributor", "bump-minor-pre-major": true, packages: { ".": {} } }) }), /initial-version/); + assert.doesNotThrow(() => makeGuard({}, { snapshot: JSON.stringify({ signoff: "Release contributor", "bump-minor-pre-major": true, packages: { ".": {} } }), requireInitialVersion: false })); }); test("Node preload guards fetch before the action entry point executes", () => {