@coderpp/yunxiao-cli 0.1.5 → 0.1.6
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 +10 -0
- package/dist/src/cli.js +20 -0
- package/dist/src/client.d.ts +56 -1
- package/dist/src/client.js +104 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,6 +118,14 @@ node dist/src/index.js mr merge 2813489 12 --yes
|
|
|
118
118
|
node dist/src/index.js mr approve-and-merge 2813489 12 --yes --merge-type squash --remove-source-branch
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
创建发布合并请求、自动评审通过并合并:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
node dist/src/index.js mr release yunxiao-cli release/1.2.3 master --yes
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
这个命令会先按仓库名称查询代码库 ID,再用当前 token 对应的用户作为评审人创建合并请求,随后评审通过并合并。合并时固定保留源分支,不会删除源分支。
|
|
128
|
+
|
|
121
129
|
查询变更文件树:
|
|
122
130
|
|
|
123
131
|
```bash
|
|
@@ -140,6 +148,7 @@ node dist/src/index.js mr patches 2813489 12
|
|
|
140
148
|
node dist/src/index.js mr list --output table
|
|
141
149
|
node dist/src/index.js mr list --output json
|
|
142
150
|
node dist/src/index.js mr review 2813489 12 --output table
|
|
151
|
+
node dist/src/index.js mr release yunxiao-cli release/1.2.3 master --yes --output json
|
|
143
152
|
node dist/src/index.js mr patches 2813489 12 --output json
|
|
144
153
|
```
|
|
145
154
|
|
|
@@ -158,6 +167,7 @@ node dist/src/index.js mr patches 2813489 12 --output json
|
|
|
158
167
|
|
|
159
168
|
- `mr merge`
|
|
160
169
|
- `mr approve-and-merge`
|
|
170
|
+
- `mr release`
|
|
161
171
|
|
|
162
172
|
这是为了避免误合并。
|
|
163
173
|
|
package/dist/src/cli.js
CHANGED
|
@@ -25,6 +25,19 @@ export async function runCli(args = process.argv.slice(2), env = process.env, io
|
|
|
25
25
|
if (!repositoryId || !localIdRaw) {
|
|
26
26
|
throw new Error(`Command mr ${command} requires <repositoryId> and <localId>.`);
|
|
27
27
|
}
|
|
28
|
+
if (command === "release") {
|
|
29
|
+
requireConfirmation(parsed.flags);
|
|
30
|
+
result = await client.releaseByRepositoryName({
|
|
31
|
+
repositoryName: repositoryId,
|
|
32
|
+
sourceBranch: localIdRaw,
|
|
33
|
+
targetBranch: requiredPositional(parsed.positionals[4], "targetBranch"),
|
|
34
|
+
title: optionalString(parsed.flags.title),
|
|
35
|
+
description: optionalString(parsed.flags.description),
|
|
36
|
+
mergeType: mergeOptions(parsed.flags).mergeType
|
|
37
|
+
});
|
|
38
|
+
printResult(result, outputFormat, stdout);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
28
41
|
const localId = parseLocalId(localIdRaw);
|
|
29
42
|
switch (command) {
|
|
30
43
|
case "review":
|
|
@@ -199,6 +212,12 @@ function parseLocalId(value) {
|
|
|
199
212
|
}
|
|
200
213
|
return localId;
|
|
201
214
|
}
|
|
215
|
+
function requiredPositional(value, name) {
|
|
216
|
+
if (!value) {
|
|
217
|
+
throw new Error(`Missing required positional argument <${name}>.`);
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
202
221
|
function requiredFlag(flags, name) {
|
|
203
222
|
const value = optionalString(flags[name]);
|
|
204
223
|
if (!value) {
|
|
@@ -233,6 +252,7 @@ Usage:
|
|
|
233
252
|
yunxiao mr review <repositoryId> <localId> [--comment "LGTM"] [--output table|json]
|
|
234
253
|
yunxiao mr merge <repositoryId> <localId> --yes [--merge-type no-fast-forward] [--output table|json]
|
|
235
254
|
yunxiao mr approve-and-merge <repositoryId> <localId> --yes [--merge-type no-fast-forward] [--output table|json]
|
|
255
|
+
yunxiao mr release <repositoryName> <sourceBranch> <targetBranch> --yes [--title "Release"] [--output table|json]
|
|
236
256
|
yunxiao mr tree <repositoryId> <localId> --from-patch-set-id <id> --to-patch-set-id <id> [--output table|json]
|
|
237
257
|
yunxiao mr patches <repositoryId> <localId> [--output table|json]
|
|
238
258
|
|
package/dist/src/client.d.ts
CHANGED
|
@@ -34,12 +34,61 @@ export interface ChangeRequestTreeOptions {
|
|
|
34
34
|
fromPatchSetId: string;
|
|
35
35
|
toPatchSetId: string;
|
|
36
36
|
}
|
|
37
|
+
export interface ListRepositoriesOptions {
|
|
38
|
+
page?: number;
|
|
39
|
+
perPage?: number;
|
|
40
|
+
orderBy?: "created_at" | "updated_at" | "last_activity_at" | string;
|
|
41
|
+
sort?: "asc" | "desc" | string;
|
|
42
|
+
search?: string;
|
|
43
|
+
archived?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface RepositorySummary {
|
|
46
|
+
id: number | string;
|
|
47
|
+
name?: string;
|
|
48
|
+
path?: string;
|
|
49
|
+
nameWithNamespace?: string;
|
|
50
|
+
pathWithNamespace?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface CurrentUser {
|
|
53
|
+
id: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
username?: string;
|
|
56
|
+
email?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface CreateChangeRequestOptions {
|
|
59
|
+
sourceBranch: string;
|
|
60
|
+
sourceProjectId: number | string;
|
|
61
|
+
targetBranch: string;
|
|
62
|
+
targetProjectId: number | string;
|
|
63
|
+
title: string;
|
|
64
|
+
createFrom?: "WEB" | "COMMAND_LINE";
|
|
65
|
+
description?: string;
|
|
66
|
+
reviewerUserIds?: string[];
|
|
67
|
+
triggerAIReviewRun?: boolean;
|
|
68
|
+
workItemIds?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface ReleaseByRepositoryNameOptions {
|
|
71
|
+
repositoryName: string;
|
|
72
|
+
sourceBranch: string;
|
|
73
|
+
targetBranch: string;
|
|
74
|
+
title?: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
mergeType?: MergeChangeRequestOptions["mergeType"];
|
|
77
|
+
}
|
|
78
|
+
export interface ReleaseResult {
|
|
79
|
+
repository: RepositorySummary;
|
|
80
|
+
changeRequest: unknown;
|
|
81
|
+
review: unknown;
|
|
82
|
+
merge: unknown;
|
|
83
|
+
}
|
|
37
84
|
export interface ApproveAndMergeOptions extends ReviewChangeRequestOptions, MergeChangeRequestOptions {
|
|
38
85
|
}
|
|
39
86
|
export declare class YunxiaoApiError extends Error {
|
|
40
87
|
readonly status: number;
|
|
41
88
|
readonly responseBody: string;
|
|
42
|
-
|
|
89
|
+
readonly method: string;
|
|
90
|
+
readonly url: string;
|
|
91
|
+
constructor(message: string, status: number, responseBody: string, method: string, url: string);
|
|
43
92
|
}
|
|
44
93
|
export declare class YunxiaoClient {
|
|
45
94
|
private readonly token;
|
|
@@ -49,12 +98,18 @@ export declare class YunxiaoClient {
|
|
|
49
98
|
private readonly fetcher;
|
|
50
99
|
constructor(options: YunxiaoClientOptions);
|
|
51
100
|
listChangeRequests(options?: ListChangeRequestsOptions): Promise<unknown>;
|
|
101
|
+
getCurrentUser(): Promise<CurrentUser>;
|
|
102
|
+
listRepositories(options?: ListRepositoriesOptions): Promise<RepositorySummary[]>;
|
|
103
|
+
createChangeRequest(repositoryId: string, options: CreateChangeRequestOptions): Promise<unknown>;
|
|
52
104
|
reviewChangeRequest(repositoryId: string, localId: number, options?: ReviewChangeRequestOptions): Promise<unknown>;
|
|
53
105
|
mergeChangeRequest(repositoryId: string, localId: number, options?: MergeChangeRequestOptions): Promise<unknown>;
|
|
54
106
|
approveAndMerge(repositoryId: string, localId: number, options?: ApproveAndMergeOptions): Promise<unknown>;
|
|
55
107
|
getChangeRequestTree(repositoryId: string, localId: number, options: ChangeRequestTreeOptions): Promise<unknown>;
|
|
56
108
|
listChangeRequestPatchSets(repositoryId: string, localId: number): Promise<unknown>;
|
|
109
|
+
releaseByRepositoryName(options: ReleaseByRepositoryNameOptions): Promise<ReleaseResult>;
|
|
57
110
|
private changeRequestsPath;
|
|
111
|
+
private repositoriesPath;
|
|
112
|
+
private repositoryPath;
|
|
58
113
|
private repositoryChangeRequestPath;
|
|
59
114
|
private request;
|
|
60
115
|
}
|
package/dist/src/client.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export class YunxiaoApiError extends Error {
|
|
2
2
|
status;
|
|
3
3
|
responseBody;
|
|
4
|
-
|
|
4
|
+
method;
|
|
5
|
+
url;
|
|
6
|
+
constructor(message, status, responseBody, method, url) {
|
|
5
7
|
super(message);
|
|
6
8
|
this.status = status;
|
|
7
9
|
this.responseBody = responseBody;
|
|
10
|
+
this.method = method;
|
|
11
|
+
this.url = url;
|
|
8
12
|
this.name = "YunxiaoApiError";
|
|
9
13
|
}
|
|
10
14
|
}
|
|
@@ -30,6 +34,26 @@ export class YunxiaoClient {
|
|
|
30
34
|
listChangeRequests(options = {}) {
|
|
31
35
|
return this.request("GET", this.changeRequestsPath(), options);
|
|
32
36
|
}
|
|
37
|
+
getCurrentUser() {
|
|
38
|
+
return this.request("GET", "/oapi/v1/platform/user");
|
|
39
|
+
}
|
|
40
|
+
listRepositories(options = {}) {
|
|
41
|
+
return this.request("GET", this.repositoriesPath(), options);
|
|
42
|
+
}
|
|
43
|
+
createChangeRequest(repositoryId, options) {
|
|
44
|
+
return this.request("POST", `${this.repositoryPath(repositoryId)}/changeRequests`, undefined, {
|
|
45
|
+
createFrom: options.createFrom ?? "COMMAND_LINE",
|
|
46
|
+
description: options.description,
|
|
47
|
+
reviewerUserIds: options.reviewerUserIds,
|
|
48
|
+
sourceBranch: options.sourceBranch,
|
|
49
|
+
sourceProjectId: normalizeProjectId(options.sourceProjectId),
|
|
50
|
+
targetBranch: options.targetBranch,
|
|
51
|
+
targetProjectId: normalizeProjectId(options.targetProjectId),
|
|
52
|
+
title: options.title,
|
|
53
|
+
triggerAIReviewRun: options.triggerAIReviewRun ?? false,
|
|
54
|
+
workItemIds: options.workItemIds
|
|
55
|
+
});
|
|
56
|
+
}
|
|
33
57
|
reviewChangeRequest(repositoryId, localId, options = {}) {
|
|
34
58
|
return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/review`, undefined, {
|
|
35
59
|
...compactBody({
|
|
@@ -66,18 +90,67 @@ export class YunxiaoClient {
|
|
|
66
90
|
listChangeRequestPatchSets(repositoryId, localId) {
|
|
67
91
|
return this.request("GET", `${this.repositoryChangeRequestPath(repositoryId, localId)}/diffs/patches`);
|
|
68
92
|
}
|
|
93
|
+
async releaseByRepositoryName(options) {
|
|
94
|
+
const [currentUser, repositories] = await Promise.all([
|
|
95
|
+
this.getCurrentUser(),
|
|
96
|
+
this.listRepositories({
|
|
97
|
+
page: 1,
|
|
98
|
+
perPage: 20,
|
|
99
|
+
search: options.repositoryName,
|
|
100
|
+
archived: false
|
|
101
|
+
})
|
|
102
|
+
]);
|
|
103
|
+
const repository = findRepositoryByName(repositories, options.repositoryName);
|
|
104
|
+
if (!repository) {
|
|
105
|
+
throw new Error(`Repository not found by name: ${options.repositoryName}`);
|
|
106
|
+
}
|
|
107
|
+
const repositoryId = String(repository.id);
|
|
108
|
+
const title = options.title ?? `Release ${options.sourceBranch} into ${options.targetBranch}`;
|
|
109
|
+
const changeRequest = await this.createChangeRequest(repositoryId, {
|
|
110
|
+
sourceBranch: options.sourceBranch,
|
|
111
|
+
targetBranch: options.targetBranch,
|
|
112
|
+
sourceProjectId: repository.id,
|
|
113
|
+
targetProjectId: repository.id,
|
|
114
|
+
reviewerUserIds: [currentUser.id],
|
|
115
|
+
title,
|
|
116
|
+
description: options.description
|
|
117
|
+
});
|
|
118
|
+
const localId = localIdFromChangeRequest(changeRequest);
|
|
119
|
+
const review = await this.reviewChangeRequest(repositoryId, localId, {
|
|
120
|
+
reviewComment: "Approved by yunxiao-cli release workflow"
|
|
121
|
+
});
|
|
122
|
+
const merge = await this.mergeChangeRequest(repositoryId, localId, {
|
|
123
|
+
mergeType: options.mergeType,
|
|
124
|
+
removeSourceBranch: false
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
repository,
|
|
128
|
+
changeRequest,
|
|
129
|
+
review,
|
|
130
|
+
merge
|
|
131
|
+
};
|
|
132
|
+
}
|
|
69
133
|
changeRequestsPath() {
|
|
70
134
|
if (this.edition === "center") {
|
|
71
135
|
return `/oapi/v1/codeup/organizations/${encodePathSegment(this.organizationId ?? "")}/changeRequests`;
|
|
72
136
|
}
|
|
73
137
|
return "/oapi/v1/codeup/changeRequests";
|
|
74
138
|
}
|
|
75
|
-
|
|
139
|
+
repositoriesPath() {
|
|
140
|
+
if (this.edition === "center") {
|
|
141
|
+
return `/oapi/v1/codeup/organizations/${encodePathSegment(this.organizationId ?? "")}/repositories`;
|
|
142
|
+
}
|
|
143
|
+
return "/oapi/v1/codeup/repositories";
|
|
144
|
+
}
|
|
145
|
+
repositoryPath(repositoryId) {
|
|
76
146
|
const encodedRepositoryId = encodePathSegment(repositoryId);
|
|
77
147
|
if (this.edition === "center") {
|
|
78
|
-
return `/oapi/v1/codeup/organizations/${encodePathSegment(this.organizationId ?? "")}/repositories/${encodedRepositoryId}
|
|
148
|
+
return `/oapi/v1/codeup/organizations/${encodePathSegment(this.organizationId ?? "")}/repositories/${encodedRepositoryId}`;
|
|
79
149
|
}
|
|
80
|
-
return `/oapi/v1/codeup/repositories/${encodedRepositoryId}
|
|
150
|
+
return `/oapi/v1/codeup/repositories/${encodedRepositoryId}`;
|
|
151
|
+
}
|
|
152
|
+
repositoryChangeRequestPath(repositoryId, localId) {
|
|
153
|
+
return `${this.repositoryPath(repositoryId)}/changeRequests/${localId}`;
|
|
81
154
|
}
|
|
82
155
|
async request(method, path, query, body) {
|
|
83
156
|
const url = new URL(`https://${this.domain}${path}`);
|
|
@@ -99,7 +172,7 @@ export class YunxiaoClient {
|
|
|
99
172
|
const response = await this.fetcher(url.toString(), init);
|
|
100
173
|
const text = await response.text();
|
|
101
174
|
if (!response.ok) {
|
|
102
|
-
throw new YunxiaoApiError(`Yunxiao API request failed with HTTP ${response.status}`, response.status, text);
|
|
175
|
+
throw new YunxiaoApiError(`Yunxiao API request failed with HTTP ${response.status}: ${method} ${url.toString()}${text ? `\n${text}` : ""}`, response.status, text, method, url.toString());
|
|
103
176
|
}
|
|
104
177
|
if (!text) {
|
|
105
178
|
return null;
|
|
@@ -116,3 +189,29 @@ function encodePathSegment(value) {
|
|
|
116
189
|
function compactBody(body) {
|
|
117
190
|
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
118
191
|
}
|
|
192
|
+
function normalizeProjectId(value) {
|
|
193
|
+
const numberValue = typeof value === "string" ? Number(value) : value;
|
|
194
|
+
return Number.isInteger(numberValue) ? numberValue : value;
|
|
195
|
+
}
|
|
196
|
+
function findRepositoryByName(repositories, repositoryName) {
|
|
197
|
+
const normalizedName = normalizeRepositoryName(repositoryName);
|
|
198
|
+
const matches = repositories.filter((repository) => [repository.name, repository.path, repository.nameWithNamespace, repository.pathWithNamespace]
|
|
199
|
+
.filter((value) => typeof value === "string")
|
|
200
|
+
.some((value) => normalizeRepositoryName(value) === normalizedName));
|
|
201
|
+
if (matches.length > 1) {
|
|
202
|
+
throw new Error(`Multiple repositories matched name: ${repositoryName}. Use a more specific path.`);
|
|
203
|
+
}
|
|
204
|
+
return matches[0];
|
|
205
|
+
}
|
|
206
|
+
function normalizeRepositoryName(value) {
|
|
207
|
+
return value.replace(/\s+\/\s+/g, "/").trim().toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
function localIdFromChangeRequest(changeRequest) {
|
|
210
|
+
if (typeof changeRequest === "object" && changeRequest !== null && "localId" in changeRequest) {
|
|
211
|
+
const localId = Number(changeRequest.localId);
|
|
212
|
+
if (Number.isInteger(localId) && localId > 0) {
|
|
213
|
+
return localId;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
throw new Error("Create change request response did not include a valid localId.");
|
|
217
|
+
}
|