@varlock/bumpy 1.9.2 → 1.10.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/config-schema.json +0 -22
- package/dist/{add-t_nY85Lo.mjs → add-B_1I_sen.mjs} +4 -4
- package/dist/{apply-release-plan-D9wl4Q0H.mjs → apply-release-plan-CmjqYo0t.mjs} +2 -2
- package/dist/{bump-file-B_7P2UZO.mjs → bump-file-B9DpXK5X.mjs} +1 -1
- package/dist/{changelog-LaYJ7aUa.mjs → changelog-A-EwWggW.mjs} +3 -3
- package/dist/{changelog-github-BXEhPeiW.mjs → changelog-github-CPVrJHyB.mjs} +2 -2
- package/dist/{check-0vJJPD24.mjs → check-pbyTB4KC.mjs} +3 -3
- package/dist/{ci-CHIpKtvI.mjs → ci-DeZ4zHmn.mjs} +35 -27
- package/dist/cli.mjs +12 -12
- package/dist/{config-48u1NbKv.mjs → config-D_4GYDJi.mjs} +1 -1
- package/dist/{generate-zNgPV9rR.mjs → generate-BlZIe3aC.mjs} +3 -3
- package/dist/{git-CpJqzpp-.mjs → git-JGLQtk-M.mjs} +1 -12
- package/dist/index.d.mts +0 -10
- package/dist/index.mjs +7 -7
- package/dist/publish-B3D-70sJ.mjs +622 -0
- package/dist/{publish-pipeline-DpmTVsnX.mjs → publish-pipeline-BRnqVylg.mjs} +2 -2
- package/dist/{release-plan-s1o52Rc-.mjs → release-plan-mK7iGeGq.mjs} +2 -2
- package/dist/{status-BvemGN6p.mjs → status-DeMcLxiM.mjs} +5 -5
- package/dist/{types-DMdVeeEm.mjs → types-Bkh-igOJ.mjs} +0 -1
- package/dist/{version-C7uFKayK.mjs → version-88KuPbWa.mjs} +4 -4
- package/package.json +1 -1
- package/dist/publish-CI7o7EEI.mjs +0 -371
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { n as log, o as __require, t as colorize } from "./logger-BgksGFuf.mjs";
|
|
2
|
+
import { a as loadConfig } from "./config-D_4GYDJi.mjs";
|
|
3
|
+
import { n as detectWorkspaces } from "./package-manager-BQPwXwu5.mjs";
|
|
4
|
+
import { s as discoverWorkspace } from "./bump-file-B9DpXK5X.mjs";
|
|
5
|
+
import { a as DependencyGraph } from "./release-plan-mK7iGeGq.mjs";
|
|
6
|
+
import { r as runArgsAsync, s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
|
|
7
|
+
import { i as loadFormatter, n as generateChangelogEntry } from "./changelog-A-EwWggW.mjs";
|
|
8
|
+
import { c as pushWithTags, s as hasUncommittedChanges } from "./git-JGLQtk-M.mjs";
|
|
9
|
+
import { t as publishPackages } from "./publish-pipeline-BRnqVylg.mjs";
|
|
10
|
+
import { CI_PLAN_CACHE_PATH } from "./ci-DeZ4zHmn.mjs";
|
|
11
|
+
//#region src/core/github-release.ts
|
|
12
|
+
/** Get the current HEAD commit SHA */
|
|
13
|
+
function getHeadSha(rootDir) {
|
|
14
|
+
return tryRunArgs([
|
|
15
|
+
"git",
|
|
16
|
+
"rev-parse",
|
|
17
|
+
"HEAD"
|
|
18
|
+
], { cwd: rootDir });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Run an async function with BUMPY_GH_TOKEN as GH_TOKEN if available.
|
|
22
|
+
*
|
|
23
|
+
* GitHub releases created with the default GITHUB_TOKEN won't trigger
|
|
24
|
+
* downstream workflows. Using BUMPY_GH_TOKEN (a PAT or App token)
|
|
25
|
+
* allows `release` events to fire follow-up workflows.
|
|
26
|
+
*
|
|
27
|
+
* Any errors are scrubbed so the token never appears in CI logs.
|
|
28
|
+
*/
|
|
29
|
+
async function withReleaseToken(fn) {
|
|
30
|
+
const token = process.env.BUMPY_GH_TOKEN;
|
|
31
|
+
if (!token) return fn();
|
|
32
|
+
const original = process.env.GH_TOKEN;
|
|
33
|
+
process.env.GH_TOKEN = token;
|
|
34
|
+
try {
|
|
35
|
+
return await fn();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
+
throw new Error(msg.replaceAll(token, "***"));
|
|
39
|
+
} finally {
|
|
40
|
+
if (original !== void 0) process.env.GH_TOKEN = original;
|
|
41
|
+
else delete process.env.GH_TOKEN;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Create individual GitHub releases for each published package */
|
|
45
|
+
async function createIndividualReleases(releases, bumpFiles, rootDir, opts = {}) {
|
|
46
|
+
if (!isGhAvailable()) {
|
|
47
|
+
log.dim(" gh CLI not found — skipping GitHub releases");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const headSha = getHeadSha(rootDir);
|
|
51
|
+
for (const release of releases) {
|
|
52
|
+
const tag = `${release.name}@${release.newVersion}`;
|
|
53
|
+
const body = opts.formatter ? await generateReleaseBody(release, bumpFiles, opts.formatter) : buildReleaseBody(release, bumpFiles);
|
|
54
|
+
const title = `${release.name} v${release.newVersion}`;
|
|
55
|
+
if (opts.dryRun) {
|
|
56
|
+
log.dim(` Would create GitHub release: ${title}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const args = [
|
|
61
|
+
"gh",
|
|
62
|
+
"release",
|
|
63
|
+
"create",
|
|
64
|
+
tag,
|
|
65
|
+
"--title",
|
|
66
|
+
title,
|
|
67
|
+
"--notes",
|
|
68
|
+
body
|
|
69
|
+
];
|
|
70
|
+
if (headSha) args.push("--target", headSha);
|
|
71
|
+
await withReleaseToken(() => runArgsAsync(args, { cwd: rootDir }));
|
|
72
|
+
log.dim(` Created GitHub release: ${title}`);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
log.warn(` Failed to create GitHub release for ${tag}: ${err instanceof Error ? err.message : err}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Generate a release body for a single package using the changelog formatter */
|
|
79
|
+
async function generateReleaseBody(release, bumpFiles, formatter) {
|
|
80
|
+
return stripVersionHeading(await generateChangelogEntry(release, bumpFiles, formatter, void 0, "github-release")).trim() || "No changelog entries.";
|
|
81
|
+
}
|
|
82
|
+
/** Strip the leading ## version heading and date sub-heading from a changelog entry */
|
|
83
|
+
function stripVersionHeading(entry) {
|
|
84
|
+
return entry.replace(/^## .+\n/, "").replace(/^<sub>.+<\/sub>\n/, "").replace(/^_.+_\n/, "");
|
|
85
|
+
}
|
|
86
|
+
function buildReleaseBody(release, bumpFiles) {
|
|
87
|
+
const lines = [];
|
|
88
|
+
const relevant = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id));
|
|
89
|
+
if (relevant.length > 0) {
|
|
90
|
+
for (const bf of relevant) if (bf.summary) lines.push(`- ${bf.summary.split("\n")[0]}`);
|
|
91
|
+
}
|
|
92
|
+
if (relevant.length === 0) {
|
|
93
|
+
const sourceList = release.bumpSources.map((s) => `\`${s.name}\` v${s.newVersion}`).join(", ");
|
|
94
|
+
if (release.isDependencyBump) lines.push(sourceList ? `- Updated dependency ${sourceList}` : "- Updated dependencies");
|
|
95
|
+
else if (release.isGroupBump) lines.push(sourceList ? `- Version bump from group with ${sourceList}` : "- Version bump from group");
|
|
96
|
+
else if (release.isCascadeBump) lines.push(sourceList ? `- Version bump from ${sourceList}` : "- Version bump via cascade rule");
|
|
97
|
+
}
|
|
98
|
+
return lines.join("\n") || "No changelog entries.";
|
|
99
|
+
}
|
|
100
|
+
function isGhAvailable() {
|
|
101
|
+
return tryRunArgs(["gh", "--version"]) !== null;
|
|
102
|
+
}
|
|
103
|
+
const METADATA_START = "<!-- bumpy-metadata";
|
|
104
|
+
const METADATA_END = "bumpy-metadata -->";
|
|
105
|
+
/** Parse bumpy metadata from a release body */
|
|
106
|
+
function parseReleaseMetadata(body) {
|
|
107
|
+
const startIdx = body.indexOf(METADATA_START);
|
|
108
|
+
const endIdx = body.indexOf(METADATA_END);
|
|
109
|
+
if (startIdx === -1 || endIdx === -1) return null;
|
|
110
|
+
const jsonStr = body.slice(startIdx + 19, endIdx).trim();
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(jsonStr);
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Serialize metadata into an HTML comment */
|
|
118
|
+
function serializeMetadata(metadata) {
|
|
119
|
+
return `${METADATA_START}\n${JSON.stringify(metadata, null, 2)}\n${METADATA_END}`;
|
|
120
|
+
}
|
|
121
|
+
/** Build the "Published to" section from target states */
|
|
122
|
+
function formatPublishedToSection(targets) {
|
|
123
|
+
const lines = ["#### Published to"];
|
|
124
|
+
for (const [name, state] of Object.entries(targets)) switch (state.status) {
|
|
125
|
+
case "success":
|
|
126
|
+
lines.push(state.url ? `- ✅ [${name}](${state.url})` : `- ✅ ${name}`);
|
|
127
|
+
break;
|
|
128
|
+
case "failed":
|
|
129
|
+
lines.push(`- ❌ ${name} — will retry on next CI run`);
|
|
130
|
+
break;
|
|
131
|
+
case "skipped":
|
|
132
|
+
lines.push(state.supersededBy ? `- ⏭️ ${name} — skipped (superseded by ${state.supersededBy})` : `- ⏭️ ${name} — skipped`);
|
|
133
|
+
break;
|
|
134
|
+
case "pending":
|
|
135
|
+
lines.push(`- ⏳ ${name}`);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
/** Build a URL for a published package on a registry */
|
|
141
|
+
function buildPublishUrl(name, version, targetType, _registry) {
|
|
142
|
+
switch (targetType) {
|
|
143
|
+
case "npm": return `https://www.npmjs.com/package/${name}/v/${version}`;
|
|
144
|
+
case "jsr": {
|
|
145
|
+
const parts = name.startsWith("@") ? name.slice(1).split("/") : [name];
|
|
146
|
+
return parts.length === 2 ? `https://jsr.io/@${parts[0]}/${parts[1]}@${version}` : `https://jsr.io/${name}@${version}`;
|
|
147
|
+
}
|
|
148
|
+
default: return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Compose a full release body from changelog content + publish status + metadata.
|
|
153
|
+
* Preserves existing changelog content when updating (only replaces the status/metadata sections).
|
|
154
|
+
*/
|
|
155
|
+
function composeReleaseBody(changelogContent, metadata) {
|
|
156
|
+
return `${changelogContent}\n\n${formatPublishedToSection(metadata.targets)}\n\n${serializeMetadata(metadata)}`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Update just the status/metadata sections of an existing release body,
|
|
160
|
+
* preserving the changelog content above.
|
|
161
|
+
*/
|
|
162
|
+
function updateReleaseBodyStatus(existingBody, metadata) {
|
|
163
|
+
const publishIdx = existingBody.indexOf("#### Published to");
|
|
164
|
+
const metaIdx = existingBody.indexOf(METADATA_START);
|
|
165
|
+
let changelogContent;
|
|
166
|
+
if (publishIdx !== -1) changelogContent = existingBody.slice(0, publishIdx).trimEnd();
|
|
167
|
+
else if (metaIdx !== -1) changelogContent = existingBody.slice(0, metaIdx).trimEnd();
|
|
168
|
+
else changelogContent = existingBody.trimEnd();
|
|
169
|
+
return composeReleaseBody(changelogContent, metadata);
|
|
170
|
+
}
|
|
171
|
+
/** Look up an existing GitHub release (draft or published) by tag */
|
|
172
|
+
async function findReleaseByTag(tag, rootDir) {
|
|
173
|
+
if (!isGhAvailable()) return null;
|
|
174
|
+
try {
|
|
175
|
+
const json = await runArgsAsync([
|
|
176
|
+
"gh",
|
|
177
|
+
"release",
|
|
178
|
+
"view",
|
|
179
|
+
tag,
|
|
180
|
+
"--json",
|
|
181
|
+
"tagName,name,body,isDraft"
|
|
182
|
+
], { cwd: rootDir });
|
|
183
|
+
const data = JSON.parse(json);
|
|
184
|
+
return {
|
|
185
|
+
tag: data.tagName,
|
|
186
|
+
title: data.name,
|
|
187
|
+
body: data.body,
|
|
188
|
+
isDraft: data.isDraft,
|
|
189
|
+
metadata: parseReleaseMetadata(data.body)
|
|
190
|
+
};
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Create a draft GitHub release */
|
|
196
|
+
async function createDraftRelease(tag, title, body, rootDir, targetSha) {
|
|
197
|
+
const args = [
|
|
198
|
+
"gh",
|
|
199
|
+
"release",
|
|
200
|
+
"create",
|
|
201
|
+
tag,
|
|
202
|
+
"--title",
|
|
203
|
+
title,
|
|
204
|
+
"--notes",
|
|
205
|
+
body,
|
|
206
|
+
"--draft"
|
|
207
|
+
];
|
|
208
|
+
if (targetSha) args.push("--target", targetSha);
|
|
209
|
+
await runArgsAsync(args, { cwd: rootDir });
|
|
210
|
+
}
|
|
211
|
+
/** Update an existing GitHub release's body */
|
|
212
|
+
async function updateReleaseBody(tag, body, rootDir) {
|
|
213
|
+
await runArgsAsync([
|
|
214
|
+
"gh",
|
|
215
|
+
"release",
|
|
216
|
+
"edit",
|
|
217
|
+
tag,
|
|
218
|
+
"--notes",
|
|
219
|
+
body
|
|
220
|
+
], { cwd: rootDir });
|
|
221
|
+
}
|
|
222
|
+
/** Finalize a draft release (remove draft status) */
|
|
223
|
+
async function finalizeRelease(tag, rootDir) {
|
|
224
|
+
await runArgsAsync([
|
|
225
|
+
"gh",
|
|
226
|
+
"release",
|
|
227
|
+
"edit",
|
|
228
|
+
tag,
|
|
229
|
+
"--draft=false"
|
|
230
|
+
], { cwd: rootDir });
|
|
231
|
+
}
|
|
232
|
+
/** Find draft releases for a package (by name prefix) that are older than the current version */
|
|
233
|
+
async function findStaleDraftReleases(packageName, currentVersion, rootDir) {
|
|
234
|
+
if (!isGhAvailable()) return [];
|
|
235
|
+
const currentTag = `${packageName}@${currentVersion}`;
|
|
236
|
+
try {
|
|
237
|
+
const json = await runArgsAsync([
|
|
238
|
+
"gh",
|
|
239
|
+
"release",
|
|
240
|
+
"list",
|
|
241
|
+
"--json",
|
|
242
|
+
"tagName,isDraft,name",
|
|
243
|
+
"--limit",
|
|
244
|
+
"20"
|
|
245
|
+
], { cwd: rootDir });
|
|
246
|
+
const releases = JSON.parse(json);
|
|
247
|
+
const stale = [];
|
|
248
|
+
for (const r of releases) {
|
|
249
|
+
if (!r.isDraft) continue;
|
|
250
|
+
if (!r.tagName.startsWith(`${packageName}@`)) continue;
|
|
251
|
+
if (r.tagName === currentTag) continue;
|
|
252
|
+
const info = await findReleaseByTag(r.tagName, rootDir);
|
|
253
|
+
if (info) stale.push({
|
|
254
|
+
tag: r.tagName,
|
|
255
|
+
body: info.body,
|
|
256
|
+
metadata: info.metadata
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return stale;
|
|
260
|
+
} catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Finalize stale draft releases as superseded.
|
|
266
|
+
* Updates their metadata targets to "skipped" and marks them as non-draft.
|
|
267
|
+
*/
|
|
268
|
+
async function finalizeSupersededDrafts(packageName, newVersion, rootDir) {
|
|
269
|
+
const staleDrafts = await findStaleDraftReleases(packageName, newVersion, rootDir);
|
|
270
|
+
for (const draft of staleDrafts) {
|
|
271
|
+
log.dim(` Finalizing draft release ${draft.tag} — superseded by ${newVersion}`);
|
|
272
|
+
if (draft.metadata) {
|
|
273
|
+
for (const [targetName, state] of Object.entries(draft.metadata.targets)) if (state.status !== "success") draft.metadata.targets[targetName] = {
|
|
274
|
+
status: "skipped",
|
|
275
|
+
reason: "superseded",
|
|
276
|
+
supersededBy: newVersion
|
|
277
|
+
};
|
|
278
|
+
const updatedBody = updateReleaseBodyStatus(draft.body, draft.metadata);
|
|
279
|
+
try {
|
|
280
|
+
await updateReleaseBody(draft.tag, updatedBody, rootDir);
|
|
281
|
+
await finalizeRelease(draft.tag, rootDir);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
log.warn(` Failed to finalize superseded release ${draft.tag}: ${err instanceof Error ? err.message : err}`);
|
|
284
|
+
}
|
|
285
|
+
} else try {
|
|
286
|
+
await finalizeRelease(draft.tag, rootDir);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
log.warn(` Failed to finalize superseded release ${draft.tag}: ${err instanceof Error ? err.message : err}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/commands/publish.ts
|
|
294
|
+
/**
|
|
295
|
+
* Publish packages that have been versioned but not yet published.
|
|
296
|
+
* Detects unpublished versions by comparing package.json versions against npm registry.
|
|
297
|
+
*/
|
|
298
|
+
async function publishCommand(rootDir, opts) {
|
|
299
|
+
const config = await loadConfig(rootDir);
|
|
300
|
+
const { packages, catalogs } = await discoverWorkspace(rootDir, config);
|
|
301
|
+
const { packageManager: detectedPm } = await detectWorkspaces(rootDir);
|
|
302
|
+
const depGraph = new DependencyGraph(packages);
|
|
303
|
+
if (!opts.dryRun && hasUncommittedChanges({ cwd: rootDir })) {
|
|
304
|
+
log.warn("You have uncommitted changes. Commit or stash them before publishing.");
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
let toPublish = await findUnpublishedWithCache(rootDir, packages, config);
|
|
308
|
+
if (opts.excludePackages && opts.excludePackages.size > 0) {
|
|
309
|
+
const excluded = toPublish.filter((r) => opts.excludePackages.has(r.name));
|
|
310
|
+
if (excluded.length > 0) {
|
|
311
|
+
for (const r of excluded) log.dim(` Skipping ${r.name}@${r.newVersion} — pending bump will supersede this version`);
|
|
312
|
+
toPublish = toPublish.filter((r) => !opts.excludePackages.has(r.name));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (opts.filter) {
|
|
316
|
+
const { matchGlob } = await import("./config-D_4GYDJi.mjs").then((n) => n.t);
|
|
317
|
+
const patterns = opts.filter.split(",").map((p) => p.trim());
|
|
318
|
+
toPublish = toPublish.filter((r) => patterns.some((p) => matchGlob(r.name, p)));
|
|
319
|
+
}
|
|
320
|
+
if (toPublish.length === 0) {
|
|
321
|
+
log.info("No unpublished packages found.");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const recoveredBumpFiles = opts.recoveredBumpFiles || [];
|
|
325
|
+
if (recoveredBumpFiles.length > 0) for (const release of toPublish) release.bumpFiles = recoveredBumpFiles.filter((bf) => bf.releases.some((r) => r.name === release.name)).map((bf) => bf.id);
|
|
326
|
+
const releasePlan = {
|
|
327
|
+
bumpFiles: recoveredBumpFiles,
|
|
328
|
+
releases: toPublish,
|
|
329
|
+
warnings: []
|
|
330
|
+
};
|
|
331
|
+
if (opts.dryRun) log.bold("Dry run — would publish:");
|
|
332
|
+
else log.bold("Publishing:");
|
|
333
|
+
for (const r of toPublish) console.log(` ${r.name}@${colorize(r.newVersion, "cyan")}`);
|
|
334
|
+
console.log();
|
|
335
|
+
const formatter = config.changelog !== false ? await loadFormatter(config.changelog, rootDir) : void 0;
|
|
336
|
+
const ghAvailable = isGhAvailable();
|
|
337
|
+
const publishTargetsByPkg = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const release of toPublish) {
|
|
339
|
+
const pkgConfig = packages.get(release.name).bumpy || {};
|
|
340
|
+
const targets = [];
|
|
341
|
+
if (pkgConfig.publishCommand) targets.push("custom");
|
|
342
|
+
else if (!pkgConfig.skipNpmPublish) targets.push("npm");
|
|
343
|
+
publishTargetsByPkg.set(release.name, targets);
|
|
344
|
+
}
|
|
345
|
+
const releaseMetadataByPkg = /* @__PURE__ */ new Map();
|
|
346
|
+
if (ghAvailable && !opts.dryRun) {
|
|
347
|
+
for (const release of toPublish) {
|
|
348
|
+
const tag = `${release.name}@${release.newVersion}`;
|
|
349
|
+
const targets = publishTargetsByPkg.get(release.name) || [];
|
|
350
|
+
if (targets.length === 0) continue;
|
|
351
|
+
const existing = await findReleaseByTag(tag, rootDir);
|
|
352
|
+
if (existing && existing.metadata) {
|
|
353
|
+
log.dim(` Found existing release for ${tag} (${existing.isDraft ? "draft" : "published"})`);
|
|
354
|
+
releaseMetadataByPkg.set(release.name, {
|
|
355
|
+
tag,
|
|
356
|
+
metadata: existing.metadata,
|
|
357
|
+
existingBody: existing.body
|
|
358
|
+
});
|
|
359
|
+
} else if (existing && !existing.metadata) log.dim(` Found existing release for ${tag} without bumpy metadata — skipping draft management`);
|
|
360
|
+
else {
|
|
361
|
+
await finalizeSupersededDrafts(release.name, release.newVersion, rootDir);
|
|
362
|
+
const changelogContent = formatter ? await generateReleaseBody(release, releasePlan.bumpFiles, formatter) : buildReleaseBody(release, releasePlan.bumpFiles);
|
|
363
|
+
const initialTargets = {};
|
|
364
|
+
for (const t of targets) initialTargets[t] = { status: "pending" };
|
|
365
|
+
const metadata = {
|
|
366
|
+
version: release.newVersion,
|
|
367
|
+
targets: initialTargets
|
|
368
|
+
};
|
|
369
|
+
const body = composeReleaseBody(changelogContent, metadata);
|
|
370
|
+
const title = `${release.name} v${release.newVersion}`;
|
|
371
|
+
const headSha = getHeadSha(rootDir);
|
|
372
|
+
try {
|
|
373
|
+
await createDraftRelease(tag, title, body, rootDir, headSha || void 0);
|
|
374
|
+
log.dim(` Created draft release: ${title}`);
|
|
375
|
+
releaseMetadataByPkg.set(release.name, {
|
|
376
|
+
tag,
|
|
377
|
+
metadata,
|
|
378
|
+
existingBody: body
|
|
379
|
+
});
|
|
380
|
+
} catch (err) {
|
|
381
|
+
log.warn(` Failed to create draft release for ${tag}: ${err instanceof Error ? err.message : err}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const release of toPublish) {
|
|
386
|
+
const info = releaseMetadataByPkg.get(release.name);
|
|
387
|
+
if (!info) continue;
|
|
388
|
+
if (!Object.values(info.metadata.targets).some((t) => t.status === "success")) {
|
|
389
|
+
const tag = info.tag;
|
|
390
|
+
const headSha = getHeadSha(rootDir);
|
|
391
|
+
const tagSha = tryRunArgs([
|
|
392
|
+
"git",
|
|
393
|
+
"rev-parse",
|
|
394
|
+
tag
|
|
395
|
+
], { cwd: rootDir });
|
|
396
|
+
if (headSha && tagSha && headSha !== tagSha) {
|
|
397
|
+
const count = tryRunArgs([
|
|
398
|
+
"git",
|
|
399
|
+
"rev-list",
|
|
400
|
+
"--count",
|
|
401
|
+
`${tag}..HEAD`
|
|
402
|
+
], { cwd: rootDir });
|
|
403
|
+
log.dim(` Moving version tag ${tag} to HEAD (includes ${count} commit(s) since versioning)`);
|
|
404
|
+
tryRunArgs([
|
|
405
|
+
"git",
|
|
406
|
+
"tag",
|
|
407
|
+
"-f",
|
|
408
|
+
tag
|
|
409
|
+
], { cwd: rootDir });
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
const tag = info.tag;
|
|
413
|
+
const headSha = getHeadSha(rootDir);
|
|
414
|
+
const tagSha = tryRunArgs([
|
|
415
|
+
"git",
|
|
416
|
+
"rev-parse",
|
|
417
|
+
tag
|
|
418
|
+
], { cwd: rootDir });
|
|
419
|
+
if (headSha && tagSha && headSha !== tagSha) {
|
|
420
|
+
const count = tryRunArgs([
|
|
421
|
+
"git",
|
|
422
|
+
"rev-list",
|
|
423
|
+
"--count",
|
|
424
|
+
`${tag}..HEAD`
|
|
425
|
+
], { cwd: rootDir });
|
|
426
|
+
log.warn(` HEAD is ${count} commit(s) ahead of version tag ${tag} — some targets already published from tagged commit`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const alreadyPublished = [];
|
|
432
|
+
for (const release of toPublish) {
|
|
433
|
+
const info = releaseMetadataByPkg.get(release.name);
|
|
434
|
+
if (!info) continue;
|
|
435
|
+
if ((publishTargetsByPkg.get(release.name) || []).every((t) => info.metadata.targets[t]?.status === "success")) alreadyPublished.push(release.name);
|
|
436
|
+
}
|
|
437
|
+
if (alreadyPublished.length > 0) {
|
|
438
|
+
for (const name of alreadyPublished) log.dim(` Skipping ${name} — all targets already published (per draft release metadata)`);
|
|
439
|
+
toPublish = toPublish.filter((r) => !alreadyPublished.includes(r.name));
|
|
440
|
+
releasePlan.releases = toPublish;
|
|
441
|
+
}
|
|
442
|
+
if (toPublish.length === 0) {
|
|
443
|
+
log.info("All packages already published successfully.");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const result = await publishPackages(releasePlan, packages, depGraph, config, rootDir, {
|
|
447
|
+
dryRun: opts.dryRun,
|
|
448
|
+
tag: opts.tag
|
|
449
|
+
}, catalogs, detectedPm);
|
|
450
|
+
if (result.published.length > 0) log.success(`🐸 Published ${result.published.length} package(s)`);
|
|
451
|
+
if (result.skipped.length > 0) log.dim(`Skipped ${result.skipped.length}: ${result.skipped.map((s) => s.name).join(", ")}`);
|
|
452
|
+
if (ghAvailable && !opts.dryRun) for (const release of releasePlan.releases) {
|
|
453
|
+
const info = releaseMetadataByPkg.get(release.name);
|
|
454
|
+
if (!info) continue;
|
|
455
|
+
const targets = publishTargetsByPkg.get(release.name) || [];
|
|
456
|
+
const published = result.published.find((p) => p.name === release.name);
|
|
457
|
+
const failed = result.failed.find((f) => f.name === release.name);
|
|
458
|
+
let changed = false;
|
|
459
|
+
for (const targetName of targets) {
|
|
460
|
+
if (info.metadata.targets[targetName]?.status === "success") continue;
|
|
461
|
+
if (published) {
|
|
462
|
+
info.metadata.targets[targetName] = {
|
|
463
|
+
status: "success",
|
|
464
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
465
|
+
url: buildPublishUrl(release.name, release.newVersion, targetName)
|
|
466
|
+
};
|
|
467
|
+
changed = true;
|
|
468
|
+
} else if (failed) {
|
|
469
|
+
info.metadata.targets[targetName] = {
|
|
470
|
+
status: "failed",
|
|
471
|
+
error: failed.error,
|
|
472
|
+
lastAttempt: (/* @__PURE__ */ new Date()).toISOString()
|
|
473
|
+
};
|
|
474
|
+
changed = true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (changed) try {
|
|
478
|
+
const updatedBody = info.existingBody ? updateReleaseBodyStatus(info.existingBody, info.metadata) : composeReleaseBody("", info.metadata);
|
|
479
|
+
await updateReleaseBody(info.tag, updatedBody, rootDir);
|
|
480
|
+
if (Object.values(info.metadata.targets).every((t) => t.status === "success")) {
|
|
481
|
+
await finalizeRelease(info.tag, rootDir);
|
|
482
|
+
log.dim(` Finalized release: ${info.tag}`);
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
log.warn(` Failed to update release for ${info.tag}: ${err instanceof Error ? err.message : err}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (result.failed.length > 0) {
|
|
489
|
+
log.error(`Failed ${result.failed.length}: ${result.failed.map((f) => `${f.name} (${f.error})`).join(", ")}`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
if (!opts.dryRun && !opts.noPush && result.published.length > 0) try {
|
|
493
|
+
log.step("Pushing tags...");
|
|
494
|
+
pushWithTags({ cwd: rootDir });
|
|
495
|
+
log.success("Pushed tags to remote");
|
|
496
|
+
} catch (err) {
|
|
497
|
+
log.warn(`Failed to push tags: ${err instanceof Error ? err.message : err}`);
|
|
498
|
+
}
|
|
499
|
+
if (!ghAvailable && result.published.length > 0) await createIndividualReleases(releasePlan.releases.filter((r) => result.published.some((p) => p.name === r.name)), releasePlan.bumpFiles, rootDir, {
|
|
500
|
+
dryRun: opts.dryRun,
|
|
501
|
+
formatter
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Try to load cached plan from `ci plan`. Returns the unpublished package names
|
|
506
|
+
* if the cache is valid, or null to fall back to registry lookups.
|
|
507
|
+
*
|
|
508
|
+
* Validates that every cached package exists in the workspace with the same version,
|
|
509
|
+
* so the cache can only filter — never fabricate — the set of packages.
|
|
510
|
+
*/
|
|
511
|
+
function loadCachedPlan(rootDir, packages) {
|
|
512
|
+
const cachePath = `${rootDir}/${CI_PLAN_CACHE_PATH}`;
|
|
513
|
+
let raw;
|
|
514
|
+
try {
|
|
515
|
+
raw = __require("node:fs").readFileSync(cachePath, "utf-8");
|
|
516
|
+
__require("node:fs").unlinkSync(cachePath);
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const cached = JSON.parse(raw);
|
|
522
|
+
if (cached?.mode !== "publish" || !Array.isArray(cached.releases)) return null;
|
|
523
|
+
const names = /* @__PURE__ */ new Set();
|
|
524
|
+
for (const r of cached.releases) {
|
|
525
|
+
if (typeof r.name !== "string" || typeof r.newVersion !== "string") return null;
|
|
526
|
+
const pkg = packages.get(r.name);
|
|
527
|
+
if (!pkg || pkg.version !== r.newVersion) {
|
|
528
|
+
log.dim(" ci plan cache is stale — falling back to registry lookups");
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
names.add(r.name);
|
|
532
|
+
}
|
|
533
|
+
log.dim(" Using cached plan from ci plan");
|
|
534
|
+
return names;
|
|
535
|
+
} catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Find unpublished packages, using the ci plan cache if available.
|
|
541
|
+
* Falls back to registry lookups if no cache or cache is invalid.
|
|
542
|
+
*/
|
|
543
|
+
async function findUnpublishedWithCache(rootDir, packages, config) {
|
|
544
|
+
const cachedNames = loadCachedPlan(rootDir, packages);
|
|
545
|
+
if (cachedNames) {
|
|
546
|
+
const unpublished = [];
|
|
547
|
+
for (const name of cachedNames) {
|
|
548
|
+
const pkg = packages.get(name);
|
|
549
|
+
unpublished.push({
|
|
550
|
+
name,
|
|
551
|
+
type: "patch",
|
|
552
|
+
oldVersion: pkg.version,
|
|
553
|
+
newVersion: pkg.version,
|
|
554
|
+
bumpFiles: [],
|
|
555
|
+
isDependencyBump: false,
|
|
556
|
+
isCascadeBump: false,
|
|
557
|
+
isGroupBump: false,
|
|
558
|
+
bumpSources: []
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return unpublished;
|
|
562
|
+
}
|
|
563
|
+
return findUnpublishedPackages(packages, config);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Find packages whose current version is not yet published.
|
|
567
|
+
*
|
|
568
|
+
* Detection strategy (per package):
|
|
569
|
+
* 1. Custom `checkPublished` command → run it, compare output to current version
|
|
570
|
+
* 2. `skipNpmPublish` or custom `publishCommand` → check git tags
|
|
571
|
+
* 3. Default → check npm registry via `npm info`
|
|
572
|
+
*/
|
|
573
|
+
async function findUnpublishedPackages(packages, _config) {
|
|
574
|
+
const unpublished = [];
|
|
575
|
+
for (const [name, pkg] of packages) {
|
|
576
|
+
if (pkg.private && !pkg.bumpy?.publishCommand) continue;
|
|
577
|
+
if (pkg.version === "0.0.0") continue;
|
|
578
|
+
if (!await checkIfPublished(name, pkg.version, pkg.bumpy)) unpublished.push({
|
|
579
|
+
name,
|
|
580
|
+
type: "patch",
|
|
581
|
+
oldVersion: pkg.version,
|
|
582
|
+
newVersion: pkg.version,
|
|
583
|
+
bumpFiles: [],
|
|
584
|
+
isDependencyBump: false,
|
|
585
|
+
isCascadeBump: false,
|
|
586
|
+
isGroupBump: false,
|
|
587
|
+
bumpSources: []
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return unpublished;
|
|
591
|
+
}
|
|
592
|
+
async function checkIfPublished(name, version, pkgConfig) {
|
|
593
|
+
const { runAsync, runArgsAsync, tryRunArgs } = await import("./shell-C8KgKnMQ.mjs").then((n) => n.a);
|
|
594
|
+
if (pkgConfig?.checkPublished) try {
|
|
595
|
+
return (await runAsync(pkgConfig.checkPublished)).trim() === version;
|
|
596
|
+
} catch {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
if (pkgConfig?.skipNpmPublish || pkgConfig?.publishCommand) {
|
|
600
|
+
const tag = `${name}@${version}`;
|
|
601
|
+
return tryRunArgs([
|
|
602
|
+
"git",
|
|
603
|
+
"tag",
|
|
604
|
+
"-l",
|
|
605
|
+
tag
|
|
606
|
+
]) === tag;
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
const args = [
|
|
610
|
+
"npm",
|
|
611
|
+
"info",
|
|
612
|
+
`${name}@${version}`,
|
|
613
|
+
"version"
|
|
614
|
+
];
|
|
615
|
+
if (pkgConfig?.registry) args.push("--registry", pkgConfig.registry);
|
|
616
|
+
return await runArgsAsync(args) === version;
|
|
617
|
+
} catch {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
//#endregion
|
|
622
|
+
export { findUnpublishedPackages, publishCommand };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { n as log, t as colorize } from "./logger-BgksGFuf.mjs";
|
|
2
2
|
import { a as readJson, u as updateJsonNestedField } from "./fs-CBXKZhoU.mjs";
|
|
3
3
|
import { r as resolveCatalogDep } from "./package-manager-BQPwXwu5.mjs";
|
|
4
|
-
import { i as stripProtocol } from "./release-plan-
|
|
4
|
+
import { i as stripProtocol } from "./release-plan-mK7iGeGq.mjs";
|
|
5
5
|
import { i as runAsync, o as sq, r as runArgsAsync, s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { l as tagExists, t as createTag } from "./git-JGLQtk-M.mjs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { unlink } from "node:fs/promises";
|
|
9
9
|
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as __commonJSMin, s as __toESM } from "./logger-BgksGFuf.mjs";
|
|
2
|
-
import { c as maxBump, l as normalizeCascadeConfig, n as DEFAULT_BUMP_RULES, o as bumpLevel, s as hasCascade } from "./types-
|
|
3
|
-
import { s as matchGlob } from "./config-
|
|
2
|
+
import { c as maxBump, l as normalizeCascadeConfig, n as DEFAULT_BUMP_RULES, o as bumpLevel, s as hasCascade } from "./types-Bkh-igOJ.mjs";
|
|
3
|
+
import { s as matchGlob } from "./config-D_4GYDJi.mjs";
|
|
4
4
|
//#region src/core/dep-graph.ts
|
|
5
5
|
var DependencyGraph = class {
|
|
6
6
|
/** Map from package name → packages that depend on it */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as log, t as colorize } from "./logger-BgksGFuf.mjs";
|
|
2
|
-
import { a as loadConfig } from "./config-
|
|
3
|
-
import { o as discoverPackages, r as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-
|
|
4
|
-
import { a as DependencyGraph, t as assembleReleasePlan } from "./release-plan-
|
|
5
|
-
import { i as getCurrentBranch, r as getChangedFiles } from "./git-
|
|
2
|
+
import { a as loadConfig } from "./config-D_4GYDJi.mjs";
|
|
3
|
+
import { o as discoverPackages, r as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-B9DpXK5X.mjs";
|
|
4
|
+
import { a as DependencyGraph, t as assembleReleasePlan } from "./release-plan-mK7iGeGq.mjs";
|
|
5
|
+
import { i as getCurrentBranch, r as getChangedFiles } from "./git-JGLQtk-M.mjs";
|
|
6
6
|
//#region src/commands/status.ts
|
|
7
7
|
async function statusCommand(rootDir, opts) {
|
|
8
8
|
const config = await loadConfig(rootDir);
|
|
@@ -29,7 +29,7 @@ async function statusCommand(rootDir, opts) {
|
|
|
29
29
|
releases = releases.filter((r) => types.includes(r.type));
|
|
30
30
|
}
|
|
31
31
|
if (opts.filter) {
|
|
32
|
-
const { matchGlob } = await import("./config-
|
|
32
|
+
const { matchGlob } = await import("./config-D_4GYDJi.mjs").then((n) => n.t);
|
|
33
33
|
const patterns = opts.filter.split(",").map((p) => p.trim());
|
|
34
34
|
releases = releases.filter((r) => patterns.some((p) => matchGlob(r.name, p)));
|
|
35
35
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { n as log, t as colorize } from "./logger-BgksGFuf.mjs";
|
|
2
|
-
import { a as loadConfig } from "./config-
|
|
2
|
+
import { a as loadConfig } from "./config-D_4GYDJi.mjs";
|
|
3
3
|
import { n as detectWorkspaces } from "./package-manager-BQPwXwu5.mjs";
|
|
4
|
-
import { o as discoverPackages, r as readBumpFiles } from "./bump-file-
|
|
5
|
-
import { a as DependencyGraph, t as assembleReleasePlan } from "./release-plan-
|
|
4
|
+
import { o as discoverPackages, r as readBumpFiles } from "./bump-file-B9DpXK5X.mjs";
|
|
5
|
+
import { a as DependencyGraph, t as assembleReleasePlan } from "./release-plan-mK7iGeGq.mjs";
|
|
6
6
|
import { n as runArgs, s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
|
|
7
|
-
import { t as applyReleasePlan } from "./apply-release-plan-
|
|
7
|
+
import { t as applyReleasePlan } from "./apply-release-plan-CmjqYo0t.mjs";
|
|
8
8
|
import { t as resolveCommitMessage } from "./commit-message-CSWVKPJ-.mjs";
|
|
9
9
|
//#region src/commands/version.ts
|
|
10
10
|
async function versionCommand(rootDir, opts = {}) {
|