From 9a9e9bb9e617132fc5242af55514f74739014e03 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:25:02 +0300 Subject: [PATCH] ci(release): add deps type and skip ci version bumps --- .commitlintrc.json | 22 ++++- .release-it.cjs | 3 +- CONTRIBUTING.md | 23 +++-- renovate.json | 4 +- tests/tooling/dependency-release.test.mjs | 104 ++++++++++++++++++++++ tests/tooling/release-it.test.mjs | 17 +++- 6 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 tests/tooling/dependency-release.test.mjs diff --git a/.commitlintrc.json b/.commitlintrc.json index 0df1d25..15cf559 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1,5 +1,25 @@ { "extends": [ "@commitlint/config-conventional" - ] + ], + "rules": { + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "deps", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test" + ] + ] + } } diff --git a/.release-it.cjs b/.release-it.cjs index c57ee41..7d5239b 100644 --- a/.release-it.cjs +++ b/.release-it.cjs @@ -97,6 +97,7 @@ function getContributors() { const types = new Map([ ["feat", "✨ Features"], ["fix", "πŸ› Bug Fixed"], + ["deps", "πŸ“¦ Dependencies"], ["perf", "⚑️ Performance Improvements"], ["refactor", "πŸ› οΈ Refactoring"], ["docs", "πŸ“ Documentation"], @@ -157,7 +158,7 @@ function whatBump(commits, currentVersion = pkg.version) { isMinor = true; } - if (["fix", "perf", "refactor", "ci"].includes(type)) { + if (["fix", "deps", "perf", "refactor"].includes(type)) { isPatch = true; } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e22e0ca..0250d34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,18 +59,21 @@ We enforce Conventional Commits via commitlint. The general format is: Common types used in this repository: - `feat`: a new feature - `fix`: a bug fix +- `deps`: dependency updates, including browser API types; produces a patch release - `perf`: performance improvement - `refactor`: code change that neither fixes a bug nor adds a feature - `docs`: documentation only changes - `test`: adding or updating tests -- `build`: changes that affect the build system or external dependencies -- `ci`: changes to CI configuration or scripts +- `build`: changes to the build system; use `deps` for dependency updates that need a release +- `ci`: changes to CI configuration or scripts; does not trigger a release by itself - `chore`: other changes that don’t modify src or test files - `revert`: reverts a previous commit Examples: - `feat(storage): add namespace support to getAll()` - `fix(secure): handle invalid cipher text in decrypt()` +- `deps: update dependencies` +- `deps(chrome): update chrome types` - `perf(mono): reduce allocations in shallowEqual()` - `docs: expand README with React adapter examples` - `ci: update release pipeline` @@ -87,12 +90,13 @@ Breaking changes: Version bumps are derived from commit history via `@release-it/conventional-changelog` and our policy: - MAJOR (`x.0.0`) β€” a breaking commit when the current version is `1.0.0` or newer. - MINOR (`0.y.0`) β€” `feat` and `revert` commits, plus breaking commits while the package is on `0.x`. -- PATCH (`0.0.z`) β€” `fix`, `perf`, `refactor`, `ci`. -- No bump by default β€” `docs`, `test`, `chore`, `build` (these do not trigger an automatic release by themselves). +- PATCH (`0.0.z`) β€” `fix`, `deps`, `perf`, `refactor`. +- No bump by default β€” `docs`, `test`, `chore`, `build`, `ci` (these do not trigger an automatic release by themselves). Notes: - Both the `type!:` syntax and a `BREAKING CHANGE:`/`BREAKING-CHANGE:` footer use the same breaking policy. - If multiple types are present, the highest applicable level wins. +- `deps` commits appear under **πŸ“¦ Dependencies** in both `CHANGELOG.md` and GitHub release notes. - Only visible types appear in the generated CHANGELOG; some meta types are hidden from release notes. ## Release process (release-it + GitHub Actions) @@ -137,10 +141,13 @@ producing a lockfile that npm 10 rejects during `npm ci`. Declaring parser 6.x k tooling and lets npm install commitlint's parser 5.x separately. After changing this dependency, verify a lockfile-only update in a clean directory followed by `npm ci`, as well as the release tooling tests. -The bot uses `fix(deps)` commits, so an approved dependency-only update produces a **patch** release of Storage, -including Chrome-types-only updates and updates to a new upstream major version. Keep the `fix(deps): ...` title when -squashing; do not change it to `chore`, `feat`, or add a breaking marker. Other commits included in a release continue -to follow the normal release policy. Review upstream compatibility before merging. +The bot uses `deps: update dependencies` commits. An approved dependency-only update produces a **patch** release +of Storage, including Chrome-types-only updates and updates to a new upstream major version. These commits appear +under **πŸ“¦ Dependencies** in the changelog and GitHub release notes. Keep the `deps: ...` title when squashing; +do not change it to `chore`, `feat`, or add a breaking marker. Other commits included in a release continue to follow +the normal release policy. Review upstream compatibility before merging. + +Changes to this policy take effect after merging them from `develop` into `main`, where Renovate reads its configuration. PR creation or updates automatically trigger CI through the GitHub App's token. **Merging is always manual.** Failed checks leave the PR open; they do not update `main` or publish a release. Fix the incompatibility, rerun a transiently diff --git a/renovate.json b/renovate.json index b6bd15a..f7837ab 100644 --- a/renovate.json +++ b/renovate.json @@ -17,8 +17,8 @@ "prCreation": "immediate", "rebaseWhen": "behind-base-branch", "semanticCommits": "enabled", - "semanticCommitType": "fix", - "semanticCommitScope": "deps", + "semanticCommitType": "deps", + "semanticCommitScope": null, "separateMajorMinor": false, "separateMinorPatch": false, "lockFileMaintenance": { diff --git a/tests/tooling/dependency-release.test.mjs b/tests/tooling/dependency-release.test.mjs new file mode 100644 index 0000000..1cefe86 --- /dev/null +++ b/tests/tooling/dependency-release.test.mjs @@ -0,0 +1,104 @@ +import {spawnSync} from "node:child_process"; +import {mkdtempSync, readFileSync, rmSync} from "node:fs"; +import {tmpdir} from "node:os"; +import {join} from "node:path"; +import {text} from "node:stream/consumers"; +import {fileURLToPath} from "node:url"; + +import conventionalCommitConfig from "@commitlint/config-conventional"; +import {expect, test} from "@jest/globals"; +import {ConventionalChangelog} from "conventional-changelog"; +import {CommitParser} from "conventional-commits-parser"; + +import createReleaseConfig from "../../.release-it.cjs"; + +const root = fileURLToPath(new URL("../../", import.meta.url)); +const renovate = JSON.parse(readFileSync(join(root, "renovate.json"), "utf8")); +const commitlint = JSON.parse(readFileSync(join(root, ".commitlintrc.json"), "utf8")); +const release = createReleaseConfig(); +const changelogOptions = release.plugins["@release-it/conventional-changelog"]; +const parse = message => new CommitParser(changelogOptions.parserOpts).parse(message); +const scope = renovate.semanticCommitScope ? `(${renovate.semanticCommitScope})` : ""; +const updateMessage = `${renovate.semanticCommitType}${scope}: update dependencies`; + +const lint = message => spawnSync(process.execPath, [join(root, "node_modules/@commitlint/cli/cli.js"), "--strict"], { + cwd: root, + input: message, + encoding: "utf8", + timeout: 10000, +}); + +test("Renovate's dependency commit passes commitlint and triggers a patch release", () => { + expect(updateMessage).toBe("deps: update dependencies"); + const result = lint(updateMessage); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + expect(changelogOptions.whatBump([parse(updateMessage)], "0.8.2")).toEqual({level: 2}); +}); + +test("commitlint preserves conventional types and rejects unknown types", () => { + expect(commitlint.rules["type-enum"][2]).toEqual(expect.arrayContaining(conventionalCommitConfig.rules["type-enum"][2])); + const result = lint("depz: update dependencies"); + expect(result.status).not.toBe(0); + expect(result.stdout).toContain("type-enum"); +}); + +test.each([ + ["deps(chrome): update chrome types", "0.8.2", 2], + ["deps(chrome): update chrome types", "1.2.3", 2], + ["deps!: require Chrome 140", "0.8.2", 1], + ["deps!: require Chrome 140", "1.2.3", 0], + ["deps: update browser\n\nBREAKING CHANGE: require Chrome 140", "1.2.3", 0], +])("applies release policy to %s at version %s", (message, version, level) => { + expect(changelogOptions.whatBump([parse(message)], version)).toEqual({level}); +}); + +test("renders dependency updates in Dependencies in the changelog and GitHub release notes", async () => { + const cwd = mkdtempSync(join(tmpdir(), "storage-dependency-release-")); + // Keep parent Git hooks and signing settings out of this isolated history. + const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_"))); + + const git = (...args) => { + const result = spawnSync("git", [ + "-c", `core.hooksPath=${join(cwd, "no-hooks")}`, + "-c", "commit.gpgSign=false", + "-c", "tag.gpgSign=false", + "-c", "user.name=Release Test", + "-c", "user.email=release-test@example.invalid", + ...args, + ], {cwd, env, encoding: "utf8", timeout: 10000}); + + if (result.status !== 0) { + throw new Error(`Fixture git ${args[0]} failed: ${result.error?.message ?? result.stderr}`); + } + }; + + try { + git("init", "--quiet"); + git("commit", "--allow-empty", "-m", "chore: initial release"); + git("tag", "v0.8.2"); + git("commit", "--allow-empty", "-m", updateMessage); + git("commit", "--allow-empty", "-m", "deps(chrome): update chrome types"); + git("commit", "--allow-empty", "-m", "fix(storage): fix stale values"); + + const changelog = await text(new ConventionalChangelog(cwd) + .loadPreset(changelogOptions.preset) + .context({...changelogOptions.context, version: "0.8.3", contributors: []}) + .commits({from: "v0.8.2"}, changelogOptions.parserOpts) + .writer(changelogOptions.writerOpts) + .writeStream()); + + const sections = changelog.split(/^### /m); + const dependencies = sections.find(section => section.startsWith("πŸ“¦ Dependencies\n")); + const fixes = sections.find(section => section.startsWith("πŸ› Bug Fixed\n")); + expect(dependencies).toContain("update dependencies"); + expect(dependencies).toContain("**chrome:** update chrome types"); + expect(dependencies).not.toContain("fix stale values"); + expect(fixes).toContain("fix stale values"); + expect(fixes).not.toContain("update dependencies"); + expect(changelog).not.toContain("🧩 Other"); + expect(release.github.releaseNotes({changelog})).toBe(changelog); + } finally { + rmSync(cwd, {recursive: true, force: true}); + } +}); diff --git a/tests/tooling/release-it.test.mjs b/tests/tooling/release-it.test.mjs index 88cdee8..02ceba2 100644 --- a/tests/tooling/release-it.test.mjs +++ b/tests/tooling/release-it.test.mjs @@ -40,15 +40,26 @@ describe("release-it version policy", () => { expect(whatBump([{type}], "0.6.0")).toEqual({level: 1}); }); - test.each(["fix", "perf", "refactor", "ci"])("uses a patch bump for %s", type => { + test.each(["fix", "deps", "perf", "refactor"])("uses a patch bump for %s", type => { expect(whatBump([{type}], "0.6.0")).toEqual({level: 2}); }); test("uses the highest non-breaking bump", () => { - expect(whatBump([{type: "fix"}, {type: "feat"}], "0.6.0")).toEqual({level: 1}); + expect(whatBump([{type: "fix"}, {type: "deps"}, {type: "feat"}], "0.6.0")).toEqual({level: 1}); }); - test.each(["docs", "test", "chore", "build"])("does not release for %s alone", type => { + test.each(["docs", "test", "chore", "build", "ci"])("does not release for %s alone", type => { expect(whatBump([{type}], "0.6.0")).toBeNull(); }); + + test("does not release for CI and other maintenance commits", () => { + expect(whatBump([{type: "ci"}, {type: "docs"}, {type: "chore"}], "1.2.3")).toBeNull(); + }); + + test.each([ + ["deps", 2], + ["feat", 1], + ])("CI commits do not change the release level of %s", (type, level) => { + expect(whatBump([{type: "ci"}, {type}], "1.2.3")).toEqual({level}); + }); });