@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 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
 
@@ -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
- constructor(message: string, status: number, responseBody: string);
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
  }
@@ -1,10 +1,14 @@
1
1
  export class YunxiaoApiError extends Error {
2
2
  status;
3
3
  responseBody;
4
- constructor(message, status, responseBody) {
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
- repositoryChangeRequestPath(repositoryId, localId) {
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}/changeRequests/${localId}`;
148
+ return `/oapi/v1/codeup/organizations/${encodePathSegment(this.organizationId ?? "")}/repositories/${encodedRepositoryId}`;
79
149
  }
80
- return `/oapi/v1/codeup/repositories/${encodedRepositoryId}/changeRequests/${localId}`;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderpp/yunxiao-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A CLI for Yunxiao Codeup merge request review and merge workflows.",
5
5
  "type": "module",
6
6
  "repository": {