bunset 1.0.10 → 1.0.11
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/CHANGELOG.md +7 -0
- package/README.md +6 -0
- package/package.json +3 -3
- package/src/changelog.test.ts +32 -1
- package/src/changelog.ts +11 -0
- package/src/cli.ts +8 -2
- package/src/config.ts +4 -0
- package/src/git.ts +17 -0
- package/src/index.ts +91 -15
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ bunx bunset [options]
|
|
|
33
33
|
| `--dry-run` | Preview changes without writing files, committing, or tagging |
|
|
34
34
|
| `--debug` | Show detailed inclusion/exclusion reasoning (implies `--dry-run`) |
|
|
35
35
|
| `--no-filter-by-package` | Include all commits in every package changelog (monorepo) |
|
|
36
|
+
| `--push` | Push the release commit and tags to the remote |
|
|
37
|
+
| `--release` | Create a GitHub release per tag with the changelog entry as the release notes (requires `--push` and the `gh` CLI) |
|
|
36
38
|
|
|
37
39
|
When bump type or scope flags are omitted, interactive prompts will ask.
|
|
38
40
|
|
|
@@ -107,6 +109,8 @@ tag = true # create git tags (default: true)
|
|
|
107
109
|
per-package-tags = false # pkg@version tags (monorepo)
|
|
108
110
|
tag-prefix = "v" # tag prefix (default: auto-detect)
|
|
109
111
|
sections = "all" # changelog sections and order ("all" or array)
|
|
112
|
+
push = false # push after tagging (default: false)
|
|
113
|
+
release = false # create GitHub release per tag (default: false)
|
|
110
114
|
dry-run = false # preview without writing
|
|
111
115
|
debug = false # detailed reasoning (implies dry-run)
|
|
112
116
|
filter-by-package = true # per-package filtering (monorepo)
|
|
@@ -124,6 +128,8 @@ filter-by-package = true # per-package filtering (monorepo)
|
|
|
124
128
|
| `dry-run` | `boolean` | `false` | Preview all changes without writing files, committing, or tagging. |
|
|
125
129
|
| `debug` | `boolean` | `false` | Show detailed inclusion/exclusion reasoning. Implies `dry-run`. |
|
|
126
130
|
| `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. |
|
|
131
|
+
| `push` | `boolean` | `false` | Push the release commit and tags to the remote after tagging. |
|
|
132
|
+
| `release` | `boolean` | `false` | Create a GitHub release for each tag with the changelog entry as the release notes. Requires `push = true` and the [`gh` CLI](https://cli.github.com/). When multiple packages share a tag, their entries are combined under per-package headings. |
|
|
127
133
|
|
|
128
134
|
## Testing
|
|
129
135
|
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunset",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"bunset": "src/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@types/bun": "
|
|
10
|
+
"@types/bun": "1.3.13"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"typescript": "^
|
|
13
|
+
"typescript": "^6.0.3"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"bun": "^1.3"
|
package/src/changelog.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { buildChangelogEntry, writeChangelog } from "./changelog.ts";
|
|
2
|
+
import { buildChangelogEntry, buildReleaseNotes, writeChangelog } from "./changelog.ts";
|
|
3
3
|
import { COMMIT_TYPES } from "./commits.ts";
|
|
4
4
|
import type { GroupedCommits } from "./types.ts";
|
|
5
5
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
@@ -202,3 +202,34 @@ describe("writeChangelog", () => {
|
|
|
202
202
|
expect(idx1).toBeLessThan(idx2);
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
|
+
|
|
206
|
+
describe("buildReleaseNotes", () => {
|
|
207
|
+
test("returns empty string for no entries", () => {
|
|
208
|
+
expect(buildReleaseNotes([])).toBe("");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("strips the version heading from a single entry", () => {
|
|
212
|
+
const entry = "## 1.2.3\n\n### Features\n\n- Add login\n";
|
|
213
|
+
expect(buildReleaseNotes([{ pkgName: "pkg-a", entry }])).toBe(
|
|
214
|
+
"### Features\n\n- Add login\n",
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("combines multiple entries under per-package headings", () => {
|
|
219
|
+
const result = buildReleaseNotes([
|
|
220
|
+
{ pkgName: "pkg-a", entry: "## 1.2.3\n\n### Features\n\n- A\n" },
|
|
221
|
+
{ pkgName: "pkg-b", entry: "## 1.2.3\n\n### Bug Fixes\n\n- B\n" },
|
|
222
|
+
]);
|
|
223
|
+
expect(result).toContain("## pkg-a");
|
|
224
|
+
expect(result).toContain("## pkg-b");
|
|
225
|
+
expect(result).toContain("### Features");
|
|
226
|
+
expect(result).toContain("### Bug Fixes");
|
|
227
|
+
expect(result).not.toMatch(/^## 1\.2\.3/m);
|
|
228
|
+
expect(result.indexOf("## pkg-a")).toBeLessThan(result.indexOf("## pkg-b"));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("handles entries without a leading version heading", () => {
|
|
232
|
+
const entry = "### Features\n\n- Plain entry\n";
|
|
233
|
+
expect(buildReleaseNotes([{ pkgName: "pkg-a", entry }])).toBe(entry);
|
|
234
|
+
});
|
|
235
|
+
});
|
package/src/changelog.ts
CHANGED
|
@@ -84,6 +84,17 @@ export function buildChangelogEntry(
|
|
|
84
84
|
return lines.join("\n");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export function buildReleaseNotes(
|
|
88
|
+
entries: { pkgName: string; entry: string }[],
|
|
89
|
+
): string {
|
|
90
|
+
const stripVersion = (e: string) => e.replace(/^## [^\n]*\n+/, "");
|
|
91
|
+
if (entries.length === 0) return "";
|
|
92
|
+
if (entries.length === 1) return stripVersion(entries[0]!.entry);
|
|
93
|
+
return entries
|
|
94
|
+
.map(({ pkgName, entry }) => `## ${pkgName}\n\n${stripVersion(entry)}`)
|
|
95
|
+
.join("\n\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
export async function writeChangelog(
|
|
88
99
|
dir: string,
|
|
89
100
|
entry: string,
|
package/src/cli.ts
CHANGED
|
@@ -19,6 +19,8 @@ Options:
|
|
|
19
19
|
--tag-prefix <str> Tag prefix (auto-detected from last tag, or "v" if no tags)
|
|
20
20
|
--sections <list> Comma-separated changelog sections, or "all" (default: all)
|
|
21
21
|
--push Push commit and tags to remote after tagging
|
|
22
|
+
--release Create a GitHub release per tag with the changelog entry
|
|
23
|
+
as the release notes (requires --push and the gh CLI)
|
|
22
24
|
--dry-run Preview changes without writing files, committing, or tagging
|
|
23
25
|
--debug Show detailed inclusion/exclusion reasoning (implies --dry-run)
|
|
24
26
|
--no-filter-by-package
|
|
@@ -75,6 +77,7 @@ Config file (.bunset.toml):
|
|
|
75
77
|
tag-prefix = "v" # tag prefix (default: auto-detect)
|
|
76
78
|
sections = "all" # changelog sections and order ("all" or array)
|
|
77
79
|
push = false # push after tagging (default: false)
|
|
80
|
+
release = false # create GitHub release (default: false)
|
|
78
81
|
dry-run = false # preview without writing
|
|
79
82
|
debug = false # detailed reasoning (implies dry-run)
|
|
80
83
|
filter-by-package = true # per-package filtering (monorepo)`);
|
|
@@ -99,6 +102,7 @@ export function resolveOptions(
|
|
|
99
102
|
"per-package-tags": { type: "boolean", default: false },
|
|
100
103
|
sections: { type: "string" },
|
|
101
104
|
push: { type: "boolean", default: false },
|
|
105
|
+
release: { type: "boolean", default: false },
|
|
102
106
|
"dry-run": { type: "boolean", default: false },
|
|
103
107
|
"filter-by-package": { type: "boolean", default: true },
|
|
104
108
|
"tag-prefix": { type: "string" },
|
|
@@ -138,6 +142,7 @@ export function resolveOptions(
|
|
|
138
142
|
?? DEFAULT_SECTIONS;
|
|
139
143
|
|
|
140
144
|
const push = values.push ? true : (config.push ?? false);
|
|
145
|
+
const release = values.release ? true : (config.release ?? false);
|
|
141
146
|
const debug = values.debug ? true : (config.debug ?? false);
|
|
142
147
|
const dryRun = debug || values["dry-run"] ? true : (config.dryRun ?? false);
|
|
143
148
|
|
|
@@ -150,11 +155,11 @@ export function resolveOptions(
|
|
|
150
155
|
?? null;
|
|
151
156
|
|
|
152
157
|
if (bump && scope && commit !== null && tag !== null) {
|
|
153
|
-
return { scope, bump, commit, tag, perPackageTags, sections, dryRun, filterByPackage, tagPrefix, push, debug };
|
|
158
|
+
return { scope, bump, commit, tag, perPackageTags, sections, dryRun, filterByPackage, tagPrefix, push, release, debug };
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
return promptForMissing(
|
|
157
|
-
{ commit, tag, perPackageTags, sections, dryRun, filterByPackage, tagPrefix, push, debug },
|
|
162
|
+
{ commit, tag, perPackageTags, sections, dryRun, filterByPackage, tagPrefix, push, release, debug },
|
|
158
163
|
bump,
|
|
159
164
|
scope,
|
|
160
165
|
isWs,
|
|
@@ -198,6 +203,7 @@ interface MergedDefaults {
|
|
|
198
203
|
filterByPackage: boolean;
|
|
199
204
|
tagPrefix: string | null;
|
|
200
205
|
push: boolean;
|
|
206
|
+
release: boolean;
|
|
201
207
|
debug: boolean;
|
|
202
208
|
}
|
|
203
209
|
|
package/src/config.ts
CHANGED
package/src/git.ts
CHANGED
|
@@ -89,3 +89,20 @@ export async function gitPush(
|
|
|
89
89
|
await $`git -C ${cwd} push --tags`.quiet();
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
export async function createGithubRelease(
|
|
94
|
+
cwd: string,
|
|
95
|
+
tag: string,
|
|
96
|
+
notes: string,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const proc = Bun.spawn(
|
|
99
|
+
["gh", "release", "create", tag, "--title", tag, "--notes-file", "-"],
|
|
100
|
+
{ cwd, stdin: "pipe", stdout: "inherit", stderr: "inherit" },
|
|
101
|
+
);
|
|
102
|
+
proc.stdin.write(notes);
|
|
103
|
+
await proc.stdin.end();
|
|
104
|
+
const exitCode = await proc.exited;
|
|
105
|
+
if (exitCode !== 0) {
|
|
106
|
+
throw new Error(`gh release create exited with code ${exitCode}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { $ } from "bun";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import { resolveOptions } from "./cli.ts";
|
|
5
6
|
import { loadConfig } from "./config.ts";
|
|
6
7
|
import {
|
|
@@ -8,13 +9,14 @@ import {
|
|
|
8
9
|
groupCommits,
|
|
9
10
|
filterCommitsForPackage,
|
|
10
11
|
} from "./commits.ts";
|
|
11
|
-
import { buildChangelogEntry, writeChangelog } from "./changelog.ts";
|
|
12
|
+
import { buildChangelogEntry, buildReleaseNotes, writeChangelog } from "./changelog.ts";
|
|
12
13
|
import {
|
|
13
14
|
getLastTag,
|
|
14
15
|
getCommitsSince,
|
|
15
16
|
getCommitFiles,
|
|
16
17
|
commitAndTag,
|
|
17
18
|
gitPush,
|
|
19
|
+
createGithubRelease,
|
|
18
20
|
} from "./git.ts";
|
|
19
21
|
import { getUpdatedDependencies } from "./deps.ts";
|
|
20
22
|
import {
|
|
@@ -71,6 +73,21 @@ if (rawCommits.length === 0) {
|
|
|
71
73
|
process.exit(1);
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
if (options.release && !options.push) {
|
|
77
|
+
console.error("--release requires --push (the tag must be on the remote).");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (options.release && !options.tag) {
|
|
82
|
+
console.error("--release requires tagging to be enabled.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.release && !options.commit) {
|
|
87
|
+
console.error("--release requires --commit (tag/push/release run inside the commit step).");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
const parsed = rawCommits.map((c) => parseCommit(c.hash, c.message, c.body));
|
|
75
92
|
|
|
76
93
|
if (dbg) {
|
|
@@ -141,11 +158,23 @@ function packageHasChanges(groups: GroupedCommits): boolean {
|
|
|
141
158
|
return options.sections.some((type) => groups[type].length > 0);
|
|
142
159
|
}
|
|
143
160
|
|
|
144
|
-
// When
|
|
161
|
+
// When scope is "all" in a workspace, also update the workspace root's package.json
|
|
162
|
+
const rootPackageJsonPath = join(cwd, "package.json");
|
|
163
|
+
const updateRoot =
|
|
164
|
+
isWs &&
|
|
165
|
+
options.scope === "all" &&
|
|
166
|
+
!packages.some((p) => p.packageJsonPath === rootPackageJsonPath);
|
|
167
|
+
const rootCurrentVersion = updateRoot
|
|
168
|
+
? ((await Bun.file(rootPackageJsonPath).json()).version ?? "0.0.0")
|
|
169
|
+
: null;
|
|
170
|
+
debug(`update root package.json: ${updateRoot}${updateRoot ? ` (current: ${rootCurrentVersion})` : ""}`);
|
|
171
|
+
|
|
172
|
+
// When using shared tags, sync all packages (and root, if updating) to the same target version
|
|
145
173
|
let targetVersion: string | null = null;
|
|
146
|
-
if (!options.perPackageTags && packages.length > 1) {
|
|
147
|
-
const
|
|
148
|
-
|
|
174
|
+
if (!options.perPackageTags && (packages.length > 1 || updateRoot)) {
|
|
175
|
+
const candidateVersions = packages.map((p) => p.version ?? "0.0.0");
|
|
176
|
+
if (updateRoot && rootCurrentVersion) candidateVersions.push(rootCurrentVersion);
|
|
177
|
+
const maxVersion = candidateVersions.reduce((max, v) => {
|
|
149
178
|
const [mj1, mn1, p1] = parseSemver(max);
|
|
150
179
|
const [mj2, mn2, p2] = parseSemver(v);
|
|
151
180
|
if (mj2 > mj1) return v;
|
|
@@ -162,6 +191,8 @@ if (options.dryRun) {
|
|
|
162
191
|
|
|
163
192
|
const tags: string[] = [];
|
|
164
193
|
const filesToCommit: string[] = [];
|
|
194
|
+
const tagEntries = new Map<string, { pkgName: string; entry: string }[]>();
|
|
195
|
+
let anyPackageUpdated = false;
|
|
165
196
|
|
|
166
197
|
for (const pkg of packages) {
|
|
167
198
|
const groups = getPackageGroups(pkg, parsed);
|
|
@@ -194,6 +225,7 @@ if (options.dryRun) {
|
|
|
194
225
|
const newVersion = targetVersion ?? bumpVersion(oldVersion, options.bump);
|
|
195
226
|
console.log(`${pkg.name}: ${oldVersion} → ${newVersion}`);
|
|
196
227
|
filesToCommit.push(pkg.packageJsonPath, `${pkg.path}/CHANGELOG.md`);
|
|
228
|
+
anyPackageUpdated = true;
|
|
197
229
|
|
|
198
230
|
const updatedDeps = await getUpdatedDependencies(
|
|
199
231
|
cwd,
|
|
@@ -211,14 +243,22 @@ if (options.dryRun) {
|
|
|
211
243
|
console.log(entry);
|
|
212
244
|
|
|
213
245
|
if (options.tag) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
246
|
+
const tag = options.perPackageTags
|
|
247
|
+
? `${pkg.name}@${newVersion}`
|
|
248
|
+
: `${tagPrefix}${newVersion}`;
|
|
249
|
+
tags.push(tag);
|
|
250
|
+
const list = tagEntries.get(tag) ?? [];
|
|
251
|
+
list.push({ pkgName: pkg.name, entry });
|
|
252
|
+
tagEntries.set(tag, list);
|
|
219
253
|
}
|
|
220
254
|
}
|
|
221
255
|
|
|
256
|
+
if (updateRoot && rootCurrentVersion && anyPackageUpdated) {
|
|
257
|
+
const newRootVersion = targetVersion ?? bumpVersion(rootCurrentVersion, options.bump);
|
|
258
|
+
console.log(`(workspace root): ${rootCurrentVersion} → ${newRootVersion}`);
|
|
259
|
+
filesToCommit.push(rootPackageJsonPath);
|
|
260
|
+
}
|
|
261
|
+
|
|
222
262
|
const uniqueTags = [...new Set(tags)];
|
|
223
263
|
|
|
224
264
|
filesToCommit.push(`${cwd}/bun.lock`);
|
|
@@ -244,6 +284,14 @@ if (options.dryRun) {
|
|
|
244
284
|
console.log("Would push commit and tags to remote.");
|
|
245
285
|
}
|
|
246
286
|
|
|
287
|
+
if (options.release) {
|
|
288
|
+
for (const tag of uniqueTags) {
|
|
289
|
+
const entries = tagEntries.get(tag) ?? [];
|
|
290
|
+
console.log(`\nWould create GitHub release ${tag} with notes:`);
|
|
291
|
+
console.log(buildReleaseNotes(entries));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
247
295
|
console.log(`\nFiles that would be modified (${filesToCommit.length}):`);
|
|
248
296
|
for (const f of filesToCommit) {
|
|
249
297
|
console.log(` ${f}`);
|
|
@@ -253,6 +301,8 @@ if (options.dryRun) {
|
|
|
253
301
|
|
|
254
302
|
const tags: string[] = [];
|
|
255
303
|
const changedFiles: string[] = [];
|
|
304
|
+
const tagEntries = new Map<string, { pkgName: string; entry: string }[]>();
|
|
305
|
+
let anyPackageUpdated = false;
|
|
256
306
|
|
|
257
307
|
for (const pkg of packages) {
|
|
258
308
|
const groups = getPackageGroups(pkg, parsed);
|
|
@@ -269,6 +319,7 @@ for (const pkg of packages) {
|
|
|
269
319
|
: await updatePackageVersion(pkg.packageJsonPath, options.bump);
|
|
270
320
|
changedFiles.push(pkg.packageJsonPath);
|
|
271
321
|
console.log(`${pkg.name}: ${oldVersion} → ${newVersion}`);
|
|
322
|
+
anyPackageUpdated = true;
|
|
272
323
|
|
|
273
324
|
const updatedDeps = await getUpdatedDependencies(
|
|
274
325
|
cwd,
|
|
@@ -285,14 +336,24 @@ for (const pkg of packages) {
|
|
|
285
336
|
changedFiles.push(`${pkg.path}/CHANGELOG.md`);
|
|
286
337
|
|
|
287
338
|
if (options.tag) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
339
|
+
const tag = options.perPackageTags
|
|
340
|
+
? `${pkg.name}@${newVersion}`
|
|
341
|
+
: `${tagPrefix}${newVersion}`;
|
|
342
|
+
tags.push(tag);
|
|
343
|
+
const list = tagEntries.get(tag) ?? [];
|
|
344
|
+
list.push({ pkgName: pkg.name, entry });
|
|
345
|
+
tagEntries.set(tag, list);
|
|
293
346
|
}
|
|
294
347
|
}
|
|
295
348
|
|
|
349
|
+
if (updateRoot && anyPackageUpdated) {
|
|
350
|
+
const { oldVersion, newVersion } = targetVersion
|
|
351
|
+
? await setPackageVersion(rootPackageJsonPath, targetVersion)
|
|
352
|
+
: await updatePackageVersion(rootPackageJsonPath, options.bump);
|
|
353
|
+
changedFiles.push(rootPackageJsonPath);
|
|
354
|
+
console.log(`(workspace root): ${oldVersion} → ${newVersion}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
296
357
|
const uniqueTags = [...new Set(tags)];
|
|
297
358
|
|
|
298
359
|
if (changedFiles.length === 0) {
|
|
@@ -320,6 +381,21 @@ if (options.commit) {
|
|
|
320
381
|
await gitPush(cwd, uniqueTags);
|
|
321
382
|
console.log("Pushed to remote.");
|
|
322
383
|
}
|
|
384
|
+
|
|
385
|
+
if (options.release) {
|
|
386
|
+
for (const tag of uniqueTags) {
|
|
387
|
+
const entries = tagEntries.get(tag) ?? [];
|
|
388
|
+
const notes = buildReleaseNotes(entries);
|
|
389
|
+
try {
|
|
390
|
+
await createGithubRelease(cwd, tag, notes);
|
|
391
|
+
console.log(`Created GitHub release: ${tag}`);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.warn(
|
|
394
|
+
`⚠ Failed to create GitHub release ${tag}: ${(err as Error).message}`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
323
399
|
}
|
|
324
400
|
|
|
325
401
|
console.log("Done.");
|