@peiyanlu/release 2.0.0 → 2.1.0
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 +150 -19
- package/dist/action.mjs +161 -165
- package/dist/cli.mjs +118 -146
- package/dist/index.d.mts +10 -102
- package/dist/index.mjs +80 -263
- package/package.json +16 -16
- package/template/release.workflow.hbs +140 -0
- package/template/release.text +0 -82
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
## 环境要求
|
|
38
38
|
|
|
39
|
-
- Node.js
|
|
39
|
+
- Node.js 22.18+
|
|
40
40
|
- Git 仓库
|
|
41
41
|
- npm 发布需要可用的 npm 身份验证;GitHub API 操作需要 GitHub token
|
|
42
42
|
|
|
@@ -53,7 +53,7 @@ pnpm add -D @peiyanlu/release
|
|
|
53
53
|
release [release-type] [options]
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
不指定 `release-type`
|
|
56
|
+
不指定 `release-type` 时,会根据提交记录推断并预选升级类型;CI 模式下自动采用该类型。可指定 `major`、`minor`、`patch` 或 `prerelease`。
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
59
|
pnpm exec release --dry-run
|
|
@@ -63,15 +63,16 @@ pnpm exec release --ci
|
|
|
63
63
|
|
|
64
64
|
常用选项:
|
|
65
65
|
|
|
66
|
-
| 选项 | 说明
|
|
67
|
-
|
|
68
|
-
| `-n, --dry-run` | 预览发布步骤,不写入变更
|
|
69
|
-
| `--ci` | CI 模式,关闭交互式流程
|
|
70
|
-
| `--skip-
|
|
71
|
-
| `--skip-
|
|
72
|
-
| `--skip-
|
|
73
|
-
|
|
|
74
|
-
| `-
|
|
66
|
+
| 选项 | 说明 |
|
|
67
|
+
|------------------|----------------------------------------------------|
|
|
68
|
+
| `-n, --dry-run` | 预览发布步骤,不写入变更 |
|
|
69
|
+
| `--ci` | CI 模式,关闭交互式流程 |
|
|
70
|
+
| `--skip-bump` | 跳过项目版本更新 |
|
|
71
|
+
| `--skip-git` | 跳过 Git 提交、标签、推送;版本推断仍读取 Git 历史 |
|
|
72
|
+
| `--skip-publish` | 跳过项目发布 |
|
|
73
|
+
| `--skip-release` | 跳过仓库托管服务上创建发布 |
|
|
74
|
+
| `-v, --version` | 显示版本 |
|
|
75
|
+
| `-h, --help` | 显示完整帮助 |
|
|
75
76
|
|
|
76
77
|
## 配置
|
|
77
78
|
|
|
@@ -89,7 +90,6 @@ export default defineConfig({
|
|
|
89
90
|
hosting: new GithubHosting({
|
|
90
91
|
token: process.env.GITHUB_TOKEN,
|
|
91
92
|
}),
|
|
92
|
-
checkout: { branch: 'release' },
|
|
93
93
|
bump: { prerelease: 'beta' },
|
|
94
94
|
commit: { sign: false },
|
|
95
95
|
publish: { tag: 'latest' },
|
|
@@ -117,10 +117,11 @@ export default defineConfig({
|
|
|
117
117
|
|
|
118
118
|
## GitHub Action
|
|
119
119
|
|
|
120
|
-
本仓库也可作为 Action 使用。Action 要求配置文件定义 `project`,并使用 `github-token` 访问 GitHub API
|
|
120
|
+
本仓库也可作为 Action 使用。Action 要求配置文件定义 `project`,并使用 `github-token` 访问 GitHub API。`workflow` 的默认值为 `full`。
|
|
121
121
|
|
|
122
122
|
```yaml
|
|
123
123
|
name: Release
|
|
124
|
+
|
|
124
125
|
on:
|
|
125
126
|
issue_comment:
|
|
126
127
|
types: [ created, deleted ]
|
|
@@ -140,26 +141,156 @@ jobs:
|
|
|
140
141
|
with:
|
|
141
142
|
fetch-depth: 0
|
|
142
143
|
fetch-tags: true
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
- uses: pnpm/action-setup@v6
|
|
145
146
|
with:
|
|
146
147
|
version: 12
|
|
147
|
-
|
|
148
|
+
|
|
148
149
|
- uses: actions/setup-node@v7
|
|
149
150
|
with:
|
|
150
151
|
node-version: 24
|
|
151
152
|
registry-url: https://registry.npmjs.org/
|
|
152
|
-
|
|
153
|
+
package-manager-cache: false
|
|
154
|
+
|
|
153
155
|
- run: pnpm install --frozen-lockfile
|
|
154
|
-
|
|
155
|
-
- uses: peiyanlu/release@
|
|
156
|
+
|
|
157
|
+
- uses: peiyanlu/release@master
|
|
156
158
|
with:
|
|
157
159
|
workflow: full
|
|
158
160
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
159
161
|
npm-token: ${{ secrets.NPM_TOKEN }}
|
|
160
162
|
```
|
|
161
163
|
|
|
162
|
-
确保仓库中已提交 `release.config.ts`,Action
|
|
164
|
+
确保仓库中已提交 `release.config.ts`,Action 会从检出的项目中加载该文件。若发布到 npm,将 `npm-token` 配置为仓库 secret;它会作为 `NODE_AUTH_TOKEN` 提供给发布步骤。也可在仓库配置 npm Trusted Publishing。
|
|
165
|
+
|
|
166
|
+
### npm Trusted Publishing
|
|
167
|
+
|
|
168
|
+
[npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) 使用 GitHub Actions 的 OIDC 身份发布 npm 包,不需要长期有效的 `NPM_TOKEN`。
|
|
169
|
+
|
|
170
|
+
1. **确认条件**:包已至少发布过一个版本,使用公开 npm registry 和 GitHub 托管的 runner。Trusted Publisher 按包和 workflow 文件分别注册。
|
|
171
|
+
2. **在 npm 注册发布者**:打开 [npmjs.com](https://www.npmjs.com/) 上对应包的 **Settings → Trusted Publisher → GitHub Actions**:填写 GitHub 用户或组织名、仓库名和 workflow 文件名(例如 `release.yml`)。
|
|
172
|
+
如果 workflow 使用 GitHub environment,也填写相同的 environment 名称。
|
|
173
|
+
3. **调整发布 job**:在发布 job 中授予 `id-token: write` 权限,使用 Node.js 22.14.0 或更高版本,以确保 npm 版本为 11.5.1 或更高版本,并从 `actions/setup-node` 移除 `registry-url`(v7 以下),从 Action step 输入中移除 `npm-token`。例如:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
name: Release
|
|
177
|
+
|
|
178
|
+
on:
|
|
179
|
+
issue_comment:
|
|
180
|
+
types: [ created, deleted ]
|
|
181
|
+
push:
|
|
182
|
+
branches:
|
|
183
|
+
- master
|
|
184
|
+
|
|
185
|
+
permissions:
|
|
186
|
+
contents: write
|
|
187
|
+
pull-requests: write
|
|
188
|
+
id-token: write # 新增
|
|
189
|
+
|
|
190
|
+
jobs:
|
|
191
|
+
release:
|
|
192
|
+
runs-on: ubuntu-latest
|
|
193
|
+
steps:
|
|
194
|
+
- uses: actions/checkout@v7
|
|
195
|
+
with:
|
|
196
|
+
fetch-depth: 0
|
|
197
|
+
fetch-tags: true
|
|
198
|
+
|
|
199
|
+
- uses: pnpm/action-setup@v6
|
|
200
|
+
with:
|
|
201
|
+
version: 12
|
|
202
|
+
|
|
203
|
+
- uses: actions/setup-node@v7
|
|
204
|
+
with:
|
|
205
|
+
node-version: 24 # npm 11.5.1+
|
|
206
|
+
registry-url: https://registry.npmjs.org/ # v7 以下移除
|
|
207
|
+
package-manager-cache: false
|
|
208
|
+
|
|
209
|
+
- run: pnpm install --frozen-lockfile
|
|
210
|
+
|
|
211
|
+
- uses: peiyanlu/release@master
|
|
212
|
+
with:
|
|
213
|
+
workflow: full
|
|
214
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
215
|
+
#npm-token: ${{ secrets.NPM_TOKEN }} # 移除
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
配置正确后,npm/pnpm 会自动通过 OIDC 认证,公开仓库发布的版本还会自动附带 provenance(来源证明),无需额外开启。
|
|
219
|
+
|
|
220
|
+
若出现 `ENEEDAUTH`:
|
|
221
|
+
核对 npm 包是否已存在、注册的仓库和 workflow 文件名、GitHub environment,以及发布 job 是否有 `id-token: write` 并使用满足版本要求的 npm CLI。
|
|
222
|
+
|
|
223
|
+
### Snapshots
|
|
224
|
+
|
|
225
|
+
为 `workflow_dispatch` 添加 `snapshot` 字符串输入:手动运行时输入非空值会执行快照 job;push 和 issue comment 事件没有该输入,因此仍按原有流程执行。
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
name: Release
|
|
229
|
+
|
|
230
|
+
on:
|
|
231
|
+
issue_comment:
|
|
232
|
+
types: [ created, deleted ]
|
|
233
|
+
push:
|
|
234
|
+
branches:
|
|
235
|
+
- master
|
|
236
|
+
workflow_dispatch:
|
|
237
|
+
inputs:
|
|
238
|
+
snapshot:
|
|
239
|
+
description: Snapshot publish tag.
|
|
240
|
+
type: string
|
|
241
|
+
|
|
242
|
+
jobs:
|
|
243
|
+
check:
|
|
244
|
+
if: inputs.snapshot == ''
|
|
245
|
+
# 保留现有 check job 的其余配置
|
|
246
|
+
|
|
247
|
+
pull-request:
|
|
248
|
+
needs: check
|
|
249
|
+
if: needs.check.outputs.workflow == 'pull-request'
|
|
250
|
+
# 保留现有 pull-request job 的其余配置
|
|
251
|
+
|
|
252
|
+
release:
|
|
253
|
+
needs: check
|
|
254
|
+
if: needs.check.outputs.workflow == 'release'
|
|
255
|
+
# 保留现有 release job 的其余配置
|
|
256
|
+
|
|
257
|
+
snapshot:
|
|
258
|
+
if: inputs.snapshot != ''
|
|
259
|
+
name: Snapshot
|
|
260
|
+
runs-on: ubuntu-latest
|
|
261
|
+
permissions:
|
|
262
|
+
contents: read
|
|
263
|
+
id-token: write
|
|
264
|
+
steps:
|
|
265
|
+
- uses: actions/checkout@v7
|
|
266
|
+
with:
|
|
267
|
+
fetch-depth: 0
|
|
268
|
+
fetch-tags: true
|
|
269
|
+
|
|
270
|
+
- uses: pnpm/action-setup@v6
|
|
271
|
+
with:
|
|
272
|
+
version: 12
|
|
273
|
+
|
|
274
|
+
- uses: actions/setup-node@v7
|
|
275
|
+
with:
|
|
276
|
+
node-version: 24
|
|
277
|
+
registry-url: https://registry.npmjs.org/
|
|
278
|
+
package-manager-cache: false
|
|
279
|
+
|
|
280
|
+
- run: pnpm install --frozen-lockfile
|
|
281
|
+
|
|
282
|
+
- uses: peiyanlu/release@master
|
|
283
|
+
with:
|
|
284
|
+
workflow: snapshot
|
|
285
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
286
|
+
bump-snapshot: ${{ inputs.snapshot }}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
在 GitHub Actions 页面手动运行 workflow 并填写 snapshot 为 canary:此时 `check`、`pull-request` 和 `release` 不执行,快照 job 发布 `1.2.0-canary.<timestamp>`。
|
|
290
|
+
|
|
291
|
+
Trusted Publisher 注册绑定到具体 workflow 文件。要让快照发布也使用同一注册,建议将快照 job 放在已注册的 workflow 中,而不是另建 workflow。
|
|
292
|
+
|
|
293
|
+
### 参数
|
|
163
294
|
|
|
164
295
|
`workflow` 支持:
|
|
165
296
|
|
package/dist/action.mjs
CHANGED
|
@@ -2,19 +2,22 @@ import { getBooleanInput, getInput, setFailed, setOutput } from "@actions/core";
|
|
|
2
2
|
import { context, getOctokit } from "@actions/github";
|
|
3
3
|
import { assert, dedent, isBoolean, isEmpty, isIndex, isNotEmpty, isNull, isString, isUndefined, newline, sleep, throwError, tryCall } from "@peiyanlu/ts-utils";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { parse, resolve } from "node:path";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { getGithubUrl, gitAdd, gitBranch, gitBranchCurrent, gitCheckoutBranch, gitCheckoutBranchForce, gitCommitMessage,
|
|
8
|
-
import "
|
|
9
|
-
import { Octokit } from "@octokit/rest";
|
|
10
|
-
import open from "open";
|
|
11
|
-
import { glob } from "tinyglobby";
|
|
12
|
-
import "@peiyanlu/node-utils";
|
|
5
|
+
import path, { join, parse, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { getGithubUrl, gitAdd, gitBranch, gitBranchCurrent, gitCheckoutBranch, gitCheckoutBranchForce, gitCommitMessage, gitConfigSet, gitFetch, gitLog, gitLsRemote, gitPush, gitPushBranch, gitRevParse, gitTag } from "@peiyanlu/cli-utils";
|
|
8
|
+
import { readJsonFileSync } from "@peiyanlu/node-utils";
|
|
13
9
|
import "conventional-changelog";
|
|
14
10
|
import "conventional-changelog-conventionalcommits";
|
|
15
11
|
import "conventional-recommended-bump";
|
|
16
12
|
import "@conventional-changelog/template";
|
|
17
|
-
|
|
13
|
+
import "semver";
|
|
14
|
+
import { Octokit } from "@octokit/rest";
|
|
15
|
+
import open from "open";
|
|
16
|
+
import { glob } from "tinyglobby";
|
|
17
|
+
import "yaml";
|
|
18
|
+
import "@clack/prompts";
|
|
19
|
+
import "ansis";
|
|
20
|
+
// src/config/index.ts
|
|
18
21
|
const loadJavaScriptFile = async (file) => {
|
|
19
22
|
const { href } = pathToFileURL(file);
|
|
20
23
|
const mod = await import(href);
|
|
@@ -43,132 +46,12 @@ const loadConfig = async (cwd = process.cwd()) => {
|
|
|
43
46
|
config: await loadJavaScriptFile(file)
|
|
44
47
|
};
|
|
45
48
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
];
|
|
53
|
-
const JSON_START = "```json";
|
|
54
|
-
const JSON_START_OFFSET = 7;
|
|
55
|
-
const JSON_END = "```";
|
|
56
|
-
/** 所有支持的拉取请求评论命令 */
|
|
57
|
-
const COMMANDS = ["!release/set-options", "!release/set-preamble"];
|
|
58
|
-
/**
|
|
59
|
-
* 判断正文是否以命令开头,并且命令后紧跟空白字符或正文结束。
|
|
60
|
-
* 这样可以避免将更长的标记(例如 `!release/set-options-foo`)误判为匹配的命令
|
|
61
|
-
*/
|
|
62
|
-
const startsWithCommand = (body, command) => {
|
|
63
|
-
if (!body.startsWith(command)) return false;
|
|
64
|
-
const nextChar = body[command.length];
|
|
65
|
-
return isUndefined(nextChar) || /\s/.test(nextChar);
|
|
66
|
-
};
|
|
67
|
-
/** 评论内容是否是支持的命令评论之一 */
|
|
68
|
-
const isCommandComment = (body) => isString(body) && COMMANDS.some((command) => startsWithCommand(body, command));
|
|
69
|
-
/** 解析 releaser 参数 */
|
|
70
|
-
const parseSetOptionsComment = (comment) => {
|
|
71
|
-
if (ALLOWED_AUTHOR_ASSOCIATIONS.includes(comment.author_association)) {
|
|
72
|
-
const { body } = comment;
|
|
73
|
-
if (body && startsWithCommand(body, "!release/set-options")) {
|
|
74
|
-
const start = body.indexOf(JSON_START);
|
|
75
|
-
const end = isIndex(start) ? body.indexOf(JSON_END, start + JSON_START_OFFSET) : -1;
|
|
76
|
-
if (!isIndex(start) || !isIndex(end)) return;
|
|
77
|
-
return body.substring(start + JSON_START_OFFSET, end).trim();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
/** 解析日志前言 */
|
|
82
|
-
function parseSetPreambleComment(comment) {
|
|
83
|
-
if (ALLOWED_AUTHOR_ASSOCIATIONS.includes(comment.author_association)) {
|
|
84
|
-
const { body } = comment;
|
|
85
|
-
if (body && startsWithCommand(body, "!release/set-preamble")) {
|
|
86
|
-
const newlineIndex = body.indexOf("\n");
|
|
87
|
-
const firstLine = isIndex(newlineIndex) ? body.slice(0, newlineIndex) : body;
|
|
88
|
-
const secondLine = isIndex(newlineIndex) ? body.slice(newlineIndex + 1).trim() : "";
|
|
89
|
-
const name = firstLine.slice(21).replace(/`/g, "").trim().split(/\s+/)[0];
|
|
90
|
-
const preamble = secondLine;
|
|
91
|
-
if (preamble) return {
|
|
92
|
-
name,
|
|
93
|
-
preamble
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
//#endregion
|
|
100
|
-
//#region src/releaser-github-action/conditions.ts
|
|
101
|
-
const ifReleaseCommit = async (releaser) => {
|
|
102
|
-
const { project, options: { dryRun } } = releaser;
|
|
103
|
-
const logger = releaser.logger.createChild("if-release-commit");
|
|
104
|
-
if (!(await gitLog(["-1", "--pretty=%B"]))?.startsWith("chore(release):")) {
|
|
105
|
-
logger.verbose("Skipping non-release commit.");
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
logger.verbose("Checking existing release tags...");
|
|
109
|
-
if (!dryRun) await gitFetch(["--tags"]);
|
|
110
|
-
const tags = await project.getTags();
|
|
111
|
-
const canRelease = tags.length > 0;
|
|
112
|
-
if (canRelease) {
|
|
113
|
-
logger.verbose(`Found release tags:`);
|
|
114
|
-
for (const tag of tags) logger.verbose(`- ${tag}`);
|
|
115
|
-
} else logger.verbose(`No release tags found.`);
|
|
116
|
-
return canRelease;
|
|
117
|
-
};
|
|
118
|
-
/**
|
|
119
|
-
* 检测当前事件是否为针对处于打开状态的发布拉取请求的受支持命令评论:
|
|
120
|
-
* (`!release/set-options` 或 `!release/set-preamble`)
|
|
121
|
-
*
|
|
122
|
-
* @returns 如果是此类评论,则返回 `目标分支`;如果是评论事件但不是命令评论,则返回 `false`;如果根本不是评论事件,则返回 `null`
|
|
123
|
-
*/
|
|
124
|
-
const ifCommandComment = () => {
|
|
125
|
-
const { eventName, payload: { issue, comment } } = context;
|
|
126
|
-
if (eventName !== "issue_comment") return null;
|
|
127
|
-
const isPullRequest = issue?.pull_request;
|
|
128
|
-
const issueAuthor = (issue?.user)?.login;
|
|
129
|
-
const issueBody = issue?.body;
|
|
130
|
-
const issueState = issue?.state;
|
|
131
|
-
const commentBody = comment?.body;
|
|
132
|
-
if (isPullRequest && issueAuthor === "github-actions[bot]" && issueState === "open" && issueBody?.includes("release-pull-request: true") && isCommandComment(commentBody)) {
|
|
133
|
-
const matches = issueBody.match(/release-branch-to:\s*(\S+)/);
|
|
134
|
-
return matches ? matches[1] : false;
|
|
135
|
-
}
|
|
136
|
-
return false;
|
|
137
|
-
};
|
|
138
|
-
//#endregion
|
|
139
|
-
//#region src/releaser-github-action/options.ts
|
|
140
|
-
const omitEmpty = (object) => {
|
|
141
|
-
const result = {};
|
|
142
|
-
for (const [key, value] of Object.entries(object)) if (isBoolean(value) || Boolean(value)) result[key] = value;
|
|
143
|
-
return result;
|
|
144
|
-
};
|
|
145
|
-
const getOptionalBooleanInput = (name) => getInput(name) ? getBooleanInput(name) : void 0;
|
|
146
|
-
const getOptionalJsonInput = (name) => {
|
|
147
|
-
const value = getInput(name);
|
|
148
|
-
return value ? JSON.parse(value) : void 0;
|
|
149
|
-
};
|
|
150
|
-
const getInputOptions = () => ({
|
|
151
|
-
checkout: omitEmpty({ branch: getInput("checkout-branch") }),
|
|
152
|
-
bump: omitEmpty({
|
|
153
|
-
version: getInput("bump-version"),
|
|
154
|
-
as: getInput("bump-as"),
|
|
155
|
-
prerelease: getInput("bump-prerelease"),
|
|
156
|
-
snapshot: getInput("bump-snapshot"),
|
|
157
|
-
firstRelease: getOptionalBooleanInput("bump-first-release"),
|
|
158
|
-
byProject: getOptionalJsonInput("bump-by-project")
|
|
159
|
-
}),
|
|
160
|
-
maintenanceBranch: omitEmpty({ enabled: getOptionalBooleanInput("maintenance-branch-enabled") }),
|
|
161
|
-
publish: omitEmpty({
|
|
162
|
-
access: getInput("publish-access"),
|
|
163
|
-
tag: getInput("publish-tag")
|
|
164
|
-
})
|
|
165
|
-
});
|
|
166
|
-
const getInputSkippedSteps = () => ({
|
|
167
|
-
bump: getOptionalBooleanInput("bump-skip"),
|
|
168
|
-
publish: getOptionalBooleanInput("publish-skip")
|
|
169
|
-
});
|
|
170
|
-
//#endregion
|
|
171
|
-
//#region src/core/logger.ts
|
|
49
|
+
// src/core/hosting/hosting.ts
|
|
50
|
+
/** Git 仓库托管服务的基类 */
|
|
51
|
+
var GitRepositoryHosting = class {};
|
|
52
|
+
// src/core/utils.ts
|
|
53
|
+
const when = (condition, ...value) => condition ? [...value] : [];
|
|
54
|
+
// src/core/logger.ts
|
|
172
55
|
const defaultPrinter = ({ dryRun, stage, message }) => {
|
|
173
56
|
console.log(`${dryRun ? `[dry-run]` : ""}[${stage}]:`, message);
|
|
174
57
|
};
|
|
@@ -248,11 +131,7 @@ var Logger = class {
|
|
|
248
131
|
return color;
|
|
249
132
|
}
|
|
250
133
|
};
|
|
251
|
-
|
|
252
|
-
//#region src/core/utils.ts
|
|
253
|
-
const when = (condition, ...value) => condition ? [...value] : [];
|
|
254
|
-
//#endregion
|
|
255
|
-
//#region src/core/releaser.ts
|
|
134
|
+
// src/core/releaser.ts
|
|
256
135
|
var Releaser = class {
|
|
257
136
|
options;
|
|
258
137
|
project;
|
|
@@ -330,7 +209,7 @@ var Releaser = class {
|
|
|
330
209
|
logger.info("Skipping checkout.");
|
|
331
210
|
return;
|
|
332
211
|
}
|
|
333
|
-
logger.info(`Checking out branch ${headBranch}...`);
|
|
212
|
+
logger.info(`Checking out branch "${headBranch}"...`);
|
|
334
213
|
this.state.headBranch = headBranch;
|
|
335
214
|
this.state.baseBranch = await this.getBaseBranch();
|
|
336
215
|
if (fetch) {
|
|
@@ -345,10 +224,10 @@ var Releaser = class {
|
|
|
345
224
|
]);
|
|
346
225
|
}
|
|
347
226
|
if (force) {
|
|
348
|
-
logger.verbose(`Force checking out branch ${headBranch}...`);
|
|
227
|
+
logger.verbose(`Force checking out branch "${headBranch}"...`);
|
|
349
228
|
if (!dryRun) await gitCheckoutBranchForce(headBranch);
|
|
350
229
|
} else {
|
|
351
|
-
logger.verbose(`Checking out branch ${headBranch}...`);
|
|
230
|
+
logger.verbose(`Checking out branch "${headBranch}"...`);
|
|
352
231
|
if (!dryRun) await gitCheckoutBranch(headBranch);
|
|
353
232
|
}
|
|
354
233
|
});
|
|
@@ -624,12 +503,7 @@ var Releaser = class {
|
|
|
624
503
|
return gitBranchCurrent();
|
|
625
504
|
}
|
|
626
505
|
};
|
|
627
|
-
|
|
628
|
-
//#region src/core/hosting/hosting.ts
|
|
629
|
-
/** Git 仓库托管服务的基类 */
|
|
630
|
-
var GitRepositoryHosting = class {};
|
|
631
|
-
//#endregion
|
|
632
|
-
//#region src/hosting-github/github.ts
|
|
506
|
+
// src/hosting-github/github.ts
|
|
633
507
|
var GithubHosting = class extends GitRepositoryHosting {
|
|
634
508
|
options;
|
|
635
509
|
octokit;
|
|
@@ -746,13 +620,6 @@ var GithubHosting = class extends GitRepositoryHosting {
|
|
|
746
620
|
});
|
|
747
621
|
}
|
|
748
622
|
}
|
|
749
|
-
async releaseCheck(options = {}) {
|
|
750
|
-
const { logger } = options;
|
|
751
|
-
logger?.verbose("Checking GitHub repository configuration...");
|
|
752
|
-
const remoteUrl = await gitConfigGet("remote.origin.url");
|
|
753
|
-
const useGithub = isNotEmpty(parseGitHubRepo(remoteUrl));
|
|
754
|
-
assert(useGithub, "Git remote \"origin\" is not a GitHub repository");
|
|
755
|
-
}
|
|
756
623
|
async getRepositoryId(project) {
|
|
757
624
|
let { options: { owner, repo } } = this;
|
|
758
625
|
const i = project.getHostedGitInfo();
|
|
@@ -811,8 +678,133 @@ var GithubHosting = class extends GitRepositoryHosting {
|
|
|
811
678
|
};
|
|
812
679
|
}
|
|
813
680
|
};
|
|
814
|
-
|
|
815
|
-
|
|
681
|
+
// src/releaser-github-action/comment.ts
|
|
682
|
+
const ALLOWED_AUTHOR_ASSOCIATIONS = [
|
|
683
|
+
"OWNER",
|
|
684
|
+
"COLLABORATOR",
|
|
685
|
+
"MEMBER"
|
|
686
|
+
];
|
|
687
|
+
const JSON_START = "```json";
|
|
688
|
+
const JSON_START_OFFSET = 7;
|
|
689
|
+
const JSON_END = "```";
|
|
690
|
+
/** 所有支持的拉取请求评论命令 */
|
|
691
|
+
const COMMANDS = ["!release/set-options", "!release/set-preamble"];
|
|
692
|
+
/**
|
|
693
|
+
* 判断正文是否以命令开头,并且命令后紧跟空白字符或正文结束。
|
|
694
|
+
* 这样可以避免将更长的标记(例如 `!release/set-options-foo`)误判为匹配的命令
|
|
695
|
+
*/
|
|
696
|
+
const startsWithCommand = (body, command) => {
|
|
697
|
+
if (!body.startsWith(command)) return false;
|
|
698
|
+
const nextChar = body[command.length];
|
|
699
|
+
return isUndefined(nextChar) || /\s/.test(nextChar);
|
|
700
|
+
};
|
|
701
|
+
/** 评论内容是否是支持的命令评论之一 */
|
|
702
|
+
const isCommandComment = (body) => isString(body) && COMMANDS.some((command) => startsWithCommand(body, command));
|
|
703
|
+
/** 解析 releaser 参数 */
|
|
704
|
+
const parseSetOptionsComment = (comment) => {
|
|
705
|
+
if (ALLOWED_AUTHOR_ASSOCIATIONS.includes(comment.author_association)) {
|
|
706
|
+
const { body } = comment;
|
|
707
|
+
if (body && startsWithCommand(body, "!release/set-options")) {
|
|
708
|
+
const start = body.indexOf(JSON_START);
|
|
709
|
+
const end = isIndex(start) ? body.indexOf(JSON_END, start + JSON_START_OFFSET) : -1;
|
|
710
|
+
if (!isIndex(start) || !isIndex(end)) return;
|
|
711
|
+
return body.substring(start + JSON_START_OFFSET, end).trim();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
/** 解析日志前言 */
|
|
716
|
+
function parseSetPreambleComment(comment) {
|
|
717
|
+
if (ALLOWED_AUTHOR_ASSOCIATIONS.includes(comment.author_association)) {
|
|
718
|
+
const { body } = comment;
|
|
719
|
+
if (body && startsWithCommand(body, "!release/set-preamble")) {
|
|
720
|
+
const newlineIndex = body.indexOf("\n");
|
|
721
|
+
const firstLine = isIndex(newlineIndex) ? body.slice(0, newlineIndex) : body;
|
|
722
|
+
const secondLine = isIndex(newlineIndex) ? body.slice(newlineIndex + 1).trim() : "";
|
|
723
|
+
const name = firstLine.slice(21).replace(/`/g, "").trim().split(/\s+/)[0];
|
|
724
|
+
const preamble = secondLine;
|
|
725
|
+
if (preamble) return {
|
|
726
|
+
name,
|
|
727
|
+
preamble
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
// src/releaser-github-action/conditions.ts
|
|
734
|
+
const ifReleaseCommit = async (releaser) => {
|
|
735
|
+
const { project, options: { dryRun } } = releaser;
|
|
736
|
+
const logger = releaser.logger.createChild("if-release-commit");
|
|
737
|
+
if (!(await gitLog(["-1", "--pretty=%B"]))?.startsWith("chore(release):")) {
|
|
738
|
+
logger.verbose("Current commit is not a release commit.");
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
const authorName = await gitLog(["-1", "--pretty=%an"]);
|
|
742
|
+
if (!authorName?.startsWith("github-actions[bot]")) {
|
|
743
|
+
logger.info(`Release commit author "${authorName ?? "unknown"}" is not "github-actions[bot]".`);
|
|
744
|
+
process.exit(0);
|
|
745
|
+
}
|
|
746
|
+
logger.verbose("Checking for existing release tags...");
|
|
747
|
+
if (!dryRun) await gitFetch(["--tags"]);
|
|
748
|
+
const tags = await project.getTags();
|
|
749
|
+
const canRelease = tags.length > 0;
|
|
750
|
+
if (canRelease) {
|
|
751
|
+
logger.verbose(`Found release tags:`);
|
|
752
|
+
for (const tag of tags) logger.verbose(`- ${tag}`);
|
|
753
|
+
} else logger.verbose(`No release tags found.`);
|
|
754
|
+
return canRelease;
|
|
755
|
+
};
|
|
756
|
+
/**
|
|
757
|
+
* 检测当前事件是否为针对处于打开状态的发布拉取请求的受支持命令评论:
|
|
758
|
+
* (`!release/set-options` 或 `!release/set-preamble`)
|
|
759
|
+
*
|
|
760
|
+
* @returns 如果是此类评论,则返回 `目标分支`;如果是评论事件但不是命令评论,则返回 `false`;如果根本不是评论事件,则返回 `null`
|
|
761
|
+
*/
|
|
762
|
+
const ifCommandComment = () => {
|
|
763
|
+
const { eventName, payload: { issue, comment } } = context;
|
|
764
|
+
if (eventName !== "issue_comment") return null;
|
|
765
|
+
const isPullRequest = issue?.pull_request;
|
|
766
|
+
const issueAuthor = (issue?.user)?.login;
|
|
767
|
+
const issueBody = issue?.body;
|
|
768
|
+
const issueState = issue?.state;
|
|
769
|
+
const commentBody = comment?.body;
|
|
770
|
+
if (isPullRequest && issueAuthor === "github-actions[bot]" && issueState === "open" && issueBody?.includes("release-pull-request: true") && isCommandComment(commentBody)) {
|
|
771
|
+
const matches = issueBody.match(/release-branch-to:\s*(\S+)/);
|
|
772
|
+
return matches ? matches[1] : false;
|
|
773
|
+
}
|
|
774
|
+
return false;
|
|
775
|
+
};
|
|
776
|
+
// src/releaser-github-action/options.ts
|
|
777
|
+
const omitEmpty = (object) => {
|
|
778
|
+
const result = {};
|
|
779
|
+
for (const [key, value] of Object.entries(object)) if (isBoolean(value) || Boolean(value)) result[key] = value;
|
|
780
|
+
return result;
|
|
781
|
+
};
|
|
782
|
+
const getOptionalBooleanInput = (name) => getInput(name) ? getBooleanInput(name) : void 0;
|
|
783
|
+
const getOptionalJsonInput = (name) => {
|
|
784
|
+
const value = getInput(name);
|
|
785
|
+
return value ? JSON.parse(value) : void 0;
|
|
786
|
+
};
|
|
787
|
+
const getInputOptions = () => ({
|
|
788
|
+
checkout: omitEmpty({ branch: getInput("checkout-branch") }),
|
|
789
|
+
bump: omitEmpty({
|
|
790
|
+
version: getInput("bump-version"),
|
|
791
|
+
as: getInput("bump-as"),
|
|
792
|
+
prerelease: getInput("bump-prerelease"),
|
|
793
|
+
snapshot: getInput("bump-snapshot"),
|
|
794
|
+
firstRelease: getOptionalBooleanInput("bump-first-release"),
|
|
795
|
+
byProject: getOptionalJsonInput("bump-by-project")
|
|
796
|
+
}),
|
|
797
|
+
maintenanceBranch: omitEmpty({ enabled: getOptionalBooleanInput("maintenance-branch-enabled") }),
|
|
798
|
+
publish: omitEmpty({
|
|
799
|
+
access: getInput("publish-access"),
|
|
800
|
+
tag: getInput("publish-tag")
|
|
801
|
+
})
|
|
802
|
+
});
|
|
803
|
+
const getInputSkippedSteps = () => ({
|
|
804
|
+
bump: getOptionalBooleanInput("bump-skip"),
|
|
805
|
+
publish: getOptionalBooleanInput("publish-skip")
|
|
806
|
+
});
|
|
807
|
+
// src/releaser-github-action/releaser.ts
|
|
816
808
|
var ReleaserGithubAction = class extends Releaser {
|
|
817
809
|
setUser(options = {}) {
|
|
818
810
|
return super.setUser({
|
|
@@ -873,7 +865,7 @@ var ReleaserGithubAction = class extends Releaser {
|
|
|
873
865
|
let optionsApplied = false;
|
|
874
866
|
let preamble;
|
|
875
867
|
const preambleByName = /* @__PURE__ */ new Map();
|
|
876
|
-
for (let i = comments.length, comment; i >= 0; i--) {
|
|
868
|
+
for (let i = comments.length - 1, comment; i >= 0; i--) {
|
|
877
869
|
comment = comments[i];
|
|
878
870
|
if (!optionsApplied) {
|
|
879
871
|
const json = parseSetOptionsComment(comment);
|
|
@@ -969,24 +961,28 @@ var ReleaserGithubAction = class extends Releaser {
|
|
|
969
961
|
}
|
|
970
962
|
}
|
|
971
963
|
};
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
964
|
+
// node_modules/.pnpm/tsdown@0.23.4_publint@0.3.25_typescript@6.0.3/node_modules/tsdown/esm-shims.js
|
|
965
|
+
const getFilename = () => fileURLToPath(import.meta.url);
|
|
966
|
+
const getDirname = () => path.dirname(getFilename());
|
|
967
|
+
readJsonFileSync(join(/* @__PURE__ */ getDirname(), "..", "package.json"));
|
|
968
|
+
// src/action.ts
|
|
975
969
|
const workflow = getInput("workflow");
|
|
970
|
+
console.log(`Triggered by: ${context.eventName}`);
|
|
971
|
+
console.log(`Current workflow: ${workflow}`);
|
|
976
972
|
if (workflow === "check" && ifCommandComment() === false) {
|
|
977
973
|
setOutput("continue", "false");
|
|
978
974
|
setOutput("workflow", "none");
|
|
979
975
|
process.exit(0);
|
|
980
976
|
}
|
|
981
|
-
const GITHUB_TOKEN = getInput("github-token");
|
|
977
|
+
const GITHUB_TOKEN = getInput("github-token", { required: true });
|
|
982
978
|
const NODE_AUTH_TOKEN = getInput("npm-token");
|
|
983
979
|
const inputOptions = getInputOptions();
|
|
984
980
|
const skippedSteps = getInputSkippedSteps();
|
|
985
981
|
if (NODE_AUTH_TOKEN) process.env.NODE_AUTH_TOKEN = NODE_AUTH_TOKEN;
|
|
986
982
|
try {
|
|
987
983
|
const { config: { project, hosting, releaser, ...options }, file } = await loadConfig(process.cwd());
|
|
984
|
+
assert(project, "Project is required.");
|
|
988
985
|
console.log(`Legacy config file: ${file}`);
|
|
989
|
-
if (!project) throwError("Project is required.");
|
|
990
986
|
const gha = new ReleaserGithubAction({
|
|
991
987
|
project,
|
|
992
988
|
octokit: getOctokit(GITHUB_TOKEN),
|
|
@@ -1013,6 +1009,7 @@ try {
|
|
|
1013
1009
|
switch (workflow) {
|
|
1014
1010
|
case "check":
|
|
1015
1011
|
const workflow = await ifReleaseCommit(gha) ? "release" : "pull-request";
|
|
1012
|
+
console.log(`Next workflow: ${workflow}`);
|
|
1016
1013
|
setOutput("continue", "true");
|
|
1017
1014
|
setOutput("workflow", workflow);
|
|
1018
1015
|
break;
|
|
@@ -1032,5 +1029,4 @@ try {
|
|
|
1032
1029
|
else setFailed("An unknown error occurred");
|
|
1033
1030
|
throw error;
|
|
1034
1031
|
}
|
|
1035
|
-
//#endregion
|
|
1036
1032
|
export {};
|