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 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.1";
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 commitsByEmail = new Map(commits.flatMap((commit) => commit.authors[0] ? [[commit.authors[0].email, commit]] : []));
97
- const loginsByEmail = /* @__PURE__ */ new Map();
98
- await Promise.all([...commitsByEmail].map(async ([email, commit]) => {
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) loginsByEmail.set(email, 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 = loginsByEmail.get(author.email);
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 = new Map(commits.flatMap((commit) => commit.authors).map((author) => [author.email, author]));
183
- await Promise.all([...authorsByEmail.values()].map(async (author) => {
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("Using the following link to create it manually:")}\n${colors.yellow(url)}\n`);
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 && !(options.release && options.dry)) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
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 environment variable. Release skipped.`)}\n\n`);
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.1",
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
  }