keepchanges 0.0.3 → 1.0.0-beta.2
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 +50 -159
- package/README.zh-CN.md +182 -0
- package/dist/cli.d.mts +1 -14
- package/dist/cli.mjs +172 -256
- package/dist/commit-FX77u6Sj.mjs +190 -0
- package/dist/index.d.mts +154 -0
- package/dist/index.mjs +2 -0
- package/package.json +6 -7
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.2";
|
|
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,33 +54,11 @@ 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",
|
|
61
|
+
tokenEnv: "GITEA_TOKEN",
|
|
195
62
|
parse(source) {
|
|
196
63
|
try {
|
|
197
64
|
const url = new URL(source.replace(/^git\+/, ""));
|
|
@@ -199,7 +66,7 @@ const giteaProvider = {
|
|
|
199
66
|
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
200
67
|
if (!path.includes("/")) return;
|
|
201
68
|
return {
|
|
202
|
-
provider:
|
|
69
|
+
provider: giteaRepository,
|
|
203
70
|
path,
|
|
204
71
|
webUrl: `${url.origin}/${path}`
|
|
205
72
|
};
|
|
@@ -217,6 +84,12 @@ const giteaProvider = {
|
|
|
217
84
|
compareUrl(repository, from, to) {
|
|
218
85
|
return `${repository.webUrl}/compare/${from}...${to}`;
|
|
219
86
|
},
|
|
87
|
+
manualReleaseUrl(repository, release, action) {
|
|
88
|
+
if (action === "edit") return `${repository.webUrl}/releases/edit/${encodeURIComponent(release.tag)}`;
|
|
89
|
+
const url = new URL(`${repository.webUrl}/releases/new`);
|
|
90
|
+
url.search = new URLSearchParams({ tag: release.tag }).toString();
|
|
91
|
+
return url.toString();
|
|
92
|
+
},
|
|
220
93
|
async resolveAuthors(commits, repository, token, fetch) {
|
|
221
94
|
const headers = {
|
|
222
95
|
accept: "application/json",
|
|
@@ -257,7 +130,8 @@ const giteaProvider = {
|
|
|
257
130
|
tag_name: release.tag,
|
|
258
131
|
name: release.name,
|
|
259
132
|
body: release.body,
|
|
260
|
-
prerelease: release.prerelease
|
|
133
|
+
prerelease: release.prerelease,
|
|
134
|
+
draft: release.draft
|
|
261
135
|
})
|
|
262
136
|
});
|
|
263
137
|
if (!response.ok) throw new Error(`Gitea release publishing failed (${response.status})`);
|
|
@@ -268,14 +142,15 @@ const giteaProvider = {
|
|
|
268
142
|
}
|
|
269
143
|
};
|
|
270
144
|
//#endregion
|
|
271
|
-
//#region src/
|
|
272
|
-
const
|
|
145
|
+
//#region src/repositories/github.ts
|
|
146
|
+
const githubRepository = {
|
|
273
147
|
name: "GitHub",
|
|
148
|
+
tokenEnv: "GITHUB_TOKEN",
|
|
274
149
|
parse(source) {
|
|
275
150
|
const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(source);
|
|
276
151
|
if (!match) return;
|
|
277
152
|
return {
|
|
278
|
-
provider:
|
|
153
|
+
provider: githubRepository,
|
|
279
154
|
path: match[1],
|
|
280
155
|
webUrl: `https://github.com/${match[1]}`
|
|
281
156
|
};
|
|
@@ -292,7 +167,8 @@ const githubProvider = {
|
|
|
292
167
|
compareUrl(repository, from, to) {
|
|
293
168
|
return `${repository.webUrl}/compare/${from}...${to}`;
|
|
294
169
|
},
|
|
295
|
-
manualReleaseUrl(repository, release) {
|
|
170
|
+
manualReleaseUrl(repository, release, action) {
|
|
171
|
+
if (action === "edit") return `${repository.webUrl}/releases/edit/${encodeURIComponent(release.tag)}`;
|
|
296
172
|
const url = new URL(`${repository.webUrl}/releases/new`);
|
|
297
173
|
url.search = new URLSearchParams({
|
|
298
174
|
title: release.name,
|
|
@@ -340,7 +216,8 @@ const githubProvider = {
|
|
|
340
216
|
tag_name: release.tag,
|
|
341
217
|
name: release.name,
|
|
342
218
|
body: release.body,
|
|
343
|
-
prerelease: release.prerelease
|
|
219
|
+
prerelease: release.prerelease,
|
|
220
|
+
draft: release.draft
|
|
344
221
|
})
|
|
345
222
|
});
|
|
346
223
|
if (!response.ok) throw new Error(`GitHub release publishing failed (${response.status})`);
|
|
@@ -352,24 +229,28 @@ const githubProvider = {
|
|
|
352
229
|
};
|
|
353
230
|
//#endregion
|
|
354
231
|
//#region src/repository.ts
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
232
|
+
const repositoryProviders = {
|
|
233
|
+
github: githubRepository,
|
|
234
|
+
gitea: giteaRepository
|
|
358
235
|
};
|
|
359
|
-
async function resolveRepository(cwd) {
|
|
236
|
+
async function resolveRepository(cwd, explicit) {
|
|
237
|
+
if (explicit) {
|
|
238
|
+
const repository = parseRepository(/^[^/:]+\/[^/]+$/.test(explicit) ? `https://github.com/${explicit}` : explicit, Object.values(repositoryProviders));
|
|
239
|
+
if (!repository) throw new Error(`Unsupported repository: ${explicit}`);
|
|
240
|
+
return repository;
|
|
241
|
+
}
|
|
360
242
|
let providerName = "";
|
|
361
243
|
let source = await readFile(resolve(cwd, "package.json"), "utf8").then((contents) => {
|
|
362
244
|
const repository = JSON.parse(contents).repository;
|
|
363
245
|
if (typeof repository === "object") providerName = repository?.provider?.toLowerCase() || "";
|
|
364
246
|
return typeof repository === "string" ? repository : repository?.url || "";
|
|
365
247
|
}).catch(() => "");
|
|
366
|
-
if (!source) source =
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
for (const provider of [githubProvider]) {
|
|
248
|
+
if (!source) source = await git(cwd, "remote", "get-url", "origin").then((output) => output.trim()).catch(() => "");
|
|
249
|
+
if (providerName) return repositoryProviders[providerName]?.parse(source);
|
|
250
|
+
return parseRepository(source, [githubRepository]);
|
|
251
|
+
}
|
|
252
|
+
function parseRepository(source, providers) {
|
|
253
|
+
for (const provider of providers) {
|
|
373
254
|
const repository = provider.parse(source);
|
|
374
255
|
if (repository) return repository;
|
|
375
256
|
}
|
|
@@ -398,22 +279,41 @@ async function updateVersion(cwd, version) {
|
|
|
398
279
|
}
|
|
399
280
|
}
|
|
400
281
|
//#endregion
|
|
401
|
-
//#region src/
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
282
|
+
//#region src/cli/output.ts
|
|
283
|
+
function printChangesPreview(preview, stdout, colors) {
|
|
284
|
+
stdout([
|
|
285
|
+
colors.dim(`keep${colors.bold("changes")} v${version}`),
|
|
286
|
+
`${colors.cyan(preview.from)}${colors.dim(" -> ")}${colors.blue(preview.tag)}${colors.dim(` (${preview.commitCount} commits)`)}`,
|
|
287
|
+
colors.dim("--------------"),
|
|
288
|
+
"",
|
|
289
|
+
preview.body.replaceAll(" ", ""),
|
|
290
|
+
"",
|
|
291
|
+
colors.dim("--------------"),
|
|
292
|
+
""
|
|
293
|
+
].join("\n"));
|
|
294
|
+
}
|
|
295
|
+
function printManualReleaseUrl(repository, release, stdout, colors, action = "create") {
|
|
296
|
+
const url = repository.provider.manualReleaseUrl?.(repository, release, action);
|
|
297
|
+
if (url) stdout(`${colors.yellow(`Using the following link to ${action} it manually:`)}\n${colors.yellow(url)}\n`);
|
|
298
|
+
}
|
|
299
|
+
function printPublishedRelease(provider, result, stdout, colors) {
|
|
300
|
+
const action = result.action.charAt(0).toUpperCase() + result.action.slice(1);
|
|
301
|
+
stdout(`${colors.green(`${action} ${provider} release: ${result.url}`)}\n`);
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/cli/createChanges.ts
|
|
305
|
+
async function createChanges(options, environment) {
|
|
306
|
+
if (options.commit && options.to !== defaultConfig.cli.to) {
|
|
307
|
+
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())]);
|
|
308
|
+
if (toCommit !== headCommit) throw new Error("--to must resolve to HEAD when used with --commit");
|
|
309
|
+
}
|
|
407
310
|
const env = environment.env ?? process.env;
|
|
408
311
|
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
409
312
|
const colors = environment.colors ?? ansis;
|
|
410
|
-
const
|
|
411
|
-
const repository = await resolveRepository(environment.cwd);
|
|
313
|
+
const tag = `v${options.version}`;
|
|
314
|
+
const repository = await resolveRepository(environment.cwd, options.repository);
|
|
412
315
|
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
|
-
}
|
|
316
|
+
validateReleaseSupport(options, repository);
|
|
417
317
|
let taggedCommit = options.release ? await getTagCommit(environment.cwd, tag) : void 0;
|
|
418
318
|
let releaseRef = taggedCommit ? tag : void 0;
|
|
419
319
|
const remoteTaggedCommit = options.release ? await getRemoteTagCommit(environment.cwd, tag) : void 0;
|
|
@@ -426,84 +326,88 @@ async function runChangelog(options, environment) {
|
|
|
426
326
|
taggedCommit = await getTagCommit(environment.cwd, tag);
|
|
427
327
|
releaseRef = tag;
|
|
428
328
|
}
|
|
429
|
-
const from = taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) :
|
|
430
|
-
const to = releaseRef ||
|
|
431
|
-
const comparisonFrom = from || (repository ?
|
|
432
|
-
const commits = await
|
|
433
|
-
if (token && repository
|
|
434
|
-
const
|
|
329
|
+
const from = options.from ?? (taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) : await getLatestTag(environment.cwd));
|
|
330
|
+
const to = releaseRef || options.to;
|
|
331
|
+
const comparisonFrom = from || (repository ? await git(environment.cwd, "rev-list", "--max-parents=0", to).then((value) => value.trim()) : "");
|
|
332
|
+
const commits = parseCommits(await readGitCommits(environment.cwd, from, to));
|
|
333
|
+
if (token && repository) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
|
|
334
|
+
const style = {
|
|
335
|
+
emoji: options.emoji,
|
|
336
|
+
capitalize: options.capitalize,
|
|
337
|
+
group: options.group
|
|
338
|
+
};
|
|
339
|
+
const { body, release } = generateChangelog({
|
|
340
|
+
version: options.version,
|
|
341
|
+
commits,
|
|
342
|
+
repository,
|
|
343
|
+
comparisonFrom
|
|
344
|
+
}, style);
|
|
435
345
|
const repositoryRelease = {
|
|
436
346
|
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;
|
|
347
|
+
name: options.name ?? tag,
|
|
348
|
+
body,
|
|
349
|
+
prerelease: options.prerelease ?? options.version.includes("-"),
|
|
350
|
+
draft: options.draft
|
|
457
351
|
};
|
|
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`);
|
|
352
|
+
const preview = {
|
|
353
|
+
from: from || comparisonFrom,
|
|
354
|
+
tag,
|
|
355
|
+
commitCount: commits.length,
|
|
356
|
+
body
|
|
468
357
|
};
|
|
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
358
|
if (options.dry) {
|
|
359
|
+
printChangesPreview(preview, stdout, colors);
|
|
474
360
|
if (options.release) {
|
|
475
|
-
printReleasePreview();
|
|
476
361
|
stdout(`${colors.yellow("Dry run. Release skipped.")}\n\n`);
|
|
477
|
-
printManualReleaseUrl();
|
|
478
|
-
}
|
|
362
|
+
printManualReleaseUrl(repository, repositoryRelease, stdout, colors, taggedCommit ? "edit" : "create");
|
|
363
|
+
}
|
|
479
364
|
return;
|
|
480
365
|
}
|
|
481
366
|
if (options.release && taggedCommit) {
|
|
482
367
|
if (!remoteTaggedCommit) await git(environment.cwd, "push", "origin", `refs/tags/${tag}`);
|
|
483
|
-
await
|
|
368
|
+
await publishRelease(repository, repositoryRelease, token, preview, environment, "edit");
|
|
484
369
|
return;
|
|
485
370
|
}
|
|
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
|
-
}
|
|
371
|
+
const outputPath = resolve(environment.cwd, options.output);
|
|
372
|
+
const currentChangelog = await readChangelog(outputPath);
|
|
373
|
+
const releaseExists = hasRelease(currentChangelog, options.version);
|
|
374
|
+
await writeChangelog(outputPath, insertRelease(currentChangelog, release));
|
|
375
|
+
const versionPath = await updateVersion(environment.cwd, options.version);
|
|
376
|
+
if (options.commit || options.release) await commitReleaseFiles(options, environment.cwd, outputPath, versionPath, releaseExists);
|
|
497
377
|
if (options.release) {
|
|
378
|
+
const gitIdentity = resolveGitIdentity(options.author);
|
|
498
379
|
await git(environment.cwd, ...gitIdentity, "tag", "-a", tag, "-m", tag);
|
|
499
380
|
await git(environment.cwd, "push", "origin", "HEAD", `refs/tags/${tag}`);
|
|
500
|
-
await
|
|
381
|
+
await publishRelease(repository, repositoryRelease, token, preview, environment);
|
|
501
382
|
}
|
|
502
383
|
}
|
|
503
|
-
function
|
|
504
|
-
|
|
384
|
+
function validateReleaseSupport(options, repository) {
|
|
385
|
+
if (!options.release) return;
|
|
386
|
+
if (!repository) throw new Error("A supported repository is required to release");
|
|
387
|
+
if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
|
|
388
|
+
}
|
|
389
|
+
async function commitReleaseFiles(options, cwd, outputPath, versionPath, releaseExists) {
|
|
390
|
+
const releasePaths = [outputPath, versionPath].filter((path) => path !== void 0);
|
|
391
|
+
if (!(await git(cwd, "status", "--porcelain", "--", ...releasePaths)).trim()) return;
|
|
392
|
+
await git(cwd, "add", "--", ...releasePaths);
|
|
393
|
+
const versionChanges = versionPath ? await git(cwd, "status", "--porcelain", "--", versionPath) : "";
|
|
394
|
+
const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${options.version} release notes` : `chore(release): v${options.version}`;
|
|
395
|
+
await git(cwd, ...resolveGitIdentity(options.author), "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
|
|
505
396
|
}
|
|
506
|
-
function
|
|
397
|
+
async function publishRelease(repository, release, token, preview, environment, manualAction = "create") {
|
|
398
|
+
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
399
|
+
const colors = environment.colors ?? ansis;
|
|
400
|
+
printChangesPreview(preview, stdout, colors);
|
|
401
|
+
if (!token || !repository.provider.publishRelease) {
|
|
402
|
+
if (!repository.provider.manualReleaseUrl) throw new Error("A repository token is required to release");
|
|
403
|
+
stdout(`${colors.red(`No ${repository.provider.name} token found, specify it via --token or ${repository.provider.tokenEnv} env. Release skipped.`)}\n\n`);
|
|
404
|
+
printManualReleaseUrl(repository, release, stdout, colors, manualAction);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const result = await repository.provider.publishRelease(repository, release, token, environment.fetch ?? globalThis.fetch);
|
|
408
|
+
printPublishedRelease(repository.provider.name, result, stdout, colors);
|
|
409
|
+
}
|
|
410
|
+
function resolveGitIdentity(author) {
|
|
507
411
|
const match = /^([^<>\r\n]+)<([^<>\r\n]+)>$/.exec(author);
|
|
508
412
|
if (!match) throw new Error("Author must use the \"Name <email>\" format");
|
|
509
413
|
return [
|
|
@@ -514,31 +418,43 @@ function resolveGitIdentity(author = defaultAuthor) {
|
|
|
514
418
|
];
|
|
515
419
|
}
|
|
516
420
|
//#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
|
-
|
|
421
|
+
//#region src/cli/options.ts
|
|
422
|
+
function resolveOptions(versionArgument, options) {
|
|
423
|
+
if (!versionArgument) throw new Error("A release version is required");
|
|
424
|
+
const version = normalizeFull(versionArgument);
|
|
425
|
+
if (!version) throw new Error(`Invalid release version: ${versionArgument}`);
|
|
426
|
+
const commit = options.commit ?? defaultConfig.cli.commit;
|
|
427
|
+
const release = options.release ?? defaultConfig.cli.release;
|
|
428
|
+
if (options.to !== void 0 && release) throw new Error("--to cannot be used with --release");
|
|
429
|
+
if (options.author !== void 0 && !commit && !release) throw new Error("--author requires --commit or --release");
|
|
430
|
+
if (!release && (options.name !== void 0 || options.draft !== void 0 || options.prerelease !== void 0)) throw new Error("--name, --draft, and --prerelease require --release");
|
|
431
|
+
return {
|
|
432
|
+
version,
|
|
433
|
+
from: options.from,
|
|
434
|
+
to: options.to ?? defaultConfig.cli.to,
|
|
435
|
+
repository: options.repository,
|
|
436
|
+
output: options.output ?? defaultConfig.cli.output,
|
|
437
|
+
dry: options.dry ?? defaultConfig.cli.dry,
|
|
438
|
+
commit,
|
|
439
|
+
release,
|
|
440
|
+
author: options.author ?? defaultConfig.cli.author,
|
|
441
|
+
token: options.token,
|
|
442
|
+
name: options.name,
|
|
443
|
+
draft: options.draft ?? defaultConfig.cli.draft,
|
|
444
|
+
prerelease: options.prerelease,
|
|
445
|
+
emoji: options.emoji ?? defaultConfig.changelog.emoji,
|
|
446
|
+
capitalize: options.capitalize ?? defaultConfig.changelog.capitalize,
|
|
447
|
+
group: options.group ?? defaultConfig.changelog.group
|
|
448
|
+
};
|
|
538
449
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/cli.ts
|
|
452
|
+
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");
|
|
453
|
+
cli.command("[version]").usage("<version> [options]").action(async (versionArgument, options) => {
|
|
454
|
+
await createChanges(resolveOptions(versionArgument, options), { cwd: process.cwd() });
|
|
542
455
|
});
|
|
456
|
+
cli.help();
|
|
457
|
+
cli.version(version);
|
|
458
|
+
cli.parse();
|
|
543
459
|
//#endregion
|
|
544
|
-
export {
|
|
460
|
+
export {};
|