Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .commitlintrc.json
Original file line number Diff line number Diff line change
@@ -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"
]
]
}
}
3 changes: 2 additions & 1 deletion .release-it.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function getContributors() {
const types = new Map([
["feat", "✨ Features"],
["fix", "🐛 Bug Fixed"],
["deps", "📦 Dependencies"],
["perf", "⚡️ Performance Improvements"],
["refactor", "🛠️ Refactoring"],
["docs", "📝 Documentation"],
Expand Down Expand Up @@ -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;
}
}
Expand Down
23 changes: 15 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
104 changes: 104 additions & 0 deletions tests/tooling/dependency-release.test.mjs
Original file line number Diff line number Diff line change
@@ -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});
}
});
17 changes: 14 additions & 3 deletions tests/tooling/release-it.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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});
});
});