@varlock/bumpy 0.0.0 → 0.0.1
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 +13 -0
- package/dist/add-u5V9V3L7.mjs +131 -0
- package/dist/ai-B8ZL2x8z.mjs +82 -0
- package/dist/apply-release-plan-DtU3rVyL.mjs +132 -0
- package/dist/changelog-github-n-3zV1p9.mjs +59 -0
- package/dist/changeset-ClCYsChu.mjs +75 -0
- package/dist/check-CkRubvuk.mjs +57 -0
- package/dist/ci-8KWWhjXl.mjs +224 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +210 -0
- package/dist/config-CJ2orhTL.mjs +215 -0
- package/dist/dep-graph-DiLeAhl9.mjs +64 -0
- package/dist/fs-DbNNEyzq.mjs +51 -0
- package/dist/generate-oOFD9ABC.mjs +158 -0
- package/dist/index.d.mts +254 -0
- package/dist/index.mjs +9 -0
- package/dist/init-Blw2GfC_.mjs +22 -0
- package/dist/js-yaml-DpZfOoD4.mjs +2031 -0
- package/dist/logger-ZqggsyGZ.mjs +176 -0
- package/dist/migrate-DvOrXSw0.mjs +114 -0
- package/dist/names-C-u50ofE.mjs +143 -0
- package/dist/prompt-BP8toAOI.mjs +46 -0
- package/dist/publish-DZ3m7qkX.mjs +197 -0
- package/dist/publish-pipeline-1M5GmbdP.mjs +291 -0
- package/dist/release-plan-CFnutSHD.mjs +173 -0
- package/dist/semver-DWO6NFKN.mjs +1360 -0
- package/dist/shell-DPlltpzb.mjs +44 -0
- package/dist/status-DRpq_Mha.mjs +106 -0
- package/dist/version-CJwf8XIA.mjs +81 -0
- package/dist/workspace-mVjawG8g.mjs +183 -0
- package/package.json +36 -6
- package/skills/add-change/SKILL.md +108 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { n as log, t as colorize } from "./logger-ZqggsyGZ.mjs";
|
|
2
|
+
import { a as readJson, c as writeJson } from "./fs-DbNNEyzq.mjs";
|
|
3
|
+
import { i as resolveCatalogDep } from "./workspace-mVjawG8g.mjs";
|
|
4
|
+
import { r as stripProtocol } from "./semver-DWO6NFKN.mjs";
|
|
5
|
+
import { i as tryRun, n as runAsync, t as run } from "./shell-DPlltpzb.mjs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { unlink } from "node:fs/promises";
|
|
8
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
//#region src/core/git.ts
|
|
10
|
+
/** Create a git tag */
|
|
11
|
+
function createTag(tag, opts) {
|
|
12
|
+
run(`git tag ${tag}`, opts);
|
|
13
|
+
}
|
|
14
|
+
/** Push commits and tags to remote */
|
|
15
|
+
function pushWithTags(opts) {
|
|
16
|
+
run("git push --follow-tags", opts);
|
|
17
|
+
}
|
|
18
|
+
/** Check if there are uncommitted changes */
|
|
19
|
+
function hasUncommittedChanges(opts) {
|
|
20
|
+
const result = tryRun("git status --porcelain", opts);
|
|
21
|
+
return result !== null && result.length > 0;
|
|
22
|
+
}
|
|
23
|
+
/** Check if a tag already exists */
|
|
24
|
+
function tagExists(tag, opts) {
|
|
25
|
+
return tryRun(`git tag -l "${tag}"`, opts) === tag;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/core/publish-pipeline.ts
|
|
29
|
+
/**
|
|
30
|
+
* Detect which CI OIDC provider is available for npm trusted publishing.
|
|
31
|
+
* Returns the provider name or null if none detected.
|
|
32
|
+
*
|
|
33
|
+
* Supported providers:
|
|
34
|
+
* - GitHub Actions: `ACTIONS_ID_TOKEN_REQUEST_URL` (set when `id-token: write` permission is granted)
|
|
35
|
+
* - GitLab CI: `GITLAB_CI` + `NPM_ID_TOKEN`
|
|
36
|
+
* - CircleCI: `CIRCLECI` + `NPM_ID_TOKEN`
|
|
37
|
+
*/
|
|
38
|
+
function detectOidcProvider() {
|
|
39
|
+
if (process.env.ACTIONS_ID_TOKEN_REQUEST_URL) return "github-actions";
|
|
40
|
+
if (process.env.GITLAB_CI && process.env.NPM_ID_TOKEN) return "gitlab";
|
|
41
|
+
if (process.env.CIRCLECI && process.env.NPM_ID_TOKEN) return "circleci";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const OIDC_NPM_UPGRADE_HINTS = {
|
|
45
|
+
"github-actions": "Add `actions/setup-node@v4` with `node-version: lts/*` to your workflow",
|
|
46
|
+
gitlab: "Use a Node.js image with npm >= 11.5.1 or run `npm install -g npm@latest`",
|
|
47
|
+
circleci: "Use a Node.js image with npm >= 11.5.1 or run `sudo npm install -g npm@latest`"
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Set up npm authentication for publishing.
|
|
51
|
+
*
|
|
52
|
+
* Handles three scenarios:
|
|
53
|
+
* 1. **Trusted publishing (OIDC)** — GitHub Actions, GitLab CI, or CircleCI with OIDC configured.
|
|
54
|
+
* npm >= 11.5.1 authenticates automatically via OIDC token exchange.
|
|
55
|
+
* No secret needed, but we check the npm version and warn if too old.
|
|
56
|
+
* 2. **Token-based auth** — `NPM_TOKEN` or `NODE_AUTH_TOKEN` env var.
|
|
57
|
+
* Writes a project-level `.npmrc` so npm can authenticate.
|
|
58
|
+
* 3. **Pre-configured** — user already has `.npmrc` with auth (e.g. via `actions/setup-node`).
|
|
59
|
+
*/
|
|
60
|
+
function setupNpmAuth(rootDir, publishManager) {
|
|
61
|
+
if (publishManager !== "npm") return;
|
|
62
|
+
const npmrcPath = resolve(rootDir, ".npmrc");
|
|
63
|
+
const existingNpmrc = existsSync(npmrcPath) ? readFileSync(npmrcPath, "utf-8") : "";
|
|
64
|
+
if (existingNpmrc.includes(":_authToken=")) {
|
|
65
|
+
log.dim(" Using existing .npmrc auth configuration");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const oidcProvider = detectOidcProvider();
|
|
69
|
+
if (oidcProvider) {
|
|
70
|
+
const npmVersion = tryRun("npm --version");
|
|
71
|
+
if (npmVersion) {
|
|
72
|
+
const [major, minor, patch] = npmVersion.split(".").map(Number);
|
|
73
|
+
if (!(major > 11 || major === 11 && (minor > 5 || minor === 5 && patch >= 1))) {
|
|
74
|
+
log.warn(` npm ${npmVersion} detected — trusted publishing (OIDC) requires npm >= 11.5.1`);
|
|
75
|
+
log.warn(` ${OIDC_NPM_UPGRADE_HINTS[oidcProvider]}`);
|
|
76
|
+
} else log.dim(` OIDC detected (${oidcProvider}) — npm ${npmVersion} will authenticate via trusted publishing`);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const token = process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN;
|
|
81
|
+
if (token) {
|
|
82
|
+
if (process.env.NPM_TOKEN && !process.env.NODE_AUTH_TOKEN) process.env.NODE_AUTH_TOKEN = token;
|
|
83
|
+
const authLine = "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}";
|
|
84
|
+
if (existingNpmrc) appendFileSync(npmrcPath, `\n${authLine}\n`);
|
|
85
|
+
else writeFileSync(npmrcPath, `${authLine}\n`);
|
|
86
|
+
log.dim(" Configured .npmrc with auth token");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (process.env.CI) {
|
|
90
|
+
log.warn(" No npm authentication detected. Publishing will likely fail.");
|
|
91
|
+
log.warn(" Options:");
|
|
92
|
+
log.warn(" • Trusted publishing (OIDC): add `id-token: write` permission + npm >= 11.5.1");
|
|
93
|
+
log.warn(" • Token auth: set NPM_TOKEN or NODE_AUTH_TOKEN environment variable");
|
|
94
|
+
log.warn(" • Manual: add `actions/setup-node` with `registry-url` to your workflow");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Publish all packages in the release plan.
|
|
99
|
+
* Order: topological (dependencies published before dependents).
|
|
100
|
+
*/
|
|
101
|
+
async function publishPackages(releasePlan, packages, depGraph, config, rootDir, opts = {}, catalogs = /* @__PURE__ */ new Map(), detectedPm = "npm") {
|
|
102
|
+
const result = {
|
|
103
|
+
published: [],
|
|
104
|
+
skipped: [],
|
|
105
|
+
failed: []
|
|
106
|
+
};
|
|
107
|
+
const publishConfig = config.publish;
|
|
108
|
+
setupNpmAuth(rootDir, publishConfig.publishManager);
|
|
109
|
+
const packManager = publishConfig.packManager === "auto" ? detectedPm : publishConfig.packManager;
|
|
110
|
+
const topoOrder = depGraph.topologicalSort(packages);
|
|
111
|
+
const releaseMap = new Map(releasePlan.releases.map((r) => [r.name, r]));
|
|
112
|
+
const ordered = [];
|
|
113
|
+
for (const name of topoOrder) {
|
|
114
|
+
const release = releaseMap.get(name);
|
|
115
|
+
if (release) ordered.push(release);
|
|
116
|
+
}
|
|
117
|
+
for (const release of ordered) {
|
|
118
|
+
const pkg = packages.get(release.name);
|
|
119
|
+
const pkgConfig = pkg.bumpy || {};
|
|
120
|
+
if (pkg.private && !pkgConfig.publishCommand) {
|
|
121
|
+
if (config.privatePackages.tag) createGitTag(release, rootDir, opts);
|
|
122
|
+
result.skipped.push({
|
|
123
|
+
name: release.name,
|
|
124
|
+
reason: "private"
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
log.step(`Publishing ${colorize(release.name, "cyan")}@${release.newVersion}`);
|
|
129
|
+
try {
|
|
130
|
+
if (pkgConfig.buildCommand) {
|
|
131
|
+
log.dim(` Building...`);
|
|
132
|
+
if (!opts.dryRun) await runAsync(pkgConfig.buildCommand, { cwd: pkg.dir });
|
|
133
|
+
}
|
|
134
|
+
if (pkgConfig.publishCommand || publishConfig.protocolResolution === "in-place") await resolveProtocolsInPlace(pkg, packages, releasePlan, catalogs);
|
|
135
|
+
if (pkgConfig.publishCommand) {
|
|
136
|
+
const commands = Array.isArray(pkgConfig.publishCommand) ? pkgConfig.publishCommand : [pkgConfig.publishCommand];
|
|
137
|
+
for (const cmd of commands) {
|
|
138
|
+
const expanded = cmd.replace(/\{\{version\}\}/g, release.newVersion).replace(/\{\{name\}\}/g, release.name);
|
|
139
|
+
log.dim(` Running: ${expanded}`);
|
|
140
|
+
if (!opts.dryRun) await runAsync(expanded, { cwd: pkg.dir });
|
|
141
|
+
}
|
|
142
|
+
} else if (!pkgConfig.skipNpmPublish) if (publishConfig.protocolResolution === "pack") await packThenPublish(pkg, pkgConfig, config, packManager, opts);
|
|
143
|
+
else await npmPublishDirect(pkg, pkgConfig, config, opts);
|
|
144
|
+
else {
|
|
145
|
+
result.skipped.push({
|
|
146
|
+
name: release.name,
|
|
147
|
+
reason: "skipNpmPublish"
|
|
148
|
+
});
|
|
149
|
+
createGitTag(release, rootDir, opts);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
createGitTag(release, rootDir, opts);
|
|
153
|
+
result.published.push({
|
|
154
|
+
name: release.name,
|
|
155
|
+
version: release.newVersion
|
|
156
|
+
});
|
|
157
|
+
log.success(` Published ${release.name}@${release.newVersion}`);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
160
|
+
log.error(` Failed to publish ${release.name}: ${errMsg}`);
|
|
161
|
+
result.failed.push({
|
|
162
|
+
name: release.name,
|
|
163
|
+
error: errMsg
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Pack with the PM (which resolves workspace:/catalog: protocols into the tarball),
|
|
171
|
+
* then publish the tarball with npm (which supports OIDC/provenance).
|
|
172
|
+
*/
|
|
173
|
+
async function packThenPublish(pkg, pkgConfig, config, packManager, opts) {
|
|
174
|
+
const packCmd = getPackCommand(packManager);
|
|
175
|
+
log.dim(` Packing with: ${packCmd}`);
|
|
176
|
+
if (opts.dryRun) {
|
|
177
|
+
const publishCmd = buildPublishCommand(pkg, pkgConfig, config, opts, "<tarball>");
|
|
178
|
+
log.dim(` Would publish with: ${publishCmd}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const tarball = parseTarballPath(await runAsync(packCmd, { cwd: pkg.dir }), pkg.dir);
|
|
182
|
+
try {
|
|
183
|
+
const publishCmd = buildPublishCommand(pkg, pkgConfig, config, opts, tarball);
|
|
184
|
+
log.dim(` Publishing: ${publishCmd}`);
|
|
185
|
+
await runAsync(publishCmd, { cwd: pkg.dir });
|
|
186
|
+
} finally {
|
|
187
|
+
try {
|
|
188
|
+
await unlink(tarball);
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Publish directly from the package directory (no tarball) */
|
|
193
|
+
async function npmPublishDirect(pkg, pkgConfig, config, opts) {
|
|
194
|
+
const cmd = buildPublishCommand(pkg, pkgConfig, config, opts);
|
|
195
|
+
log.dim(` Running: ${cmd}`);
|
|
196
|
+
if (!opts.dryRun) await runAsync(cmd, { cwd: pkg.dir });
|
|
197
|
+
}
|
|
198
|
+
function getPackCommand(pm) {
|
|
199
|
+
switch (pm) {
|
|
200
|
+
case "pnpm": return "pnpm pack";
|
|
201
|
+
case "bun": return "bun pm pack";
|
|
202
|
+
case "yarn": return "yarn pack";
|
|
203
|
+
default: return "npm pack";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function buildPublishCommand(pkg, pkgConfig, config, opts, tarball) {
|
|
207
|
+
const publishManager = config.publish.publishManager;
|
|
208
|
+
const parts = [];
|
|
209
|
+
if (publishManager === "yarn") parts.push("yarn npm publish");
|
|
210
|
+
else parts.push(`${publishManager} publish`);
|
|
211
|
+
if (tarball) parts.push(tarball);
|
|
212
|
+
const access = pkgConfig?.access || config.access;
|
|
213
|
+
parts.push(`--access ${access}`);
|
|
214
|
+
if (pkgConfig?.registry) parts.push(`--registry ${pkgConfig.registry}`);
|
|
215
|
+
if (opts.tag) parts.push(`--tag ${opts.tag}`);
|
|
216
|
+
if (config.publish.publishArgs.length > 0) parts.push(...config.publish.publishArgs);
|
|
217
|
+
return parts.join(" ");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Parse the tarball path from pack command output.
|
|
221
|
+
* Each PM has different output formats:
|
|
222
|
+
* npm/pnpm: tarball filename on the last line
|
|
223
|
+
* bun: tarball filename mid-output, summary lines after
|
|
224
|
+
* yarn: 'success Wrote tarball to "/path/to/foo.tgz".'
|
|
225
|
+
*/
|
|
226
|
+
function parseTarballPath(output, cwd) {
|
|
227
|
+
const tgzMatch = output.match(/(?:^|["'\s])([^\s"']*\.tgz)/m);
|
|
228
|
+
if (tgzMatch) {
|
|
229
|
+
const tarball = tgzMatch[1];
|
|
230
|
+
return tarball.startsWith("/") ? tarball : resolve(cwd, tarball);
|
|
231
|
+
}
|
|
232
|
+
const lines = output.trim().split("\n").filter(Boolean);
|
|
233
|
+
const lastLine = lines[lines.length - 1]?.trim() || "";
|
|
234
|
+
return lastLine.startsWith("/") ? lastLine : resolve(cwd, lastLine);
|
|
235
|
+
}
|
|
236
|
+
function createGitTag(release, rootDir, opts) {
|
|
237
|
+
const tag = `${release.name}@${release.newVersion}`;
|
|
238
|
+
if (opts.dryRun) {
|
|
239
|
+
log.dim(` Would create tag: ${tag}`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (tagExists(tag, { cwd: rootDir })) {
|
|
243
|
+
log.dim(` Tag ${tag} already exists, skipping`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
createTag(tag, { cwd: rootDir });
|
|
247
|
+
log.dim(` Tagged: ${tag}`);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Resolve workspace:/catalog: protocols by rewriting package.json in-place.
|
|
251
|
+
* Used for custom publish commands and "in-place" protocolResolution mode.
|
|
252
|
+
*/
|
|
253
|
+
async function resolveProtocolsInPlace(pkg, packages, releasePlan, catalogs) {
|
|
254
|
+
const pkgJsonPath = resolve(pkg.dir, "package.json");
|
|
255
|
+
const pkgJson = await readJson(pkgJsonPath);
|
|
256
|
+
let modified = false;
|
|
257
|
+
const releaseMap = new Map(releasePlan.releases.map((r) => [r.name, r]));
|
|
258
|
+
for (const depField of [
|
|
259
|
+
"dependencies",
|
|
260
|
+
"devDependencies",
|
|
261
|
+
"peerDependencies",
|
|
262
|
+
"optionalDependencies"
|
|
263
|
+
]) {
|
|
264
|
+
const deps = pkgJson[depField];
|
|
265
|
+
if (!deps) continue;
|
|
266
|
+
for (const [depName, range] of Object.entries(deps)) {
|
|
267
|
+
let resolved = null;
|
|
268
|
+
if (range.startsWith("catalog:")) {
|
|
269
|
+
resolved = resolveCatalogDep(depName, range, catalogs);
|
|
270
|
+
if (!resolved) {
|
|
271
|
+
log.warn(` Could not resolve ${depName}: "${range}" — catalog entry not found`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
} else if (range.startsWith("workspace:")) {
|
|
275
|
+
const cleanRange = stripProtocol(range);
|
|
276
|
+
if (cleanRange === "*" || cleanRange === "^" || cleanRange === "~") {
|
|
277
|
+
const depPkg = packages.get(depName);
|
|
278
|
+
const version = releaseMap.get(depName)?.newVersion || depPkg?.version || "0.0.0";
|
|
279
|
+
resolved = `${cleanRange === "*" ? "^" : cleanRange}${version}`;
|
|
280
|
+
} else resolved = cleanRange;
|
|
281
|
+
}
|
|
282
|
+
if (resolved) {
|
|
283
|
+
deps[depName] = resolved;
|
|
284
|
+
modified = true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (modified) await writeJson(pkgJsonPath, pkgJson);
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
export { hasUncommittedChanges as n, pushWithTags as r, publishPackages as t };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { g as parseIsolatedBump, h as maxBump, l as DEFAULT_BUMP_RULES, m as hasCascade, p as bumpLevel, s as matchGlob } from "./config-CJ2orhTL.mjs";
|
|
2
|
+
import { n as satisfies, r as stripProtocol, t as bumpVersion } from "./semver-DWO6NFKN.mjs";
|
|
3
|
+
//#region src/core/release-plan.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build a release plan from pending changesets, the dependency graph, and config.
|
|
6
|
+
* This is the core algorithm of bumpy.
|
|
7
|
+
*/
|
|
8
|
+
function assembleReleasePlan(changesets, packages, depGraph, config) {
|
|
9
|
+
if (changesets.length === 0) return {
|
|
10
|
+
changesets: [],
|
|
11
|
+
releases: []
|
|
12
|
+
};
|
|
13
|
+
const planned = /* @__PURE__ */ new Map();
|
|
14
|
+
const cascadeOverrides = /* @__PURE__ */ new Map();
|
|
15
|
+
for (const cs of changesets) for (const release of cs.releases) {
|
|
16
|
+
if (!packages.has(release.name)) continue;
|
|
17
|
+
const { bump, isolated } = parseIsolatedBump(release.type);
|
|
18
|
+
const existing = planned.get(release.name);
|
|
19
|
+
if (existing) {
|
|
20
|
+
existing.type = maxBump(existing.type, bump);
|
|
21
|
+
if (!isolated) existing.isolated = false;
|
|
22
|
+
existing.changesets.add(cs.id);
|
|
23
|
+
} else planned.set(release.name, {
|
|
24
|
+
type: bump,
|
|
25
|
+
isolated,
|
|
26
|
+
isDependencyBump: false,
|
|
27
|
+
isCascadeBump: false,
|
|
28
|
+
changesets: new Set([cs.id])
|
|
29
|
+
});
|
|
30
|
+
if (hasCascade(release)) {
|
|
31
|
+
if (!cascadeOverrides.has(release.name)) cascadeOverrides.set(release.name, /* @__PURE__ */ new Map());
|
|
32
|
+
const overrides = cascadeOverrides.get(release.name);
|
|
33
|
+
for (const [pattern, bumpType] of Object.entries(release.cascade)) {
|
|
34
|
+
const existing = overrides.get(pattern);
|
|
35
|
+
overrides.set(pattern, maxBump(existing, bumpType));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const group of config.fixed) {
|
|
40
|
+
let groupBump;
|
|
41
|
+
let groupIsolated = true;
|
|
42
|
+
for (const nameOrGlob of group) for (const [name, bump] of planned) if (matchGlob(name, nameOrGlob)) {
|
|
43
|
+
groupBump = maxBump(groupBump, bump.type);
|
|
44
|
+
if (!bump.isolated) groupIsolated = false;
|
|
45
|
+
}
|
|
46
|
+
if (!groupBump) continue;
|
|
47
|
+
for (const nameOrGlob of group) for (const [name] of packages) {
|
|
48
|
+
if (!matchGlob(name, nameOrGlob)) continue;
|
|
49
|
+
const existing = planned.get(name);
|
|
50
|
+
if (existing) {
|
|
51
|
+
existing.type = groupBump;
|
|
52
|
+
existing.isolated = groupIsolated;
|
|
53
|
+
} else planned.set(name, {
|
|
54
|
+
type: groupBump,
|
|
55
|
+
isolated: groupIsolated,
|
|
56
|
+
isDependencyBump: false,
|
|
57
|
+
isCascadeBump: false,
|
|
58
|
+
changesets: /* @__PURE__ */ new Set()
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const group of config.linked) {
|
|
63
|
+
let groupBump;
|
|
64
|
+
for (const nameOrGlob of group) for (const [name, bump] of planned) if (matchGlob(name, nameOrGlob)) groupBump = maxBump(groupBump, bump.type);
|
|
65
|
+
if (!groupBump) continue;
|
|
66
|
+
for (const nameOrGlob of group) for (const [name] of packages) {
|
|
67
|
+
if (!matchGlob(name, nameOrGlob)) continue;
|
|
68
|
+
const existing = planned.get(name);
|
|
69
|
+
if (existing) existing.type = groupBump;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let changed = true;
|
|
73
|
+
let iterations = 0;
|
|
74
|
+
const MAX_ITERATIONS = 100;
|
|
75
|
+
while (changed && iterations < MAX_ITERATIONS) {
|
|
76
|
+
changed = false;
|
|
77
|
+
iterations++;
|
|
78
|
+
for (const [pkgName, bump] of planned) {
|
|
79
|
+
if (bump.isolated) continue;
|
|
80
|
+
const csOverrides = cascadeOverrides.get(pkgName);
|
|
81
|
+
if (csOverrides) for (const [pattern, cascadeBumpType] of csOverrides) for (const [targetName] of packages) {
|
|
82
|
+
if (!matchGlob(targetName, pattern)) continue;
|
|
83
|
+
if (applyBump(planned, targetName, cascadeBumpType, false, true, bump.changesets)) changed = true;
|
|
84
|
+
}
|
|
85
|
+
const cascadeTo = packages.get(pkgName)?.bumpy?.cascadeTo;
|
|
86
|
+
if (cascadeTo) for (const [pattern, rule] of Object.entries(cascadeTo)) {
|
|
87
|
+
if (!shouldTrigger(bump.type, rule.trigger)) continue;
|
|
88
|
+
const cascadeBump = rule.bumpAs === "match" ? bump.type : rule.bumpAs;
|
|
89
|
+
for (const [targetName] of packages) {
|
|
90
|
+
if (!matchGlob(targetName, pattern)) continue;
|
|
91
|
+
if (applyBump(planned, targetName, cascadeBump, false, true, bump.changesets)) changed = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const dependents = depGraph.getDependents(pkgName);
|
|
95
|
+
for (const dep of dependents) {
|
|
96
|
+
const rule = resolveRule(dep.name, pkgName, dep.depType, packages, config);
|
|
97
|
+
if (!shouldTrigger(bump.type, rule.trigger)) continue;
|
|
98
|
+
if (config.updateInternalDependencies === "out-of-range") {
|
|
99
|
+
if (satisfies(bumpVersion(packages.get(pkgName).version, bump.type), stripProtocol(dep.versionRange))) continue;
|
|
100
|
+
}
|
|
101
|
+
if (config.updateInternalDependencies === "none") continue;
|
|
102
|
+
if (config.updateInternalDependencies === "minor" && bumpLevel(bump.type) < bumpLevel("minor")) continue;
|
|
103
|
+
const depBump = rule.bumpAs === "match" ? bump.type : rule.bumpAs;
|
|
104
|
+
if (applyBump(planned, dep.name, depBump, true, false, bump.changesets)) changed = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const releases = [];
|
|
109
|
+
for (const [name, bump] of planned) {
|
|
110
|
+
const pkg = packages.get(name);
|
|
111
|
+
if (!pkg) continue;
|
|
112
|
+
const newVersion = bumpVersion(pkg.version, bump.type);
|
|
113
|
+
releases.push({
|
|
114
|
+
name,
|
|
115
|
+
type: bump.type,
|
|
116
|
+
oldVersion: pkg.version,
|
|
117
|
+
newVersion,
|
|
118
|
+
changesets: [...bump.changesets],
|
|
119
|
+
isDependencyBump: bump.isDependencyBump,
|
|
120
|
+
isCascadeBump: bump.isCascadeBump
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
releases.sort((a, b) => a.name.localeCompare(b.name));
|
|
124
|
+
return {
|
|
125
|
+
changesets,
|
|
126
|
+
releases
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Apply a bump to a package, upgrading if already planned. Returns true if anything changed. */
|
|
130
|
+
function applyBump(planned, name, type, isDependencyBump, isCascadeBump, sourceChangesets) {
|
|
131
|
+
const existing = planned.get(name);
|
|
132
|
+
if (existing) {
|
|
133
|
+
const newType = maxBump(existing.type, type);
|
|
134
|
+
if (newType === existing.type) return false;
|
|
135
|
+
existing.type = newType;
|
|
136
|
+
if (isDependencyBump) existing.isDependencyBump = true;
|
|
137
|
+
if (isCascadeBump) existing.isCascadeBump = true;
|
|
138
|
+
for (const cs of sourceChangesets) existing.changesets.add(cs);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
planned.set(name, {
|
|
142
|
+
type,
|
|
143
|
+
isolated: false,
|
|
144
|
+
isDependencyBump,
|
|
145
|
+
isCascadeBump,
|
|
146
|
+
changesets: new Set(sourceChangesets)
|
|
147
|
+
});
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
/** Check if a bump level meets the trigger threshold */
|
|
151
|
+
function shouldTrigger(bumpType, trigger) {
|
|
152
|
+
if (trigger === "none") return false;
|
|
153
|
+
return bumpLevel(bumpType) >= bumpLevel(trigger);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the dependency bump rule for a specific dependent/dependency pair.
|
|
157
|
+
* Priority: specificDependencyRules > per-package depType rules > global depType rules > defaults
|
|
158
|
+
*/
|
|
159
|
+
function resolveRule(dependentName, dependencyName, depType, packages, config) {
|
|
160
|
+
const dependent = packages.get(dependentName);
|
|
161
|
+
if (dependent?.bumpy?.specificDependencyRules?.[dependencyName]) return dependent.bumpy.specificDependencyRules[dependencyName];
|
|
162
|
+
if (dependent?.bumpy?.specificDependencyRules) {
|
|
163
|
+
for (const [pattern, rule] of Object.entries(dependent.bumpy.specificDependencyRules)) if (matchGlob(dependencyName, pattern)) return rule;
|
|
164
|
+
}
|
|
165
|
+
if (dependent?.bumpy?.dependencyBumpRules?.[depType]) return dependent.bumpy.dependencyBumpRules[depType];
|
|
166
|
+
if (config.dependencyBumpRules[depType]) return config.dependencyBumpRules[depType];
|
|
167
|
+
return DEFAULT_BUMP_RULES[depType] || {
|
|
168
|
+
trigger: "patch",
|
|
169
|
+
bumpAs: "patch"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
export { assembleReleasePlan as t };
|