keepchanges 1.0.1 → 1.0.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 +153 -88
- package/README.zh-CN.md +145 -79
- package/dist/cli.mjs +121 -27
- package/dist/{commit-DBopyhs1.mjs → commit-DswG6QDH.mjs} +3 -0
- package/dist/index.d.mts +15 -1
- package/dist/index.mjs +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# keepchanges
|
|
2
2
|
|
|
3
|
-
Generate and maintain
|
|
3
|
+
A release CLI for Conventional Commits. Generate and maintain a changelog, optionally bump
|
|
4
|
+
the project version, create a release commit and Git tag, publish a GitHub or Gitea Release,
|
|
5
|
+
and upload GitHub Release assets.
|
|
4
6
|
|
|
5
7
|
Inspired by [changelogithub](https://github.com/antfu-collective/changelogithub).
|
|
6
8
|
|
|
@@ -8,17 +10,20 @@ Inspired by [changelogithub](https://github.com/antfu-collective/changelogithub)
|
|
|
8
10
|
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
|
-
- Generates release notes from Conventional Commits between
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
13
|
+
- Generates release notes from Conventional Commits between Git tags
|
|
14
|
+
- Inserts new releases into an existing `CHANGELOG.md` and updates `package.json#version`
|
|
15
|
+
for npm projects
|
|
16
|
+
- Disables version bumps and changelog writes independently with `--no-bump` and
|
|
17
|
+
`--no-changelog`
|
|
18
|
+
- Creates release commits and annotated tags, then pushes them to the remote repository
|
|
19
|
+
- Supports building artifacts from a fixed release commit before publishing
|
|
20
|
+
- Creates or updates GitHub and Gitea Releases
|
|
21
|
+
- Uploads files, directories, or glob matches as GitHub Release assets
|
|
22
|
+
- Adds repository links for commits, pull requests, comparisons, authors, and co-authors
|
|
18
23
|
|
|
19
24
|
The current release includes `feat`, `fix`, `perf`, breaking changes marked with `!`,
|
|
20
|
-
and commits with a `BREAKING CHANGE` or `BREAKING-CHANGE` trailer. Other commit
|
|
21
|
-
|
|
25
|
+
and commits with a `BREAKING CHANGE` or `BREAKING-CHANGE` trailer. Other commit types
|
|
26
|
+
are ignored.
|
|
22
27
|
|
|
23
28
|
## Quick start
|
|
24
29
|
|
|
@@ -36,50 +41,90 @@ The version may also include a leading `v`:
|
|
|
36
41
|
npx keepchanges v1.1.0
|
|
37
42
|
```
|
|
38
43
|
|
|
39
|
-
By default, the command writes `CHANGELOG.md`. For an npm package, it also
|
|
40
|
-
|
|
44
|
+
By default, the command writes `CHANGELOG.md`. For an npm package, it also updates
|
|
45
|
+
`package.json#version` to `1.1.0`, but it does not create a Git commit.
|
|
41
46
|
|
|
42
|
-
##
|
|
47
|
+
## Release workflows
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
Combine the release stages to match how your project manages versions and builds artifacts:
|
|
50
|
+
|
|
51
|
+
| Workflow | Bump version | Write changelog | Build timing | Release sequence |
|
|
52
|
+
| --- | --- | --- | --- | --- |
|
|
53
|
+
| Full version release | Yes | Yes | After creating the release commit | `--tag`, build, then `--release --asset` |
|
|
54
|
+
| Externally managed version | No | Yes | Before release | Build, then `--release --no-bump --asset` |
|
|
55
|
+
| Artifacts only | No | No | Before release | Build, then `--release --no-bump --no-changelog --asset` |
|
|
56
|
+
|
|
57
|
+
### Full version release
|
|
58
|
+
|
|
59
|
+
Let keepchanges update the version and changelog, then create the release commit and tag.
|
|
60
|
+
Build from that exact commit before publishing the same tag and uploading its artifacts:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx keepchanges 1.1.0 --tag
|
|
64
|
+
pnpm build
|
|
65
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release --asset dist
|
|
46
66
|
```
|
|
47
67
|
|
|
48
|
-
|
|
68
|
+
The second command reuses the existing tag. It does not move the tag or commit the artifacts.
|
|
49
69
|
|
|
50
|
-
|
|
51
|
-
| --- | --- | --- |
|
|
52
|
-
| `<version>` | Required | Version to generate. Accepts `1.1.0` or `v1.1.0`. A version containing `-`, such as `1.1.0-beta.1`, is treated as a prerelease. |
|
|
53
|
-
| `--from <ref>` | Latest matching tag | Overrides the starting Git ref used to read commits. A version such as `1.0.0` uses the configured tag prefix. |
|
|
54
|
-
| `--to <ref>` | `HEAD` | Sets the ending Git ref. A version such as `1.1.0` uses the configured tag prefix. It cannot be combined with `--release`, and must resolve to the current `HEAD` when used with `--commit`. |
|
|
55
|
-
| `--repository <source>` | Auto-detected | Sets a GitHub `owner/repo` slug or a complete GitHub/Gitea URL. It takes precedence over `package.json` and `origin`. |
|
|
56
|
-
| `--output <path>` | `CHANGELOG.md` | Sets the changelog file path. Relative paths are resolved from the current working directory. |
|
|
57
|
-
| `--dry` | `false` | Prints the current release preview without writing files or performing commit, tag, push, or release API mutations. |
|
|
58
|
-
| `--commit` | `false` | Creates a Git commit after writing. It commits only the changelog and detected version file, using `chore(release): v<version>` by default. |
|
|
59
|
-
| `--release` | `false` | Runs the complete release flow: writes files, creates or reuses a release commit, creates an annotated tag, pushes `HEAD` and the tag, then creates or updates the repository Release. This implies `--commit`. |
|
|
60
|
-
| `--author <author>` | Release bot | Sets the generated release commit author in `"Name <email>"` format; requires `--commit` or `--release`. |
|
|
61
|
-
| `-t, --token <token>` | Environment | Resolves authors and publishes Releases. GitHub precedence is `--token`, `GITHUB_TOKEN`, then `GH_TOKEN`; Gitea uses `GITEA_TOKEN`. |
|
|
62
|
-
| `--tag-prefix <prefix>` | `v` | Sets the prefix used to find and create version tags, such as `package@`. |
|
|
63
|
-
| `--no-tag-prefix` | `false` | Finds and creates version tags without a prefix. |
|
|
64
|
-
| `--name <name>` | Version tag | Sets the remote Release name; only valid with `--release`. |
|
|
65
|
-
| `-d, --draft` | `false` | Creates a draft Release; only valid with `--release`. |
|
|
66
|
-
| `--prerelease` | Inferred | Explicitly marks a prerelease. By default it is inferred from `-` in the version; only valid with `--release`. |
|
|
67
|
-
| `--emoji` | `true` | Controls section title emojis. As with changelogithub, CAC's `--no-emoji` form disables them. |
|
|
68
|
-
| `--capitalize` | `true` | Controls entry capitalization; `--no-capitalize` disables it. |
|
|
69
|
-
| `--group` | `true` | Groups repeated scopes; `--no-group` disables it. |
|
|
70
|
+
### Changelog without a version bump
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
If another tool or workflow manages the project version, build first, then let keepchanges
|
|
73
|
+
write the changelog, create the commit and tag, and publish the assets:
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
```bash
|
|
76
|
+
pnpm build
|
|
77
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release \
|
|
78
|
+
--no-bump \
|
|
79
|
+
--asset dist
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Artifacts only
|
|
83
|
+
|
|
84
|
+
If the release needs neither a version update nor a changelog write, build first, then create
|
|
85
|
+
the tag and Release from the current `HEAD`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pnpm build
|
|
89
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release \
|
|
90
|
+
--no-bump \
|
|
91
|
+
--no-changelog \
|
|
92
|
+
--asset dist
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## GitHub Release assets
|
|
96
|
+
|
|
97
|
+
`--asset` accepts a file, directory, or glob. Directories are traversed recursively, and the
|
|
98
|
+
option is repeatable, so one release can include multiple folders or match sets:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release \
|
|
102
|
+
--asset 'packages/*/dist' \
|
|
103
|
+
--asset artifacts \
|
|
104
|
+
--asset checksums.txt
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Quote globs so keepchanges, rather than the shell, expands them consistently. Before publishing,
|
|
108
|
+
keepchanges verifies that every path exists, every glob matches at least one file, and all final
|
|
109
|
+
file names are unique. Updating an existing Release replaces assets with the same name.
|
|
110
|
+
|
|
111
|
+
Asset uploads currently support GitHub only and require a GitHub token. Token precedence is
|
|
112
|
+
`--token`, `GITHUB_TOKEN`, then `GH_TOKEN`. Without `--asset`, a regular `--release` can run
|
|
113
|
+
without a token: it commits, tags, and pushes before printing a manual Release URL.
|
|
114
|
+
|
|
115
|
+
## Common tasks
|
|
116
|
+
|
|
117
|
+
Write to a different changelog file:
|
|
74
118
|
|
|
75
119
|
```bash
|
|
76
120
|
npx keepchanges 1.1.0 --output docs/CHANGELOG.md
|
|
77
121
|
```
|
|
78
122
|
|
|
79
|
-
Preview
|
|
123
|
+
Preview without changing files or the remote repository:
|
|
80
124
|
|
|
81
125
|
```bash
|
|
82
126
|
npx keepchanges 1.1.0 --dry
|
|
127
|
+
npx keepchanges 1.1.0 --release --dry
|
|
83
128
|
```
|
|
84
129
|
|
|
85
130
|
Write the changelog, update the version, and create a commit:
|
|
@@ -95,36 +140,71 @@ npx keepchanges 1.1.0 --commit \
|
|
|
95
140
|
--author "Release Author <release@example.com>"
|
|
96
141
|
```
|
|
97
142
|
|
|
98
|
-
Create a GitHub Release:
|
|
143
|
+
Create a GitHub Release without assets:
|
|
99
144
|
|
|
100
145
|
```bash
|
|
101
146
|
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release
|
|
102
147
|
```
|
|
103
148
|
|
|
104
|
-
Use version tags without a prefix:
|
|
149
|
+
Use version tags without a prefix or with a package-specific prefix:
|
|
105
150
|
|
|
106
151
|
```bash
|
|
107
152
|
npx keepchanges 1.1.0 --no-tag-prefix
|
|
153
|
+
npx keepchanges 1.1.0 --tag-prefix 'package@'
|
|
108
154
|
```
|
|
109
155
|
|
|
110
|
-
|
|
156
|
+
Regenerate a historical release:
|
|
111
157
|
|
|
112
158
|
```bash
|
|
113
|
-
npx keepchanges 1.1.0 --
|
|
159
|
+
npx keepchanges 1.0.0 --to 1.0.0 --dry
|
|
114
160
|
```
|
|
115
161
|
|
|
116
|
-
|
|
117
|
-
release API:
|
|
162
|
+
## Command reference
|
|
118
163
|
|
|
119
|
-
```
|
|
120
|
-
npx keepchanges
|
|
164
|
+
```text
|
|
165
|
+
npx keepchanges <version> [options]
|
|
121
166
|
```
|
|
122
167
|
|
|
123
|
-
|
|
168
|
+
| Argument or option | Default | Description |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| `<version>` | Required | Version to generate, such as `1.1.0`, `v1.1.0`, or `1.1.0-beta.1`. |
|
|
171
|
+
| `--from <ref>` | Inferred from target | Sets the exclusive starting Git ref used to read commits. |
|
|
172
|
+
| `--to <ref>` | `HEAD` | Sets the ending Git ref. Cannot be combined with `--tag` or `--release`. |
|
|
173
|
+
| `--repository <source>` | Auto-detected | Sets a GitHub `owner/repo` slug or complete GitHub/Gitea URL. |
|
|
174
|
+
| `--output <path>` | `CHANGELOG.md` | Sets the changelog file path. |
|
|
175
|
+
| `--dry` | `false` | Prints a preview without writing files or performing commit, tag, push, or release API mutations. |
|
|
176
|
+
| `--commit` | `false` | Creates a Git commit after writing. |
|
|
177
|
+
| `--tag` | `false` | Writes, commits, creates an annotated tag, and pushes without creating a repository Release. |
|
|
178
|
+
| `--release` | `false` | Commits, tags, pushes, and creates or updates a repository Release; implies `--commit`. |
|
|
179
|
+
| `--asset <path>` | None | Uploads GitHub Release assets from files, directories, or globs. Repeatable; requires `--release` and a GitHub token. |
|
|
180
|
+
| `--bump` / `--no-bump` | `true` | Controls whether the detected project version file is updated. |
|
|
181
|
+
| `--changelog` / `--no-changelog` | `true` | Controls whether the changelog is written. Release notes are generated either way. |
|
|
182
|
+
| `--author <author>` | Release bot | Sets the release commit author in `"Name <email>"` format. |
|
|
183
|
+
| `-t, --token <token>` | Environment | Sets the repository access token used to resolve authors and publish Releases. |
|
|
184
|
+
| `--tag-prefix <prefix>` | `v` | Sets the prefix used to find and create version tags. |
|
|
185
|
+
| `--no-tag-prefix` | `false` | Finds and creates version tags without a prefix. |
|
|
186
|
+
| `--name <name>` | Version tag | Sets the remote Release name; only valid with `--release`. |
|
|
187
|
+
| `-d, --draft` | `false` | Creates a draft Release; only valid with `--release`. |
|
|
188
|
+
| `--prerelease` | Inferred | Marks a prerelease; only valid with `--release`. |
|
|
189
|
+
| `--emoji` / `--no-emoji` | `true` | Controls section title emojis. |
|
|
190
|
+
| `--capitalize` / `--no-capitalize` | `true` | Controls changelog entry capitalization. |
|
|
191
|
+
| `--group` / `--no-group` | `true` | Controls grouping for repeated scopes. |
|
|
124
192
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
193
|
+
## Behavior
|
|
194
|
+
|
|
195
|
+
### Changelog and repository metadata
|
|
196
|
+
|
|
197
|
+
Without `--from`, keepchanges finds the nearest earlier version tag that matches the configured
|
|
198
|
+
tag prefix. If no earlier tag exists, it reads from the first reachable commit. Stable releases
|
|
199
|
+
compare with the previous stable tag; prereleases compare with the nearest previous tag.
|
|
200
|
+
|
|
201
|
+
The repository is resolved from `--repository`, then `package.json#repository`, and finally the
|
|
202
|
+
Git `origin`. When a repository is available, entries include commit and pull request links, and
|
|
203
|
+
the release ends with a version comparison link. Entries use Git author names and include
|
|
204
|
+
`Co-Authored-By` participants. With a provider token, keepchanges attempts to resolve email
|
|
205
|
+
addresses to usernames. Bot accounts are omitted.
|
|
206
|
+
|
|
207
|
+
A self-hosted Gitea repository must be declared explicitly in `package.json`:
|
|
128
208
|
|
|
129
209
|
```json
|
|
130
210
|
{
|
|
@@ -136,53 +216,38 @@ must be declared explicitly in `package.json`:
|
|
|
136
216
|
}
|
|
137
217
|
```
|
|
138
218
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
support author resolution and Release publishing. Gitea uses `GITEA_TOKEN` to
|
|
142
|
-
resolve primary commit authors and publish Releases.
|
|
143
|
-
|
|
144
|
-
Version tags use the `v` prefix by default. The configured prefix applies to
|
|
145
|
-
new tags, existing-tag lookup, version-shaped `--from` and `--to` values,
|
|
146
|
-
comparison links, and repository Release names. Branch names, commit hashes,
|
|
147
|
-
`HEAD`, and other non-version refs are used unchanged. Tag lookup only considers
|
|
148
|
-
the configured prefix, so independent tag sequences do not affect each other.
|
|
219
|
+
GitHub and Gitea both support author resolution and Release publishing. Gitea uses
|
|
220
|
+
`GITEA_TOKEN`, but does not currently support asset uploads.
|
|
149
221
|
|
|
150
|
-
|
|
151
|
-
participants. Bot accounts are omitted. With the corresponding provider token,
|
|
152
|
-
the CLI attempts to resolve email addresses to usernames.
|
|
222
|
+
### Tags and history ranges
|
|
153
223
|
|
|
154
|
-
|
|
155
|
-
and
|
|
224
|
+
Version tags use the `v` prefix by default. The configured prefix applies to tag creation and
|
|
225
|
+
lookup, version-shaped `--from` and `--to` values, comparison links, and Release names. Branch
|
|
226
|
+
names, commit hashes, `HEAD`, and other non-version refs remain unchanged. Tag sequences with
|
|
227
|
+
different prefixes are independent.
|
|
156
228
|
|
|
157
|
-
When the
|
|
229
|
+
When `--to` is provided without `--from`, keepchanges finds the previous matching tag relative
|
|
230
|
+
to that target instead of the current `HEAD`. With `--commit`, `--to` must resolve to the current
|
|
231
|
+
`HEAD`.
|
|
158
232
|
|
|
159
|
-
|
|
160
|
-
2. Creates or reuses a release commit.
|
|
161
|
-
3. Creates an annotated tag.
|
|
162
|
-
4. Pushes `HEAD` and the tag to `origin`.
|
|
163
|
-
5. Creates a repository Release, or prints a manual URL without a GitHub/Gitea token.
|
|
233
|
+
### Commit and release safety
|
|
164
234
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Release. A remote-only tag is fetched, while a local-only tag is pushed. The
|
|
168
|
-
command stops without force-updating when local and remote tags point to
|
|
169
|
-
different commits.
|
|
235
|
+
`--commit` commits only the changelog and detected version file. Other staged and unstaged
|
|
236
|
+
changes remain untouched.
|
|
170
237
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
Release for a new tag and the edit page for an existing tag.
|
|
238
|
+
`--release` reuses an existing tag, regenerates its Release notes, and creates or updates the
|
|
239
|
+
Release. A remote-only tag is fetched, while a local-only tag is pushed. If local and remote
|
|
240
|
+
tags point to different commits, the command stops without force-updating or moving the tag.
|
|
175
241
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
> for a completely non-mutating preview.
|
|
242
|
+
`--dry` takes precedence over other options and prevents all file writes and remote mutations.
|
|
243
|
+
Without `--asset` and a provider token, `--release` still writes locally, commits, tags, and
|
|
244
|
+
pushes before printing a URL for manually creating or editing the Release.
|
|
180
245
|
|
|
181
246
|
## Programmatic API
|
|
182
247
|
|
|
183
|
-
The package root exports `generateChangelog`, the commit parser, default config,
|
|
184
|
-
|
|
185
|
-
|
|
248
|
+
The package root exports `generateChangelog`, the commit parser, default config, and core types.
|
|
249
|
+
The CLI is available only as the `keepchanges` binary. Programmatic callers can override the
|
|
250
|
+
changelog style:
|
|
186
251
|
|
|
187
252
|
```ts
|
|
188
253
|
import { generateChangelog } from 'keepchanges'
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# keepchanges
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
面向 Conventional Commits 的发布 CLI:生成并维护 changelog,并按需更新项目版本、
|
|
4
|
+
创建 release commit 和 Git tag、发布 GitHub/Gitea Release,以及上传 GitHub Release 附件。
|
|
4
5
|
|
|
5
6
|
灵感来自 [changelogithub](https://github.com/antfu-collective/changelogithub)。
|
|
6
7
|
|
|
@@ -8,13 +9,14 @@
|
|
|
8
9
|
|
|
9
10
|
## 功能
|
|
10
11
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
12
|
+
- 根据 Git tag 之间的 Conventional Commits 生成 Release notes
|
|
13
|
+
- 将新版本插入现有 `CHANGELOG.md`,并为 npm 项目更新 `package.json#version`
|
|
14
|
+
- 可分别通过 `--no-bump` 和 `--no-changelog` 关闭版本更新与 changelog 写入
|
|
15
|
+
- 可自动创建 release commit、annotated tag 并推送到远程仓库
|
|
16
|
+
- 支持先固定 release commit,再基于该 commit 构建产物
|
|
17
|
+
- 可创建或更新 GitHub/Gitea Release
|
|
18
|
+
- 可将文件、目录或 glob 匹配结果上传为 GitHub Release 附件
|
|
19
|
+
- 为 commit、PR、版本对比、作者和共同作者生成仓库平台信息
|
|
18
20
|
|
|
19
21
|
当前会收录 `feat`、`fix`、`perf`、带 `!` 的破坏性变更,以及包含
|
|
20
22
|
`BREAKING CHANGE` 或 `BREAKING-CHANGE` trailer 的提交。其他提交类型会被忽略。
|
|
@@ -29,56 +31,95 @@
|
|
|
29
31
|
npx keepchanges 1.1.0
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
版本号也可以包含 `v` 前缀:
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
37
|
npx keepchanges v1.1.0
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
`package.json#version` 更新为 `1.1.0
|
|
40
|
+
默认会写入 `CHANGELOG.md`。如果当前项目是 npm 包,还会将
|
|
41
|
+
`package.json#version` 更新为 `1.1.0`,但不会创建 Git commit。
|
|
40
42
|
|
|
41
|
-
##
|
|
43
|
+
## 发布工作流
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
keepchanges 可以根据项目的版本管理和构建方式组合发布阶段:
|
|
46
|
+
|
|
47
|
+
| 场景 | 更新版本 | 写入 changelog | 构建时机 | 发布方式 |
|
|
48
|
+
| --- | --- | --- | --- | --- |
|
|
49
|
+
| 完整版本发布 | 是 | 是 | 创建 release commit 后 | `--tag`,构建,再 `--release --asset` |
|
|
50
|
+
| 外部管理版本 | 否 | 是 | 发布前 | 构建,再 `--release --no-bump --asset` |
|
|
51
|
+
| 仅发布构建产物 | 否 | 否 | 发布前 | 构建,再 `--release --no-bump --no-changelog --asset` |
|
|
52
|
+
|
|
53
|
+
### 完整版本发布
|
|
54
|
+
|
|
55
|
+
先由 keepchanges 更新版本和 changelog、创建 commit 和 tag,再基于该 commit
|
|
56
|
+
构建产物。随后发布同一个 tag,并上传构建产物:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx keepchanges 1.1.0 --tag
|
|
60
|
+
pnpm build
|
|
61
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release --asset dist
|
|
45
62
|
```
|
|
46
63
|
|
|
47
|
-
|
|
64
|
+
第二次执行会复用已有 tag,不会移动 tag 或重新提交构建产物。
|
|
48
65
|
|
|
49
|
-
|
|
50
|
-
| --- | --- | --- |
|
|
51
|
-
| `<version>` | 必填 | 要生成的版本号。可以传入 `1.1.0` 或 `v1.1.0`。包含 `-` 的版本会作为预发布版本,例如 `1.1.0-beta.1`。 |
|
|
52
|
-
| `--from <ref>` | 匹配前缀的最近 tag | 指定读取 commit 的起始 Git ref,并覆盖自动选择结果。`1.0.0` 这类版本会自动应用配置的 tag 前缀。 |
|
|
53
|
-
| `--to <ref>` | `HEAD` | 指定读取 commit 的结束 Git ref。`1.1.0` 这类版本会自动应用配置的 tag 前缀。不能与 `--release` 一起使用;与 `--commit` 一起使用时必须指向当前 `HEAD`。 |
|
|
54
|
-
| `--repository <source>` | 自动检测 | 指定 `owner/repo` 格式的 GitHub 仓库或 GitHub/Gitea 完整 URL。优先级高于 `package.json` 和 `origin`。 |
|
|
55
|
-
| `--output <path>` | `CHANGELOG.md` | 指定 changelog 文件路径。相对路径以当前工作目录为基准。 |
|
|
56
|
-
| `--dry` | `false` | 输出当前版本预览,不写入文件,也不执行 commit、tag、push 或发布 API。 |
|
|
57
|
-
| `--commit` | `false` | 写入文件后创建 Git commit。只提交 changelog 和检测到的版本文件,默认提交信息为 `chore(release): v<version>`。 |
|
|
58
|
-
| `--release` | `false` | 执行完整发布流程:写入文件、创建或复用 release commit、创建 annotated tag、推送 `HEAD` 和 tag,然后创建或更新仓库 Release。该参数隐含 `--commit`。 |
|
|
59
|
-
| `--author <author>` | release bot | 设置自动创建的 release commit 作者,格式必须为 `"Name <email>"`;需要与 `--commit` 或 `--release` 一起使用。 |
|
|
60
|
-
| `-t, --token <token>` | 环境变量 | 仓库访问令牌,用于解析作者及发布 Release。GitHub 优先级为 `--token`、`GITHUB_TOKEN`、`GH_TOKEN`;Gitea 使用 `GITEA_TOKEN`。 |
|
|
61
|
-
| `--tag-prefix <prefix>` | `v` | 设置查找和创建版本 tag 时使用的前缀,例如 `package@`。 |
|
|
62
|
-
| `--no-tag-prefix` | `false` | 查找和创建不带前缀的版本 tag。 |
|
|
63
|
-
| `--name <name>` | 版本 tag | 设置远程 Release 名称;仅适用于 `--release`。 |
|
|
64
|
-
| `-d, --draft` | `false` | 创建 draft Release;仅适用于 `--release`。 |
|
|
65
|
-
| `--prerelease` | 根据版本推断 | 显式标记为 prerelease;默认根据版本是否包含 `-` 推断,仅适用于 `--release`。 |
|
|
66
|
-
| `--emoji` | `true` | 控制 section 标题 emoji。与 changelogithub 一样,可使用 CAC 的 `--no-emoji` 形式关闭。 |
|
|
67
|
-
| `--capitalize` | `true` | 控制 changelog 条目首字母大写;可使用 `--no-capitalize` 关闭。 |
|
|
68
|
-
| `--group` | `true` | 当 scope 重复时分组;可使用 `--no-group` 关闭。 |
|
|
66
|
+
### 保留 changelog,跳过版本更新
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
如果版本由其他工具或流程管理,可以先构建,再让 keepchanges 写入 changelog、
|
|
69
|
+
创建 commit 和 tag,并发布附件:
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
```bash
|
|
72
|
+
pnpm build
|
|
73
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release \
|
|
74
|
+
--no-bump \
|
|
75
|
+
--asset dist
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 只发布构建产物
|
|
79
|
+
|
|
80
|
+
如果不需要版本更新和 changelog,可以先构建,再从当前 `HEAD` 创建 tag 和 Release:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pnpm build
|
|
84
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release \
|
|
85
|
+
--no-bump \
|
|
86
|
+
--no-changelog \
|
|
87
|
+
--asset dist
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## GitHub Release 附件
|
|
91
|
+
|
|
92
|
+
`--asset` 支持单个文件、目录和 glob。目录会递归展开;该参数可以重复使用,
|
|
93
|
+
因此可以同时上传多个文件夹或匹配结果:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release \
|
|
97
|
+
--asset 'packages/*/dist' \
|
|
98
|
+
--asset artifacts \
|
|
99
|
+
--asset checksums.txt
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
建议始终给 glob 加引号,确保由 keepchanges 而不是 shell 展开。发布前会验证所有
|
|
103
|
+
附件:路径必须存在、glob 必须至少匹配一个文件,并且最终文件名必须唯一。
|
|
104
|
+
更新已有 Release 时,同名附件会被替换。
|
|
105
|
+
|
|
106
|
+
附件上传目前只支持 GitHub,并且必须提供 GitHub token。token 的读取顺序为
|
|
107
|
+
`--token`、`GITHUB_TOKEN`、`GH_TOKEN`。如果没有指定 `--asset`,则普通
|
|
108
|
+
`--release` 可以在没有 token 时完成 commit、tag 和 push,随后输出手动发布链接。
|
|
109
|
+
|
|
110
|
+
## 常见任务
|
|
111
|
+
|
|
112
|
+
指定 changelog 文件:
|
|
73
113
|
|
|
74
114
|
```bash
|
|
75
115
|
npx keepchanges 1.1.0 --output docs/CHANGELOG.md
|
|
76
116
|
```
|
|
77
117
|
|
|
78
|
-
|
|
118
|
+
预览结果,不修改文件或远程仓库:
|
|
79
119
|
|
|
80
120
|
```bash
|
|
81
121
|
npx keepchanges 1.1.0 --dry
|
|
122
|
+
npx keepchanges 1.1.0 --release --dry
|
|
82
123
|
```
|
|
83
124
|
|
|
84
125
|
写入 changelog、更新版本并创建 commit:
|
|
@@ -87,41 +128,77 @@ npx keepchanges 1.1.0 --dry
|
|
|
87
128
|
npx keepchanges 1.1.0 --commit
|
|
88
129
|
```
|
|
89
130
|
|
|
90
|
-
指定 release commit
|
|
131
|
+
指定 release commit 作者:
|
|
91
132
|
|
|
92
133
|
```bash
|
|
93
134
|
npx keepchanges 1.1.0 --commit \
|
|
94
135
|
--author "Release Author <release@example.com>"
|
|
95
136
|
```
|
|
96
137
|
|
|
97
|
-
|
|
138
|
+
创建不带附件的 GitHub Release:
|
|
98
139
|
|
|
99
140
|
```bash
|
|
100
141
|
GITHUB_TOKEN=github_pat_xxx npx keepchanges 1.1.0 --release
|
|
101
142
|
```
|
|
102
143
|
|
|
103
|
-
|
|
144
|
+
使用无前缀或 package 专属版本 tag:
|
|
104
145
|
|
|
105
146
|
```bash
|
|
106
147
|
npx keepchanges 1.1.0 --no-tag-prefix
|
|
148
|
+
npx keepchanges 1.1.0 --tag-prefix 'package@'
|
|
107
149
|
```
|
|
108
150
|
|
|
109
|
-
|
|
151
|
+
重新生成历史版本:
|
|
110
152
|
|
|
111
153
|
```bash
|
|
112
|
-
npx keepchanges 1.1.0 --
|
|
154
|
+
npx keepchanges 1.0.0 --to 1.0.0 --dry
|
|
113
155
|
```
|
|
114
156
|
|
|
115
|
-
|
|
157
|
+
## 命令参考
|
|
116
158
|
|
|
117
|
-
```
|
|
118
|
-
npx keepchanges
|
|
159
|
+
```text
|
|
160
|
+
npx keepchanges <version> [options]
|
|
119
161
|
```
|
|
120
162
|
|
|
121
|
-
|
|
163
|
+
| 参数 | 默认值 | 说明 |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `<version>` | 必填 | 要生成的版本号,例如 `1.1.0`、`v1.1.0` 或 `1.1.0-beta.1`。 |
|
|
166
|
+
| `--from <ref>` | 根据目标推断 | 指定读取 commit 时不包含在结果中的起始 Git ref。 |
|
|
167
|
+
| `--to <ref>` | `HEAD` | 指定结束 Git ref。不能与 `--tag` 或 `--release` 一起使用。 |
|
|
168
|
+
| `--repository <source>` | 自动检测 | 指定 GitHub `owner/repo` 或 GitHub/Gitea 完整 URL。 |
|
|
169
|
+
| `--output <path>` | `CHANGELOG.md` | 指定 changelog 文件路径。 |
|
|
170
|
+
| `--dry` | `false` | 输出预览,不执行文件写入、commit、tag、push 或发布 API。 |
|
|
171
|
+
| `--commit` | `false` | 写入后创建 Git commit。 |
|
|
172
|
+
| `--tag` | `false` | 写入、commit、创建 annotated tag 并 push,但不创建仓库 Release。 |
|
|
173
|
+
| `--release` | `false` | 完成 commit、tag、push,并创建或更新仓库 Release;隐含 `--commit`。 |
|
|
174
|
+
| `--asset <path>` | 无 | 上传 GitHub Release 附件;支持文件、目录、glob 和重复传参。需要 `--release` 和 GitHub token。 |
|
|
175
|
+
| `--bump` / `--no-bump` | `true` | 控制是否更新检测到的项目版本文件。 |
|
|
176
|
+
| `--changelog` / `--no-changelog` | `true` | 控制是否写入 changelog;关闭后仍会生成 Release notes。 |
|
|
177
|
+
| `--author <author>` | release bot | 指定 release commit 作者,格式为 `"Name <email>"`。 |
|
|
178
|
+
| `-t, --token <token>` | 环境变量 | 指定仓库访问令牌,用于解析作者和发布 Release。 |
|
|
179
|
+
| `--tag-prefix <prefix>` | `v` | 指定查找和创建版本 tag 时使用的前缀。 |
|
|
180
|
+
| `--no-tag-prefix` | `false` | 查找和创建不带前缀的版本 tag。 |
|
|
181
|
+
| `--name <name>` | 版本 tag | 指定远程 Release 名称;仅适用于 `--release`。 |
|
|
182
|
+
| `-d, --draft` | `false` | 创建 draft Release;仅适用于 `--release`。 |
|
|
183
|
+
| `--prerelease` | 根据版本推断 | 标记 prerelease;仅适用于 `--release`。 |
|
|
184
|
+
| `--emoji` / `--no-emoji` | `true` | 控制 section 标题 emoji。 |
|
|
185
|
+
| `--capitalize` / `--no-capitalize` | `true` | 控制 changelog 条目首字母大写。 |
|
|
186
|
+
| `--group` / `--no-group` | `true` | 控制重复 scope 分组。 |
|
|
187
|
+
|
|
188
|
+
## 行为说明
|
|
189
|
+
|
|
190
|
+
### Changelog 和仓库信息
|
|
122
191
|
|
|
123
|
-
|
|
124
|
-
|
|
192
|
+
省略 `--from` 时,keepchanges 会查找目标之前匹配 tag 前缀的最近版本 tag。
|
|
193
|
+
如果不存在前一个 tag,则从首个可达 commit 开始读取。稳定版本与前一个稳定 tag
|
|
194
|
+
对比,预发布版本与最近的前一个 tag 对比。
|
|
195
|
+
|
|
196
|
+
仓库地址依次读取 `--repository`、`package.json#repository` 和 Git `origin`。
|
|
197
|
+
识别仓库后,记录会包含 commit 和 PR 链接,末尾会包含版本对比链接。默认使用
|
|
198
|
+
Git 作者名并包含 `Co-Authored-By` 参与者;提供平台 token 后,会尝试将邮箱解析为
|
|
199
|
+
用户名。bot 账号会被忽略。
|
|
200
|
+
|
|
201
|
+
自托管 Gitea 仓库需要在 `package.json` 中显式声明:
|
|
125
202
|
|
|
126
203
|
```json
|
|
127
204
|
{
|
|
@@ -133,47 +210,36 @@ GitHub 地址会被自动识别。自托管 Gitea 需要在 `package.json` 中
|
|
|
133
210
|
}
|
|
134
211
|
```
|
|
135
212
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
解析 commit 主作者并发布 Release。
|
|
213
|
+
GitHub 和 Gitea 均支持作者解析与 Release 发布。Gitea 使用 `GITEA_TOKEN`,
|
|
214
|
+
但目前不支持附件上传。
|
|
139
215
|
|
|
140
|
-
|
|
141
|
-
版本形式的 `--from` 和 `--to`、比较链接及仓库 Release 名称。分支名、commit
|
|
142
|
-
hash、`HEAD` 等非版本 ref 保持不变。自动查找 tag 时只考虑配置的前缀,因此
|
|
143
|
-
彼此独立的 tag 序列不会相互影响。
|
|
216
|
+
### Tag 和历史范围
|
|
144
217
|
|
|
145
|
-
|
|
146
|
-
|
|
218
|
+
版本 tag 默认使用 `v` 前缀。配置的前缀会应用于 tag 创建和查找、版本形式的
|
|
219
|
+
`--from` 和 `--to`、比较链接及 Release 名称。分支名、commit hash 和 `HEAD`
|
|
220
|
+
等非版本 ref 保持不变。不同前缀的 tag 序列彼此独立。
|
|
147
221
|
|
|
148
|
-
`--
|
|
149
|
-
|
|
222
|
+
显式传入 `--to` 而不传 `--from` 时,会相对于该目标查找前一个匹配的 tag,
|
|
223
|
+
而不是使用当前 `HEAD` 的最近 tag。与 `--commit` 一起使用时,`--to` 必须指向
|
|
224
|
+
当前 `HEAD`。
|
|
150
225
|
|
|
151
|
-
|
|
226
|
+
### Commit 和发布安全
|
|
152
227
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
3. 创建 annotated tag。
|
|
156
|
-
4. 将 `HEAD` 和 tag 推送到 `origin`。
|
|
157
|
-
5. 创建仓库 Release;GitHub/Gitea 没有 token 时输出手动发布链接。
|
|
158
|
-
|
|
159
|
-
`--release` 在 tag 已存在时不会移动 tag。它会根据前一个版本到现有 tag
|
|
160
|
-
重新生成 Release notes,并创建或更新 Release。只有远程存在 tag 时会先将它
|
|
161
|
-
拉取到本地;只有本地存在 tag 时会推送该 tag。本地与远程 tag 指向不同 commit
|
|
162
|
-
时会停止,不会强制覆盖。
|
|
228
|
+
`--commit` 只提交 changelog 和检测到的版本文件,其他已暂存或未暂存的改动会
|
|
229
|
+
保持原状。
|
|
163
230
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
231
|
+
`--release` 会复用已有 tag,重新生成该版本的 Release notes,并创建或更新
|
|
232
|
+
Release。远程独有的 tag 会被拉取,本地独有的 tag 会被推送;本地和远程 tag
|
|
233
|
+
指向不同 commit 时会停止,不会强制覆盖或移动 tag。
|
|
167
234
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
> `--release --dry`。
|
|
235
|
+
`--dry` 优先于其他选项,不会执行任何文件写入或远程修改。未指定 `--asset` 且
|
|
236
|
+
没有平台 token 时,`--release` 仍会执行本地写入、commit、tag 和 push,随后
|
|
237
|
+
提供手动创建或编辑 Release 的链接。
|
|
172
238
|
|
|
173
239
|
## 程序化 API
|
|
174
240
|
|
|
175
241
|
包根入口导出 `generateChangelog`、commit parser、默认配置及核心类型。
|
|
176
|
-
CLI 仅作为 `keepchanges`
|
|
242
|
+
CLI 仅作为 `keepchanges` 二进制命令。程序化调用可以覆盖 changelog 样式:
|
|
177
243
|
|
|
178
244
|
```ts
|
|
179
245
|
import { generateChangelog } from 'keepchanges'
|
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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-
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
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-DswG6QDH.mjs";
|
|
3
|
+
import { lstat, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { normalizeFull } from "verkit";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { cac } from "cac";
|
|
7
|
-
import { resolve } from "node:path";
|
|
7
|
+
import { basename, resolve } from "node:path";
|
|
8
8
|
import ansis from "ansis";
|
|
9
|
+
import { glob, isDynamicPattern } from "tinyglobby";
|
|
9
10
|
import { x } from "tinyexec";
|
|
10
11
|
//#region package.json
|
|
11
|
-
var version = "1.0.
|
|
12
|
+
var version = "1.0.3";
|
|
12
13
|
//#endregion
|
|
13
14
|
//#region src/git.ts
|
|
14
15
|
function versionTagPattern(tagPrefix) {
|
|
@@ -43,7 +44,9 @@ async function getPreviousTag(cwd, tag, releaseRef, tagPrefix) {
|
|
|
43
44
|
if (releaseVersion && !releaseVersion.includes("-")) {
|
|
44
45
|
const previousStable = (await git(cwd, "tag", "--merged", releaseRef, "--sort=-version:refname")).trim().split("\n").find((candidate) => {
|
|
45
46
|
if (candidate === tag || !candidate.startsWith(tagPrefix)) return false;
|
|
46
|
-
const
|
|
47
|
+
const versionTag = candidate.slice(tagPrefix.length);
|
|
48
|
+
if (!/^\d/.test(versionTag)) return false;
|
|
49
|
+
const version = normalizeFull(versionTag);
|
|
47
50
|
return Boolean(version && !version.includes("-"));
|
|
48
51
|
});
|
|
49
52
|
if (previousStable) return previousStable;
|
|
@@ -164,6 +167,7 @@ const giteaRepository = {
|
|
|
164
167
|
const githubRepository = {
|
|
165
168
|
name: "GitHub",
|
|
166
169
|
tokenEnv: "GITHUB_TOKEN",
|
|
170
|
+
supportsReleaseAssets: true,
|
|
167
171
|
parse(source) {
|
|
168
172
|
const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(source);
|
|
169
173
|
if (!match) return;
|
|
@@ -222,10 +226,13 @@ const githubRepository = {
|
|
|
222
226
|
let url = releasesUrl;
|
|
223
227
|
let method = "POST";
|
|
224
228
|
let action = "created";
|
|
229
|
+
let existingAssets = [];
|
|
225
230
|
if (existing.ok) {
|
|
226
|
-
|
|
231
|
+
const data = await existing.json();
|
|
232
|
+
url = `${releasesUrl}/${data.id}`;
|
|
227
233
|
method = "PATCH";
|
|
228
234
|
action = "updated";
|
|
235
|
+
existingAssets = data.assets ?? [];
|
|
229
236
|
} else if (existing.status !== 404) throw new Error(`GitHub release lookup failed (${existing.status})`);
|
|
230
237
|
const response = await fetch(url, {
|
|
231
238
|
method,
|
|
@@ -239,8 +246,33 @@ const githubRepository = {
|
|
|
239
246
|
})
|
|
240
247
|
});
|
|
241
248
|
if (!response.ok) throw new Error(`GitHub release publishing failed (${response.status})`);
|
|
249
|
+
const data = await response.json();
|
|
250
|
+
if (release.assets?.length) {
|
|
251
|
+
if (!data.upload_url) throw new Error("GitHub release response did not include an upload URL");
|
|
252
|
+
const uploadUrl = data.upload_url.replace(/\{.*$/, "");
|
|
253
|
+
for (const asset of release.assets) {
|
|
254
|
+
const existingAsset = existingAssets.find((candidate) => candidate.name === asset.name);
|
|
255
|
+
if (existingAsset) {
|
|
256
|
+
const deleteResponse = await fetch(`https://api.github.com/repos/${repository.path}/releases/assets/${existingAsset.id}`, {
|
|
257
|
+
method: "DELETE",
|
|
258
|
+
headers
|
|
259
|
+
});
|
|
260
|
+
if (!deleteResponse.ok) throw new Error(`GitHub release asset replacement failed for ${asset.name} (${deleteResponse.status})`);
|
|
261
|
+
}
|
|
262
|
+
const uploadResponse = await fetch(`${uploadUrl}?name=${encodeURIComponent(asset.name)}`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: {
|
|
265
|
+
"accept": "application/vnd.github+json",
|
|
266
|
+
"authorization": `Bearer ${token}`,
|
|
267
|
+
"content-type": "application/octet-stream"
|
|
268
|
+
},
|
|
269
|
+
body: asset.data
|
|
270
|
+
});
|
|
271
|
+
if (!uploadResponse.ok) throw new Error(`GitHub release asset upload failed for ${asset.name} (${uploadResponse.status})`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
242
274
|
return {
|
|
243
|
-
url:
|
|
275
|
+
url: data.html_url,
|
|
244
276
|
action
|
|
245
277
|
};
|
|
246
278
|
}
|
|
@@ -330,7 +362,7 @@ function printPublishedRelease(provider, result, stdout, colors) {
|
|
|
330
362
|
//#endregion
|
|
331
363
|
//#region src/cli/createChanges.ts
|
|
332
364
|
async function createChanges(options, environment) {
|
|
333
|
-
if (options.commit && options.to !== defaultConfig.cli.to) {
|
|
365
|
+
if ((options.commit || options.tag || options.release) && options.to !== defaultConfig.cli.to) {
|
|
334
366
|
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())]);
|
|
335
367
|
if (toCommit !== headCommit) throw new Error("--to must resolve to HEAD when used with --commit");
|
|
336
368
|
}
|
|
@@ -341,9 +373,12 @@ async function createChanges(options, environment) {
|
|
|
341
373
|
const repository = await resolveRepository(environment.cwd, options.repository);
|
|
342
374
|
const token = repository?.provider.token(options.token, env);
|
|
343
375
|
validateReleaseSupport(options, repository);
|
|
344
|
-
|
|
376
|
+
if (options.release && !options.dry && options.assets.length && !token) throw new Error(`A ${repository.provider.name} token is required to upload release assets`);
|
|
377
|
+
const releaseAssets = options.release && !options.dry && options.assets.length ? await readReleaseAssets(environment.cwd, options.assets) : [];
|
|
378
|
+
const createsTag = options.tag || options.release;
|
|
379
|
+
let taggedCommit = createsTag ? await getTagCommit(environment.cwd, tag) : void 0;
|
|
345
380
|
let releaseRef = taggedCommit ? tag : void 0;
|
|
346
|
-
const remoteTaggedCommit =
|
|
381
|
+
const remoteTaggedCommit = createsTag ? await getRemoteTagCommit(environment.cwd, tag) : void 0;
|
|
347
382
|
if (taggedCommit && remoteTaggedCommit && taggedCommit !== remoteTaggedCommit) throw new Error(`Tag ${tag} differs between local and origin`);
|
|
348
383
|
if (remoteTaggedCommit && !taggedCommit) if (options.dry) {
|
|
349
384
|
taggedCommit = remoteTaggedCommit;
|
|
@@ -353,10 +388,11 @@ async function createChanges(options, environment) {
|
|
|
353
388
|
taggedCommit = await getTagCommit(environment.cwd, tag);
|
|
354
389
|
releaseRef = tag;
|
|
355
390
|
}
|
|
356
|
-
const from = options.from ?? (taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef, options.tagPrefix) : await getLatestTag(environment.cwd, options.tagPrefix));
|
|
357
391
|
const to = releaseRef || options.to;
|
|
358
|
-
const resolvedFrom = from ? await resolveVersionRef(environment.cwd, from, options.tagPrefix) : "";
|
|
359
392
|
const resolvedTo = await resolveVersionRef(environment.cwd, to, options.tagPrefix);
|
|
393
|
+
const inferFromTarget = releaseRef !== void 0 || options.to !== defaultConfig.cli.to;
|
|
394
|
+
const from = options.from ?? (inferFromTarget ? await getPreviousTag(environment.cwd, tag, resolvedTo, options.tagPrefix) : await getLatestTag(environment.cwd, options.tagPrefix));
|
|
395
|
+
const resolvedFrom = from ? await resolveVersionRef(environment.cwd, from, options.tagPrefix) : "";
|
|
360
396
|
const comparisonFrom = resolvedFrom || (repository ? await git(environment.cwd, "rev-list", "--max-parents=0", resolvedTo).then((value) => value.trim()) : "");
|
|
361
397
|
const commits = parseCommits(await readGitCommits(environment.cwd, resolvedFrom, resolvedTo));
|
|
362
398
|
if (token && repository) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
|
|
@@ -393,38 +429,45 @@ async function createChanges(options, environment) {
|
|
|
393
429
|
}
|
|
394
430
|
return;
|
|
395
431
|
}
|
|
396
|
-
if (
|
|
432
|
+
if (createsTag && taggedCommit) {
|
|
397
433
|
if (!remoteTaggedCommit) await git(environment.cwd, "push", "origin", `refs/tags/${tag}`);
|
|
398
|
-
|
|
434
|
+
if (!options.release) return;
|
|
435
|
+
await publishRelease(repository, repositoryRelease, token, preview, environment, releaseAssets, "edit");
|
|
399
436
|
return;
|
|
400
437
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
438
|
+
let outputPath;
|
|
439
|
+
let releaseExists = false;
|
|
440
|
+
if (options.changelog) {
|
|
441
|
+
outputPath = resolve(environment.cwd, options.output);
|
|
442
|
+
const currentChangelog = await readChangelog(outputPath);
|
|
443
|
+
releaseExists = hasRelease(currentChangelog, options.version);
|
|
444
|
+
await writeChangelog(outputPath, insertRelease(currentChangelog, release));
|
|
445
|
+
}
|
|
446
|
+
const versionPath = options.bump ? await updateVersion(environment.cwd, options.version) : void 0;
|
|
447
|
+
if (options.commit || createsTag) await commitReleaseFiles(options, environment.cwd, outputPath, versionPath, releaseExists, tag);
|
|
448
|
+
if (createsTag) {
|
|
408
449
|
const gitIdentity = resolveGitIdentity(options.author);
|
|
409
450
|
await git(environment.cwd, ...gitIdentity, "tag", "-a", tag, "-m", tag);
|
|
410
451
|
await git(environment.cwd, "push", "origin", "HEAD", `refs/tags/${tag}`);
|
|
411
|
-
await publishRelease(repository, repositoryRelease, token, preview, environment);
|
|
452
|
+
if (options.release) await publishRelease(repository, repositoryRelease, token, preview, environment, releaseAssets);
|
|
412
453
|
}
|
|
413
454
|
}
|
|
414
455
|
function validateReleaseSupport(options, repository) {
|
|
415
456
|
if (!options.release) return;
|
|
416
457
|
if (!repository) throw new Error("A supported repository is required to release");
|
|
417
458
|
if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
|
|
459
|
+
if (options.assets.length && !repository.provider.supportsReleaseAssets) throw new Error(`${repository.provider.name} does not support release assets`);
|
|
418
460
|
}
|
|
419
461
|
async function commitReleaseFiles(options, cwd, outputPath, versionPath, releaseExists, tag) {
|
|
420
462
|
const releasePaths = [outputPath, versionPath].filter((path) => path !== void 0);
|
|
463
|
+
if (!releasePaths.length) return;
|
|
421
464
|
if (!(await git(cwd, "status", "--porcelain", "--", ...releasePaths)).trim()) return;
|
|
422
465
|
await git(cwd, "add", "--", ...releasePaths);
|
|
423
466
|
const versionChanges = versionPath ? await git(cwd, "status", "--porcelain", "--", versionPath) : "";
|
|
424
467
|
const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} ${tag} release notes` : `chore(release): ${tag}`;
|
|
425
468
|
await git(cwd, ...resolveGitIdentity(options.author), "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
|
|
426
469
|
}
|
|
427
|
-
async function publishRelease(repository, release, token, preview, environment, manualAction = "create") {
|
|
470
|
+
async function publishRelease(repository, release, token, preview, environment, assets, manualAction = "create") {
|
|
428
471
|
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
429
472
|
const colors = environment.colors ?? ansis;
|
|
430
473
|
printChangesPreview(preview, stdout, colors);
|
|
@@ -434,9 +477,52 @@ async function publishRelease(repository, release, token, preview, environment,
|
|
|
434
477
|
printManualReleaseUrl(repository, release, stdout, colors, manualAction);
|
|
435
478
|
return;
|
|
436
479
|
}
|
|
437
|
-
const result = await repository.provider.publishRelease(repository,
|
|
480
|
+
const result = await repository.provider.publishRelease(repository, {
|
|
481
|
+
...release,
|
|
482
|
+
assets
|
|
483
|
+
}, token, environment.fetch ?? globalThis.fetch);
|
|
438
484
|
printPublishedRelease(repository.provider.name, result, stdout, colors);
|
|
439
485
|
}
|
|
486
|
+
async function readReleaseAssets(cwd, paths) {
|
|
487
|
+
const files = /* @__PURE__ */ new Set();
|
|
488
|
+
async function collect(path) {
|
|
489
|
+
const metadata = await lstat(path).catch(() => void 0);
|
|
490
|
+
if (!metadata) throw new Error(`Release asset does not exist: ${path}`);
|
|
491
|
+
if (metadata.isFile()) {
|
|
492
|
+
files.add(path);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (!metadata.isDirectory()) throw new Error(`Release asset must be a file or directory: ${path}`);
|
|
496
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
497
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) await collect(resolve(path, entry.name));
|
|
498
|
+
}
|
|
499
|
+
for (const path of paths) {
|
|
500
|
+
if (!isDynamicPattern(path)) {
|
|
501
|
+
await collect(resolve(cwd, path));
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const matches = await glob(path, {
|
|
505
|
+
absolute: true,
|
|
506
|
+
cwd,
|
|
507
|
+
dot: true,
|
|
508
|
+
expandDirectories: false,
|
|
509
|
+
followSymbolicLinks: false,
|
|
510
|
+
onlyFiles: false
|
|
511
|
+
});
|
|
512
|
+
if (!matches.length) throw new Error(`Release asset pattern did not match: ${path}`);
|
|
513
|
+
for (const match of matches.sort((left, right) => left.localeCompare(right))) await collect(match);
|
|
514
|
+
}
|
|
515
|
+
const names = /* @__PURE__ */ new Set();
|
|
516
|
+
return Promise.all([...files].map(async (path) => {
|
|
517
|
+
const name = basename(path);
|
|
518
|
+
if (names.has(name)) throw new Error(`Release assets must have unique file names: ${name}`);
|
|
519
|
+
names.add(name);
|
|
520
|
+
return {
|
|
521
|
+
name,
|
|
522
|
+
data: await readFile(path)
|
|
523
|
+
};
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
440
526
|
function resolveGitIdentity(author) {
|
|
441
527
|
const match = /^([^<>\r\n]+)<([^<>\r\n]+)>$/.exec(author);
|
|
442
528
|
if (!match) throw new Error("Author must use the \"Name <email>\" format");
|
|
@@ -459,8 +545,9 @@ function resolveOptions(versionArgument, options) {
|
|
|
459
545
|
if (typeof options.tagPrefix === "string") tagPrefix = options.tagPrefix;
|
|
460
546
|
else if (options.tagPrefix === false) tagPrefix = "";
|
|
461
547
|
if (options.to !== void 0 && release) throw new Error("--to cannot be used with --release");
|
|
462
|
-
if (options.
|
|
463
|
-
if (
|
|
548
|
+
if (options.to !== void 0 && options.tag) throw new Error("--to cannot be used with --tag");
|
|
549
|
+
if (options.author !== void 0 && !commit && !options.tag && !release) throw new Error("--author requires --commit, --tag, or --release");
|
|
550
|
+
if (!release && (options.name !== void 0 || options.draft !== void 0 || options.prerelease !== void 0 || (options.asset?.length ?? 0) > 0)) throw new Error("--name, --draft, --prerelease, and --asset require --release");
|
|
464
551
|
return {
|
|
465
552
|
version,
|
|
466
553
|
from: options.from,
|
|
@@ -469,7 +556,11 @@ function resolveOptions(versionArgument, options) {
|
|
|
469
556
|
output: options.output ?? defaultConfig.cli.output,
|
|
470
557
|
dry: options.dry ?? defaultConfig.cli.dry,
|
|
471
558
|
commit,
|
|
559
|
+
tag: options.tag ?? defaultConfig.cli.tag,
|
|
472
560
|
release,
|
|
561
|
+
assets: options.asset ?? [],
|
|
562
|
+
bump: options.bump ?? defaultConfig.cli.bump,
|
|
563
|
+
changelog: options.changelog ?? defaultConfig.cli.changelog,
|
|
473
564
|
author: options.author ?? defaultConfig.cli.author,
|
|
474
565
|
token: options.token,
|
|
475
566
|
tagPrefix,
|
|
@@ -483,7 +574,10 @@ function resolveOptions(versionArgument, options) {
|
|
|
483
574
|
}
|
|
484
575
|
//#endregion
|
|
485
576
|
//#region src/cli.ts
|
|
486
|
-
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("--
|
|
577
|
+
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("--tag", "Commit, tag, and push without publishing a release").option("--release", "Publish a repository release").option("--asset <path>", "Release asset file, directory, or glob", {
|
|
578
|
+
type: [],
|
|
579
|
+
default: []
|
|
580
|
+
}).option("--bump", "Update the project version; --no-bump disables it", { default: defaultConfig.cli.bump }).option("--changelog", "Write the changelog; --no-changelog disables it", { default: defaultConfig.cli.changelog }).option("--author <author>", "Commit author in \"Name <email>\" format").option("-t, --token <token>", "Repository token").option("--no-tag-prefix", "Create version tags without a prefix").option("--tag-prefix <prefix>", "Prefix used for version tags", { default: defaultConfig.cli.tagPrefix }).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");
|
|
487
581
|
cli.command("<version>").usage("<version> [options]").action(async (versionArgument, options) => {
|
|
488
582
|
await createChanges(resolveOptions(versionArgument, options), { cwd: process.cwd() });
|
|
489
583
|
});
|
|
@@ -7,7 +7,10 @@ const defaultConfig = {
|
|
|
7
7
|
to: "HEAD",
|
|
8
8
|
dry: false,
|
|
9
9
|
commit: false,
|
|
10
|
+
tag: false,
|
|
10
11
|
release: false,
|
|
12
|
+
bump: true,
|
|
13
|
+
changelog: true,
|
|
11
14
|
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>",
|
|
12
15
|
tagPrefix: "v",
|
|
13
16
|
draft: false
|
package/dist/index.d.mts
CHANGED
|
@@ -19,6 +19,13 @@ interface RepositoryRelease {
|
|
|
19
19
|
body: string;
|
|
20
20
|
prerelease: boolean;
|
|
21
21
|
draft: boolean;
|
|
22
|
+
assets?: RepositoryReleaseAsset[];
|
|
23
|
+
}
|
|
24
|
+
interface RepositoryReleaseAsset {
|
|
25
|
+
/** File name shown on the Release; names must be unique within a Release. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Exact bytes sent to the repository provider. */
|
|
28
|
+
data: Uint8Array;
|
|
22
29
|
}
|
|
23
30
|
interface RepositoryReleaseResult {
|
|
24
31
|
url: string;
|
|
@@ -28,6 +35,7 @@ type ManualReleaseAction = 'create' | 'edit';
|
|
|
28
35
|
interface RepositoryProvider {
|
|
29
36
|
name: string;
|
|
30
37
|
tokenEnv: string;
|
|
38
|
+
supportsReleaseAssets?: boolean;
|
|
31
39
|
parse: (source: string) => Repository | undefined;
|
|
32
40
|
token: (explicit: string | undefined, env: NodeJS.ProcessEnv) => string | undefined;
|
|
33
41
|
commitUrl: (repository: Repository, hash: string) => string;
|
|
@@ -88,7 +96,10 @@ interface KeepChangesConfig {
|
|
|
88
96
|
to: string;
|
|
89
97
|
dry: boolean;
|
|
90
98
|
commit: boolean;
|
|
99
|
+
tag: boolean;
|
|
91
100
|
release: boolean;
|
|
101
|
+
bump: boolean;
|
|
102
|
+
changelog: boolean;
|
|
92
103
|
author: string;
|
|
93
104
|
tagPrefix: string;
|
|
94
105
|
draft: boolean;
|
|
@@ -101,7 +112,10 @@ declare const defaultConfig: {
|
|
|
101
112
|
to: string;
|
|
102
113
|
dry: false;
|
|
103
114
|
commit: false;
|
|
115
|
+
tag: false;
|
|
104
116
|
release: false;
|
|
117
|
+
bump: true;
|
|
118
|
+
changelog: true;
|
|
105
119
|
author: string;
|
|
106
120
|
tagPrefix: string;
|
|
107
121
|
draft: false;
|
|
@@ -154,4 +168,4 @@ declare function writeChangelog(path: string, changelog: string): Promise<void>;
|
|
|
154
168
|
declare function hasRelease(changelog: string, version: string): boolean;
|
|
155
169
|
declare function insertRelease(changelog: string, release: string): string;
|
|
156
170
|
//#endregion
|
|
157
|
-
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 };
|
|
171
|
+
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 RepositoryReleaseAsset, type RepositoryReleaseResult, defaultConfig, generateChangelog, hasRelease, insertRelease, parseCommit, parseCommits, readChangelog, resolveChangelogConfig, writeChangelog };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as insertRelease, c as defaultConfig, i as hasRelease, l as resolveChangelogConfig, n as parseCommits, o as readChangelog, r as generateChangelog, s as writeChangelog, t as parseCommit } from "./commit-
|
|
1
|
+
import { a as insertRelease, c as defaultConfig, i as hasRelease, l as resolveChangelogConfig, n as parseCommits, o as readChangelog, r as generateChangelog, s as writeChangelog, t as parseCommit } from "./commit-DswG6QDH.mjs";
|
|
2
2
|
export { 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.
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"description": "Generate and maintain CHANGELOG.md from Conventional Commits.",
|
|
6
6
|
"author": "edram",
|
|
7
7
|
"license": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"ansis": "^4.3.1",
|
|
37
37
|
"cac": "^7.0.0",
|
|
38
38
|
"tinyexec": "^1.2.4",
|
|
39
|
+
"tinyglobby": "^0.2.17",
|
|
39
40
|
"verkit": "^0.3.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|