@coderpp/yunxiao-cli 0.1.10 → 0.2.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 +187 -11
- package/dist/src/arguments.d.ts +11 -0
- package/dist/src/arguments.js +271 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +290 -172
- package/dist/src/client.d.ts +19 -7
- package/dist/src/client.js +100 -141
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +106 -0
- package/dist/src/errors.d.ts +14 -0
- package/dist/src/errors.js +42 -0
- package/dist/src/mr.d.ts +13 -0
- package/dist/src/mr.js +202 -0
- package/dist/src/pagination.d.ts +10 -0
- package/dist/src/pagination.js +35 -0
- package/dist/src/pipeline.d.ts +3 -0
- package/dist/src/pipeline.js +50 -0
- package/dist/src/release.d.ts +2 -0
- package/dist/src/release.js +203 -0
- package/dist/src/transport.d.ts +16 -0
- package/dist/src/transport.js +119 -0
- package/dist/src/workitem-content.d.ts +28 -0
- package/dist/src/workitem-content.js +201 -0
- package/package.json +8 -3
package/dist/src/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { YunxiaoApiError } from "./transport.js";
|
|
1
2
|
export type YunxiaoEdition = "center" | "region";
|
|
2
3
|
export type Fetcher = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
3
4
|
export interface YunxiaoClientOptions {
|
|
@@ -6,6 +7,8 @@ export interface YunxiaoClientOptions {
|
|
|
6
7
|
organizationId?: string;
|
|
7
8
|
edition?: YunxiaoEdition;
|
|
8
9
|
fetcher?: Fetcher;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
retries?: number;
|
|
9
12
|
}
|
|
10
13
|
export interface ListChangeRequestsOptions {
|
|
11
14
|
page?: number;
|
|
@@ -151,11 +154,13 @@ export interface ReleaseByRepositoryNameOptions {
|
|
|
151
154
|
sourceBranch: string;
|
|
152
155
|
targetBranch: string;
|
|
153
156
|
createOnly?: boolean;
|
|
157
|
+
resume?: number;
|
|
154
158
|
title?: string;
|
|
155
159
|
description?: string;
|
|
156
160
|
mergeType?: MergeChangeRequestOptions["mergeType"];
|
|
157
161
|
}
|
|
158
162
|
export interface ReleaseResult {
|
|
163
|
+
status?: string;
|
|
159
164
|
repository: RepositorySummary;
|
|
160
165
|
changeRequest: unknown;
|
|
161
166
|
review: unknown | null;
|
|
@@ -163,14 +168,9 @@ export interface ReleaseResult {
|
|
|
163
168
|
}
|
|
164
169
|
export interface ApproveAndMergeOptions extends ReviewChangeRequestOptions, MergeChangeRequestOptions {
|
|
165
170
|
}
|
|
166
|
-
export declare class YunxiaoApiError extends Error {
|
|
167
|
-
readonly status: number;
|
|
168
|
-
readonly responseBody: string;
|
|
169
|
-
readonly method: string;
|
|
170
|
-
readonly url: string;
|
|
171
|
-
constructor(message: string, status: number, responseBody: string, method: string, url: string);
|
|
172
|
-
}
|
|
173
171
|
export declare class YunxiaoClient {
|
|
172
|
+
private readonly timeoutMs;
|
|
173
|
+
private readonly retries;
|
|
174
174
|
private readonly token;
|
|
175
175
|
private readonly domain;
|
|
176
176
|
private readonly organizationId?;
|
|
@@ -189,6 +189,8 @@ export declare class YunxiaoClient {
|
|
|
189
189
|
getMember(memberId: string): Promise<unknown>;
|
|
190
190
|
searchWorkitems(options: SearchWorkitemsOptions): Promise<unknown>;
|
|
191
191
|
getWorkitem(workitemId: string): Promise<unknown>;
|
|
192
|
+
getWorkitemFile(workitemId: string, fileId: string): Promise<unknown>;
|
|
193
|
+
listWorkitemAttachments(workitemId: string): Promise<unknown>;
|
|
192
194
|
updateWorkitem(workitemId: string, options: UpdateWorkitemOptions): Promise<unknown>;
|
|
193
195
|
listWorkitemActivities(workitemId: string): Promise<unknown>;
|
|
194
196
|
listWorkitemComments(workitemId: string): Promise<unknown>;
|
|
@@ -199,6 +201,16 @@ export declare class YunxiaoClient {
|
|
|
199
201
|
listLatestFailedPipelineRuns(options?: ListLatestFailedPipelineRunsOptions): Promise<LatestFailedPipelineRun[]>;
|
|
200
202
|
getPipelineRun(pipelineId: string, pipelineRunId: string): Promise<unknown>;
|
|
201
203
|
retryPipelineJobRun(pipelineId: string, pipelineRunId: string, jobId: string): Promise<unknown>;
|
|
204
|
+
getChangeRequest(repositoryId: string, localId: number): Promise<unknown>;
|
|
205
|
+
updateChangeRequest(repositoryId: string, localId: number, body: Record<string, unknown>): Promise<unknown>;
|
|
206
|
+
closeChangeRequest(repositoryId: string, localId: number): Promise<unknown>;
|
|
207
|
+
listChangeRequestComments(repositoryId: string, localId: number, body?: Record<string, unknown>): Promise<unknown>;
|
|
208
|
+
createChangeRequestComment(repositoryId: string, localId: number, body: Record<string, unknown>): Promise<unknown>;
|
|
209
|
+
compare(repositoryId: string, from: string, to: string, straight: boolean): Promise<unknown>;
|
|
210
|
+
getRepository(repositoryId: string): Promise<unknown>;
|
|
211
|
+
getPipelineJobLog(pipelineId: string, runId: string, jobId: string): Promise<unknown>;
|
|
212
|
+
startPipeline(pipelineId: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
213
|
+
cancelPipeline(pipelineId: string, runId: string): Promise<unknown>;
|
|
202
214
|
createChangeRequest(repositoryId: string, options: CreateChangeRequestOptions): Promise<unknown>;
|
|
203
215
|
reviewChangeRequest(repositoryId: string, localId: number, options?: ReviewChangeRequestOptions): Promise<unknown>;
|
|
204
216
|
mergeChangeRequest(repositoryId: string, localId: number, options?: MergeChangeRequestOptions): Promise<unknown>;
|
package/dist/src/client.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
constructor(message, status, responseBody, method, url) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.status = status;
|
|
9
|
-
this.responseBody = responseBody;
|
|
10
|
-
this.method = method;
|
|
11
|
-
this.url = url;
|
|
12
|
-
this.name = "YunxiaoApiError";
|
|
13
|
-
}
|
|
14
|
-
}
|
|
1
|
+
import { release } from "./release.js";
|
|
2
|
+
import { requestJson } from "./transport.js";
|
|
3
|
+
export { YunxiaoApiError } from "./transport.js";
|
|
4
|
+
import { paginate } from "./pagination.js";
|
|
5
|
+
import { CliError } from "./errors.js";
|
|
15
6
|
export class YunxiaoClient {
|
|
7
|
+
timeoutMs;
|
|
8
|
+
retries;
|
|
16
9
|
token;
|
|
17
10
|
domain;
|
|
18
11
|
organizationId;
|
|
@@ -20,15 +13,28 @@ export class YunxiaoClient {
|
|
|
20
13
|
fetcher;
|
|
21
14
|
constructor(options) {
|
|
22
15
|
if (!options.token) {
|
|
23
|
-
throw new
|
|
16
|
+
throw new CliError("INVALID_CONFIG", "Yunxiao token is required. Set YUNXIAO_TOKEN or pass --token.");
|
|
24
17
|
}
|
|
18
|
+
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
19
|
+
this.retries = options.retries ?? 2;
|
|
20
|
+
if (!Number.isSafeInteger(this.timeoutMs) ||
|
|
21
|
+
this.timeoutMs < 1 ||
|
|
22
|
+
!Number.isSafeInteger(this.retries) ||
|
|
23
|
+
this.retries < 0 ||
|
|
24
|
+
this.retries > 10)
|
|
25
|
+
throw new CliError("INVALID_CONFIG", "Invalid timeout or retry count.");
|
|
25
26
|
this.token = options.token;
|
|
26
27
|
this.domain = stripProtocol(options.domain ?? "openapi-rdc.aliyuncs.com");
|
|
27
28
|
this.organizationId = options.organizationId;
|
|
28
|
-
this.edition =
|
|
29
|
+
this.edition =
|
|
30
|
+
options.edition ?? (options.organizationId ? "center" : "region");
|
|
29
31
|
this.fetcher = options.fetcher ?? fetch;
|
|
32
|
+
if (!["center", "region"].includes(this.edition))
|
|
33
|
+
throw new CliError("INVALID_CONFIG", "Invalid edition.");
|
|
34
|
+
if (!/^[a-zA-Z0-9.-]+(?::[0-9]+)?$/.test(this.domain))
|
|
35
|
+
throw new CliError("INVALID_CONFIG", "Domain must be an HTTPS hostname, optionally with port.");
|
|
30
36
|
if (this.edition === "center" && !this.organizationId) {
|
|
31
|
-
throw new
|
|
37
|
+
throw new CliError("INVALID_CONFIG", "Center edition requires organizationId. Set YUNXIAO_ORGANIZATION_ID or pass --organization-id.");
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
listChangeRequests(options = {}) {
|
|
@@ -53,7 +59,7 @@ export class YunxiaoClient {
|
|
|
53
59
|
return this.request("POST", `${this.projectPath(projectId)}/members`, undefined, {
|
|
54
60
|
operatorId: options.operatorId,
|
|
55
61
|
roleId: options.roleId,
|
|
56
|
-
userIds: options.userIds
|
|
62
|
+
userIds: options.userIds,
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
65
|
deleteProjectMember(projectId, options) {
|
|
@@ -61,7 +67,7 @@ export class YunxiaoClient {
|
|
|
61
67
|
operatorId: options.operatorId,
|
|
62
68
|
roleIds: [options.roleId],
|
|
63
69
|
userId: options.userIds.length === 1 ? options.userIds[0] : undefined,
|
|
64
|
-
userIds: options.userIds.length > 1 ? options.userIds : undefined
|
|
70
|
+
userIds: options.userIds.length > 1 ? options.userIds : undefined,
|
|
65
71
|
});
|
|
66
72
|
}
|
|
67
73
|
listMembers(options = {}) {
|
|
@@ -79,12 +85,18 @@ export class YunxiaoClient {
|
|
|
79
85
|
perPage: options.perPage,
|
|
80
86
|
sort: options.sort,
|
|
81
87
|
spaceId: options.spaceId,
|
|
82
|
-
spaceType: options.spaceType ?? "Project"
|
|
88
|
+
spaceType: options.spaceType ?? "Project",
|
|
83
89
|
});
|
|
84
90
|
}
|
|
85
91
|
getWorkitem(workitemId) {
|
|
86
92
|
return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}`);
|
|
87
93
|
}
|
|
94
|
+
getWorkitemFile(workitemId, fileId) {
|
|
95
|
+
return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/files/${encodePathSegment(fileId)}`);
|
|
96
|
+
}
|
|
97
|
+
listWorkitemAttachments(workitemId) {
|
|
98
|
+
return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/attachments`);
|
|
99
|
+
}
|
|
88
100
|
updateWorkitem(workitemId, options) {
|
|
89
101
|
return this.request("PUT", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}`, undefined, options);
|
|
90
102
|
}
|
|
@@ -98,14 +110,14 @@ export class YunxiaoClient {
|
|
|
98
110
|
return this.request("POST", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/comments`, undefined, {
|
|
99
111
|
content: options.content,
|
|
100
112
|
operatorId: options.operatorId,
|
|
101
|
-
parentId: options.parentId
|
|
113
|
+
parentId: options.parentId,
|
|
102
114
|
});
|
|
103
115
|
}
|
|
104
116
|
async getWorkitemTimeline(workitemId) {
|
|
105
117
|
const [workitem, activities, comments] = await Promise.all([
|
|
106
118
|
this.getWorkitem(workitemId),
|
|
107
119
|
this.listWorkitemActivities(workitemId),
|
|
108
|
-
this.listWorkitemComments(workitemId)
|
|
120
|
+
this.listWorkitemComments(workitemId),
|
|
109
121
|
]);
|
|
110
122
|
return { workitem, activities, comments };
|
|
111
123
|
}
|
|
@@ -116,7 +128,7 @@ export class YunxiaoClient {
|
|
|
116
128
|
startTime: options.startTime,
|
|
117
129
|
endTme: options.endTime,
|
|
118
130
|
status: options.status,
|
|
119
|
-
triggerMode: options.triggerMode
|
|
131
|
+
triggerMode: options.triggerMode,
|
|
120
132
|
});
|
|
121
133
|
}
|
|
122
134
|
listPipelines(options = {}) {
|
|
@@ -128,7 +140,7 @@ export class YunxiaoClient {
|
|
|
128
140
|
statusList: options.statusList,
|
|
129
141
|
pipelineName: options.pipelineName,
|
|
130
142
|
createStartTime: options.createStartTime,
|
|
131
|
-
createEndTime: options.createEndTime
|
|
143
|
+
createEndTime: options.createEndTime,
|
|
132
144
|
});
|
|
133
145
|
}
|
|
134
146
|
async listLatestFailedPipelineRuns(options = {}) {
|
|
@@ -140,13 +152,15 @@ export class YunxiaoClient {
|
|
|
140
152
|
perPage,
|
|
141
153
|
executeStartTime: startTime,
|
|
142
154
|
executeEndTime: endTime,
|
|
143
|
-
statusList: "FAIL"
|
|
155
|
+
statusList: "FAIL",
|
|
144
156
|
});
|
|
145
157
|
const uniquePipelines = uniqueBy(pipelines, (pipeline) => String(recordValue(pipeline, "pipelineId") ?? recordValue(pipeline, "id")));
|
|
146
158
|
const results = [];
|
|
147
159
|
for (const pipeline of uniquePipelines) {
|
|
148
160
|
const pipelineId = recordValue(pipeline, "pipelineId") ?? recordValue(pipeline, "id");
|
|
149
|
-
if (pipelineId === undefined ||
|
|
161
|
+
if (pipelineId === undefined ||
|
|
162
|
+
pipelineId === null ||
|
|
163
|
+
pipelineId === "") {
|
|
150
164
|
continue;
|
|
151
165
|
}
|
|
152
166
|
const runs = await this.listAllPipelineRuns(String(pipelineId), {
|
|
@@ -154,17 +168,20 @@ export class YunxiaoClient {
|
|
|
154
168
|
perPage,
|
|
155
169
|
startTime,
|
|
156
170
|
endTime,
|
|
157
|
-
status: "FAIL"
|
|
171
|
+
status: "FAIL",
|
|
158
172
|
});
|
|
159
173
|
const latestRun = runs
|
|
160
174
|
.filter((run) => timestampInRange(recordValue(run, "startTime"), startTime, endTime))
|
|
161
|
-
.sort((left, right) => Number(recordValue(right, "startTime") ?? 0) -
|
|
175
|
+
.sort((left, right) => Number(recordValue(right, "startTime") ?? 0) -
|
|
176
|
+
Number(recordValue(left, "startTime") ?? 0))[0];
|
|
162
177
|
if (!latestRun) {
|
|
163
178
|
continue;
|
|
164
179
|
}
|
|
165
180
|
results.push({
|
|
166
181
|
pipelineId: normalizeNumericId(pipelineId),
|
|
167
|
-
pipelineName: stringRecordValue(pipeline, "pipelineName") ??
|
|
182
|
+
pipelineName: stringRecordValue(pipeline, "pipelineName") ??
|
|
183
|
+
stringRecordValue(pipeline, "name") ??
|
|
184
|
+
"",
|
|
168
185
|
pipelineRunId: normalizeNumericId(recordValue(latestRun, "pipelineRunId") ?? ""),
|
|
169
186
|
status: stringRecordValue(latestRun, "status"),
|
|
170
187
|
triggerMode: numericOrStringValue(recordValue(latestRun, "triggerMode")),
|
|
@@ -173,7 +190,7 @@ export class YunxiaoClient {
|
|
|
173
190
|
startTimeText: formatTimestamp(recordValue(latestRun, "startTime")),
|
|
174
191
|
endTime: numericOrStringValue(recordValue(latestRun, "endTime")),
|
|
175
192
|
endTimeText: formatTimestamp(recordValue(latestRun, "endTime")),
|
|
176
|
-
creatorAccountId: stringRecordValue(latestRun, "creatorAccountId")
|
|
193
|
+
creatorAccountId: stringRecordValue(latestRun, "creatorAccountId"),
|
|
177
194
|
});
|
|
178
195
|
}
|
|
179
196
|
return results.sort((left, right) => Number(right.startTime ?? 0) - Number(left.startTime ?? 0));
|
|
@@ -184,6 +201,36 @@ export class YunxiaoClient {
|
|
|
184
201
|
retryPipelineJobRun(pipelineId, pipelineRunId, jobId) {
|
|
185
202
|
return this.request("PUT", `${this.pipelinePath(pipelineId)}/pipelineRuns/${encodePathSegment(pipelineRunId)}/jobs/${encodePathSegment(jobId)}/retry`);
|
|
186
203
|
}
|
|
204
|
+
getChangeRequest(repositoryId, localId) {
|
|
205
|
+
return this.request("GET", this.repositoryChangeRequestPath(repositoryId, localId));
|
|
206
|
+
}
|
|
207
|
+
updateChangeRequest(repositoryId, localId, body) {
|
|
208
|
+
return this.request("PUT", this.repositoryChangeRequestPath(repositoryId, localId), undefined, body);
|
|
209
|
+
}
|
|
210
|
+
closeChangeRequest(repositoryId, localId) {
|
|
211
|
+
return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/close`);
|
|
212
|
+
}
|
|
213
|
+
listChangeRequestComments(repositoryId, localId, body = {}) {
|
|
214
|
+
return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/comments/list`, undefined, body);
|
|
215
|
+
}
|
|
216
|
+
createChangeRequestComment(repositoryId, localId, body) {
|
|
217
|
+
return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/comments`, undefined, body);
|
|
218
|
+
}
|
|
219
|
+
compare(repositoryId, from, to, straight) {
|
|
220
|
+
return this.request("GET", `${this.repositoryPath(repositoryId)}/compares`, { from, to, straight });
|
|
221
|
+
}
|
|
222
|
+
getRepository(repositoryId) {
|
|
223
|
+
return this.request("GET", this.repositoryPath(repositoryId));
|
|
224
|
+
}
|
|
225
|
+
getPipelineJobLog(pipelineId, runId, jobId) {
|
|
226
|
+
return this.request("GET", `${this.pipelinePath(pipelineId)}/runs/${encodePathSegment(runId)}/job/${encodePathSegment(jobId)}/log`);
|
|
227
|
+
}
|
|
228
|
+
startPipeline(pipelineId, params) {
|
|
229
|
+
return this.request("POST", `${this.pipelinePath(pipelineId)}/runs`, undefined, { params: params === undefined ? undefined : JSON.stringify(params) });
|
|
230
|
+
}
|
|
231
|
+
cancelPipeline(pipelineId, runId) {
|
|
232
|
+
return this.request("PUT", `${this.pipelinePath(pipelineId)}/runs/${encodePathSegment(runId)}`);
|
|
233
|
+
}
|
|
187
234
|
createChangeRequest(repositoryId, options) {
|
|
188
235
|
return this.request("POST", `${this.repositoryPath(repositoryId)}/changeRequests`, undefined, {
|
|
189
236
|
createFrom: options.createFrom ?? "WEB",
|
|
@@ -195,37 +242,37 @@ export class YunxiaoClient {
|
|
|
195
242
|
targetProjectId: normalizeProjectId(options.targetProjectId),
|
|
196
243
|
title: options.title,
|
|
197
244
|
triggerAIReviewRun: options.triggerAIReviewRun ?? false,
|
|
198
|
-
workItemIds: options.workItemIds
|
|
245
|
+
workItemIds: options.workItemIds,
|
|
199
246
|
});
|
|
200
247
|
}
|
|
201
248
|
reviewChangeRequest(repositoryId, localId, options = {}) {
|
|
202
249
|
return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/review`, undefined, {
|
|
203
250
|
...compactBody({
|
|
204
251
|
reviewComment: options.reviewComment,
|
|
205
|
-
submitDraftCommentIds: options.submitDraftCommentIds
|
|
252
|
+
submitDraftCommentIds: options.submitDraftCommentIds,
|
|
206
253
|
}),
|
|
207
|
-
reviewOpinion: options.reviewOpinion ?? "PASS"
|
|
254
|
+
reviewOpinion: options.reviewOpinion ?? "PASS",
|
|
208
255
|
});
|
|
209
256
|
}
|
|
210
257
|
mergeChangeRequest(repositoryId, localId, options = {}) {
|
|
211
258
|
return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/merge`, undefined, {
|
|
212
259
|
...compactBody({
|
|
213
260
|
mergeMessage: options.mergeMessage,
|
|
214
|
-
removeSourceBranch: options.removeSourceBranch
|
|
261
|
+
removeSourceBranch: options.removeSourceBranch,
|
|
215
262
|
}),
|
|
216
|
-
mergeType: options.mergeType ?? "no-fast-forward"
|
|
263
|
+
mergeType: options.mergeType ?? "no-fast-forward",
|
|
217
264
|
});
|
|
218
265
|
}
|
|
219
266
|
async approveAndMerge(repositoryId, localId, options = {}) {
|
|
220
267
|
await this.reviewChangeRequest(repositoryId, localId, {
|
|
221
268
|
reviewComment: options.reviewComment,
|
|
222
269
|
reviewOpinion: options.reviewOpinion ?? "PASS",
|
|
223
|
-
submitDraftCommentIds: options.submitDraftCommentIds
|
|
270
|
+
submitDraftCommentIds: options.submitDraftCommentIds,
|
|
224
271
|
});
|
|
225
272
|
return this.mergeChangeRequest(repositoryId, localId, {
|
|
226
273
|
mergeMessage: options.mergeMessage,
|
|
227
274
|
mergeType: options.mergeType,
|
|
228
|
-
removeSourceBranch: options.removeSourceBranch
|
|
275
|
+
removeSourceBranch: options.removeSourceBranch,
|
|
229
276
|
});
|
|
230
277
|
}
|
|
231
278
|
getChangeRequestTree(repositoryId, localId, options) {
|
|
@@ -235,52 +282,7 @@ export class YunxiaoClient {
|
|
|
235
282
|
return this.request("GET", `${this.repositoryChangeRequestPath(repositoryId, localId)}/diffs/patches`);
|
|
236
283
|
}
|
|
237
284
|
async releaseByRepositoryName(options) {
|
|
238
|
-
|
|
239
|
-
this.getCurrentUser(),
|
|
240
|
-
this.listRepositories({
|
|
241
|
-
page: 1,
|
|
242
|
-
perPage: 20,
|
|
243
|
-
search: options.repositoryName,
|
|
244
|
-
archived: false
|
|
245
|
-
})
|
|
246
|
-
]);
|
|
247
|
-
const repository = findRepositoryByName(repositories, options.repositoryName);
|
|
248
|
-
if (!repository) {
|
|
249
|
-
throw new Error(`Repository not found by name: ${options.repositoryName}`);
|
|
250
|
-
}
|
|
251
|
-
const repositoryId = String(repository.id);
|
|
252
|
-
const title = options.title ?? `Release ${options.sourceBranch} into ${options.targetBranch}`;
|
|
253
|
-
const changeRequest = await this.createChangeRequest(repositoryId, {
|
|
254
|
-
sourceBranch: options.sourceBranch,
|
|
255
|
-
targetBranch: options.targetBranch,
|
|
256
|
-
sourceProjectId: repository.id,
|
|
257
|
-
targetProjectId: repository.id,
|
|
258
|
-
reviewerUserIds: [currentUser.id],
|
|
259
|
-
title,
|
|
260
|
-
description: options.description
|
|
261
|
-
});
|
|
262
|
-
const localId = localIdFromChangeRequest(changeRequest);
|
|
263
|
-
if (options.createOnly) {
|
|
264
|
-
return {
|
|
265
|
-
repository,
|
|
266
|
-
changeRequest,
|
|
267
|
-
review: null,
|
|
268
|
-
merge: null
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
const review = await this.reviewChangeRequest(repositoryId, localId, {
|
|
272
|
-
reviewComment: "Approved by yunxiao-cli release workflow"
|
|
273
|
-
});
|
|
274
|
-
const merge = await this.mergeChangeRequest(repositoryId, localId, {
|
|
275
|
-
mergeType: options.mergeType,
|
|
276
|
-
removeSourceBranch: false
|
|
277
|
-
});
|
|
278
|
-
return {
|
|
279
|
-
repository,
|
|
280
|
-
changeRequest,
|
|
281
|
-
review,
|
|
282
|
-
merge
|
|
283
|
-
};
|
|
285
|
+
return release(this, options);
|
|
284
286
|
}
|
|
285
287
|
changeRequestsPath() {
|
|
286
288
|
if (this.edition === "center") {
|
|
@@ -338,35 +340,15 @@ export class YunxiaoClient {
|
|
|
338
340
|
return `${this.pipelinesPath()}/${encodedPipelineId}`;
|
|
339
341
|
}
|
|
340
342
|
async listAllPipelines(options) {
|
|
341
|
-
|
|
342
|
-
const perPage = options.perPage ?? 30;
|
|
343
|
-
for (let page = options.page ?? 1;; page += 1) {
|
|
344
|
-
const response = await this.listPipelines({ ...options, page, perPage });
|
|
345
|
-
const pageRows = arrayRecords(response);
|
|
346
|
-
rows.push(...pageRows);
|
|
347
|
-
if (pageRows.length < perPage) {
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return rows;
|
|
343
|
+
return (await paginate((page, perPage) => this.listPipelines({ ...options, page, perPage }), { ...options, allPages: true }));
|
|
352
344
|
}
|
|
353
345
|
async listAllPipelineRuns(pipelineId, options) {
|
|
354
|
-
|
|
355
|
-
const perPage = options.perPage ?? 30;
|
|
356
|
-
for (let page = options.page ?? 1;; page += 1) {
|
|
357
|
-
const response = await this.listPipelineRuns(pipelineId, { ...options, page, perPage });
|
|
358
|
-
const pageRows = arrayRecords(response);
|
|
359
|
-
rows.push(...pageRows);
|
|
360
|
-
if (pageRows.length < perPage) {
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return rows;
|
|
346
|
+
return (await paginate((page, perPage) => this.listPipelineRuns(pipelineId, { ...options, page, perPage }), { ...options, allPages: true }));
|
|
365
347
|
}
|
|
366
348
|
async request(method, path, query, body) {
|
|
367
349
|
const url = new URL(`https://${this.domain}${path}`);
|
|
368
350
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
369
|
-
if (value !== undefined && value !== null
|
|
351
|
+
if (value !== undefined && value !== null) {
|
|
370
352
|
url.searchParams.set(key, String(value));
|
|
371
353
|
}
|
|
372
354
|
}
|
|
@@ -374,21 +356,20 @@ export class YunxiaoClient {
|
|
|
374
356
|
method,
|
|
375
357
|
headers: {
|
|
376
358
|
"content-type": "application/json",
|
|
377
|
-
"x-yunxiao-token": this.token
|
|
378
|
-
}
|
|
359
|
+
"x-yunxiao-token": this.token,
|
|
360
|
+
},
|
|
379
361
|
};
|
|
380
362
|
if (body) {
|
|
381
363
|
init.body = JSON.stringify(compactBody(body));
|
|
382
364
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
return JSON.parse(text);
|
|
365
|
+
return requestJson(this.fetcher, url, init, {
|
|
366
|
+
timeoutMs: this.timeoutMs,
|
|
367
|
+
retries: this.retries,
|
|
368
|
+
token: this.token,
|
|
369
|
+
readOnly: method === "GET" ||
|
|
370
|
+
path.endsWith(":search") ||
|
|
371
|
+
path.endsWith("/comments/list"),
|
|
372
|
+
});
|
|
392
373
|
}
|
|
393
374
|
}
|
|
394
375
|
function stripProtocol(domain) {
|
|
@@ -425,7 +406,7 @@ function uniqueBy(values, keyFn) {
|
|
|
425
406
|
}
|
|
426
407
|
function timestampInRange(value, startTime, endTime) {
|
|
427
408
|
const timestamp = Number(value);
|
|
428
|
-
return Number.isFinite(timestamp) && timestamp >= startTime && timestamp <= endTime;
|
|
409
|
+
return (Number.isFinite(timestamp) && timestamp >= startTime && timestamp <= endTime);
|
|
429
410
|
}
|
|
430
411
|
function normalizeNumericId(value) {
|
|
431
412
|
const numberValue = Number(value);
|
|
@@ -467,31 +448,9 @@ function triggerModeName(value) {
|
|
|
467
448
|
}
|
|
468
449
|
}
|
|
469
450
|
function compactBody(body) {
|
|
470
|
-
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null
|
|
451
|
+
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null));
|
|
471
452
|
}
|
|
472
453
|
function normalizeProjectId(value) {
|
|
473
454
|
const numberValue = typeof value === "string" ? Number(value) : value;
|
|
474
455
|
return Number.isInteger(numberValue) ? numberValue : value;
|
|
475
456
|
}
|
|
476
|
-
function findRepositoryByName(repositories, repositoryName) {
|
|
477
|
-
const normalizedName = normalizeRepositoryName(repositoryName);
|
|
478
|
-
const matches = repositories.filter((repository) => [repository.name, repository.path, repository.nameWithNamespace, repository.pathWithNamespace]
|
|
479
|
-
.filter((value) => typeof value === "string")
|
|
480
|
-
.some((value) => normalizeRepositoryName(value) === normalizedName));
|
|
481
|
-
if (matches.length > 1) {
|
|
482
|
-
throw new Error(`Multiple repositories matched name: ${repositoryName}. Use a more specific path.`);
|
|
483
|
-
}
|
|
484
|
-
return matches[0];
|
|
485
|
-
}
|
|
486
|
-
function normalizeRepositoryName(value) {
|
|
487
|
-
return value.replace(/\s+\/\s+/g, "/").trim().toLowerCase();
|
|
488
|
-
}
|
|
489
|
-
function localIdFromChangeRequest(changeRequest) {
|
|
490
|
-
if (typeof changeRequest === "object" && changeRequest !== null && "localId" in changeRequest) {
|
|
491
|
-
const localId = Number(changeRequest.localId);
|
|
492
|
-
if (Number.isInteger(localId) && localId > 0) {
|
|
493
|
-
return localId;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
throw new Error("Create change request response did not include a valid localId.");
|
|
497
|
-
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Flags } from "./arguments.js";
|
|
2
|
+
export declare function configPath(f: Flags, env: NodeJS.ProcessEnv): string;
|
|
3
|
+
export declare function resolveConfig(f: Flags, env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv>;
|
|
4
|
+
export declare function configCommand(command: string, name: string | undefined, f: Flags, env: NodeJS.ProcessEnv): Promise<unknown>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readFile, mkdir, writeFile, rename } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { CliError } from "./errors.js";
|
|
6
|
+
const keys = {
|
|
7
|
+
domain: "YUNXIAO_DOMAIN",
|
|
8
|
+
"organization-id": "YUNXIAO_ORGANIZATION_ID",
|
|
9
|
+
edition: "YUNXIAO_EDITION",
|
|
10
|
+
"project-ids": "YUNXIAO_PROJECT_IDS",
|
|
11
|
+
category: "YUNXIAO_WORKITEM_CATEGORIES",
|
|
12
|
+
};
|
|
13
|
+
export function configPath(f, env) {
|
|
14
|
+
return String(f.config ??
|
|
15
|
+
env.YUNXIAO_CONFIG ??
|
|
16
|
+
join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "yunxiao", "config.json"));
|
|
17
|
+
}
|
|
18
|
+
async function readConfig(file) {
|
|
19
|
+
try {
|
|
20
|
+
const c = JSON.parse(await readFile(file, "utf8"));
|
|
21
|
+
if (!c ||
|
|
22
|
+
!c.profiles ||
|
|
23
|
+
typeof c.profiles !== "object" ||
|
|
24
|
+
Array.isArray(c.profiles))
|
|
25
|
+
throw Error();
|
|
26
|
+
for (const [name, p] of Object.entries(c.profiles)) {
|
|
27
|
+
if (!name || !p || typeof p !== "object" || Array.isArray(p))
|
|
28
|
+
throw Error();
|
|
29
|
+
for (const [k, v] of Object.entries(p))
|
|
30
|
+
if (![...Object.keys(keys), "token-env"].includes(k) ||
|
|
31
|
+
typeof v !== "string")
|
|
32
|
+
throw Error();
|
|
33
|
+
}
|
|
34
|
+
if (c.current !== undefined &&
|
|
35
|
+
(typeof c.current !== "string" || !Object.hasOwn(c.profiles, c.current)))
|
|
36
|
+
throw Error();
|
|
37
|
+
return c;
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (e.code === "ENOENT")
|
|
41
|
+
return { profiles: {} };
|
|
42
|
+
throw new CliError("INVALID_CONFIG", "Invalid configuration file; expected profiles with supported string fields.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function resolveConfig(f, env) {
|
|
46
|
+
const c = await readConfig(configPath(f, env));
|
|
47
|
+
const name = String(f.profile ?? env.YUNXIAO_PROFILE ?? c.current ?? "");
|
|
48
|
+
if (!name)
|
|
49
|
+
return { ...env };
|
|
50
|
+
if (!Object.hasOwn(c.profiles, name))
|
|
51
|
+
throw new CliError("INVALID_CONFIG", `Profile not found: ${name}`);
|
|
52
|
+
const p = c.profiles[name];
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const [k, v] of Object.entries(keys))
|
|
55
|
+
if (p[k])
|
|
56
|
+
result[v] = p[k];
|
|
57
|
+
if (p["token-env"])
|
|
58
|
+
result.YUNXIAO_TOKEN = env[p["token-env"]];
|
|
59
|
+
for (const [k, v] of Object.entries(env))
|
|
60
|
+
if (v !== undefined)
|
|
61
|
+
result[k] = v;
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
export async function configCommand(command, name, f, env) {
|
|
65
|
+
const file = configPath(f, env);
|
|
66
|
+
const c = await readConfig(file);
|
|
67
|
+
if (command === "list")
|
|
68
|
+
return Object.keys(c.profiles).map((name) => ({
|
|
69
|
+
name,
|
|
70
|
+
current: name === c.current,
|
|
71
|
+
}));
|
|
72
|
+
if (command === "show") {
|
|
73
|
+
const selected = String(f.profile ?? env.YUNXIAO_PROFILE ?? c.current ?? "");
|
|
74
|
+
if (!Object.hasOwn(c.profiles, selected))
|
|
75
|
+
throw new CliError("INVALID_CONFIG", "Select an existing profile.");
|
|
76
|
+
return { name: selected, ...c.profiles[selected] };
|
|
77
|
+
}
|
|
78
|
+
if (!name ||
|
|
79
|
+
!/^[-\w.]+$/.test(name) ||
|
|
80
|
+
["__proto__", "constructor", "prototype"].includes(name))
|
|
81
|
+
throw new CliError("INVALID_CONFIG", "Invalid profile name.");
|
|
82
|
+
if (command === "use") {
|
|
83
|
+
if (!Object.hasOwn(c.profiles, name))
|
|
84
|
+
throw new CliError("INVALID_CONFIG", `Profile not found: ${name}`);
|
|
85
|
+
c.current = name;
|
|
86
|
+
}
|
|
87
|
+
else if (command === "set") {
|
|
88
|
+
if (f.token !== undefined)
|
|
89
|
+
throw new CliError("INVALID_CONFIG", "Use --token-env to reference a credential environment variable.");
|
|
90
|
+
const p = { ...(Object.hasOwn(c.profiles, name) ? c.profiles[name] : {}) };
|
|
91
|
+
for (const k of [...Object.keys(keys), "token-env"])
|
|
92
|
+
if (f[k] !== undefined)
|
|
93
|
+
p[k] = String(f[k]);
|
|
94
|
+
if (p["token-env"] && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(p["token-env"]))
|
|
95
|
+
throw new CliError("INVALID_CONFIG", "Invalid --token-env.");
|
|
96
|
+
c.profiles[name] = p;
|
|
97
|
+
}
|
|
98
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
99
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
100
|
+
await writeFile(temp, JSON.stringify(c, null, 2) + "\n", {
|
|
101
|
+
mode: 0o600,
|
|
102
|
+
flag: "wx",
|
|
103
|
+
});
|
|
104
|
+
await rename(temp, file);
|
|
105
|
+
return { file, profile: name, current: c.current };
|
|
106
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class CliError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly exitCode: number;
|
|
4
|
+
readonly details?: unknown | undefined;
|
|
5
|
+
constructor(code: string, message: string, exitCode?: number, details?: unknown | undefined);
|
|
6
|
+
}
|
|
7
|
+
export declare function redact(text: string, secrets?: string[]): string;
|
|
8
|
+
export declare function errorInfo(error: unknown): {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
exitCode: number;
|
|
12
|
+
details?: unknown;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
exitCode;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, exitCode = 2, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.exitCode = exitCode;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function redact(text, secrets = []) {
|
|
13
|
+
for (const secret of secrets)
|
|
14
|
+
if (secret)
|
|
15
|
+
text = text.split(secret).join("[REDACTED]");
|
|
16
|
+
return text
|
|
17
|
+
.replace(/pt-[\w.-]+/g, "[REDACTED]")
|
|
18
|
+
.replace(/https?:\/\/[^\s"<>]+\?[^\s"<>]+/g, "[URL query redacted]");
|
|
19
|
+
}
|
|
20
|
+
export function errorInfo(error) {
|
|
21
|
+
if (error instanceof CliError)
|
|
22
|
+
return error;
|
|
23
|
+
const e = error;
|
|
24
|
+
const status = e?.status;
|
|
25
|
+
const exitCode = status === 401 || status === 403
|
|
26
|
+
? 3
|
|
27
|
+
: status === 404
|
|
28
|
+
? 4
|
|
29
|
+
: status === 409 || status === 412
|
|
30
|
+
? 5
|
|
31
|
+
: status === 429 || (status ?? 0) >= 500
|
|
32
|
+
? 6
|
|
33
|
+
: status && status >= 400
|
|
34
|
+
? 2
|
|
35
|
+
: 1;
|
|
36
|
+
return {
|
|
37
|
+
code: e?.apiCode ?? (status ? `HTTP_${status}` : "ERROR"),
|
|
38
|
+
message: e?.message ?? String(error),
|
|
39
|
+
exitCode,
|
|
40
|
+
requestId: e?.requestId,
|
|
41
|
+
};
|
|
42
|
+
}
|