@coderpp/yunxiao-cli 0.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 +115 -0
- package/dist/src/cli.d.ts +7 -0
- package/dist/src/cli.js +204 -0
- package/dist/src/client.d.ts +60 -0
- package/dist/src/client.js +118 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +4 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @coderpp/yunxiao-cli
|
|
2
|
+
|
|
3
|
+
用于评审和合并云效 Codeup 合并请求的命令行工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run build
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
本地运行:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node dist/src/index.js --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
如果后续发布为 npm 包,安装后可直接使用 `yunxiao` 命令。
|
|
19
|
+
|
|
20
|
+
## 通过 npx 使用
|
|
21
|
+
|
|
22
|
+
发布到 npm 后,可以直接用 `npx` 执行:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @coderpp/yunxiao-cli mr list --state opened
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
也可以指定版本:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @coderpp/yunxiao-cli@0.1.0 mr list --json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
包内的可执行命令名仍然是 `yunxiao`,全局安装后可这样使用:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g @coderpp/yunxiao-cli
|
|
38
|
+
yunxiao mr list --state opened
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 配置
|
|
42
|
+
|
|
43
|
+
不要把个人访问令牌写入代码或提交到仓库。建议通过环境变量提供:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export YUNXIAO_TOKEN="pt-..."
|
|
47
|
+
export YUNXIAO_DOMAIN="openapi-rdc.aliyuncs.com"
|
|
48
|
+
export YUNXIAO_ORGANIZATION_ID="your-organization-id"
|
|
49
|
+
export YUNXIAO_EDITION="center"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
说明:
|
|
53
|
+
|
|
54
|
+
- 中心版默认使用 `openapi-rdc.aliyuncs.com`,并需要 `YUNXIAO_ORGANIZATION_ID`。
|
|
55
|
+
- Region 版设置 `YUNXIAO_EDITION=region`,通常不需要组织 ID。
|
|
56
|
+
- 也可以用命令参数覆盖配置:`--token`、`--domain`、`--organization-id`、`--edition`。
|
|
57
|
+
|
|
58
|
+
## 命令
|
|
59
|
+
|
|
60
|
+
查询合并请求列表:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
node dist/src/index.js mr list --state opened --project-ids 2813489 --per-page 20
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
输出 JSON:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node dist/src/index.js mr list --state opened --json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
评审通过指定合并请求:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
node dist/src/index.js mr review 2813489 12 --comment "LGTM"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
合并指定合并请求:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node dist/src/index.js mr merge 2813489 12 --yes
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
评审通过并合并:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
node dist/src/index.js mr approve-and-merge 2813489 12 --yes --merge-type squash --remove-source-branch
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
查询变更文件树:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
node dist/src/index.js mr tree 2813489 12 \
|
|
94
|
+
--from-patch-set-id target-version-id \
|
|
95
|
+
--to-patch-set-id source-version-id
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
查询版本列表:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
node dist/src/index.js mr patches 2813489 12
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 参数补充
|
|
105
|
+
|
|
106
|
+
`repositoryId` 可以是代码库 ID,也可以是仓库完整路径,工具会自动做 URL 编码。
|
|
107
|
+
|
|
108
|
+
会改变仓库状态的命令需要 `--yes`:
|
|
109
|
+
|
|
110
|
+
- `mr merge`
|
|
111
|
+
- `mr approve-and-merge`
|
|
112
|
+
|
|
113
|
+
这是为了避免误合并。
|
|
114
|
+
|
|
115
|
+
未指定 `--merge-type` 时,默认使用 `no-fast-forward`。
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Fetcher } from "./client.js";
|
|
2
|
+
export interface CliIo {
|
|
3
|
+
fetcher?: Fetcher;
|
|
4
|
+
stdout?: (text: string) => void;
|
|
5
|
+
stderr?: (text: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function runCli(args?: string[], env?: NodeJS.ProcessEnv, io?: CliIo): Promise<number>;
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { YunxiaoClient } from "./client.js";
|
|
2
|
+
const mergeTypes = new Set(["ff-only", "no-fast-forward", "squash", "rebase"]);
|
|
3
|
+
export async function runCli(args = process.argv.slice(2), env = process.env, io = {}) {
|
|
4
|
+
const stdout = io.stdout ?? ((text) => process.stdout.write(text));
|
|
5
|
+
const stderr = io.stderr ?? ((text) => process.stderr.write(text));
|
|
6
|
+
try {
|
|
7
|
+
const parsed = parseArgs(args);
|
|
8
|
+
if (parsed.positionals.length === 0 || parsed.flags.help) {
|
|
9
|
+
stdout(helpText());
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
const [scope, command, repositoryId, localIdRaw] = parsed.positionals;
|
|
13
|
+
if (scope !== "mr" || !command) {
|
|
14
|
+
throw new Error(`Unknown command: ${parsed.positionals.join(" ")}`);
|
|
15
|
+
}
|
|
16
|
+
const client = createClient(parsed.flags, env, io.fetcher);
|
|
17
|
+
let result;
|
|
18
|
+
if (command === "list") {
|
|
19
|
+
result = await client.listChangeRequests(listOptions(parsed.flags));
|
|
20
|
+
printResult(result, Boolean(parsed.flags.json), stdout);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (!repositoryId || !localIdRaw) {
|
|
24
|
+
throw new Error(`Command mr ${command} requires <repositoryId> and <localId>.`);
|
|
25
|
+
}
|
|
26
|
+
const localId = parseLocalId(localIdRaw);
|
|
27
|
+
switch (command) {
|
|
28
|
+
case "review":
|
|
29
|
+
result = await client.reviewChangeRequest(repositoryId, localId, {
|
|
30
|
+
reviewComment: optionalString(parsed.flags.comment)
|
|
31
|
+
});
|
|
32
|
+
break;
|
|
33
|
+
case "merge":
|
|
34
|
+
requireConfirmation(parsed.flags);
|
|
35
|
+
result = await client.mergeChangeRequest(repositoryId, localId, mergeOptions(parsed.flags));
|
|
36
|
+
break;
|
|
37
|
+
case "approve-and-merge":
|
|
38
|
+
requireConfirmation(parsed.flags);
|
|
39
|
+
result = await client.approveAndMerge(repositoryId, localId, {
|
|
40
|
+
reviewComment: optionalString(parsed.flags.comment),
|
|
41
|
+
...mergeOptions(parsed.flags)
|
|
42
|
+
});
|
|
43
|
+
break;
|
|
44
|
+
case "tree":
|
|
45
|
+
result = await client.getChangeRequestTree(repositoryId, localId, {
|
|
46
|
+
fromPatchSetId: requiredFlag(parsed.flags, "from-patch-set-id"),
|
|
47
|
+
toPatchSetId: requiredFlag(parsed.flags, "to-patch-set-id")
|
|
48
|
+
});
|
|
49
|
+
break;
|
|
50
|
+
case "patches":
|
|
51
|
+
result = await client.listChangeRequestPatchSets(repositoryId, localId);
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unknown mr command: ${command}`);
|
|
55
|
+
}
|
|
56
|
+
printResult(result, Boolean(parsed.flags.json), stdout);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function createClient(flags, env, fetcher) {
|
|
65
|
+
return new YunxiaoClient({
|
|
66
|
+
token: stringFlag(flags.token) ?? env.YUNXIAO_TOKEN ?? "",
|
|
67
|
+
domain: stringFlag(flags.domain) ?? env.YUNXIAO_DOMAIN ?? "openapi-rdc.aliyuncs.com",
|
|
68
|
+
organizationId: stringFlag(flags["organization-id"]) ?? env.YUNXIAO_ORGANIZATION_ID,
|
|
69
|
+
edition: (stringFlag(flags.edition) ?? env.YUNXIAO_EDITION),
|
|
70
|
+
fetcher
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function parseArgs(args) {
|
|
74
|
+
const positionals = [];
|
|
75
|
+
const flags = {};
|
|
76
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
77
|
+
const arg = args[index];
|
|
78
|
+
if (!arg.startsWith("--")) {
|
|
79
|
+
positionals.push(arg);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const [rawName, inlineValue] = arg.slice(2).split("=", 2);
|
|
83
|
+
if (inlineValue !== undefined) {
|
|
84
|
+
flags[rawName] = inlineValue;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const next = args[index + 1];
|
|
88
|
+
if (next && !next.startsWith("--") && !["json", "yes", "remove-source-branch", "help"].includes(rawName)) {
|
|
89
|
+
flags[rawName] = next;
|
|
90
|
+
index += 1;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
flags[rawName] = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { positionals, flags };
|
|
97
|
+
}
|
|
98
|
+
function listOptions(flags) {
|
|
99
|
+
return {
|
|
100
|
+
page: optionalNumber(flags.page),
|
|
101
|
+
perPage: optionalNumber(flags["per-page"]),
|
|
102
|
+
projectIds: optionalString(flags["project-ids"]),
|
|
103
|
+
authorIds: optionalString(flags["author-ids"]),
|
|
104
|
+
reviewerIds: optionalString(flags["reviewer-ids"]),
|
|
105
|
+
state: optionalString(flags.state),
|
|
106
|
+
search: optionalString(flags.search),
|
|
107
|
+
orderBy: optionalString(flags["order-by"]),
|
|
108
|
+
sort: optionalString(flags.sort),
|
|
109
|
+
createdBefore: optionalString(flags["created-before"]),
|
|
110
|
+
createdAfter: optionalString(flags["created-after"])
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function mergeOptions(flags) {
|
|
114
|
+
const mergeType = optionalString(flags["merge-type"]) ?? "no-fast-forward";
|
|
115
|
+
if (!mergeTypes.has(mergeType)) {
|
|
116
|
+
throw new Error(`Invalid --merge-type: ${mergeType}`);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
mergeMessage: optionalString(flags.message),
|
|
120
|
+
mergeType: mergeType,
|
|
121
|
+
removeSourceBranch: Boolean(flags["remove-source-branch"])
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function requireConfirmation(flags) {
|
|
125
|
+
if (!flags.yes) {
|
|
126
|
+
throw new Error("This command changes repository state. Re-run with --yes after you have reviewed the merge request.");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function printResult(result, asJson, stdout) {
|
|
130
|
+
if (asJson) {
|
|
131
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(result)) {
|
|
135
|
+
stdout(formatTable(result));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
139
|
+
}
|
|
140
|
+
function formatTable(rows) {
|
|
141
|
+
if (rows.length === 0) {
|
|
142
|
+
return "No records found.\n";
|
|
143
|
+
}
|
|
144
|
+
const normalizedRows = rows.map((row) => (isRecord(row) ? row : { value: row }));
|
|
145
|
+
const keys = ["localId", "projectId", "title", "state", "sourceBranch", "targetBranch", "detailUrl"].filter((key) => normalizedRows.some((row) => row[key] !== undefined));
|
|
146
|
+
const widths = keys.map((key) => Math.max(key.length, ...normalizedRows.map((row) => String(row[key] ?? "").length)));
|
|
147
|
+
const line = (values) => values.map((value, index) => value.padEnd(widths[index])).join(" ");
|
|
148
|
+
return [
|
|
149
|
+
line(keys),
|
|
150
|
+
line(keys.map((_, index) => "-".repeat(widths[index]))),
|
|
151
|
+
...normalizedRows.map((row) => line(keys.map((key) => String(row[key] ?? ""))))
|
|
152
|
+
].join("\n") + "\n";
|
|
153
|
+
}
|
|
154
|
+
function parseLocalId(value) {
|
|
155
|
+
const localId = Number(value);
|
|
156
|
+
if (!Number.isInteger(localId) || localId <= 0) {
|
|
157
|
+
throw new Error(`<localId> must be a positive integer: ${value}`);
|
|
158
|
+
}
|
|
159
|
+
return localId;
|
|
160
|
+
}
|
|
161
|
+
function requiredFlag(flags, name) {
|
|
162
|
+
const value = optionalString(flags[name]);
|
|
163
|
+
if (!value) {
|
|
164
|
+
throw new Error(`Missing required flag --${name}.`);
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
function optionalString(value) {
|
|
169
|
+
return typeof value === "string" ? value : undefined;
|
|
170
|
+
}
|
|
171
|
+
function stringFlag(value) {
|
|
172
|
+
return optionalString(value);
|
|
173
|
+
}
|
|
174
|
+
function optionalNumber(value) {
|
|
175
|
+
if (typeof value !== "string") {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const number = Number(value);
|
|
179
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
180
|
+
throw new Error(`Expected a positive integer, got: ${value}`);
|
|
181
|
+
}
|
|
182
|
+
return number;
|
|
183
|
+
}
|
|
184
|
+
function isRecord(value) {
|
|
185
|
+
return typeof value === "object" && value !== null;
|
|
186
|
+
}
|
|
187
|
+
function helpText() {
|
|
188
|
+
return `yunxiao - Yunxiao Codeup merge request helper
|
|
189
|
+
|
|
190
|
+
Usage:
|
|
191
|
+
yunxiao mr list [--state opened] [--project-ids 2813489] [--json]
|
|
192
|
+
yunxiao mr review <repositoryId> <localId> [--comment "LGTM"]
|
|
193
|
+
yunxiao mr merge <repositoryId> <localId> --yes [--merge-type no-fast-forward]
|
|
194
|
+
yunxiao mr approve-and-merge <repositoryId> <localId> --yes [--merge-type no-fast-forward]
|
|
195
|
+
yunxiao mr tree <repositoryId> <localId> --from-patch-set-id <id> --to-patch-set-id <id>
|
|
196
|
+
yunxiao mr patches <repositoryId> <localId>
|
|
197
|
+
|
|
198
|
+
Config:
|
|
199
|
+
YUNXIAO_TOKEN Personal access token
|
|
200
|
+
YUNXIAO_DOMAIN API domain, default openapi-rdc.aliyuncs.com
|
|
201
|
+
YUNXIAO_ORGANIZATION_ID Required for center edition
|
|
202
|
+
YUNXIAO_EDITION center or region
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type YunxiaoEdition = "center" | "region";
|
|
2
|
+
export type Fetcher = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
export interface YunxiaoClientOptions {
|
|
4
|
+
token: string;
|
|
5
|
+
domain?: string;
|
|
6
|
+
organizationId?: string;
|
|
7
|
+
edition?: YunxiaoEdition;
|
|
8
|
+
fetcher?: Fetcher;
|
|
9
|
+
}
|
|
10
|
+
export interface ListChangeRequestsOptions {
|
|
11
|
+
page?: number;
|
|
12
|
+
perPage?: number;
|
|
13
|
+
projectIds?: string;
|
|
14
|
+
authorIds?: string;
|
|
15
|
+
reviewerIds?: string;
|
|
16
|
+
state?: "opened" | "merged" | "closed" | string;
|
|
17
|
+
search?: string;
|
|
18
|
+
orderBy?: "created_at" | "updated_at" | string;
|
|
19
|
+
sort?: "asc" | "desc" | string;
|
|
20
|
+
createdBefore?: string;
|
|
21
|
+
createdAfter?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ReviewChangeRequestOptions {
|
|
24
|
+
reviewComment?: string;
|
|
25
|
+
reviewOpinion?: "PASS" | "NOT_PASS";
|
|
26
|
+
submitDraftCommentIds?: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface MergeChangeRequestOptions {
|
|
29
|
+
mergeMessage?: string;
|
|
30
|
+
mergeType?: "ff-only" | "no-fast-forward" | "squash" | "rebase";
|
|
31
|
+
removeSourceBranch?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface ChangeRequestTreeOptions {
|
|
34
|
+
fromPatchSetId: string;
|
|
35
|
+
toPatchSetId: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ApproveAndMergeOptions extends ReviewChangeRequestOptions, MergeChangeRequestOptions {
|
|
38
|
+
}
|
|
39
|
+
export declare class YunxiaoApiError extends Error {
|
|
40
|
+
readonly status: number;
|
|
41
|
+
readonly responseBody: string;
|
|
42
|
+
constructor(message: string, status: number, responseBody: string);
|
|
43
|
+
}
|
|
44
|
+
export declare class YunxiaoClient {
|
|
45
|
+
private readonly token;
|
|
46
|
+
private readonly domain;
|
|
47
|
+
private readonly organizationId?;
|
|
48
|
+
private readonly edition;
|
|
49
|
+
private readonly fetcher;
|
|
50
|
+
constructor(options: YunxiaoClientOptions);
|
|
51
|
+
listChangeRequests(options?: ListChangeRequestsOptions): Promise<unknown>;
|
|
52
|
+
reviewChangeRequest(repositoryId: string, localId: number, options?: ReviewChangeRequestOptions): Promise<unknown>;
|
|
53
|
+
mergeChangeRequest(repositoryId: string, localId: number, options?: MergeChangeRequestOptions): Promise<unknown>;
|
|
54
|
+
approveAndMerge(repositoryId: string, localId: number, options?: ApproveAndMergeOptions): Promise<unknown>;
|
|
55
|
+
getChangeRequestTree(repositoryId: string, localId: number, options: ChangeRequestTreeOptions): Promise<unknown>;
|
|
56
|
+
listChangeRequestPatchSets(repositoryId: string, localId: number): Promise<unknown>;
|
|
57
|
+
private changeRequestsPath;
|
|
58
|
+
private repositoryChangeRequestPath;
|
|
59
|
+
private request;
|
|
60
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export class YunxiaoApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
responseBody;
|
|
4
|
+
constructor(message, status, responseBody) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.responseBody = responseBody;
|
|
8
|
+
this.name = "YunxiaoApiError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class YunxiaoClient {
|
|
12
|
+
token;
|
|
13
|
+
domain;
|
|
14
|
+
organizationId;
|
|
15
|
+
edition;
|
|
16
|
+
fetcher;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
if (!options.token) {
|
|
19
|
+
throw new Error("Yunxiao token is required. Set YUNXIAO_TOKEN or pass --token.");
|
|
20
|
+
}
|
|
21
|
+
this.token = options.token;
|
|
22
|
+
this.domain = stripProtocol(options.domain ?? "openapi-rdc.aliyuncs.com");
|
|
23
|
+
this.organizationId = options.organizationId;
|
|
24
|
+
this.edition = options.edition ?? (options.organizationId ? "center" : "region");
|
|
25
|
+
this.fetcher = options.fetcher ?? fetch;
|
|
26
|
+
if (this.edition === "center" && !this.organizationId) {
|
|
27
|
+
throw new Error("Center edition requires organizationId. Set YUNXIAO_ORGANIZATION_ID or pass --organization-id.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
listChangeRequests(options = {}) {
|
|
31
|
+
return this.request("GET", this.changeRequestsPath(), options);
|
|
32
|
+
}
|
|
33
|
+
reviewChangeRequest(repositoryId, localId, options = {}) {
|
|
34
|
+
return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/review`, undefined, {
|
|
35
|
+
...compactBody({
|
|
36
|
+
reviewComment: options.reviewComment,
|
|
37
|
+
submitDraftCommentIds: options.submitDraftCommentIds
|
|
38
|
+
}),
|
|
39
|
+
reviewOpinion: options.reviewOpinion ?? "PASS"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
mergeChangeRequest(repositoryId, localId, options = {}) {
|
|
43
|
+
return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/merge`, undefined, {
|
|
44
|
+
...compactBody({
|
|
45
|
+
mergeMessage: options.mergeMessage,
|
|
46
|
+
removeSourceBranch: options.removeSourceBranch
|
|
47
|
+
}),
|
|
48
|
+
mergeType: options.mergeType ?? "no-fast-forward"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async approveAndMerge(repositoryId, localId, options = {}) {
|
|
52
|
+
await this.reviewChangeRequest(repositoryId, localId, {
|
|
53
|
+
reviewComment: options.reviewComment,
|
|
54
|
+
reviewOpinion: options.reviewOpinion ?? "PASS",
|
|
55
|
+
submitDraftCommentIds: options.submitDraftCommentIds
|
|
56
|
+
});
|
|
57
|
+
return this.mergeChangeRequest(repositoryId, localId, {
|
|
58
|
+
mergeMessage: options.mergeMessage,
|
|
59
|
+
mergeType: options.mergeType,
|
|
60
|
+
removeSourceBranch: options.removeSourceBranch
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
getChangeRequestTree(repositoryId, localId, options) {
|
|
64
|
+
return this.request("GET", `${this.repositoryChangeRequestPath(repositoryId, localId)}/diffs/changeTree`, options);
|
|
65
|
+
}
|
|
66
|
+
listChangeRequestPatchSets(repositoryId, localId) {
|
|
67
|
+
return this.request("GET", `${this.repositoryChangeRequestPath(repositoryId, localId)}/diffs/patches`);
|
|
68
|
+
}
|
|
69
|
+
changeRequestsPath() {
|
|
70
|
+
if (this.edition === "center") {
|
|
71
|
+
return `/oapi/v1/codeup/organizations/${encodePathSegment(this.organizationId ?? "")}/changeRequests`;
|
|
72
|
+
}
|
|
73
|
+
return "/oapi/v1/codeup/changeRequests";
|
|
74
|
+
}
|
|
75
|
+
repositoryChangeRequestPath(repositoryId, localId) {
|
|
76
|
+
const encodedRepositoryId = encodePathSegment(repositoryId);
|
|
77
|
+
if (this.edition === "center") {
|
|
78
|
+
return `/oapi/v1/codeup/organizations/${encodePathSegment(this.organizationId ?? "")}/repositories/${encodedRepositoryId}/changeRequests/${localId}`;
|
|
79
|
+
}
|
|
80
|
+
return `/oapi/v1/codeup/repositories/${encodedRepositoryId}/changeRequests/${localId}`;
|
|
81
|
+
}
|
|
82
|
+
async request(method, path, query, body) {
|
|
83
|
+
const url = new URL(`https://${this.domain}${path}`);
|
|
84
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
85
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
86
|
+
url.searchParams.set(key, String(value));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const init = {
|
|
90
|
+
method,
|
|
91
|
+
headers: {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
"x-yunxiao-token": this.token
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
if (body) {
|
|
97
|
+
init.body = JSON.stringify(compactBody(body));
|
|
98
|
+
}
|
|
99
|
+
const response = await this.fetcher(url.toString(), init);
|
|
100
|
+
const text = await response.text();
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new YunxiaoApiError(`Yunxiao API request failed with HTTP ${response.status}`, response.status, text);
|
|
103
|
+
}
|
|
104
|
+
if (!text) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return JSON.parse(text);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function stripProtocol(domain) {
|
|
111
|
+
return domain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
112
|
+
}
|
|
113
|
+
function encodePathSegment(value) {
|
|
114
|
+
return encodeURIComponent(value);
|
|
115
|
+
}
|
|
116
|
+
function compactBody(body) {
|
|
117
|
+
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
118
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coderpp/yunxiao-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A CLI for Yunxiao Codeup merge request review and merge workflows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"yunxiao": "dist/src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.json",
|
|
11
|
+
"prepack": "npm run build",
|
|
12
|
+
"test": "tsc -p tsconfig.json && node --test dist/test/*.test.js"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^20.12.12",
|
|
16
|
+
"typescript": "^5.4.5"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.17"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/src",
|
|
23
|
+
"README.md"
|
|
24
|
+
]
|
|
25
|
+
}
|