keepchanges 1.0.0-beta.1 → 1.0.0-beta.3
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 +2 -1
- package/README.zh-CN.md +2 -1
- package/dist/cli.mjs +35 -21
- package/dist/index.d.mts +4 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -150,7 +150,8 @@ different commits.
|
|
|
150
150
|
|
|
151
151
|
Stable releases compare with the previous stable tag. Prereleases compare with
|
|
152
152
|
the nearest previous tag. With `--release`, `--dry` takes precedence and
|
|
153
|
-
prevents all file writes and remote mutations.
|
|
153
|
+
prevents all file writes and remote mutations. Its manual URL opens a new
|
|
154
|
+
Release for a new tag and the edit page for an existing tag.
|
|
154
155
|
|
|
155
156
|
> [!NOTE]
|
|
156
157
|
> A regular `--release` without a token still writes files, commits, tags, and
|
package/README.zh-CN.md
CHANGED
|
@@ -143,7 +143,8 @@ GitHub 和 Gitea 均支持作者解析和 Release 发布;Gitea 使用 `GITEA_T
|
|
|
143
143
|
时会停止,不会强制覆盖。
|
|
144
144
|
|
|
145
145
|
稳定版本会与前一个稳定 tag 对比,预发布版本会与最近的前一个 tag 对比。与
|
|
146
|
-
`--release` 搭配的 `--dry`
|
|
146
|
+
`--release` 搭配的 `--dry` 优先级最高,不会执行任何写入或远程修改。手动链接
|
|
147
|
+
会为新 tag 打开创建 Release 页面,为已有 tag 打开编辑页面。
|
|
147
148
|
|
|
148
149
|
> [!NOTE]
|
|
149
150
|
> 没有 token 的普通 `--release` 仍会执行本地写入、commit、tag 和 push,
|
package/dist/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import ansis from "ansis";
|
|
9
9
|
import { x } from "tinyexec";
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "1.0.0-beta.
|
|
11
|
+
var version = "1.0.0-beta.3";
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/git.ts
|
|
14
14
|
async function git(cwd, ...args) {
|
|
@@ -58,6 +58,7 @@ async function readGitCommits(cwd, from, to) {
|
|
|
58
58
|
//#region src/repositories/gitea.ts
|
|
59
59
|
const giteaRepository = {
|
|
60
60
|
name: "Gitea",
|
|
61
|
+
tokenEnv: "GITEA_TOKEN",
|
|
61
62
|
parse(source) {
|
|
62
63
|
try {
|
|
63
64
|
const url = new URL(source.replace(/^git\+/, ""));
|
|
@@ -83,7 +84,8 @@ const giteaRepository = {
|
|
|
83
84
|
compareUrl(repository, from, to) {
|
|
84
85
|
return `${repository.webUrl}/compare/${from}...${to}`;
|
|
85
86
|
},
|
|
86
|
-
manualReleaseUrl(repository, release) {
|
|
87
|
+
manualReleaseUrl(repository, release, action) {
|
|
88
|
+
if (action === "edit") return `${repository.webUrl}/releases/edit/${encodeURIComponent(release.tag)}`;
|
|
87
89
|
const url = new URL(`${repository.webUrl}/releases/new`);
|
|
88
90
|
url.search = new URLSearchParams({ tag: release.tag }).toString();
|
|
89
91
|
return url.toString();
|
|
@@ -93,17 +95,18 @@ const giteaRepository = {
|
|
|
93
95
|
accept: "application/json",
|
|
94
96
|
authorization: `token ${token}`
|
|
95
97
|
};
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
const authorsByEmail = collectRepositoryAuthors(commits);
|
|
99
|
+
await Promise.all([...authorsByEmail.values()].map(async (info) => {
|
|
100
|
+
const { author, commit } = info;
|
|
101
|
+
if (!commit) return;
|
|
99
102
|
try {
|
|
100
103
|
const response = await fetch(`${new URL(repository.webUrl).origin}/api/v1/repos/${repository.path}/git/commits/${commit.hash}`, { headers });
|
|
101
104
|
if (!response.ok) return;
|
|
102
105
|
const data = await response.json();
|
|
103
|
-
if (data.author?.login)
|
|
106
|
+
if (data.author?.login) author.login = data.author.login;
|
|
104
107
|
} catch {}
|
|
105
108
|
}));
|
|
106
|
-
for (const commit of commits) for (const author of commit.authors) author.login =
|
|
109
|
+
for (const commit of commits) for (const author of commit.authors) author.login = authorsByEmail.get(author.email)?.author.login;
|
|
107
110
|
},
|
|
108
111
|
async publishRelease(repository, release, token, fetch) {
|
|
109
112
|
const headers = {
|
|
@@ -143,6 +146,7 @@ const giteaRepository = {
|
|
|
143
146
|
//#region src/repositories/github.ts
|
|
144
147
|
const githubRepository = {
|
|
145
148
|
name: "GitHub",
|
|
149
|
+
tokenEnv: "GITHUB_TOKEN",
|
|
146
150
|
parse(source) {
|
|
147
151
|
const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(source);
|
|
148
152
|
if (!match) return;
|
|
@@ -164,7 +168,8 @@ const githubRepository = {
|
|
|
164
168
|
compareUrl(repository, from, to) {
|
|
165
169
|
return `${repository.webUrl}/compare/${from}...${to}`;
|
|
166
170
|
},
|
|
167
|
-
manualReleaseUrl(repository, release) {
|
|
171
|
+
manualReleaseUrl(repository, release, action) {
|
|
172
|
+
if (action === "edit") return `${repository.webUrl}/releases/edit/${encodeURIComponent(release.tag)}`;
|
|
168
173
|
const url = new URL(`${repository.webUrl}/releases/new`);
|
|
169
174
|
url.search = new URLSearchParams({
|
|
170
175
|
title: release.name,
|
|
@@ -179,15 +184,15 @@ const githubRepository = {
|
|
|
179
184
|
accept: "application/vnd.github.v3+json",
|
|
180
185
|
authorization: `token ${token}`
|
|
181
186
|
};
|
|
182
|
-
const authorsByEmail =
|
|
183
|
-
await Promise.all([...authorsByEmail.values()].map(async (
|
|
187
|
+
const authorsByEmail = collectRepositoryAuthors(commits);
|
|
188
|
+
await Promise.all([...authorsByEmail.values()].map(async (info) => {
|
|
189
|
+
const { author, commit } = info;
|
|
184
190
|
try {
|
|
185
191
|
author.login = (await (await fetch(`https://api.github.com/search/users?q=${encodeURIComponent(`${author.email} type:user in:email`)}`, { headers })).json()).items?.[0]?.login;
|
|
186
|
-
const commit = commits.find((commit) => commit.authors[0].email === author.email);
|
|
187
192
|
if (!author.login && commit) author.login = (await (await fetch(`https://api.github.com/repos/${repository.path}/commits/${commit.hash}`, { headers })).json()).author?.login;
|
|
188
193
|
} catch {}
|
|
189
194
|
}));
|
|
190
|
-
for (const commit of commits) for (const author of commit.authors) author.login = authorsByEmail.get(author.email)?.login;
|
|
195
|
+
for (const commit of commits) for (const author of commit.authors) author.login = authorsByEmail.get(author.email)?.author.login;
|
|
191
196
|
},
|
|
192
197
|
async publishRelease(repository, release, token, fetch) {
|
|
193
198
|
const headers = {
|
|
@@ -225,6 +230,15 @@ const githubRepository = {
|
|
|
225
230
|
};
|
|
226
231
|
//#endregion
|
|
227
232
|
//#region src/repository.ts
|
|
233
|
+
function collectRepositoryAuthors(commits) {
|
|
234
|
+
const authorsByEmail = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const commit of commits) commit.authors.forEach((author, index) => {
|
|
236
|
+
const info = authorsByEmail.get(author.email) ?? { author };
|
|
237
|
+
if (index === 0 && !info.commit) info.commit = commit;
|
|
238
|
+
authorsByEmail.set(author.email, info);
|
|
239
|
+
});
|
|
240
|
+
return authorsByEmail;
|
|
241
|
+
}
|
|
228
242
|
const repositoryProviders = {
|
|
229
243
|
github: githubRepository,
|
|
230
244
|
gitea: giteaRepository
|
|
@@ -288,9 +302,9 @@ function printChangesPreview(preview, stdout, colors) {
|
|
|
288
302
|
""
|
|
289
303
|
].join("\n"));
|
|
290
304
|
}
|
|
291
|
-
function printManualReleaseUrl(repository, release, stdout, colors) {
|
|
292
|
-
const url = repository.provider.manualReleaseUrl?.(repository, release);
|
|
293
|
-
if (url) stdout(`${colors.yellow(
|
|
305
|
+
function printManualReleaseUrl(repository, release, stdout, colors, action = "create") {
|
|
306
|
+
const url = repository.provider.manualReleaseUrl?.(repository, release, action);
|
|
307
|
+
if (url) stdout(`${colors.yellow(`Using the following link to ${action} it manually:`)}\n${colors.yellow(url)}\n`);
|
|
294
308
|
}
|
|
295
309
|
function printPublishedRelease(provider, result, stdout, colors) {
|
|
296
310
|
const action = result.action.charAt(0).toUpperCase() + result.action.slice(1);
|
|
@@ -326,7 +340,7 @@ async function createChanges(options, environment) {
|
|
|
326
340
|
const to = releaseRef || options.to;
|
|
327
341
|
const comparisonFrom = from || (repository ? await git(environment.cwd, "rev-list", "--max-parents=0", to).then((value) => value.trim()) : "");
|
|
328
342
|
const commits = parseCommits(await readGitCommits(environment.cwd, from, to));
|
|
329
|
-
if (token && repository
|
|
343
|
+
if (token && repository) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
|
|
330
344
|
const style = {
|
|
331
345
|
emoji: options.emoji,
|
|
332
346
|
capitalize: options.capitalize,
|
|
@@ -355,13 +369,13 @@ async function createChanges(options, environment) {
|
|
|
355
369
|
printChangesPreview(preview, stdout, colors);
|
|
356
370
|
if (options.release) {
|
|
357
371
|
stdout(`${colors.yellow("Dry run. Release skipped.")}\n\n`);
|
|
358
|
-
printManualReleaseUrl(repository, repositoryRelease, stdout, colors);
|
|
372
|
+
printManualReleaseUrl(repository, repositoryRelease, stdout, colors, taggedCommit ? "edit" : "create");
|
|
359
373
|
}
|
|
360
374
|
return;
|
|
361
375
|
}
|
|
362
376
|
if (options.release && taggedCommit) {
|
|
363
377
|
if (!remoteTaggedCommit) await git(environment.cwd, "push", "origin", `refs/tags/${tag}`);
|
|
364
|
-
await publishRelease(repository, repositoryRelease, token, preview, environment);
|
|
378
|
+
await publishRelease(repository, repositoryRelease, token, preview, environment, "edit");
|
|
365
379
|
return;
|
|
366
380
|
}
|
|
367
381
|
const outputPath = resolve(environment.cwd, options.output);
|
|
@@ -390,14 +404,14 @@ async function commitReleaseFiles(options, cwd, outputPath, versionPath, release
|
|
|
390
404
|
const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${options.version} release notes` : `chore(release): v${options.version}`;
|
|
391
405
|
await git(cwd, ...resolveGitIdentity(options.author), "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
|
|
392
406
|
}
|
|
393
|
-
async function publishRelease(repository, release, token, preview, environment) {
|
|
407
|
+
async function publishRelease(repository, release, token, preview, environment, manualAction = "create") {
|
|
394
408
|
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
395
409
|
const colors = environment.colors ?? ansis;
|
|
396
410
|
printChangesPreview(preview, stdout, colors);
|
|
397
411
|
if (!token || !repository.provider.publishRelease) {
|
|
398
412
|
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
|
|
400
|
-
printManualReleaseUrl(repository, release, stdout, colors);
|
|
413
|
+
stdout(`${colors.red(`No ${repository.provider.name} token found, specify it via --token or ${repository.provider.tokenEnv} env. Release skipped.`)}\n\n`);
|
|
414
|
+
printManualReleaseUrl(repository, release, stdout, colors, manualAction);
|
|
401
415
|
return;
|
|
402
416
|
}
|
|
403
417
|
const result = await repository.provider.publishRelease(repository, release, token, environment.fetch ?? globalThis.fetch);
|
package/dist/index.d.mts
CHANGED
|
@@ -24,8 +24,10 @@ interface RepositoryReleaseResult {
|
|
|
24
24
|
url: string;
|
|
25
25
|
action: 'created' | 'updated';
|
|
26
26
|
}
|
|
27
|
+
type ManualReleaseAction = 'create' | 'edit';
|
|
27
28
|
interface RepositoryProvider {
|
|
28
29
|
name: string;
|
|
30
|
+
tokenEnv: string;
|
|
29
31
|
parse: (source: string) => Repository | undefined;
|
|
30
32
|
token: (explicit: string | undefined, env: NodeJS.ProcessEnv) => string | undefined;
|
|
31
33
|
commitUrl: (repository: Repository, hash: string) => string;
|
|
@@ -33,7 +35,7 @@ interface RepositoryProvider {
|
|
|
33
35
|
compareUrl: (repository: Repository, from: string, to: string) => string;
|
|
34
36
|
resolveAuthors?: (commits: RepositoryCommit[], repository: Repository, token: string, fetch: typeof globalThis.fetch) => Promise<void>;
|
|
35
37
|
publishRelease?: (repository: Repository, release: RepositoryRelease, token: string, fetch: typeof globalThis.fetch) => Promise<RepositoryReleaseResult>;
|
|
36
|
-
manualReleaseUrl?: (repository: Repository, release: RepositoryRelease) => string;
|
|
38
|
+
manualReleaseUrl?: (repository: Repository, release: RepositoryRelease, action?: ManualReleaseAction) => string;
|
|
37
39
|
}
|
|
38
40
|
//#endregion
|
|
39
41
|
//#region src/git.d.ts
|
|
@@ -149,4 +151,4 @@ declare function writeChangelog(path: string, changelog: string): Promise<void>;
|
|
|
149
151
|
declare function hasRelease(changelog: string, version: string): boolean;
|
|
150
152
|
declare function insertRelease(changelog: string, release: string): string;
|
|
151
153
|
//#endregion
|
|
152
|
-
export { type ChangelogConfig, type ChangelogConfigOverrides, type ChangelogMessages, type ChangelogSectionConfig, type Commit, type GenerateChangelogOptions, type GeneratedChangelog, type KeepChangesConfig, type RawCommit, type Repository, type RepositoryAuthor, type RepositoryCommit, type RepositoryProvider, type RepositoryRelease, type RepositoryReleaseResult, defaultConfig, generateChangelog, hasRelease, insertRelease, parseCommit, parseCommits, readChangelog, resolveChangelogConfig, writeChangelog };
|
|
154
|
+
export { type ChangelogConfig, type ChangelogConfigOverrides, type ChangelogMessages, type ChangelogSectionConfig, type Commit, type GenerateChangelogOptions, type GeneratedChangelog, type KeepChangesConfig, type ManualReleaseAction, type RawCommit, type Repository, type RepositoryAuthor, type RepositoryCommit, type RepositoryProvider, type RepositoryRelease, type RepositoryReleaseResult, defaultConfig, generateChangelog, hasRelease, insertRelease, parseCommit, parseCommits, readChangelog, resolveChangelogConfig, writeChangelog };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keepchanges",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.3",
|
|
5
5
|
"description": "Generate and maintain CHANGELOG.md from Conventional Commits.",
|
|
6
6
|
"author": "edram",
|
|
7
7
|
"license": "MIT",
|
|
@@ -41,19 +41,19 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@antfu/eslint-config": "^9.2.0",
|
|
43
43
|
"@types/node": "^26.1.1",
|
|
44
|
-
"bumpp": "^11.1.0",
|
|
45
44
|
"eslint": "^10.7.0",
|
|
46
45
|
"tsdown": "^0.22.5",
|
|
46
|
+
"tsx": "^4.23.8",
|
|
47
47
|
"typescript": "^6.0.3",
|
|
48
48
|
"vitest": "^4.1.10"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "tsdown",
|
|
52
52
|
"dev": "tsdown --watch",
|
|
53
|
+
"start": "tsx ./src/cli.ts",
|
|
53
54
|
"lint": "eslint",
|
|
54
55
|
"lint:fix": "eslint --fix",
|
|
55
56
|
"test": "pnpm run build && vitest run",
|
|
56
|
-
"typecheck": "tsc --noEmit"
|
|
57
|
-
"release": "bumpp"
|
|
57
|
+
"typecheck": "tsc --noEmit"
|
|
58
58
|
}
|
|
59
59
|
}
|