keepchanges 0.0.3 → 1.0.0-beta.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/README.md +48 -158
- package/README.zh-CN.md +181 -0
- package/dist/cli.d.mts +1 -14
- package/dist/cli.mjs +166 -254
- package/dist/commit-FX77u6Sj.mjs +190 -0
- package/dist/index.d.mts +152 -0
- package/dist/index.mjs +2 -0
- package/package.json +3 -4
package/dist/cli.mjs
CHANGED
|
@@ -1,122 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as insertRelease, c as defaultConfig, i as hasRelease, n as parseCommits, o as readChangelog, r as generateChangelog, s as writeChangelog } from "./commit-FX77u6Sj.mjs";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { normalizeFull } from "verkit";
|
|
3
5
|
import process from "node:process";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { cac } from "cac";
|
|
6
7
|
import { resolve } from "node:path";
|
|
7
8
|
import ansis from "ansis";
|
|
8
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
-
import { normalizeFull } from "verkit";
|
|
10
9
|
import { x } from "tinyexec";
|
|
11
10
|
//#region package.json
|
|
12
|
-
var version = "0.0.
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region src/changelog.ts
|
|
15
|
-
function createChangelog(version, commits, repository, comparisonFrom) {
|
|
16
|
-
const body = [
|
|
17
|
-
...renderSection(commits.filter((commit) => commit.isBreaking), "🚨 Breaking Changes", repository),
|
|
18
|
-
...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "feat"), "🚀 Features", repository),
|
|
19
|
-
...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "fix"), "🐞 Bug Fixes", repository),
|
|
20
|
-
...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "perf"), "🏎 Performance", repository),
|
|
21
|
-
...repository && comparisonFrom ? ["", `##### [View changes on ${repository.provider.name}](${repository.provider.compareUrl(repository, comparisonFrom, `v${version}`)})`] : []
|
|
22
|
-
].join("\n").trim();
|
|
23
|
-
return {
|
|
24
|
-
body,
|
|
25
|
-
release: [
|
|
26
|
-
`## v${version}`,
|
|
27
|
-
"",
|
|
28
|
-
body,
|
|
29
|
-
""
|
|
30
|
-
].join("\n")
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
async function readChangelog(path) {
|
|
34
|
-
return readFile(path, "utf8").catch((error) => {
|
|
35
|
-
if (error.code === "ENOENT") return "# Changelog\n";
|
|
36
|
-
throw error;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function writeChangelog(path, changelog) {
|
|
40
|
-
return writeFile(path, changelog);
|
|
41
|
-
}
|
|
42
|
-
function hasRelease(changelog, version) {
|
|
43
|
-
const releaseVersion = normalizeFull(version);
|
|
44
|
-
return releaseHeadings(changelog).some((heading) => heading.version === releaseVersion);
|
|
45
|
-
}
|
|
46
|
-
function insertRelease(changelog, release) {
|
|
47
|
-
const releaseVersion = releaseHeadings(release)[0]?.version;
|
|
48
|
-
const headings = releaseHeadings(changelog);
|
|
49
|
-
const existingReleaseIndex = headings.findIndex((heading) => heading.version === releaseVersion);
|
|
50
|
-
if (existingReleaseIndex !== -1) {
|
|
51
|
-
const start = headings[existingReleaseIndex].index;
|
|
52
|
-
const end = headings[existingReleaseIndex + 1]?.index ?? changelog.length;
|
|
53
|
-
changelog = changelog.slice(0, start) + changelog.slice(end);
|
|
54
|
-
}
|
|
55
|
-
const firstRelease = releaseHeadings(changelog)[0];
|
|
56
|
-
if (!firstRelease) return `${changelog.trimEnd()}\n\n${release.trim()}\n`;
|
|
57
|
-
const preamble = changelog.slice(0, firstRelease.index).trimEnd();
|
|
58
|
-
const history = changelog.slice(firstRelease.index).trim();
|
|
59
|
-
return `${preamble}\n\n${release.trim()}\n\n${history}\n`;
|
|
60
|
-
}
|
|
61
|
-
function releaseHeadings(changelog) {
|
|
62
|
-
return [...changelog.matchAll(/^##[^\S\r\n]+(\S+)/gm)].flatMap((heading) => {
|
|
63
|
-
const version = normalizeFull(heading[1]);
|
|
64
|
-
return version ? [{
|
|
65
|
-
index: heading.index,
|
|
66
|
-
version
|
|
67
|
-
}] : [];
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
function renderSection(commits, title, repository) {
|
|
71
|
-
const renderedCommits = commits.map((commit) => {
|
|
72
|
-
const reference = repository ? `[<samp>(${commit.hash.slice(0, 5)})</samp>](${repository.provider.commitUrl(repository, commit.hash)})` : "";
|
|
73
|
-
const authorNames = commit.authors.map((author) => author.login ? `@${author.login}` : `**${escapeHtml(author.name)}**`);
|
|
74
|
-
const authors = authorNames.length > 1 ? `${authorNames.slice(0, -1).join(", ")} and ${authorNames.at(-1)}` : authorNames[0] || "";
|
|
75
|
-
const pullRequests = commit.pullRequests.map((pullRequest) => repository ? `[${pullRequest}](${repository.provider.pullRequestUrl(repository, pullRequest)})` : pullRequest).join(", ");
|
|
76
|
-
const details = [
|
|
77
|
-
authors ? `by ${authors}` : "",
|
|
78
|
-
pullRequests ? `in ${pullRequests}` : "",
|
|
79
|
-
reference
|
|
80
|
-
].filter(Boolean).join(" ");
|
|
81
|
-
const suffix = details ? ` - ${details}` : "";
|
|
82
|
-
return {
|
|
83
|
-
scope: commit.scope,
|
|
84
|
-
line: `${escapeHtml(capitalize$1(commit.description))}${suffix}`
|
|
85
|
-
};
|
|
86
|
-
});
|
|
87
|
-
if (!renderedCommits.length) return [];
|
|
88
|
-
const commitsByScope = /* @__PURE__ */ new Map();
|
|
89
|
-
for (const { scope, line } of renderedCommits) {
|
|
90
|
-
const lines = commitsByScope.get(scope) || [];
|
|
91
|
-
lines.push(line);
|
|
92
|
-
commitsByScope.set(scope, lines);
|
|
93
|
-
}
|
|
94
|
-
const lines = [...commitsByScope].some(([scope, lines]) => Boolean(scope) && lines.length > 1) ? [...commitsByScope.keys()].sort().flatMap((scope) => {
|
|
95
|
-
const scopedLines = (commitsByScope.get(scope) || []).reverse().map((line) => `${scope ? " " : ""}- ${line}`);
|
|
96
|
-
return scope ? [`- **${escapeHtml(scope)}**:`, ...scopedLines] : scopedLines;
|
|
97
|
-
}) : renderedCommits.reverse().map(({ scope, line }) => {
|
|
98
|
-
return `- ${scope ? `**${escapeHtml(scope)}**: ` : ""}${line}`;
|
|
99
|
-
});
|
|
100
|
-
return [
|
|
101
|
-
"",
|
|
102
|
-
`### ${title}`,
|
|
103
|
-
"",
|
|
104
|
-
...lines
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
function capitalize$1(value) {
|
|
108
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
109
|
-
}
|
|
110
|
-
function escapeHtml(value) {
|
|
111
|
-
const htmlEntities = {
|
|
112
|
-
"&": "&",
|
|
113
|
-
"<": "<",
|
|
114
|
-
">": ">",
|
|
115
|
-
"\"": """,
|
|
116
|
-
"'": "'"
|
|
117
|
-
};
|
|
118
|
-
return value.replace(/[&<>"']/g, (character) => htmlEntities[character]);
|
|
119
|
-
}
|
|
11
|
+
var version = "1.0.0-beta.1";
|
|
120
12
|
//#endregion
|
|
121
13
|
//#region src/git.ts
|
|
122
14
|
async function git(cwd, ...args) {
|
|
@@ -145,11 +37,8 @@ async function getRemoteTagCommit(cwd, tag) {
|
|
|
145
37
|
const refs = (await git(cwd, "ls-remote", "--tags", "origin", `refs/tags/${tag}`, `refs/tags/${tag}^{}`)).trim().split("\n").filter(Boolean);
|
|
146
38
|
return (refs.find((line) => line.endsWith("^{}")) || refs[0])?.split(/\s+/)[0];
|
|
147
39
|
}
|
|
148
|
-
async function
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
function parseGitLog(log) {
|
|
152
|
-
const fields = log.split("\0");
|
|
40
|
+
async function readGitCommits(cwd, from, to) {
|
|
41
|
+
const fields = (await git(cwd, "log", from ? `${from}..${to}` : to, "--format=%h%x00%an%x00%ae%x00%s%x00%b%x00")).split("\0");
|
|
153
42
|
const commits = [];
|
|
154
43
|
for (let index = 0; index + 4 < fields.length; index += 5) {
|
|
155
44
|
const subject = fields[index + 3].trim();
|
|
@@ -165,32 +54,9 @@ function parseGitLog(log) {
|
|
|
165
54
|
}
|
|
166
55
|
return commits;
|
|
167
56
|
}
|
|
168
|
-
function parseCommit(hash, subject, body, author) {
|
|
169
|
-
const match = /^(?<type>[a-z]+)(?:\((?<scope>[^()\r\n]+)\))?(?<breaking>!)?: (?<description>.+)$/i.exec(subject);
|
|
170
|
-
if (!match?.groups) return null;
|
|
171
|
-
const pullRequestPattern = /\([ a-z]*(#\d+)\s*\)/gi;
|
|
172
|
-
const pullRequests = [...new Set([...match.groups.description.matchAll(pullRequestPattern)].map((reference) => reference[1]))];
|
|
173
|
-
const authors = [author];
|
|
174
|
-
for (const coAuthor of body.matchAll(/^Co-Authored-By:([^<\r\n]+)<([^>\r\n]+)>[^\S\r\n]*$/gim)) {
|
|
175
|
-
const email = coAuthor[2].trim();
|
|
176
|
-
if (!authors.some((author) => author.email === email)) authors.push({
|
|
177
|
-
name: coAuthor[1].trim(),
|
|
178
|
-
email
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
hash,
|
|
183
|
-
authors: authors.filter((author) => !/\[bot\]|dependabot|\(bot\)/i.test(author.name)),
|
|
184
|
-
type: match.groups.type.toLowerCase(),
|
|
185
|
-
scope: match.groups.scope || "",
|
|
186
|
-
description: match.groups.description.replace(pullRequestPattern, "").trim(),
|
|
187
|
-
pullRequests,
|
|
188
|
-
isBreaking: Boolean(match.groups.breaking) || /^BREAKING(?: |-)CHANGE:/im.test(body)
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
57
|
//#endregion
|
|
192
|
-
//#region src/
|
|
193
|
-
const
|
|
58
|
+
//#region src/repositories/gitea.ts
|
|
59
|
+
const giteaRepository = {
|
|
194
60
|
name: "Gitea",
|
|
195
61
|
parse(source) {
|
|
196
62
|
try {
|
|
@@ -199,7 +65,7 @@ const giteaProvider = {
|
|
|
199
65
|
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
200
66
|
if (!path.includes("/")) return;
|
|
201
67
|
return {
|
|
202
|
-
provider:
|
|
68
|
+
provider: giteaRepository,
|
|
203
69
|
path,
|
|
204
70
|
webUrl: `${url.origin}/${path}`
|
|
205
71
|
};
|
|
@@ -217,6 +83,11 @@ const giteaProvider = {
|
|
|
217
83
|
compareUrl(repository, from, to) {
|
|
218
84
|
return `${repository.webUrl}/compare/${from}...${to}`;
|
|
219
85
|
},
|
|
86
|
+
manualReleaseUrl(repository, release) {
|
|
87
|
+
const url = new URL(`${repository.webUrl}/releases/new`);
|
|
88
|
+
url.search = new URLSearchParams({ tag: release.tag }).toString();
|
|
89
|
+
return url.toString();
|
|
90
|
+
},
|
|
220
91
|
async resolveAuthors(commits, repository, token, fetch) {
|
|
221
92
|
const headers = {
|
|
222
93
|
accept: "application/json",
|
|
@@ -257,7 +128,8 @@ const giteaProvider = {
|
|
|
257
128
|
tag_name: release.tag,
|
|
258
129
|
name: release.name,
|
|
259
130
|
body: release.body,
|
|
260
|
-
prerelease: release.prerelease
|
|
131
|
+
prerelease: release.prerelease,
|
|
132
|
+
draft: release.draft
|
|
261
133
|
})
|
|
262
134
|
});
|
|
263
135
|
if (!response.ok) throw new Error(`Gitea release publishing failed (${response.status})`);
|
|
@@ -268,14 +140,14 @@ const giteaProvider = {
|
|
|
268
140
|
}
|
|
269
141
|
};
|
|
270
142
|
//#endregion
|
|
271
|
-
//#region src/
|
|
272
|
-
const
|
|
143
|
+
//#region src/repositories/github.ts
|
|
144
|
+
const githubRepository = {
|
|
273
145
|
name: "GitHub",
|
|
274
146
|
parse(source) {
|
|
275
147
|
const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(source);
|
|
276
148
|
if (!match) return;
|
|
277
149
|
return {
|
|
278
|
-
provider:
|
|
150
|
+
provider: githubRepository,
|
|
279
151
|
path: match[1],
|
|
280
152
|
webUrl: `https://github.com/${match[1]}`
|
|
281
153
|
};
|
|
@@ -340,7 +212,8 @@ const githubProvider = {
|
|
|
340
212
|
tag_name: release.tag,
|
|
341
213
|
name: release.name,
|
|
342
214
|
body: release.body,
|
|
343
|
-
prerelease: release.prerelease
|
|
215
|
+
prerelease: release.prerelease,
|
|
216
|
+
draft: release.draft
|
|
344
217
|
})
|
|
345
218
|
});
|
|
346
219
|
if (!response.ok) throw new Error(`GitHub release publishing failed (${response.status})`);
|
|
@@ -352,24 +225,28 @@ const githubProvider = {
|
|
|
352
225
|
};
|
|
353
226
|
//#endregion
|
|
354
227
|
//#region src/repository.ts
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
228
|
+
const repositoryProviders = {
|
|
229
|
+
github: githubRepository,
|
|
230
|
+
gitea: giteaRepository
|
|
358
231
|
};
|
|
359
|
-
async function resolveRepository(cwd) {
|
|
232
|
+
async function resolveRepository(cwd, explicit) {
|
|
233
|
+
if (explicit) {
|
|
234
|
+
const repository = parseRepository(/^[^/:]+\/[^/]+$/.test(explicit) ? `https://github.com/${explicit}` : explicit, Object.values(repositoryProviders));
|
|
235
|
+
if (!repository) throw new Error(`Unsupported repository: ${explicit}`);
|
|
236
|
+
return repository;
|
|
237
|
+
}
|
|
360
238
|
let providerName = "";
|
|
361
239
|
let source = await readFile(resolve(cwd, "package.json"), "utf8").then((contents) => {
|
|
362
240
|
const repository = JSON.parse(contents).repository;
|
|
363
241
|
if (typeof repository === "object") providerName = repository?.provider?.toLowerCase() || "";
|
|
364
242
|
return typeof repository === "string" ? repository : repository?.url || "";
|
|
365
243
|
}).catch(() => "");
|
|
366
|
-
if (!source) source =
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
for (const provider of [githubProvider]) {
|
|
244
|
+
if (!source) source = await git(cwd, "remote", "get-url", "origin").then((output) => output.trim()).catch(() => "");
|
|
245
|
+
if (providerName) return repositoryProviders[providerName]?.parse(source);
|
|
246
|
+
return parseRepository(source, [githubRepository]);
|
|
247
|
+
}
|
|
248
|
+
function parseRepository(source, providers) {
|
|
249
|
+
for (const provider of providers) {
|
|
373
250
|
const repository = provider.parse(source);
|
|
374
251
|
if (repository) return repository;
|
|
375
252
|
}
|
|
@@ -398,22 +275,41 @@ async function updateVersion(cwd, version) {
|
|
|
398
275
|
}
|
|
399
276
|
}
|
|
400
277
|
//#endregion
|
|
401
|
-
//#region src/
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
278
|
+
//#region src/cli/output.ts
|
|
279
|
+
function printChangesPreview(preview, stdout, colors) {
|
|
280
|
+
stdout([
|
|
281
|
+
colors.dim(`keep${colors.bold("changes")} v${version}`),
|
|
282
|
+
`${colors.cyan(preview.from)}${colors.dim(" -> ")}${colors.blue(preview.tag)}${colors.dim(` (${preview.commitCount} commits)`)}`,
|
|
283
|
+
colors.dim("--------------"),
|
|
284
|
+
"",
|
|
285
|
+
preview.body.replaceAll(" ", ""),
|
|
286
|
+
"",
|
|
287
|
+
colors.dim("--------------"),
|
|
288
|
+
""
|
|
289
|
+
].join("\n"));
|
|
290
|
+
}
|
|
291
|
+
function printManualReleaseUrl(repository, release, stdout, colors) {
|
|
292
|
+
const url = repository.provider.manualReleaseUrl?.(repository, release);
|
|
293
|
+
if (url) stdout(`${colors.yellow("Using the following link to create it manually:")}\n${colors.yellow(url)}\n`);
|
|
294
|
+
}
|
|
295
|
+
function printPublishedRelease(provider, result, stdout, colors) {
|
|
296
|
+
const action = result.action.charAt(0).toUpperCase() + result.action.slice(1);
|
|
297
|
+
stdout(`${colors.green(`${action} ${provider} release: ${result.url}`)}\n`);
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/cli/createChanges.ts
|
|
301
|
+
async function createChanges(options, environment) {
|
|
302
|
+
if (options.commit && options.to !== defaultConfig.cli.to) {
|
|
303
|
+
const [toCommit, headCommit] = await Promise.all([git(environment.cwd, "rev-parse", options.to).then((value) => value.trim()), git(environment.cwd, "rev-parse", "HEAD").then((value) => value.trim())]);
|
|
304
|
+
if (toCommit !== headCommit) throw new Error("--to must resolve to HEAD when used with --commit");
|
|
305
|
+
}
|
|
407
306
|
const env = environment.env ?? process.env;
|
|
408
307
|
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
409
308
|
const colors = environment.colors ?? ansis;
|
|
410
|
-
const
|
|
411
|
-
const repository = await resolveRepository(environment.cwd);
|
|
309
|
+
const tag = `v${options.version}`;
|
|
310
|
+
const repository = await resolveRepository(environment.cwd, options.repository);
|
|
412
311
|
const token = repository?.provider.token(options.token, env);
|
|
413
|
-
|
|
414
|
-
if (!repository) throw new Error("A supported repository is required to release");
|
|
415
|
-
if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
|
|
416
|
-
}
|
|
312
|
+
validateReleaseSupport(options, repository);
|
|
417
313
|
let taggedCommit = options.release ? await getTagCommit(environment.cwd, tag) : void 0;
|
|
418
314
|
let releaseRef = taggedCommit ? tag : void 0;
|
|
419
315
|
const remoteTaggedCommit = options.release ? await getRemoteTagCommit(environment.cwd, tag) : void 0;
|
|
@@ -426,84 +322,88 @@ async function runChangelog(options, environment) {
|
|
|
426
322
|
taggedCommit = await getTagCommit(environment.cwd, tag);
|
|
427
323
|
releaseRef = tag;
|
|
428
324
|
}
|
|
429
|
-
const from = taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) :
|
|
430
|
-
const to = releaseRef ||
|
|
431
|
-
const comparisonFrom = from || (repository ?
|
|
432
|
-
const commits = await
|
|
325
|
+
const from = options.from ?? (taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) : await getLatestTag(environment.cwd));
|
|
326
|
+
const to = releaseRef || options.to;
|
|
327
|
+
const comparisonFrom = from || (repository ? await git(environment.cwd, "rev-list", "--max-parents=0", to).then((value) => value.trim()) : "");
|
|
328
|
+
const commits = parseCommits(await readGitCommits(environment.cwd, from, to));
|
|
433
329
|
if (token && repository && !(options.release && options.dry)) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
|
|
434
|
-
const
|
|
330
|
+
const style = {
|
|
331
|
+
emoji: options.emoji,
|
|
332
|
+
capitalize: options.capitalize,
|
|
333
|
+
group: options.group
|
|
334
|
+
};
|
|
335
|
+
const { body, release } = generateChangelog({
|
|
336
|
+
version: options.version,
|
|
337
|
+
commits,
|
|
338
|
+
repository,
|
|
339
|
+
comparisonFrom
|
|
340
|
+
}, style);
|
|
435
341
|
const repositoryRelease = {
|
|
436
342
|
tag,
|
|
437
|
-
name: tag,
|
|
438
|
-
body
|
|
439
|
-
prerelease: version
|
|
440
|
-
|
|
441
|
-
const printReleasePreview = () => {
|
|
442
|
-
stdout([
|
|
443
|
-
colors.dim(`keep${colors.bold("changes")} v${version}`),
|
|
444
|
-
`${colors.cyan(from || comparisonFrom)}${colors.dim(" -> ")}${colors.blue(tag)}${colors.dim(` (${commits.length} commits)`)}`,
|
|
445
|
-
colors.dim("--------------"),
|
|
446
|
-
"",
|
|
447
|
-
releaseBody.replaceAll(" ", ""),
|
|
448
|
-
"",
|
|
449
|
-
colors.dim("--------------"),
|
|
450
|
-
""
|
|
451
|
-
].join("\n"));
|
|
452
|
-
};
|
|
453
|
-
const printManualReleaseUrl = () => {
|
|
454
|
-
const url = repository.provider.manualReleaseUrl?.(repository, repositoryRelease);
|
|
455
|
-
if (url) stdout(`${colors.yellow("Using the following link to create it manually:")}\n${colors.yellow(url)}\n`);
|
|
456
|
-
return url;
|
|
343
|
+
name: options.name ?? tag,
|
|
344
|
+
body,
|
|
345
|
+
prerelease: options.prerelease ?? options.version.includes("-"),
|
|
346
|
+
draft: options.draft
|
|
457
347
|
};
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
printManualReleaseUrl();
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
const result = await repository.provider.publishRelease(repository, repositoryRelease, token, environment.fetch ?? globalThis.fetch);
|
|
467
|
-
stdout(`${colors.green(`${capitalize(result.action)} ${repository.provider.name} release: ${result.url}`)}\n`);
|
|
348
|
+
const preview = {
|
|
349
|
+
from: from || comparisonFrom,
|
|
350
|
+
tag,
|
|
351
|
+
commitCount: commits.length,
|
|
352
|
+
body
|
|
468
353
|
};
|
|
469
|
-
const outputPath = resolve(environment.cwd, options.output || "CHANGELOG.md");
|
|
470
|
-
const currentChangelog = await readChangelog(outputPath);
|
|
471
|
-
const releaseExists = hasRelease(currentChangelog, version$1);
|
|
472
|
-
const changelog = insertRelease(currentChangelog, release);
|
|
473
354
|
if (options.dry) {
|
|
355
|
+
printChangesPreview(preview, stdout, colors);
|
|
474
356
|
if (options.release) {
|
|
475
|
-
printReleasePreview();
|
|
476
357
|
stdout(`${colors.yellow("Dry run. Release skipped.")}\n\n`);
|
|
477
|
-
printManualReleaseUrl();
|
|
478
|
-
}
|
|
358
|
+
printManualReleaseUrl(repository, repositoryRelease, stdout, colors);
|
|
359
|
+
}
|
|
479
360
|
return;
|
|
480
361
|
}
|
|
481
362
|
if (options.release && taggedCommit) {
|
|
482
363
|
if (!remoteTaggedCommit) await git(environment.cwd, "push", "origin", `refs/tags/${tag}`);
|
|
483
|
-
await
|
|
364
|
+
await publishRelease(repository, repositoryRelease, token, preview, environment);
|
|
484
365
|
return;
|
|
485
366
|
}
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const versionChanges = versionPath ? await git(environment.cwd, "status", "--porcelain", "--", versionPath) : "";
|
|
493
|
-
const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${version$1} release notes` : `chore(release): v${version$1}`;
|
|
494
|
-
await git(environment.cwd, ...gitIdentity, "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
367
|
+
const outputPath = resolve(environment.cwd, options.output);
|
|
368
|
+
const currentChangelog = await readChangelog(outputPath);
|
|
369
|
+
const releaseExists = hasRelease(currentChangelog, options.version);
|
|
370
|
+
await writeChangelog(outputPath, insertRelease(currentChangelog, release));
|
|
371
|
+
const versionPath = await updateVersion(environment.cwd, options.version);
|
|
372
|
+
if (options.commit || options.release) await commitReleaseFiles(options, environment.cwd, outputPath, versionPath, releaseExists);
|
|
497
373
|
if (options.release) {
|
|
374
|
+
const gitIdentity = resolveGitIdentity(options.author);
|
|
498
375
|
await git(environment.cwd, ...gitIdentity, "tag", "-a", tag, "-m", tag);
|
|
499
376
|
await git(environment.cwd, "push", "origin", "HEAD", `refs/tags/${tag}`);
|
|
500
|
-
await
|
|
377
|
+
await publishRelease(repository, repositoryRelease, token, preview, environment);
|
|
501
378
|
}
|
|
502
379
|
}
|
|
503
|
-
function
|
|
504
|
-
|
|
380
|
+
function validateReleaseSupport(options, repository) {
|
|
381
|
+
if (!options.release) return;
|
|
382
|
+
if (!repository) throw new Error("A supported repository is required to release");
|
|
383
|
+
if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
|
|
384
|
+
}
|
|
385
|
+
async function commitReleaseFiles(options, cwd, outputPath, versionPath, releaseExists) {
|
|
386
|
+
const releasePaths = [outputPath, versionPath].filter((path) => path !== void 0);
|
|
387
|
+
if (!(await git(cwd, "status", "--porcelain", "--", ...releasePaths)).trim()) return;
|
|
388
|
+
await git(cwd, "add", "--", ...releasePaths);
|
|
389
|
+
const versionChanges = versionPath ? await git(cwd, "status", "--porcelain", "--", versionPath) : "";
|
|
390
|
+
const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${options.version} release notes` : `chore(release): v${options.version}`;
|
|
391
|
+
await git(cwd, ...resolveGitIdentity(options.author), "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
|
|
505
392
|
}
|
|
506
|
-
function
|
|
393
|
+
async function publishRelease(repository, release, token, preview, environment) {
|
|
394
|
+
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
395
|
+
const colors = environment.colors ?? ansis;
|
|
396
|
+
printChangesPreview(preview, stdout, colors);
|
|
397
|
+
if (!token || !repository.provider.publishRelease) {
|
|
398
|
+
if (!repository.provider.manualReleaseUrl) throw new Error("A repository token is required to release");
|
|
399
|
+
stdout(`${colors.red(`No ${repository.provider.name} token found, specify it via --token or environment variable. Release skipped.`)}\n\n`);
|
|
400
|
+
printManualReleaseUrl(repository, release, stdout, colors);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const result = await repository.provider.publishRelease(repository, release, token, environment.fetch ?? globalThis.fetch);
|
|
404
|
+
printPublishedRelease(repository.provider.name, result, stdout, colors);
|
|
405
|
+
}
|
|
406
|
+
function resolveGitIdentity(author) {
|
|
507
407
|
const match = /^([^<>\r\n]+)<([^<>\r\n]+)>$/.exec(author);
|
|
508
408
|
if (!match) throw new Error("Author must use the \"Name <email>\" format");
|
|
509
409
|
return [
|
|
@@ -514,31 +414,43 @@ function resolveGitIdentity(author = defaultAuthor) {
|
|
|
514
414
|
];
|
|
515
415
|
}
|
|
516
416
|
//#endregion
|
|
517
|
-
//#region src/cli.ts
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
417
|
+
//#region src/cli/options.ts
|
|
418
|
+
function resolveOptions(versionArgument, options) {
|
|
419
|
+
if (!versionArgument) throw new Error("A release version is required");
|
|
420
|
+
const version = normalizeFull(versionArgument);
|
|
421
|
+
if (!version) throw new Error(`Invalid release version: ${versionArgument}`);
|
|
422
|
+
const commit = options.commit ?? defaultConfig.cli.commit;
|
|
423
|
+
const release = options.release ?? defaultConfig.cli.release;
|
|
424
|
+
if (options.to !== void 0 && release) throw new Error("--to cannot be used with --release");
|
|
425
|
+
if (options.author !== void 0 && !commit && !release) throw new Error("--author requires --commit or --release");
|
|
426
|
+
if (!release && (options.name !== void 0 || options.draft !== void 0 || options.prerelease !== void 0)) throw new Error("--name, --draft, and --prerelease require --release");
|
|
427
|
+
return {
|
|
428
|
+
version,
|
|
429
|
+
from: options.from,
|
|
430
|
+
to: options.to ?? defaultConfig.cli.to,
|
|
431
|
+
repository: options.repository,
|
|
432
|
+
output: options.output ?? defaultConfig.cli.output,
|
|
433
|
+
dry: options.dry ?? defaultConfig.cli.dry,
|
|
434
|
+
commit,
|
|
435
|
+
release,
|
|
436
|
+
author: options.author ?? defaultConfig.cli.author,
|
|
437
|
+
token: options.token,
|
|
438
|
+
name: options.name,
|
|
439
|
+
draft: options.draft ?? defaultConfig.cli.draft,
|
|
440
|
+
prerelease: options.prerelease,
|
|
441
|
+
emoji: options.emoji ?? defaultConfig.changelog.emoji,
|
|
442
|
+
capitalize: options.capitalize ?? defaultConfig.changelog.capitalize,
|
|
443
|
+
group: options.group ?? defaultConfig.changelog.group
|
|
444
|
+
};
|
|
538
445
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/cli.ts
|
|
448
|
+
const cli = cac("keepchanges").option("--from <ref>", "Start Git reference").option("--to <ref>", "End Git reference").option("--repository <source>", "Repository slug or URL").option("--output <path>", "Changelog file path").option("--dry", "Preview without modifying files or remotes").option("--commit", "Commit the changelog and version update").option("--release", "Publish a repository release").option("--author <author>", "Commit author in \"Name <email>\" format").option("-t, --token <token>", "Repository token").option("--name <name>", "Repository release name").option("-d, --draft", "Create a draft repository release").option("--prerelease", "Mark the repository release as prerelease").option("--emoji", "Use emojis in changelog section titles").option("--capitalize", "Capitalize changelog entries").option("--group", "Group repeated commit scopes");
|
|
449
|
+
cli.command("[version]").usage("<version> [options]").action(async (versionArgument, options) => {
|
|
450
|
+
await createChanges(resolveOptions(versionArgument, options), { cwd: process.cwd() });
|
|
542
451
|
});
|
|
452
|
+
cli.help();
|
|
453
|
+
cli.version(version);
|
|
454
|
+
cli.parse();
|
|
543
455
|
//#endregion
|
|
544
|
-
export {
|
|
456
|
+
export {};
|