bunset 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -24
- package/package.json +1 -1
- package/src/changelog.test.ts +63 -11
- package/src/changelog.ts +15 -0
- package/src/cli.ts +34 -8
- package/src/commits.test.ts +89 -12
- package/src/commits.ts +32 -8
- package/src/config.ts +4 -0
- package/src/git.ts +14 -7
- package/src/index.ts +86 -4
- package/src/types.ts +5 -2
package/README.md
CHANGED
|
@@ -2,25 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
A zero-dependency CLI tool that automates version bumping and changelog generation for Bun workspace monorepos and single packages.
|
|
4
4
|
|
|
5
|
-
It reads git commit messages since the last tag, categorizes them by type prefix (`
|
|
5
|
+
It reads git commit messages since the last tag, categorizes them by type prefix (`feat:`, `fix:`, `test:`), bumps semantic versions, updates `CHANGELOG.md` per package, and optionally commits and tags the result.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
bun
|
|
10
|
+
bun add bunset
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Or link it globally:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
bun link
|
|
23
|
-
bunset [options]
|
|
16
|
+
bunx bunset [options]
|
|
24
17
|
```
|
|
25
18
|
|
|
26
19
|
### Options
|
|
@@ -33,11 +26,12 @@ bunset [options]
|
|
|
33
26
|
| `--all` | Update all workspace packages |
|
|
34
27
|
| `--changed` | Update only changed packages (default for workspaces) |
|
|
35
28
|
| `--no-commit` | Do not commit changes to git (commits by default) |
|
|
36
|
-
| `--tag` | Tag the commit with new version (default) |
|
|
29
|
+
| `--no-tag` | Tag the commit with new version (default) |
|
|
37
30
|
| `--per-package-tags` | Use `pkg@1.2.3` tags instead of prefixed tags |
|
|
38
|
-
| `--tag-prefix` | Tag prefix (
|
|
31
|
+
| `--tag-prefix` | Tag prefix (auto-detected from last tag, or `v` if no tags) |
|
|
39
32
|
| `--sections` | Comma-separated changelog sections (default: `feat,fix,perf`) |
|
|
40
33
|
| `--dry-run` | Preview changes without writing files, committing, or tagging |
|
|
34
|
+
| `--debug` | Show detailed inclusion/exclusion reasoning (implies `--dry-run`) |
|
|
41
35
|
| `--no-filter-by-package` | Include all commits in every package changelog (monorepo) |
|
|
42
36
|
|
|
43
37
|
When bump type or scope flags are omitted, interactive prompts will ask.
|
|
@@ -71,7 +65,22 @@ All these styles work:
|
|
|
71
65
|
|
|
72
66
|
An optional scope groups commits under a `#### scope` sub-heading within their type section in the changelog.
|
|
73
67
|
|
|
74
|
-
|
|
68
|
+
#### Breaking Changes
|
|
69
|
+
|
|
70
|
+
Append `!` before the colon (or closing bracket) to mark a commit as a breaking change, per [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/):
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
feat!: Remove old API
|
|
74
|
+
feat(auth)!: Change token format
|
|
75
|
+
[feat!] Remove old API
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A `BREAKING CHANGE:` or `BREAKING-CHANGE:` footer in the commit body is also detected.
|
|
79
|
+
|
|
80
|
+
Breaking commits are collected into a **Breaking Changes** section at the top of each changelog entry, regardless of which `--sections` are configured. If breaking changes are detected and the bump type is not `major`, a warning is printed.
|
|
81
|
+
|
|
82
|
+
#### Recognized Type Keywords
|
|
83
|
+
|
|
75
84
|
- `feat`, `feature` — listed under **Features**
|
|
76
85
|
- `fix`, `bug`, `bugfix` — listed under **Bug Fixes**
|
|
77
86
|
- `refactor` — listed under **Refactors**
|
|
@@ -82,26 +91,39 @@ Recognized type keywords:
|
|
|
82
91
|
- `build` — listed under **Build**
|
|
83
92
|
- `ops` — listed under **Ops**
|
|
84
93
|
- `chore` — listed under **Chores**
|
|
94
|
+
- `ci` — listed under **CI**
|
|
85
95
|
|
|
86
96
|
Only sections listed in `--sections` (or the `sections` config option) appear in the changelog. The default is `feat,fix,perf`.
|
|
87
97
|
|
|
88
98
|
### Config File
|
|
89
99
|
|
|
90
|
-
Place a `.bunset.toml` in your project root to set persistent defaults so you don't have to pass the same flags every time
|
|
100
|
+
Place a `.bunset.toml` in your project root to set persistent defaults so you don't have to pass the same flags every time. All fields are optional. CLI flags always take priority over config values.
|
|
91
101
|
|
|
92
102
|
```toml
|
|
93
|
-
bump = "patch"
|
|
94
|
-
scope = "changed"
|
|
95
|
-
commit = true
|
|
96
|
-
tag = true
|
|
97
|
-
per-package-tags = false
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
bump = "patch" # "patch" | "minor" | "major"
|
|
104
|
+
scope = "changed" # "all" | "changed"
|
|
105
|
+
commit = true # auto-commit (default: true)
|
|
106
|
+
tag = true # create git tags (default: true)
|
|
107
|
+
per-package-tags = false # pkg@version tags (monorepo)
|
|
108
|
+
tag-prefix = "v" # tag prefix (default: auto-detect)
|
|
109
|
+
sections = ["feat", "fix", "perf"] # changelog sections and order
|
|
110
|
+
dry-run = false # preview without writing
|
|
111
|
+
debug = false # detailed reasoning (implies dry-run)
|
|
112
|
+
filter-by-package = true # per-package filtering (monorepo)
|
|
102
113
|
```
|
|
103
114
|
|
|
104
|
-
|
|
115
|
+
| Key | Type | Default | Description |
|
|
116
|
+
|-----|------|---------|-------------|
|
|
117
|
+
| `bump` | `string` | _(prompt)_ | Version bump type: `"patch"`, `"minor"`, or `"major"`. Skips the interactive prompt when set. |
|
|
118
|
+
| `scope` | `string` | _(prompt)_ | Package scope: `"all"` or `"changed"`. Skips the interactive prompt when set (monorepo only). |
|
|
119
|
+
| `commit` | `boolean` | `true` | Whether to auto-commit the version bump and changelog changes. |
|
|
120
|
+
| `tag` | `boolean` | `true` | Whether to create git tags for released versions. |
|
|
121
|
+
| `per-package-tags` | `boolean` | `false` | Use `pkg@1.2.3` tags instead of prefixed tags. In a monorepo, packages with no matching commits are skipped entirely. |
|
|
122
|
+
| `tag-prefix` | `string` | _(auto)_ | Prefix for version tags. Auto-detected from the last git tag when not set (falls back to `"v"` if no tags exist). Set to `""` for bare version numbers, or e.g. `"project-v"` for `project-v1.2.3`. |
|
|
123
|
+
| `sections` | `string[]` | `["feat", "fix", "perf"]` | Which commit types to include in the changelog and in what order. Accepts any recognized type keyword. |
|
|
124
|
+
| `dry-run` | `boolean` | `false` | Preview all changes without writing files, committing, or tagging. |
|
|
125
|
+
| `debug` | `boolean` | `false` | Show detailed inclusion/exclusion reasoning. Implies `dry-run`. |
|
|
126
|
+
| `filter-by-package` | `boolean` | `true` | In a monorepo, only include commits that touched files within each package. Disable with `false` to include all commits in every changelog. |
|
|
105
127
|
|
|
106
128
|
## Testing
|
|
107
129
|
|
package/package.json
CHANGED
package/src/changelog.test.ts
CHANGED
|
@@ -18,6 +18,7 @@ function emptyGroups(): GroupedCommits {
|
|
|
18
18
|
build: [],
|
|
19
19
|
ops: [],
|
|
20
20
|
chore: [],
|
|
21
|
+
ci: [],
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -26,13 +27,13 @@ describe("buildChangelogEntry", () => {
|
|
|
26
27
|
const groups: GroupedCommits = {
|
|
27
28
|
...emptyGroups(),
|
|
28
29
|
feature: [
|
|
29
|
-
{ hash: "a", message: "", type: "feature", commitScope: null, files: [], description: "Add login" },
|
|
30
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, breaking: false, files: [], description: "Add login" },
|
|
30
31
|
],
|
|
31
32
|
bugfix: [
|
|
32
|
-
{ hash: "b", message: "", type: "bugfix", commitScope: null, files: [], description: "Fix crash" },
|
|
33
|
+
{ hash: "b", message: "", type: "bugfix", commitScope: null, breaking: false, files: [], description: "Fix crash" },
|
|
33
34
|
],
|
|
34
35
|
test: [
|
|
35
|
-
{ hash: "c", message: "", type: "test", commitScope: null, files: [], description: "Add tests" },
|
|
36
|
+
{ hash: "c", message: "", type: "test", commitScope: null, breaking: false, files: [], description: "Add tests" },
|
|
36
37
|
],
|
|
37
38
|
};
|
|
38
39
|
const entry = buildChangelogEntry("1.2.0", groups, [], COMMIT_TYPES);
|
|
@@ -49,10 +50,10 @@ describe("buildChangelogEntry", () => {
|
|
|
49
50
|
const groups: GroupedCommits = {
|
|
50
51
|
...emptyGroups(),
|
|
51
52
|
feature: [
|
|
52
|
-
{ hash: "a", message: "", type: "feature", commitScope: null, files: [], description: "New thing" },
|
|
53
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, breaking: false, files: [], description: "New thing" },
|
|
53
54
|
],
|
|
54
55
|
test: [
|
|
55
|
-
{ hash: "b", message: "", type: "test", commitScope: null, files: [], description: "Add tests" },
|
|
56
|
+
{ hash: "b", message: "", type: "test", commitScope: null, breaking: false, files: [], description: "Add tests" },
|
|
56
57
|
],
|
|
57
58
|
};
|
|
58
59
|
const entry = buildChangelogEntry("1.0.0", groups);
|
|
@@ -64,7 +65,7 @@ describe("buildChangelogEntry", () => {
|
|
|
64
65
|
const groups: GroupedCommits = {
|
|
65
66
|
...emptyGroups(),
|
|
66
67
|
feature: [
|
|
67
|
-
{ hash: "a", message: "", type: "feature", commitScope: null, files: [], description: "New thing" },
|
|
68
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, breaking: false, files: [], description: "New thing" },
|
|
68
69
|
],
|
|
69
70
|
};
|
|
70
71
|
const entry = buildChangelogEntry("1.0.0", groups);
|
|
@@ -77,10 +78,10 @@ describe("buildChangelogEntry", () => {
|
|
|
77
78
|
const groups: GroupedCommits = {
|
|
78
79
|
...emptyGroups(),
|
|
79
80
|
feature: [
|
|
80
|
-
{ hash: "a", message: "", type: "feature", commitScope: null, files: [], description: "Global feature" },
|
|
81
|
-
{ hash: "b", message: "", type: "feature", commitScope: "auth", files: [], description: "Add login" },
|
|
82
|
-
{ hash: "c", message: "", type: "feature", commitScope: "auth", files: [], description: "Add logout" },
|
|
83
|
-
{ hash: "d", message: "", type: "feature", commitScope: "ui", files: [], description: "New dashboard" },
|
|
81
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, breaking: false, files: [], description: "Global feature" },
|
|
82
|
+
{ hash: "b", message: "", type: "feature", commitScope: "auth", breaking: false, files: [], description: "Add login" },
|
|
83
|
+
{ hash: "c", message: "", type: "feature", commitScope: "auth", breaking: false, files: [], description: "Add logout" },
|
|
84
|
+
{ hash: "d", message: "", type: "feature", commitScope: "ui", breaking: false, files: [], description: "New dashboard" },
|
|
84
85
|
],
|
|
85
86
|
};
|
|
86
87
|
const entry = buildChangelogEntry("1.0.0", groups);
|
|
@@ -102,7 +103,7 @@ describe("buildChangelogEntry", () => {
|
|
|
102
103
|
const groups: GroupedCommits = {
|
|
103
104
|
...emptyGroups(),
|
|
104
105
|
bugfix: [
|
|
105
|
-
{ hash: "a", message: "", type: "bugfix", commitScope: "parser", files: [], description: "Fix edge case" },
|
|
106
|
+
{ hash: "a", message: "", type: "bugfix", commitScope: "parser", breaking: false, files: [], description: "Fix edge case" },
|
|
106
107
|
],
|
|
107
108
|
};
|
|
108
109
|
const entry = buildChangelogEntry("1.0.0", groups);
|
|
@@ -118,6 +119,57 @@ describe("buildChangelogEntry", () => {
|
|
|
118
119
|
expect(entry).toContain("### Updated Dependencies");
|
|
119
120
|
expect(entry).toContain("- `lodash`: 4.18.0");
|
|
120
121
|
});
|
|
122
|
+
|
|
123
|
+
test("renders Breaking Changes section first when breaking commits exist", () => {
|
|
124
|
+
const groups: GroupedCommits = {
|
|
125
|
+
...emptyGroups(),
|
|
126
|
+
feature: [
|
|
127
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, breaking: true, files: [], description: "Remove old API" },
|
|
128
|
+
{ hash: "b", message: "", type: "feature", commitScope: null, breaking: false, files: [], description: "Add new API" },
|
|
129
|
+
],
|
|
130
|
+
bugfix: [
|
|
131
|
+
{ hash: "c", message: "", type: "bugfix", commitScope: "parser", breaking: true, files: [], description: "Change return type" },
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
const entry = buildChangelogEntry("2.0.0", groups, [], ["feature", "bugfix"]);
|
|
135
|
+
expect(entry).toContain("### Breaking Changes");
|
|
136
|
+
expect(entry).toContain("- **features**: Remove old API");
|
|
137
|
+
expect(entry).toContain("- **bug fixes(parser)**: Change return type");
|
|
138
|
+
|
|
139
|
+
// Breaking Changes section appears before Features
|
|
140
|
+
const breakingIdx = entry.indexOf("### Breaking Changes");
|
|
141
|
+
const featuresIdx = entry.indexOf("### Features");
|
|
142
|
+
expect(breakingIdx).toBeLessThan(featuresIdx);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("breaking section appears even when commit type is not in sections", () => {
|
|
146
|
+
const groups: GroupedCommits = {
|
|
147
|
+
...emptyGroups(),
|
|
148
|
+
chore: [
|
|
149
|
+
{ hash: "a", message: "", type: "chore", commitScope: null, breaking: true, files: [], description: "Drop Node 14 support" },
|
|
150
|
+
],
|
|
151
|
+
feature: [
|
|
152
|
+
{ hash: "b", message: "", type: "feature", commitScope: null, breaking: false, files: [], description: "Add feature" },
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
// sections only includes "feature", not "chore"
|
|
156
|
+
const entry = buildChangelogEntry("3.0.0", groups, [], ["feature"]);
|
|
157
|
+
expect(entry).toContain("### Breaking Changes");
|
|
158
|
+
expect(entry).toContain("- **chores**: Drop Node 14 support");
|
|
159
|
+
// Chore section itself should not appear
|
|
160
|
+
expect(entry).not.toContain("### Chores");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("no Breaking Changes section when no breaking commits", () => {
|
|
164
|
+
const groups: GroupedCommits = {
|
|
165
|
+
...emptyGroups(),
|
|
166
|
+
feature: [
|
|
167
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, breaking: false, files: [], description: "Add feature" },
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
const entry = buildChangelogEntry("1.1.0", groups);
|
|
171
|
+
expect(entry).not.toContain("### Breaking Changes");
|
|
172
|
+
});
|
|
121
173
|
});
|
|
122
174
|
|
|
123
175
|
describe("writeChangelog", () => {
|
package/src/changelog.ts
CHANGED
|
@@ -12,6 +12,7 @@ const SECTION_HEADINGS: Record<CommitType, string> = {
|
|
|
12
12
|
build: "Build",
|
|
13
13
|
ops: "Ops",
|
|
14
14
|
chore: "Chores",
|
|
15
|
+
ci: "CI",
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export function buildChangelogEntry(
|
|
@@ -22,6 +23,20 @@ export function buildChangelogEntry(
|
|
|
22
23
|
): string {
|
|
23
24
|
const lines: string[] = [`## ${version}`, ""];
|
|
24
25
|
|
|
26
|
+
// Collect breaking changes across all types
|
|
27
|
+
const breakingCommits = Object.values(groups).flat().filter((c) => c.breaking);
|
|
28
|
+
if (breakingCommits.length > 0) {
|
|
29
|
+
lines.push("### Breaking Changes", "");
|
|
30
|
+
for (const c of breakingCommits) {
|
|
31
|
+
const typeLabel = c.type ? SECTION_HEADINGS[c.type].toLowerCase() : "unknown";
|
|
32
|
+
const prefix = c.commitScope
|
|
33
|
+
? `**${typeLabel}(${c.commitScope})**`
|
|
34
|
+
: `**${typeLabel}**`;
|
|
35
|
+
lines.push(`- ${prefix}: ${c.description}`);
|
|
36
|
+
}
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
for (const type of sections) {
|
|
26
41
|
const commits = groups[type];
|
|
27
42
|
if (commits.length === 0) continue;
|
package/src/cli.ts
CHANGED
|
@@ -16,9 +16,10 @@ Options:
|
|
|
16
16
|
--no-commit Do not commit the version bump and changelog
|
|
17
17
|
--no-tag Do not create git tags for released versions
|
|
18
18
|
--per-package-tags Use package-scoped tags (pkg@version) instead of prefixed
|
|
19
|
-
--tag-prefix <str> Tag prefix (
|
|
19
|
+
--tag-prefix <str> Tag prefix (auto-detected from last tag, or "v" if no tags)
|
|
20
20
|
--sections <list> Comma-separated changelog sections (default: feat,fix,perf)
|
|
21
21
|
--dry-run Preview changes without writing files, committing, or tagging
|
|
22
|
+
--debug Show detailed inclusion/exclusion reasoning (implies --dry-run)
|
|
22
23
|
--no-filter-by-package
|
|
23
24
|
Include all commits in every package changelog (monorepo)
|
|
24
25
|
--help, -h Show this help message
|
|
@@ -36,6 +37,15 @@ Commit format:
|
|
|
36
37
|
|
|
37
38
|
An optional scope groups commits under a sub-heading in the changelog.
|
|
38
39
|
|
|
40
|
+
Breaking changes (Conventional Commits 1.0.0):
|
|
41
|
+
Append ! before the colon to mark a breaking change:
|
|
42
|
+
feat!: Remove old API
|
|
43
|
+
feat(auth)!: Change token format
|
|
44
|
+
[feat!] Remove old API
|
|
45
|
+
Or include a "BREAKING CHANGE:" footer in the commit body.
|
|
46
|
+
Breaking commits are collected into a "Breaking Changes" section at the
|
|
47
|
+
top of the changelog entry. A warning is printed if the bump is not major.
|
|
48
|
+
|
|
39
49
|
Recognized type keywords:
|
|
40
50
|
feat, feature → Features
|
|
41
51
|
fix, bug, bugfix → Bug Fixes
|
|
@@ -47,12 +57,25 @@ Commit format:
|
|
|
47
57
|
build → Build
|
|
48
58
|
ops → Ops
|
|
49
59
|
chore → Chores
|
|
60
|
+
ci → CI
|
|
50
61
|
Only sections listed in --sections (or config) are included in the changelog.
|
|
51
62
|
Default sections: feat, fix, perf.
|
|
52
63
|
|
|
53
|
-
Config:
|
|
64
|
+
Config file (.bunset.toml):
|
|
54
65
|
Place a .bunset.toml in your project root to set persistent defaults.
|
|
55
|
-
CLI flags always override config values.
|
|
66
|
+
All fields are optional. CLI flags always override config values.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
bump = "patch" # "patch" | "minor" | "major"
|
|
70
|
+
scope = "changed" # "all" | "changed"
|
|
71
|
+
commit = true # auto-commit (default: true)
|
|
72
|
+
tag = true # create git tags (default: true)
|
|
73
|
+
per-package-tags = false # pkg@version tags (monorepo)
|
|
74
|
+
tag-prefix = "v" # tag prefix (default: auto-detect)
|
|
75
|
+
sections = ["feat", "fix", "perf"] # changelog sections and order
|
|
76
|
+
dry-run = false # preview without writing
|
|
77
|
+
debug = false # detailed reasoning (implies dry-run)
|
|
78
|
+
filter-by-package = true # per-package filtering (monorepo)`);
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
export function resolveOptions(
|
|
@@ -74,6 +97,7 @@ export function resolveOptions(
|
|
|
74
97
|
"dry-run": { type: "boolean", default: false },
|
|
75
98
|
"filter-by-package": { type: "boolean", default: true },
|
|
76
99
|
"tag-prefix": { type: "string" },
|
|
100
|
+
debug: { type: "boolean", default: false },
|
|
77
101
|
help: { type: "boolean", short: "h", default: false },
|
|
78
102
|
},
|
|
79
103
|
strict: true,
|
|
@@ -100,7 +124,8 @@ export function resolveOptions(
|
|
|
100
124
|
?? config.sections
|
|
101
125
|
?? DEFAULT_SECTIONS;
|
|
102
126
|
|
|
103
|
-
const
|
|
127
|
+
const debug = values.debug ? true : (config.debug ?? false);
|
|
128
|
+
const dryRun = debug || values["dry-run"] ? true : (config.dryRun ?? false);
|
|
104
129
|
|
|
105
130
|
const filterByPackage = values["filter-by-package"] === false
|
|
106
131
|
? false
|
|
@@ -108,14 +133,14 @@ export function resolveOptions(
|
|
|
108
133
|
|
|
109
134
|
const tagPrefix = values["tag-prefix"] as string | undefined
|
|
110
135
|
?? config.tagPrefix
|
|
111
|
-
??
|
|
136
|
+
?? null;
|
|
112
137
|
|
|
113
138
|
if (bump && scope) {
|
|
114
|
-
return { scope, bump, commit, tag, perPackageTags, sections, dryRun, filterByPackage, tagPrefix };
|
|
139
|
+
return { scope, bump, commit, tag, perPackageTags, sections, dryRun, filterByPackage, tagPrefix, debug };
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
return promptForMissing(
|
|
118
|
-
{ commit, tag, perPackageTags, sections, dryRun, filterByPackage, tagPrefix },
|
|
143
|
+
{ commit, tag, perPackageTags, sections, dryRun, filterByPackage, tagPrefix, debug },
|
|
119
144
|
bump,
|
|
120
145
|
scope,
|
|
121
146
|
isWs,
|
|
@@ -156,7 +181,8 @@ interface MergedDefaults {
|
|
|
156
181
|
sections: CommitType[];
|
|
157
182
|
dryRun: boolean;
|
|
158
183
|
filterByPackage: boolean;
|
|
159
|
-
tagPrefix: string;
|
|
184
|
+
tagPrefix: string | null;
|
|
185
|
+
debug: boolean;
|
|
160
186
|
}
|
|
161
187
|
|
|
162
188
|
async function promptForMissing(
|
package/src/commits.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ describe("parseCommit", () => {
|
|
|
8
8
|
const result = parseCommit("abc123", "[feat] Add user auth");
|
|
9
9
|
expect(result.type).toBe("feature");
|
|
10
10
|
expect(result.commitScope).toBeNull();
|
|
11
|
+
expect(result.breaking).toBe(false);
|
|
11
12
|
expect(result.files).toEqual([]);
|
|
12
13
|
expect(result.description).toBe("Add user auth");
|
|
13
14
|
});
|
|
@@ -204,18 +205,94 @@ describe("parseCommit", () => {
|
|
|
204
205
|
const result = parseCommit("vwx234", "[feat No closing bracket");
|
|
205
206
|
expect(result.type).toBeNull();
|
|
206
207
|
});
|
|
208
|
+
|
|
209
|
+
// Breaking change `!` marker
|
|
210
|
+
test("parses feat!: as breaking", () => {
|
|
211
|
+
const result = parseCommit("brk1", "feat!: breaking change");
|
|
212
|
+
expect(result.type).toBe("feature");
|
|
213
|
+
expect(result.breaking).toBe(true);
|
|
214
|
+
expect(result.description).toBe("breaking change");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("parses feat(auth)!: as breaking with scope", () => {
|
|
218
|
+
const result = parseCommit("brk2", "feat(auth)!: breaking");
|
|
219
|
+
expect(result.type).toBe("feature");
|
|
220
|
+
expect(result.commitScope).toBe("auth");
|
|
221
|
+
expect(result.breaking).toBe(true);
|
|
222
|
+
expect(result.description).toBe("breaking");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("parses [feat!] as breaking", () => {
|
|
226
|
+
const result = parseCommit("brk3", "[feat!] breaking");
|
|
227
|
+
expect(result.type).toBe("feature");
|
|
228
|
+
expect(result.breaking).toBe(true);
|
|
229
|
+
expect(result.description).toBe("breaking");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("parses [feat(auth)!]: as breaking with scope", () => {
|
|
233
|
+
const result = parseCommit("brk4", "[feat(auth)!]: breaking");
|
|
234
|
+
expect(result.type).toBe("feature");
|
|
235
|
+
expect(result.commitScope).toBe("auth");
|
|
236
|
+
expect(result.breaking).toBe(true);
|
|
237
|
+
expect(result.description).toBe("breaking");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("parses [feat(auth)!] as breaking with scope and no colon", () => {
|
|
241
|
+
const result = parseCommit("brk4b", "[feat(auth)!] breaking");
|
|
242
|
+
expect(result.type).toBe("feature");
|
|
243
|
+
expect(result.commitScope).toBe("auth");
|
|
244
|
+
expect(result.breaking).toBe(true);
|
|
245
|
+
expect(result.description).toBe("breaking");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("parses fix!: as breaking", () => {
|
|
249
|
+
const result = parseCommit("brk5", "fix!: desc");
|
|
250
|
+
expect(result.type).toBe("bugfix");
|
|
251
|
+
expect(result.breaking).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// BREAKING CHANGE footer in body
|
|
255
|
+
test("detects BREAKING CHANGE: in body", () => {
|
|
256
|
+
const result = parseCommit("brk6", "feat: add feature", "BREAKING CHANGE: old API removed");
|
|
257
|
+
expect(result.type).toBe("feature");
|
|
258
|
+
expect(result.breaking).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("detects BREAKING-CHANGE: in body", () => {
|
|
262
|
+
const result = parseCommit("brk7", "feat: add feature", "BREAKING-CHANGE: old API removed");
|
|
263
|
+
expect(result.type).toBe("feature");
|
|
264
|
+
expect(result.breaking).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("body without breaking footer is not breaking", () => {
|
|
268
|
+
const result = parseCommit("brk8", "feat: add feature", "Just some extra details");
|
|
269
|
+
expect(result.breaking).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ci type
|
|
273
|
+
test("parses ci: commit", () => {
|
|
274
|
+
const result = parseCommit("ci1", "ci: setup pipeline");
|
|
275
|
+
expect(result.type).toBe("ci");
|
|
276
|
+
expect(result.description).toBe("setup pipeline");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("parses [ci] commit", () => {
|
|
280
|
+
const result = parseCommit("ci2", "[ci] Add GitHub Actions");
|
|
281
|
+
expect(result.type).toBe("ci");
|
|
282
|
+
expect(result.description).toBe("Add GitHub Actions");
|
|
283
|
+
});
|
|
207
284
|
});
|
|
208
285
|
|
|
209
286
|
describe("groupCommits", () => {
|
|
210
287
|
test("groups commits by type", () => {
|
|
211
288
|
const commits: ParsedCommit[] = [
|
|
212
|
-
{ hash: "a", message: "", type: "feature", commitScope: null, description: "F1", files: [] },
|
|
213
|
-
{ hash: "b", message: "", type: "bugfix", commitScope: null, description: "B1", files: [] },
|
|
214
|
-
{ hash: "c", message: "", type: "test", commitScope: null, description: "T1", files: [] },
|
|
215
|
-
{ hash: "d", message: "", type: "feature", commitScope: "auth", description: "F2", files: [] },
|
|
216
|
-
{ hash: "e", message: "", type: "refactor", commitScope: null, description: "R1", files: [] },
|
|
217
|
-
{ hash: "f", message: "", type: "chore", commitScope: null, description: "C1", files: [] },
|
|
218
|
-
{ hash: "g", message: "", type: null, commitScope: null, description: "Ignored", files: [] },
|
|
289
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, description: "F1", breaking: false, files: [] },
|
|
290
|
+
{ hash: "b", message: "", type: "bugfix", commitScope: null, description: "B1", breaking: false, files: [] },
|
|
291
|
+
{ hash: "c", message: "", type: "test", commitScope: null, description: "T1", breaking: false, files: [] },
|
|
292
|
+
{ hash: "d", message: "", type: "feature", commitScope: "auth", description: "F2", breaking: false, files: [] },
|
|
293
|
+
{ hash: "e", message: "", type: "refactor", commitScope: null, description: "R1", breaking: false, files: [] },
|
|
294
|
+
{ hash: "f", message: "", type: "chore", commitScope: null, description: "C1", breaking: false, files: [] },
|
|
295
|
+
{ hash: "g", message: "", type: null, commitScope: null, description: "Ignored", breaking: false, files: [] },
|
|
219
296
|
];
|
|
220
297
|
const groups = groupCommits(commits);
|
|
221
298
|
expect(groups.feature).toHaveLength(2);
|
|
@@ -237,10 +314,10 @@ describe("groupCommits", () => {
|
|
|
237
314
|
|
|
238
315
|
describe("filterCommitsForPackage", () => {
|
|
239
316
|
const commits: ParsedCommit[] = [
|
|
240
|
-
{ hash: "a", message: "", type: "feature", commitScope: null, description: "A feat", files: ["packages/a/src/index.ts"] },
|
|
241
|
-
{ hash: "b", message: "", type: "bugfix", commitScope: null, description: "B fix", files: ["packages/b/src/main.ts"] },
|
|
242
|
-
{ hash: "c", message: "", type: "feature", commitScope: null, description: "Both", files: ["packages/a/lib/x.ts", "packages/b/lib/y.ts"] },
|
|
243
|
-
{ hash: "d", message: "", type: "feature", commitScope: null, description: "No files", files: [] },
|
|
317
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, description: "A feat", breaking: false, files: ["packages/a/src/index.ts"] },
|
|
318
|
+
{ hash: "b", message: "", type: "bugfix", commitScope: null, description: "B fix", breaking: false, files: ["packages/b/src/main.ts"] },
|
|
319
|
+
{ hash: "c", message: "", type: "feature", commitScope: null, description: "Both", breaking: false, files: ["packages/a/lib/x.ts", "packages/b/lib/y.ts"] },
|
|
320
|
+
{ hash: "d", message: "", type: "feature", commitScope: null, description: "No files", breaking: false, files: [] },
|
|
244
321
|
];
|
|
245
322
|
|
|
246
323
|
test("filters commits to only those touching the package", () => {
|
|
@@ -257,7 +334,7 @@ describe("filterCommitsForPackage", () => {
|
|
|
257
334
|
|
|
258
335
|
test("includes commits with no files (fallback)", () => {
|
|
259
336
|
const noFileCommits: ParsedCommit[] = [
|
|
260
|
-
{ hash: "x", message: "", type: "feature", commitScope: null, description: "X", files: [] },
|
|
337
|
+
{ hash: "x", message: "", type: "feature", commitScope: null, description: "X", breaking: false, files: [] },
|
|
261
338
|
];
|
|
262
339
|
const filtered = filterCommitsForPackage(noFileCommits, "/root/packages/a", "/root");
|
|
263
340
|
expect(filtered).toHaveLength(1);
|
package/src/commits.ts
CHANGED
|
@@ -16,6 +16,7 @@ const TYPE_MAP: Record<string, CommitType> = {
|
|
|
16
16
|
build: "build",
|
|
17
17
|
ops: "ops",
|
|
18
18
|
chore: "chore",
|
|
19
|
+
ci: "ci",
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
export const COMMIT_TYPES: CommitType[] = [
|
|
@@ -29,34 +30,57 @@ export const COMMIT_TYPES: CommitType[] = [
|
|
|
29
30
|
"build",
|
|
30
31
|
"ops",
|
|
31
32
|
"chore",
|
|
33
|
+
"ci",
|
|
32
34
|
];
|
|
33
35
|
|
|
34
36
|
export function normalizeType(keyword: string): CommitType | null {
|
|
35
37
|
return TYPE_MAP[keyword.toLowerCase()] ?? null;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// Matches: [type(scope)] desc, [type]: desc, type(scope)
|
|
39
|
-
const COMMIT_PATTERN = /^\[([^\]]+)\]:?\s*(.*)$|^(\w+(?:\([^)]*\))
|
|
40
|
+
// Matches: [type(scope)!] desc, [type]: desc, type(scope)!: desc, type: desc
|
|
41
|
+
const COMMIT_PATTERN = /^\[([^\]]+)\]:?\s*(.*)$|^(\w+(?:\([^)]*\))?!?):\s+(.*)$/;
|
|
40
42
|
const SCOPE_PATTERN = /^(\w+)\(([^)]*)\)$/;
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
const BREAKING_FOOTER_PATTERN = /^BREAKING[ -]CHANGE:\s/m;
|
|
45
|
+
|
|
46
|
+
export function parseCommit(hash: string, message: string, body?: string): ParsedCommit {
|
|
43
47
|
const trimmed = message.trim();
|
|
44
48
|
const match = COMMIT_PATTERN.exec(trimmed);
|
|
45
49
|
|
|
46
50
|
if (!match) {
|
|
47
|
-
return { hash, message: trimmed, type: null, commitScope: null, description: trimmed, files: [] };
|
|
51
|
+
return { hash, message: trimmed, type: null, commitScope: null, description: trimmed, breaking: false, files: [] };
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
let raw = (match[1] ?? match[3])!.trim();
|
|
51
55
|
const description = (match[2] ?? match[4])!.trim();
|
|
52
56
|
|
|
57
|
+
// Detect trailing `!` breaking marker
|
|
58
|
+
let breaking = false;
|
|
59
|
+
if (raw.endsWith("!")) {
|
|
60
|
+
breaking = true;
|
|
61
|
+
raw = raw.slice(0, -1);
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
const scopeMatch = SCOPE_PATTERN.exec(raw);
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
let keyword: string;
|
|
66
|
+
let commitScope: string | null;
|
|
67
|
+
|
|
68
|
+
if (scopeMatch) {
|
|
69
|
+
keyword = scopeMatch[1]!;
|
|
70
|
+
commitScope = scopeMatch[2]!.trim();
|
|
71
|
+
} else {
|
|
72
|
+
keyword = raw;
|
|
73
|
+
commitScope = null;
|
|
74
|
+
}
|
|
56
75
|
|
|
57
76
|
const type = TYPE_MAP[keyword.toLowerCase()] ?? null;
|
|
58
77
|
|
|
59
|
-
|
|
78
|
+
// Scan body for BREAKING CHANGE footer
|
|
79
|
+
if (!breaking && body && BREAKING_FOOTER_PATTERN.test(body)) {
|
|
80
|
+
breaking = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { hash, message: trimmed, type, commitScope, description, breaking, files: [] };
|
|
60
84
|
}
|
|
61
85
|
|
|
62
86
|
export function filterCommitsForPackage(
|
package/src/config.ts
CHANGED
|
@@ -49,6 +49,10 @@ export async function loadConfig(
|
|
|
49
49
|
config.tagPrefix = raw["tag-prefix"];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
if (typeof raw.debug === "boolean") {
|
|
53
|
+
config.debug = raw.debug;
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
if (Array.isArray(raw.sections)) {
|
|
53
57
|
const sections: CommitType[] = [];
|
|
54
58
|
for (const s of raw.sections) {
|
package/src/git.ts
CHANGED
|
@@ -12,24 +12,31 @@ export async function getLastTag(cwd: string): Promise<string | null> {
|
|
|
12
12
|
export async function getCommitsSince(
|
|
13
13
|
cwd: string,
|
|
14
14
|
sinceRef: string | null,
|
|
15
|
-
): Promise<{ hash: string; message: string }[]> {
|
|
15
|
+
): Promise<{ hash: string; message: string; body: string }[]> {
|
|
16
|
+
const fmt = "%x00%H%x00%s%x00%b";
|
|
16
17
|
let result;
|
|
17
18
|
if (sinceRef) {
|
|
18
19
|
result =
|
|
19
|
-
await $`git -C ${cwd} log ${sinceRef}..HEAD --pretty=format
|
|
20
|
+
await $`git -C ${cwd} log ${sinceRef}..HEAD --pretty=format:${fmt} --no-merges`.quiet();
|
|
20
21
|
} else {
|
|
21
22
|
result =
|
|
22
|
-
await $`git -C ${cwd} log --pretty=format
|
|
23
|
+
await $`git -C ${cwd} log --pretty=format:${fmt} --no-merges`.quiet();
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const text = result.text().trim();
|
|
26
27
|
if (!text) return [];
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
-
|
|
29
|
+
const commits: { hash: string; message: string; body: string }[] = [];
|
|
30
|
+
// Split on the leading \x00 that starts each record
|
|
31
|
+
const records = text.split("\x00");
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
// records[0] is empty (before the first \x00), then groups of 3: hash, subject, body
|
|
34
|
+
for (let i = 1; i + 2 < records.length; i += 3) {
|
|
35
|
+
commits.push({
|
|
36
|
+
hash: records[i]!.trim(),
|
|
37
|
+
message: records[i + 1]!.trim(),
|
|
38
|
+
body: records[i + 2]!.trim(),
|
|
39
|
+
});
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
return commits;
|
package/src/index.ts
CHANGED
|
@@ -29,25 +29,87 @@ const isWs = await isWorkspace(cwd);
|
|
|
29
29
|
const config = await loadConfig(cwd);
|
|
30
30
|
const options = await resolveOptions(isWs, config);
|
|
31
31
|
|
|
32
|
+
const dbg = options.debug;
|
|
33
|
+
function debug(msg: string): void {
|
|
34
|
+
if (dbg) console.log(`[debug] ${msg}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (dbg) {
|
|
38
|
+
console.log("--- Debug Mode (dry-run implied) ---\n");
|
|
39
|
+
debug(`cwd: ${cwd}`);
|
|
40
|
+
debug(`workspace: ${isWs}`);
|
|
41
|
+
debug(`config loaded: ${JSON.stringify(config)}`);
|
|
42
|
+
debug(`resolved options: ${JSON.stringify(options)}`);
|
|
43
|
+
console.log("");
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
const allPackages = await getAllPackages(cwd);
|
|
33
47
|
const lastTag = await getLastTag(cwd);
|
|
34
48
|
const rawCommits = await getCommitsSince(cwd, lastTag);
|
|
35
49
|
|
|
50
|
+
// Resolve tagPrefix: explicit value wins, otherwise infer from last tag
|
|
51
|
+
let tagPrefix: string;
|
|
52
|
+
if (options.tagPrefix !== null) {
|
|
53
|
+
tagPrefix = options.tagPrefix;
|
|
54
|
+
debug(`tag prefix explicit: "${tagPrefix}"`);
|
|
55
|
+
} else if (lastTag) {
|
|
56
|
+
const semverMatch = lastTag.match(/\d+\.\d+\.\d+/);
|
|
57
|
+
tagPrefix = semverMatch ? lastTag.slice(0, semverMatch.index) : "v";
|
|
58
|
+
debug(`tag prefix auto-detected: "${tagPrefix}" (from tag: ${lastTag})`);
|
|
59
|
+
} else {
|
|
60
|
+
tagPrefix = "v";
|
|
61
|
+
debug(`tag prefix default: "${tagPrefix}" (no previous tags found)`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
debug(`last tag: ${lastTag ?? "(none)"}`);
|
|
65
|
+
debug(`raw commits since tag: ${rawCommits.length}`);
|
|
66
|
+
|
|
36
67
|
if (rawCommits.length === 0) {
|
|
37
68
|
console.log("No commits found since last tag. Nothing to do.");
|
|
38
69
|
process.exit(0);
|
|
39
70
|
}
|
|
40
71
|
|
|
41
|
-
const parsed = rawCommits.map((c) => parseCommit(c.hash, c.message));
|
|
72
|
+
const parsed = rawCommits.map((c) => parseCommit(c.hash, c.message, c.body));
|
|
73
|
+
|
|
74
|
+
if (dbg) {
|
|
75
|
+
console.log("");
|
|
76
|
+
debug("--- Parsed commits ---");
|
|
77
|
+
for (const c of parsed) {
|
|
78
|
+
const typeStr = c.type ?? "UNRECOGNIZED";
|
|
79
|
+
const scopeStr = c.commitScope ? `(${c.commitScope})` : "";
|
|
80
|
+
const included = c.type && options.sections.includes(c.type) ? "INCLUDED" : "EXCLUDED";
|
|
81
|
+
debug(` ${c.hash.slice(0, 7)} ${typeStr}${scopeStr}: ${c.description} → ${included} (section: ${c.type ?? "none"})`);
|
|
82
|
+
}
|
|
83
|
+
console.log("");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Warn if breaking changes detected with non-major bump
|
|
87
|
+
const hasBreaking = parsed.some((c) => c.breaking);
|
|
88
|
+
if (hasBreaking && options.bump !== "major") {
|
|
89
|
+
console.log(`\u26a0 Breaking changes detected but bump is "${options.bump}" (not "major").`);
|
|
90
|
+
if (dbg) {
|
|
91
|
+
for (const c of parsed.filter((c) => c.breaking)) {
|
|
92
|
+
debug(` breaking: ${c.hash.slice(0, 7)} ${c.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
console.log("");
|
|
96
|
+
}
|
|
42
97
|
|
|
43
98
|
// In a monorepo with filtering, fetch the file list for each commit
|
|
44
99
|
const shouldFilter = isWs && options.filterByPackage;
|
|
100
|
+
debug(`per-package filtering: ${shouldFilter ? "enabled" : "disabled"}`);
|
|
45
101
|
if (shouldFilter) {
|
|
46
102
|
await Promise.all(
|
|
47
103
|
parsed.map(async (commit) => {
|
|
48
104
|
commit.files = await getCommitFiles(cwd, commit.hash);
|
|
49
105
|
}),
|
|
50
106
|
);
|
|
107
|
+
if (dbg) {
|
|
108
|
+
for (const c of parsed) {
|
|
109
|
+
debug(` ${c.hash.slice(0, 7)} files: ${c.files.length > 0 ? c.files.join(", ") : "(none)"}`);
|
|
110
|
+
}
|
|
111
|
+
console.log("");
|
|
112
|
+
}
|
|
51
113
|
}
|
|
52
114
|
|
|
53
115
|
const globalGroups = groupCommits(parsed);
|
|
@@ -62,6 +124,8 @@ let packages =
|
|
|
62
124
|
? await getChangedPackages(cwd, allPackages, lastTag)
|
|
63
125
|
: allPackages;
|
|
64
126
|
|
|
127
|
+
debug(`scope: ${options.scope}, packages to process: ${packages.map((p) => p.name).join(", ") || "(none)"}`);
|
|
128
|
+
|
|
65
129
|
if (packages.length === 0) {
|
|
66
130
|
console.log("No changed packages found. Nothing to do.");
|
|
67
131
|
process.exit(0);
|
|
@@ -81,7 +145,7 @@ function packageHasChanges(groups: GroupedCommits): boolean {
|
|
|
81
145
|
}
|
|
82
146
|
|
|
83
147
|
if (options.dryRun) {
|
|
84
|
-
console.log("--- Dry Run ---\n");
|
|
148
|
+
if (!dbg) console.log("--- Dry Run ---\n");
|
|
85
149
|
|
|
86
150
|
const tags: string[] = [];
|
|
87
151
|
|
|
@@ -89,6 +153,24 @@ if (options.dryRun) {
|
|
|
89
153
|
const groups = getPackageGroups(pkg, parsed);
|
|
90
154
|
const hasChanges = packageHasChanges(groups);
|
|
91
155
|
|
|
156
|
+
if (dbg) {
|
|
157
|
+
debug(`--- Package: ${pkg.name} ---`);
|
|
158
|
+
debug(` path: ${pkg.path}`);
|
|
159
|
+
debug(` current version: ${pkg.version ?? "0.0.0"}`);
|
|
160
|
+
for (const section of options.sections) {
|
|
161
|
+
const commits = groups[section];
|
|
162
|
+
if (commits.length > 0) {
|
|
163
|
+
debug(` ${section}: ${commits.length} commit(s)`);
|
|
164
|
+
for (const c of commits) {
|
|
165
|
+
debug(` - ${c.hash.slice(0, 7)} ${c.description}${c.commitScope ? ` (scope: ${c.commitScope})` : ""}`);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
debug(` ${section}: 0 commits`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
debug(` has matching commits: ${hasChanges}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
92
174
|
if (!hasChanges && options.perPackageTags) {
|
|
93
175
|
console.log(`${pkg.name}: no matching commits, skipping.`);
|
|
94
176
|
continue;
|
|
@@ -117,7 +199,7 @@ if (options.dryRun) {
|
|
|
117
199
|
if (options.perPackageTags) {
|
|
118
200
|
tags.push(`${pkg.name}@${newVersion}`);
|
|
119
201
|
} else {
|
|
120
|
-
tags.push(`${
|
|
202
|
+
tags.push(`${tagPrefix}${newVersion}`);
|
|
121
203
|
}
|
|
122
204
|
}
|
|
123
205
|
}
|
|
@@ -177,7 +259,7 @@ for (const pkg of packages) {
|
|
|
177
259
|
if (options.perPackageTags) {
|
|
178
260
|
tags.push(`${pkg.name}@${newVersion}`);
|
|
179
261
|
} else {
|
|
180
|
-
tags.push(`${
|
|
262
|
+
tags.push(`${tagPrefix}${newVersion}`);
|
|
181
263
|
}
|
|
182
264
|
}
|
|
183
265
|
}
|
package/src/types.ts
CHANGED
|
@@ -10,7 +10,8 @@ export type CommitType =
|
|
|
10
10
|
| "docs"
|
|
11
11
|
| "build"
|
|
12
12
|
| "ops"
|
|
13
|
-
| "chore"
|
|
13
|
+
| "chore"
|
|
14
|
+
| "ci";
|
|
14
15
|
|
|
15
16
|
export interface CliOptions {
|
|
16
17
|
scope: PackageScope;
|
|
@@ -21,7 +22,8 @@ export interface CliOptions {
|
|
|
21
22
|
sections: CommitType[];
|
|
22
23
|
dryRun: boolean;
|
|
23
24
|
filterByPackage: boolean;
|
|
24
|
-
tagPrefix: string;
|
|
25
|
+
tagPrefix: string | null;
|
|
26
|
+
debug: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface ParsedCommit {
|
|
@@ -30,6 +32,7 @@ export interface ParsedCommit {
|
|
|
30
32
|
type: CommitType | null;
|
|
31
33
|
commitScope: string | null;
|
|
32
34
|
description: string;
|
|
35
|
+
breaking: boolean;
|
|
33
36
|
files: string[];
|
|
34
37
|
}
|
|
35
38
|
|