@varlock/bumpy 0.0.2 → 1.1.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/.claude-plugin/plugin.json +2 -2
- package/config-schema.json +327 -0
- package/dist/add-BmNL5VwL.mjs +323 -0
- package/dist/{ai-CQhUyHAG.mjs → ai-sMYUf3lP.mjs} +21 -4
- package/dist/{apply-release-plan-D6TSrcwX.mjs → apply-release-plan-0kH62jhu.mjs} +35 -26
- package/dist/bump-file-DVqR3k67.mjs +157 -0
- package/dist/{changelog-github-Du62krXi.mjs → changelog-github-DkACMj0j.mjs} +23 -21
- package/dist/check-BjWF6SJm.mjs +65 -0
- package/dist/{ci-D6LQbR38.mjs → ci-DY58ugIi.mjs} +138 -91
- package/dist/{ci-setup-C6FlOfW5.mjs → ci-setup-BQwktQEe.mjs} +3 -3
- package/dist/cli.mjs +36 -41
- package/dist/commit-message-BwsowSds.mjs +23 -0
- package/dist/{config-BkwIEaQg.mjs → config-B-Qg3DZH.mjs} +30 -24
- package/dist/fs-DYR2XuFE.mjs +81 -0
- package/dist/generate-DX46X-rW.mjs +186 -0
- package/dist/{git-CGHVXXKw.mjs → git-YDedMddc.mjs} +54 -2
- package/dist/index.d.mts +68 -39
- package/dist/index.mjs +9 -9
- package/dist/init-DkTPs_WQ.mjs +196 -0
- package/dist/{names-Ck8cun7B.mjs → names-C-TuOPbd.mjs} +1 -1
- package/dist/{js-yaml-DpZfOoD4.mjs → package-manager-Clsmr-9r.mjs} +79 -1
- package/dist/picomatch-DMmqYjgq.mjs +1870 -0
- package/dist/{publish-D_7RqEYL.mjs → publish-CGB4TIKD.mjs} +26 -25
- package/dist/{publish-pipeline-ChnqW8nR.mjs → publish-pipeline-CXuqce1N.mjs} +24 -19
- package/dist/release-plan-JNir7bSM.mjs +264 -0
- package/dist/{semver-BTzYh8vc.mjs → semver-BJzWIuRz.mjs} +13 -3
- package/dist/{shell-Dj7JRD_q.mjs → shell-CY7OD48z.mjs} +20 -2
- package/dist/{status--Q8yAxQ4.mjs → status-EGYqULJg.mjs} +26 -22
- package/dist/{version-cAUkfYPx.mjs → version-BcfidiVX.mjs} +23 -22
- package/dist/{workspace-CxEKakDm.mjs → workspace-DWXlwcH4.mjs} +3 -3
- package/package.json +16 -1
- package/skills/add-change/SKILL.md +18 -14
- package/dist/add-BjyVIUlr.mjs +0 -175
- package/dist/changeset-UCZdSRDv.mjs +0 -108
- package/dist/check-jIwike9F.mjs +0 -51
- package/dist/fs-0AtnPUUe.mjs +0 -51
- package/dist/generate-Btrsn1qi.mjs +0 -177
- package/dist/init-B0q3wEQW.mjs +0 -22
- package/dist/migrate-CfQNwD0T.mjs +0 -121
- package/dist/package-manager-DcI5TdDE.mjs +0 -80
- package/dist/release-plan-BEzwApuK.mjs +0 -173
- /package/dist/{clack-CDRCHrC-.mjs → clack-C6bVkGxf.mjs} +0 -0
- /package/dist/{dep-graph-E-9-eQ2J.mjs → dep-graph-DiLeAhl9.mjs} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { n as log } from "./logger-C2dEe5Su.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { d as writeText, n as exists, t as ensureDir } from "./fs-DYR2XuFE.mjs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
//#region src/commands/ai.ts
|
|
7
8
|
const SUPPORTED_TARGETS = [
|
|
9
|
+
"claude",
|
|
8
10
|
"opencode",
|
|
9
11
|
"cursor",
|
|
10
12
|
"codex"
|
|
@@ -13,13 +15,16 @@ async function aiSetupCommand(rootDir, opts) {
|
|
|
13
15
|
const target = opts.target;
|
|
14
16
|
if (!target) {
|
|
15
17
|
log.error(`Please specify a target: bumpy ai setup --target <${SUPPORTED_TARGETS.join("|")}>`);
|
|
16
|
-
log.dim(" Claude Code users: install the plugin instead — claude plugin install @varlock/bumpy");
|
|
17
18
|
process.exit(1);
|
|
18
19
|
}
|
|
19
20
|
if (!SUPPORTED_TARGETS.includes(target)) {
|
|
20
21
|
log.error(`Unknown target: "${target}". Supported: ${SUPPORTED_TARGETS.join(", ")}`);
|
|
21
22
|
process.exit(1);
|
|
22
23
|
}
|
|
24
|
+
if (target === "claude") {
|
|
25
|
+
setupClaude();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
23
28
|
const promptContent = await loadPromptTemplate();
|
|
24
29
|
switch (target) {
|
|
25
30
|
case "opencode":
|
|
@@ -36,6 +41,18 @@ async function aiSetupCommand(rootDir, opts) {
|
|
|
36
41
|
async function loadPromptTemplate() {
|
|
37
42
|
return (await readFile(resolve(dirname(fileURLToPath(import.meta.url)), "../../skills/add-change/SKILL.md"), "utf-8")).replace(/^---\n[\s\S]*?\n---\n\n?/, "");
|
|
38
43
|
}
|
|
44
|
+
/** Install as a Claude Code plugin */
|
|
45
|
+
function setupClaude() {
|
|
46
|
+
log.step("Installing Claude Code plugin...");
|
|
47
|
+
try {
|
|
48
|
+
execSync("claude plugin install @varlock/bumpy", { stdio: "inherit" });
|
|
49
|
+
log.success("Installed Claude Code plugin");
|
|
50
|
+
log.dim(" Usage: /bumpy:add-change in Claude Code");
|
|
51
|
+
} catch {
|
|
52
|
+
log.error("Failed to install Claude Code plugin. Make sure `claude` is installed and available in your PATH.");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
39
56
|
/** Install as an OpenCode custom command */
|
|
40
57
|
async function setupOpenCode(rootDir, promptContent) {
|
|
41
58
|
const commandsDir = resolve(rootDir, ".opencode", "commands");
|
|
@@ -43,7 +60,7 @@ async function setupOpenCode(rootDir, promptContent) {
|
|
|
43
60
|
await ensureDir(commandsDir);
|
|
44
61
|
if (await exists(targetPath)) log.warn(".opencode/commands/add-bumpy-change.md already exists — overwriting");
|
|
45
62
|
await writeText(targetPath, `---
|
|
46
|
-
description: Create a bumpy
|
|
63
|
+
description: Create a bumpy bump file to track package version bumps
|
|
47
64
|
---
|
|
48
65
|
|
|
49
66
|
${promptContent}`);
|
|
@@ -58,7 +75,7 @@ async function setupCursor(rootDir, promptContent) {
|
|
|
58
75
|
await ensureDir(rulesDir);
|
|
59
76
|
if (await exists(targetPath)) log.warn(".cursor/rules/add-bumpy-change.mdc already exists — overwriting");
|
|
60
77
|
await writeText(targetPath, `---
|
|
61
|
-
description: Create a bumpy
|
|
78
|
+
description: Create a bumpy bump file to track package version bumps
|
|
62
79
|
globs:
|
|
63
80
|
alwaysApply: false
|
|
64
81
|
---
|
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import { n as log } from "./logger-C2dEe5Su.mjs";
|
|
2
|
-
import { a as readJson, c as
|
|
3
|
-
import {
|
|
4
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { a as readJson, c as updateJsonFields, d as writeText, i as listFiles, l as updateJsonNestedField, n as exists, o as readText, s as removeFile } from "./fs-DYR2XuFE.mjs";
|
|
3
|
+
import { r as getBumpyDir } from "./config-B-Qg3DZH.mjs";
|
|
4
|
+
import { relative, resolve } from "node:path";
|
|
5
|
+
import { realpathSync } from "node:fs";
|
|
5
6
|
//#region src/core/changelog.ts
|
|
6
7
|
/** Default formatter — version heading, date, bullet points */
|
|
7
8
|
const defaultFormatter = (ctx) => {
|
|
8
|
-
const { release,
|
|
9
|
+
const { release, bumpFiles, date } = ctx;
|
|
9
10
|
const lines = [];
|
|
10
11
|
lines.push(`## ${release.newVersion}`);
|
|
11
12
|
lines.push("");
|
|
12
13
|
lines.push(`_${date}_`);
|
|
13
14
|
lines.push("");
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
for (const
|
|
17
|
-
const summaryLines =
|
|
15
|
+
const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id));
|
|
16
|
+
if (relevantBumpFiles.length > 0) {
|
|
17
|
+
for (const bf of relevantBumpFiles) if (bf.summary) {
|
|
18
|
+
const summaryLines = bf.summary.split("\n");
|
|
18
19
|
lines.push(`- ${summaryLines[0]}`);
|
|
19
20
|
for (let i = 1; i < summaryLines.length; i++) if (summaryLines[i].trim()) lines.push(` ${summaryLines[i]}`);
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
if (release.isDependencyBump &&
|
|
23
|
-
if (release.isCascadeBump && !release.isDependencyBump &&
|
|
23
|
+
if (release.isDependencyBump && relevantBumpFiles.length === 0) lines.push("- Updated dependencies");
|
|
24
|
+
if (release.isCascadeBump && !release.isDependencyBump && relevantBumpFiles.length === 0) lines.push("- Version bump via cascade rule");
|
|
24
25
|
lines.push("");
|
|
25
26
|
return lines.join("\n");
|
|
26
27
|
};
|
|
27
28
|
const BUILTIN_FORMATTERS = {
|
|
28
29
|
default: defaultFormatter,
|
|
29
30
|
github: async () => {
|
|
30
|
-
const { createGithubFormatter } = await import("./changelog-github-
|
|
31
|
+
const { createGithubFormatter } = await import("./changelog-github-DkACMj0j.mjs");
|
|
31
32
|
return createGithubFormatter();
|
|
32
33
|
}
|
|
33
34
|
};
|
|
@@ -37,20 +38,24 @@ const BUILTIN_FORMATTERS = {
|
|
|
37
38
|
*/
|
|
38
39
|
async function loadFormatter(changelog, rootDir) {
|
|
39
40
|
const [name, options] = Array.isArray(changelog) ? changelog : [changelog, {}];
|
|
41
|
+
if (name === "github") {
|
|
42
|
+
const { createGithubFormatter } = await import("./changelog-github-DkACMj0j.mjs");
|
|
43
|
+
return createGithubFormatter(options);
|
|
44
|
+
}
|
|
40
45
|
if (typeof name === "string" && BUILTIN_FORMATTERS[name]) {
|
|
41
46
|
const builtin = BUILTIN_FORMATTERS[name];
|
|
42
47
|
if (typeof builtin === "function" && builtin.length === 0) return builtin();
|
|
43
48
|
return builtin;
|
|
44
49
|
}
|
|
45
|
-
if (name === "github") {
|
|
46
|
-
const { createGithubFormatter } = await import("./changelog-github-Du62krXi.mjs");
|
|
47
|
-
return createGithubFormatter(options);
|
|
48
|
-
}
|
|
49
50
|
if (typeof name === "string") try {
|
|
50
51
|
let modulePath;
|
|
51
52
|
if (name.startsWith(".")) {
|
|
52
53
|
modulePath = resolve(rootDir, name);
|
|
53
|
-
|
|
54
|
+
try {
|
|
55
|
+
modulePath = realpathSync(modulePath);
|
|
56
|
+
} catch {}
|
|
57
|
+
const rel = relative(realpathSync(rootDir), modulePath);
|
|
58
|
+
if (rel.startsWith("..") || resolve("/", rel) === resolve("/")) throw new Error(`Changelog formatter path "${name}" resolves outside the project root`);
|
|
54
59
|
} else modulePath = name;
|
|
55
60
|
const mod = await import(modulePath);
|
|
56
61
|
const exported = mod.default || mod.changelogFormatter;
|
|
@@ -68,10 +73,10 @@ async function loadFormatter(changelog, rootDir) {
|
|
|
68
73
|
return defaultFormatter;
|
|
69
74
|
}
|
|
70
75
|
/** Generate a changelog entry using the configured formatter */
|
|
71
|
-
async function generateChangelogEntry(release,
|
|
76
|
+
async function generateChangelogEntry(release, bumpFiles, formatter = defaultFormatter, date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]) {
|
|
72
77
|
return formatter({
|
|
73
78
|
release,
|
|
74
|
-
|
|
79
|
+
bumpFiles,
|
|
75
80
|
date
|
|
76
81
|
});
|
|
77
82
|
}
|
|
@@ -87,14 +92,14 @@ function prependToChangelog(existingContent, newEntry) {
|
|
|
87
92
|
}
|
|
88
93
|
//#endregion
|
|
89
94
|
//#region src/core/apply-release-plan.ts
|
|
90
|
-
/** Apply the release plan: bump versions, update changelogs, delete
|
|
95
|
+
/** Apply the release plan: bump versions, update changelogs, delete bump files */
|
|
91
96
|
async function applyReleasePlan(releasePlan, packages, rootDir, config) {
|
|
92
97
|
const releaseMap = new Map(releasePlan.releases.map((r) => [r.name, r]));
|
|
93
|
-
const formatter = await loadFormatter(config.changelog, rootDir);
|
|
98
|
+
const formatter = config.changelog !== false ? await loadFormatter(config.changelog, rootDir) : null;
|
|
94
99
|
for (const release of releasePlan.releases) {
|
|
95
100
|
const pkgJsonPath = resolve(packages.get(release.name).dir, "package.json");
|
|
96
101
|
const pkgJson = await readJson(pkgJsonPath);
|
|
97
|
-
|
|
102
|
+
await updateJsonFields(pkgJsonPath, { version: release.newVersion });
|
|
98
103
|
for (const depField of [
|
|
99
104
|
"dependencies",
|
|
100
105
|
"devDependencies",
|
|
@@ -106,19 +111,23 @@ async function applyReleasePlan(releasePlan, packages, rootDir, config) {
|
|
|
106
111
|
for (const [depName, range] of Object.entries(deps)) {
|
|
107
112
|
const depRelease = releaseMap.get(depName);
|
|
108
113
|
if (!depRelease) continue;
|
|
109
|
-
|
|
114
|
+
await updateJsonNestedField(pkgJsonPath, depField, depName, updateRange(range, depRelease.newVersion));
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
|
-
await writeJson(pkgJsonPath, pkgJson);
|
|
113
117
|
}
|
|
114
|
-
for (const release of releasePlan.releases) {
|
|
118
|
+
if (formatter) for (const release of releasePlan.releases) {
|
|
115
119
|
const changelogPath = resolve(packages.get(release.name).dir, "CHANGELOG.md");
|
|
116
|
-
const entry = await generateChangelogEntry(release, releasePlan.
|
|
120
|
+
const entry = await generateChangelogEntry(release, releasePlan.bumpFiles, formatter);
|
|
117
121
|
let existingContent = "";
|
|
118
122
|
if (await exists(changelogPath)) existingContent = await readText(changelogPath);
|
|
119
123
|
await writeText(changelogPath, prependToChangelog(existingContent, entry));
|
|
120
124
|
}
|
|
121
|
-
|
|
125
|
+
const bumpyDir = getBumpyDir(rootDir);
|
|
126
|
+
const allBumpFiles = await listFiles(bumpyDir, ".md");
|
|
127
|
+
for (const file of allBumpFiles) {
|
|
128
|
+
if (file === "README.md") continue;
|
|
129
|
+
await removeFile(resolve(bumpyDir, file));
|
|
130
|
+
}
|
|
122
131
|
}
|
|
123
132
|
/** Update a version range to include a new version, preserving the range prefix */
|
|
124
133
|
function updateRange(range, newVersion) {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { n as log } from "./logger-C2dEe5Su.mjs";
|
|
2
|
+
import { d as writeText, i as listFiles, o as readText } from "./fs-DYR2XuFE.mjs";
|
|
3
|
+
import { r as getBumpyDir } from "./config-B-Qg3DZH.mjs";
|
|
4
|
+
import { i as jsYaml } from "./package-manager-Clsmr-9r.mjs";
|
|
5
|
+
import { s as tryRunArgs } from "./shell-CY7OD48z.mjs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
//#region src/core/bump-file.ts
|
|
8
|
+
const VALID_BUMP_TYPES = new Set([
|
|
9
|
+
"major",
|
|
10
|
+
"minor",
|
|
11
|
+
"patch",
|
|
12
|
+
"none"
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Reject package names that contain characters which could cause injection
|
|
16
|
+
* when used in git tags, markdown, URLs, or shell-quoted strings.
|
|
17
|
+
* Intentionally permissive — we don't enforce npm naming rules because
|
|
18
|
+
* bumpy may be used with other registries or non-JS packages.
|
|
19
|
+
*/
|
|
20
|
+
function validatePackageName(name) {
|
|
21
|
+
if (!name || name.length > 214) return false;
|
|
22
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) return false;
|
|
23
|
+
if (/[<>"'`&;|$(){}[\]\\!#%\s]/.test(name)) return false;
|
|
24
|
+
if (name.startsWith("-")) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
/** Read all bump files from .bumpy/ directory, sorted by git creation order */
|
|
28
|
+
async function readBumpFiles(rootDir) {
|
|
29
|
+
const dir = getBumpyDir(rootDir);
|
|
30
|
+
const files = await listFiles(dir, ".md");
|
|
31
|
+
const bumpFiles = [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
if (file === "README.md") continue;
|
|
34
|
+
const bf = await parseBumpFileFromPath(resolve(dir, file));
|
|
35
|
+
if (bf) bumpFiles.push(bf);
|
|
36
|
+
}
|
|
37
|
+
const creationOrder = getBumpFileCreationOrder(rootDir);
|
|
38
|
+
if (creationOrder.size > 0) bumpFiles.sort((a, b) => {
|
|
39
|
+
return (creationOrder.get(a.id) ?? Infinity) - (creationOrder.get(b.id) ?? Infinity) || a.id.localeCompare(b.id);
|
|
40
|
+
});
|
|
41
|
+
return bumpFiles;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Use `git log` to get the commit timestamp when each bump file was first added.
|
|
45
|
+
* Returns a map of bump file ID → unix timestamp (seconds).
|
|
46
|
+
*/
|
|
47
|
+
function getBumpFileCreationOrder(rootDir) {
|
|
48
|
+
const order = /* @__PURE__ */ new Map();
|
|
49
|
+
const result = tryRunArgs([
|
|
50
|
+
"git",
|
|
51
|
+
"log",
|
|
52
|
+
"--diff-filter=A",
|
|
53
|
+
"--format=%at",
|
|
54
|
+
"--name-only",
|
|
55
|
+
"--",
|
|
56
|
+
".bumpy/*.md"
|
|
57
|
+
], { cwd: rootDir });
|
|
58
|
+
if (!result) return order;
|
|
59
|
+
let currentTimestamp = 0;
|
|
60
|
+
for (const line of result.split("\n")) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (!trimmed) continue;
|
|
63
|
+
if (/^\d+$/.test(trimmed)) currentTimestamp = parseInt(trimmed, 10);
|
|
64
|
+
else if (trimmed.startsWith(".bumpy/") && trimmed.endsWith(".md")) {
|
|
65
|
+
const id = trimmed.replace(/^\.bumpy\//, "").replace(/\.md$/, "");
|
|
66
|
+
order.set(id, currentTimestamp);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return order;
|
|
70
|
+
}
|
|
71
|
+
/** Parse a single bump file from disk */
|
|
72
|
+
async function parseBumpFileFromPath(filePath) {
|
|
73
|
+
return parseBumpFile(await readText(filePath), fileToId(filePath));
|
|
74
|
+
}
|
|
75
|
+
/** Parse bump file content (for testing) */
|
|
76
|
+
function parseBumpFile(content, id) {
|
|
77
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
78
|
+
if (!match) return null;
|
|
79
|
+
const frontmatter = match[1];
|
|
80
|
+
const summary = match[2].trim();
|
|
81
|
+
const parsed = jsYaml.load(frontmatter);
|
|
82
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
83
|
+
const releases = [];
|
|
84
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
85
|
+
if (!validatePackageName(name)) {
|
|
86
|
+
log.warn(`Skipping invalid package name in bump file "${id}": ${name}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
if (!VALID_BUMP_TYPES.has(value)) {
|
|
91
|
+
log.warn(`Skipping unknown bump type "${value}" for ${name} in bump file "${id}"`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
releases.push({
|
|
95
|
+
name,
|
|
96
|
+
type: value
|
|
97
|
+
});
|
|
98
|
+
} else if (value && typeof value === "object") {
|
|
99
|
+
const obj = value;
|
|
100
|
+
if (!VALID_BUMP_TYPES.has(obj.bump)) {
|
|
101
|
+
log.warn(`Skipping unknown bump type "${obj.bump}" for ${name} in bump file "${id}"`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const release = {
|
|
105
|
+
name,
|
|
106
|
+
type: obj.bump,
|
|
107
|
+
cascade: obj.cascade || {}
|
|
108
|
+
};
|
|
109
|
+
releases.push(release);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (releases.length === 0) return null;
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
releases,
|
|
116
|
+
summary
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Write a bump file */
|
|
120
|
+
async function writeBumpFile(rootDir, filename, releases, summary) {
|
|
121
|
+
const filePath = resolve(getBumpyDir(rootDir), `${filename}.md`);
|
|
122
|
+
const frontmatter = {};
|
|
123
|
+
for (const release of releases) if ("cascade" in release && Object.keys(release.cascade).length > 0) frontmatter[release.name] = {
|
|
124
|
+
bump: release.type,
|
|
125
|
+
cascade: release.cascade
|
|
126
|
+
};
|
|
127
|
+
else frontmatter[release.name] = release.type;
|
|
128
|
+
await writeText(filePath, `---\n${jsYaml.dump(frontmatter, {
|
|
129
|
+
lineWidth: -1,
|
|
130
|
+
quotingType: "\""
|
|
131
|
+
}).trim()}\n---\n\n${summary}\n`);
|
|
132
|
+
return filePath;
|
|
133
|
+
}
|
|
134
|
+
function fileToId(filePath) {
|
|
135
|
+
return filePath.split("/").pop().replace(/\.md$/, "");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Given a list of changed file paths (relative to root), extract the IDs
|
|
139
|
+
* of bump files that were added/modified. Shared by `check` and `ci check`.
|
|
140
|
+
*/
|
|
141
|
+
function extractBumpFileIdsFromChangedFiles(changedFiles) {
|
|
142
|
+
return new Set(changedFiles.filter((f) => /^\.bumpy\/.*\.md$/.test(f) && !f.endsWith("README.md")).map((f) => f.replace(/^\.bumpy\//, "").replace(/\.md$/, "")));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Filter bump files to only those added/modified on the current branch.
|
|
146
|
+
* Returns the filtered bump files and whether any changed bump file was
|
|
147
|
+
* empty (has no releases — signals intentionally no releases needed).
|
|
148
|
+
*/
|
|
149
|
+
function filterBranchBumpFiles(allBumpFiles, changedFiles) {
|
|
150
|
+
const branchBumpFileIds = extractBumpFileIdsFromChangedFiles(changedFiles);
|
|
151
|
+
return {
|
|
152
|
+
branchBumpFiles: allBumpFiles.filter((bf) => branchBumpFileIds.has(bf.id)),
|
|
153
|
+
branchBumpFileIds
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
export { writeBumpFile as i, parseBumpFile as n, readBumpFiles as r, filterBranchBumpFiles as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { s as tryRunArgs } from "./shell-CY7OD48z.mjs";
|
|
2
2
|
//#region src/core/changelog-github.ts
|
|
3
3
|
/**
|
|
4
4
|
* GitHub-enhanced changelog formatter.
|
|
@@ -7,12 +7,14 @@ import { o as tryRunArgs } from "./shell-Dj7JRD_q.mjs";
|
|
|
7
7
|
* Usage in config:
|
|
8
8
|
* "changelog": "github"
|
|
9
9
|
* "changelog": ["github", { "repo": "dmno-dev/bumpy" }]
|
|
10
|
-
* "changelog": ["github", { "
|
|
10
|
+
* "changelog": ["github", { "thankContributors": false }]
|
|
11
|
+
* "changelog": ["github", { "internalAuthors": ["theoephraim"] }]
|
|
11
12
|
*/
|
|
12
13
|
function createGithubFormatter(options = {}) {
|
|
14
|
+
const thankContributors = options.thankContributors ?? true;
|
|
13
15
|
const internalAuthorsSet = new Set((options.internalAuthors ?? []).map((a) => a.toLowerCase()));
|
|
14
16
|
return async (ctx) => {
|
|
15
|
-
const { release,
|
|
17
|
+
const { release, bumpFiles, date } = ctx;
|
|
16
18
|
const repoSlug = options.repo ?? detectRepo();
|
|
17
19
|
const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
|
|
18
20
|
const lines = [];
|
|
@@ -20,25 +22,25 @@ function createGithubFormatter(options = {}) {
|
|
|
20
22
|
lines.push("");
|
|
21
23
|
lines.push(`_${date}_`);
|
|
22
24
|
lines.push("");
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
if (!
|
|
26
|
-
const { cleanSummary, overrides } = extractSummaryMeta(
|
|
27
|
-
const gitInfo =
|
|
25
|
+
const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id));
|
|
26
|
+
if (relevantBumpFiles.length > 0) for (const bf of relevantBumpFiles) {
|
|
27
|
+
if (!bf.summary) continue;
|
|
28
|
+
const { cleanSummary, overrides } = extractSummaryMeta(bf.summary);
|
|
29
|
+
const gitInfo = resolveBumpFileInfo(bf.id, repoSlug, serverUrl, overrides);
|
|
28
30
|
const summaryLines = cleanSummary.split("\n");
|
|
29
31
|
const firstLine = linkifyIssueRefs(summaryLines[0], serverUrl, repoSlug);
|
|
30
|
-
const prefix = formatPrefix(gitInfo, serverUrl, repoSlug, internalAuthorsSet);
|
|
32
|
+
const prefix = formatPrefix(gitInfo, serverUrl, repoSlug, thankContributors, internalAuthorsSet);
|
|
31
33
|
lines.push(`-${prefix ? ` ${prefix} -` : ""} ${firstLine}`);
|
|
32
34
|
for (let i = 1; i < summaryLines.length; i++) if (summaryLines[i].trim()) lines.push(` ${linkifyIssueRefs(summaryLines[i], serverUrl, repoSlug)}`);
|
|
33
35
|
}
|
|
34
|
-
if (release.isDependencyBump &&
|
|
35
|
-
if (release.isCascadeBump && !release.isDependencyBump &&
|
|
36
|
+
if (release.isDependencyBump && relevantBumpFiles.length === 0) lines.push("- Updated dependencies");
|
|
37
|
+
if (release.isCascadeBump && !release.isDependencyBump && relevantBumpFiles.length === 0) lines.push("- Version bump via cascade rule");
|
|
36
38
|
lines.push("");
|
|
37
39
|
return lines.join("\n");
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
|
-
* Extract metadata lines (pr, commit, author) from a
|
|
43
|
+
* Extract metadata lines (pr, commit, author) from a bump file summary.
|
|
42
44
|
* These override git-derived info, matching the behavior of @changesets/changelog-github.
|
|
43
45
|
*/
|
|
44
46
|
function extractSummaryMeta(summary) {
|
|
@@ -60,10 +62,10 @@ function extractSummaryMeta(summary) {
|
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
|
-
* Resolve PR, commit, and author info for a
|
|
65
|
+
* Resolve PR, commit, and author info for a bump file.
|
|
64
66
|
* Summary overrides take precedence over git-derived info.
|
|
65
67
|
*/
|
|
66
|
-
function
|
|
68
|
+
function resolveBumpFileInfo(bumpFileId, repo, serverUrl, overrides) {
|
|
67
69
|
if (overrides.pr !== void 0) {
|
|
68
70
|
const prInfo = lookupPr(overrides.pr, repo);
|
|
69
71
|
return {
|
|
@@ -73,7 +75,7 @@ function resolveChangesetInfo(changesetId, repo, serverUrl, overrides) {
|
|
|
73
75
|
author: overrides.authors?.[0] ?? prInfo?.author
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
|
-
const gitInfo =
|
|
78
|
+
const gitInfo = findBumpFileCommitInfo(bumpFileId, repo);
|
|
77
79
|
return {
|
|
78
80
|
prNumber: gitInfo?.prNumber,
|
|
79
81
|
prUrl: gitInfo?.prUrl,
|
|
@@ -106,10 +108,10 @@ function lookupPr(prNumber, repo) {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
/**
|
|
109
|
-
* Find the PR that introduced a
|
|
111
|
+
* Find the PR that introduced a bump file by checking git log
|
|
110
112
|
* for the commit that added the file, then looking up the PR.
|
|
111
113
|
*/
|
|
112
|
-
function
|
|
114
|
+
function findBumpFileCommitInfo(bumpFileId, repo) {
|
|
113
115
|
try {
|
|
114
116
|
const commitOutput = tryRunArgs([
|
|
115
117
|
"git",
|
|
@@ -117,8 +119,8 @@ function findChangesetCommitInfo(changesetId, repo) {
|
|
|
117
119
|
"--diff-filter=A",
|
|
118
120
|
"--format=%H",
|
|
119
121
|
"--",
|
|
120
|
-
`.bumpy/${
|
|
121
|
-
`.changeset/${
|
|
122
|
+
`.bumpy/${bumpFileId}.md`,
|
|
123
|
+
`.changeset/${bumpFileId}.md`
|
|
122
124
|
]);
|
|
123
125
|
if (!commitOutput) return null;
|
|
124
126
|
const commitHash = commitOutput.split("\n")[0].trim();
|
|
@@ -155,14 +157,14 @@ function findChangesetCommitInfo(changesetId, repo) {
|
|
|
155
157
|
* Build the prefix portion of a changelog line: PR link, commit link, thanks.
|
|
156
158
|
* Matches the format used by @changesets/changelog-github.
|
|
157
159
|
*/
|
|
158
|
-
function formatPrefix(info, serverUrl, repo, internalAuthors) {
|
|
160
|
+
function formatPrefix(info, serverUrl, repo, thankContributors, internalAuthors) {
|
|
159
161
|
const parts = [];
|
|
160
162
|
if (info.prNumber && info.prUrl) parts.push(`[#${info.prNumber}](${info.prUrl})`);
|
|
161
163
|
if (info.commitHash && repo) {
|
|
162
164
|
const short = info.commitHash.slice(0, 7);
|
|
163
165
|
parts.push(`[\`${short}\`](${serverUrl}/${repo}/commit/${info.commitHash})`);
|
|
164
166
|
}
|
|
165
|
-
if (info.author && !internalAuthors.has(info.author.toLowerCase())) parts.push(`Thanks [@${info.author}](${serverUrl}/${info.author})!`);
|
|
167
|
+
if (thankContributors && info.author && !internalAuthors.has(info.author.toLowerCase())) parts.push(`Thanks [@${info.author}](${serverUrl}/${info.author})!`);
|
|
166
168
|
return parts.join(" ");
|
|
167
169
|
}
|
|
168
170
|
/**
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { n as log, o as __toESM, t as colorize } from "./logger-C2dEe5Su.mjs";
|
|
2
|
+
import { a as loadConfig, o as loadPackageConfig } from "./config-B-Qg3DZH.mjs";
|
|
3
|
+
import { n as discoverWorkspace } from "./workspace-DWXlwcH4.mjs";
|
|
4
|
+
import { r as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-DVqR3k67.mjs";
|
|
5
|
+
import { r as getChangedFiles } from "./git-YDedMddc.mjs";
|
|
6
|
+
import { t as require_picomatch } from "./picomatch-DMmqYjgq.mjs";
|
|
7
|
+
import { relative } from "node:path";
|
|
8
|
+
//#region src/commands/check.ts
|
|
9
|
+
var import_picomatch = /* @__PURE__ */ __toESM(require_picomatch(), 1);
|
|
10
|
+
/**
|
|
11
|
+
* Local check: detect which packages have changed on this branch
|
|
12
|
+
* and verify they have corresponding bump files.
|
|
13
|
+
* Designed for pre-push hooks — no GitHub API needed.
|
|
14
|
+
*/
|
|
15
|
+
async function checkCommand(rootDir) {
|
|
16
|
+
const config = await loadConfig(rootDir);
|
|
17
|
+
const { packages } = await discoverWorkspace(rootDir, config);
|
|
18
|
+
const baseBranch = config.baseBranch;
|
|
19
|
+
const changedFiles = getChangedFiles(rootDir, baseBranch);
|
|
20
|
+
if (changedFiles.length === 0) {
|
|
21
|
+
log.info("No changed files detected.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { branchBumpFiles, branchBumpFileIds } = filterBranchBumpFiles(await readBumpFiles(rootDir), changedFiles);
|
|
25
|
+
if (branchBumpFileIds.size > branchBumpFiles.length) {
|
|
26
|
+
log.success("Empty bump file found — no releases needed.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const coveredPackages = /* @__PURE__ */ new Set();
|
|
30
|
+
for (const bf of branchBumpFiles) for (const release of bf.releases) coveredPackages.add(release.name);
|
|
31
|
+
const changedPackages = await findChangedPackages(changedFiles, packages, rootDir, config);
|
|
32
|
+
if (changedPackages.length === 0) {
|
|
33
|
+
log.info("No managed packages have changed.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const missing = changedPackages.filter((name) => !coveredPackages.has(name));
|
|
37
|
+
if (missing.length === 0) {
|
|
38
|
+
log.success(`🐸 All ${changedPackages.length} changed package(s) have bump files.`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
log.warn(`${missing.length} changed package(s) missing bump files:\n`);
|
|
42
|
+
for (const name of missing) console.log(` ${colorize(name, "yellow")}`);
|
|
43
|
+
console.log();
|
|
44
|
+
log.dim("Run `bumpy add` to create a bump file, or `bumpy add --empty` if no release is needed.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
/** Map changed files to the packages they belong to */
|
|
48
|
+
async function findChangedPackages(changedFiles, packages, rootDir, config) {
|
|
49
|
+
const changed = /* @__PURE__ */ new Set();
|
|
50
|
+
const matchers = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const [name, pkg] of packages) {
|
|
52
|
+
const patterns = (await loadPackageConfig(pkg.dir, config, name)).changedFilePatterns ?? config.changedFilePatterns;
|
|
53
|
+
matchers.set(name, (0, import_picomatch.default)(patterns));
|
|
54
|
+
}
|
|
55
|
+
for (const file of changedFiles) for (const [name, pkg] of packages) {
|
|
56
|
+
const pkgRelDir = relative(rootDir, pkg.dir);
|
|
57
|
+
if (file.startsWith(pkgRelDir + "/")) {
|
|
58
|
+
const relToPackage = file.slice(pkgRelDir.length + 1);
|
|
59
|
+
if (matchers.get(name)(relToPackage)) changed.add(name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [...changed];
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { checkCommand };
|