@taole/deploy-helper 1.0.1 → 1.0.3
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 +1 -4
- package/lib/offlinePkg.mjs +21 -2
- package/lib/pipelineApi.mjs +17 -16
- package/lib/yunxiaoFlowApi.mjs +115 -0
- package/package.json +4 -5
- package/modules/alibabacloud-devops-mcp-server/dist/common/errors.js +0 -69
- package/modules/alibabacloud-devops-mcp-server/dist/common/modularTemplates.js +0 -483
- package/modules/alibabacloud-devops-mcp-server/dist/common/pipelineTemplates.js +0 -19
- package/modules/alibabacloud-devops-mcp-server/dist/common/types.js +0 -1119
- package/modules/alibabacloud-devops-mcp-server/dist/common/utils.js +0 -353
- package/modules/alibabacloud-devops-mcp-server/dist/common/version.js +0 -1
- package/modules/alibabacloud-devops-mcp-server/dist/index.js +0 -1067
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/branches.js +0 -144
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/changeRequestComments.js +0 -89
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/changeRequests.js +0 -203
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/compare.js +0 -26
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/files.js +0 -233
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/repositories.js +0 -64
- package/modules/alibabacloud-devops-mcp-server/dist/operations/flow/hostGroup.js +0 -48
- package/modules/alibabacloud-devops-mcp-server/dist/operations/flow/pipeline.js +0 -514
- package/modules/alibabacloud-devops-mcp-server/dist/operations/flow/pipelineJob.js +0 -113
- package/modules/alibabacloud-devops-mcp-server/dist/operations/flow/serviceConnection.js +0 -23
- package/modules/alibabacloud-devops-mcp-server/dist/operations/organization/members.js +0 -94
- package/modules/alibabacloud-devops-mcp-server/dist/operations/organization/organization.js +0 -73
- package/modules/alibabacloud-devops-mcp-server/dist/operations/packages/artifacts.js +0 -64
- package/modules/alibabacloud-devops-mcp-server/dist/operations/packages/repositories.js +0 -35
- package/modules/alibabacloud-devops-mcp-server/dist/operations/projex/project.js +0 -206
- package/modules/alibabacloud-devops-mcp-server/dist/operations/projex/sprint.js +0 -30
- package/modules/alibabacloud-devops-mcp-server/dist/operations/projex/workitem.js +0 -264
package/README.md
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# deploy-helper
|
|
2
2
|
脚本部署用
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
使用里边封装的api, 但是太吵了(debug信息有点多,还没法关闭, 所以没有直接使用它的npm包)
|
|
7
|
-
npm包地址 https://www.npmjs.com/package/alibabacloud-devops-mcp-server
|
|
4
|
+
云效流水线相关 HTTP 调用已收敛为 `lib/yunxiaoFlowApi.mjs`(直接调 OpenAPI,无 MCP/Zod 依赖)。
|
|
8
5
|
|
package/lib/offlinePkg.mjs
CHANGED
|
@@ -13,8 +13,27 @@ function genArchive(outputPath, dir) {
|
|
|
13
13
|
const archive = archiver('zip', {
|
|
14
14
|
zlib: { level: 1 } // Sets the compression level.
|
|
15
15
|
});
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
let settled = false;
|
|
17
|
+
const onError = (err) => {
|
|
18
|
+
if (settled) return;
|
|
19
|
+
settled = true;
|
|
20
|
+
try {
|
|
21
|
+
archive.abort();
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
output.destroy(err);
|
|
26
|
+
reject(err);
|
|
27
|
+
};
|
|
28
|
+
archive.on('error', onError);
|
|
29
|
+
output.on('error', onError);
|
|
30
|
+
// 必须等目标文件流关闭后再继续:仅 archive 'finish' 时缓冲区可能尚未完全刷盘,
|
|
31
|
+
// 后续 md5/上传会读到缺 END 中央目录的截断 zip(如 ADM-ZIP: No END header found)。
|
|
32
|
+
output.on('close', () => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
18
37
|
archive.pipe(output);
|
|
19
38
|
archive.directory(dir, false);
|
|
20
39
|
archive.finalize();
|
package/lib/pipelineApi.mjs
CHANGED
|
@@ -2,13 +2,14 @@ import { homedir } from 'node:os';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import simpleGit from 'simple-git';
|
|
5
|
-
import { log
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
import { log } from './util.mjs';
|
|
6
|
+
import {
|
|
7
|
+
yunxiaoRequest,
|
|
8
|
+
listPipelines,
|
|
9
|
+
getPipeline,
|
|
10
|
+
getPipelineRun,
|
|
11
|
+
createPipelineRun,
|
|
12
|
+
} from './yunxiaoFlowApi.mjs';
|
|
12
13
|
|
|
13
14
|
export const organizationId = "5ec8bb7bd1d1abe63b55cd33";
|
|
14
15
|
|
|
@@ -18,7 +19,7 @@ export const organizationId = "5ec8bb7bd1d1abe63b55cd33";
|
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* 获取当前阶段和任务
|
|
21
|
-
* @param {
|
|
22
|
+
* @param {object} runDetail 流水线执行详情(云效 runs 接口原始结构)
|
|
22
23
|
*/
|
|
23
24
|
function getCurrentJob(runDetail) {
|
|
24
25
|
let currentStage = null;
|
|
@@ -58,7 +59,7 @@ function getCurrentJob(runDetail) {
|
|
|
58
59
|
*/
|
|
59
60
|
async function passPipelineJob(pipelineID, runId, jobId) {
|
|
60
61
|
const url = `/oapi/v1/flow/organizations/${organizationId}/pipelines/${pipelineID}/pipelineRuns/${runId}/jobs/${jobId}/pass`;
|
|
61
|
-
const response = await
|
|
62
|
+
const response = await yunxiaoRequest(url, {
|
|
62
63
|
method: "POST",
|
|
63
64
|
});
|
|
64
65
|
return Boolean(response);
|
|
@@ -135,7 +136,7 @@ export function setDevToken(token) {
|
|
|
135
136
|
async function getPipelineInfoByName(name) {
|
|
136
137
|
let pipeline = null;
|
|
137
138
|
try {
|
|
138
|
-
const pipelines = await
|
|
139
|
+
const pipelines = await listPipelines(organizationId, { pipelineName: name, perPage: 50, page: 1 });
|
|
139
140
|
if (pipelines && Array.isArray(pipelines.items) && pipelines.items.length > 0) {
|
|
140
141
|
pipeline = pipelines.items.find(item => item.name === name);
|
|
141
142
|
}
|
|
@@ -158,7 +159,7 @@ async function getPipelineInfoByName(name) {
|
|
|
158
159
|
async function waitPipelineRunFinish(pipelineID, runId, interval = 5000) {
|
|
159
160
|
const pipelineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineID}/builds/${runId}`;
|
|
160
161
|
log(`开始轮询至流水线执行完成`);
|
|
161
|
-
let runDetail = await
|
|
162
|
+
let runDetail = await getPipelineRun(organizationId, pipelineID, runId);
|
|
162
163
|
let passFailedCount = 0;
|
|
163
164
|
let passFailedMaxCount = 3;
|
|
164
165
|
while (runDetail.status === "RUNNING" || runDetail.status === "WAITING" || runDetail.status === "INIT") {
|
|
@@ -189,7 +190,7 @@ async function waitPipelineRunFinish(pipelineID, runId, interval = 5000) {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
192
|
-
runDetail = await
|
|
193
|
+
runDetail = await getPipelineRun(organizationId, pipelineID, runId);
|
|
193
194
|
}
|
|
194
195
|
if (runDetail.status === "SUCCESS") {
|
|
195
196
|
log(`流水线执行完成,当前状态: ${runDetail.status}`);
|
|
@@ -212,7 +213,7 @@ async function getPipelineInfoDetail(pipelineName, targetBranch = "") {
|
|
|
212
213
|
let hasDetail = false;
|
|
213
214
|
if (/^\d+$/.test(pipelineName)) {
|
|
214
215
|
pipelineID = pipelineName;
|
|
215
|
-
pipelineInfo = await
|
|
216
|
+
pipelineInfo = await getPipeline(organizationId, pipelineID);
|
|
216
217
|
pipelineInfo.id = pipelineID;
|
|
217
218
|
hasDetail = true;
|
|
218
219
|
} else {
|
|
@@ -224,7 +225,7 @@ async function getPipelineInfoDetail(pipelineName, targetBranch = "") {
|
|
|
224
225
|
// 如果需要指定分支,则需要获取源码仓库信息
|
|
225
226
|
if (targetBranch) {
|
|
226
227
|
if (!hasDetail) {
|
|
227
|
-
pipelineInfo = await
|
|
228
|
+
pipelineInfo = await getPipeline(organizationId, pipelineID);
|
|
228
229
|
pipelineInfo.id = pipelineID;
|
|
229
230
|
}
|
|
230
231
|
if (pipelineInfo && pipelineInfo.pipelineConfig && Array.isArray(pipelineInfo.pipelineConfig.sources)) {
|
|
@@ -266,7 +267,7 @@ export async function triggerPipeline(pipelineName, opts = {}) {
|
|
|
266
267
|
throw new Error(`未设置云效token, 请检查云效token是否正确`);
|
|
267
268
|
}
|
|
268
269
|
const { pipelineInfo, runParams } = await getPipelineInfoDetail(pipelineName, opts.branch);
|
|
269
|
-
const runId = await
|
|
270
|
+
const runId = await createPipelineRun(organizationId, pipelineInfo.id, runParams);
|
|
270
271
|
const pipelineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineInfo.id}/builds/${runId}`;
|
|
271
272
|
log(`流水线${pipelineInfo.name}[${pipelineInfo.id}]触发成功,流水线执行id: ${runId}, 流水线执行详情: ${pipelineRunDetailUrl}`);
|
|
272
273
|
if (waitResult) {
|
|
@@ -338,7 +339,7 @@ async function runSinglePipeline(pipeline, index, total) {
|
|
|
338
339
|
await git.raw("push", "origin", `${force2Branch.src}:${force2Branch.dest}`, "--force");
|
|
339
340
|
log(`仓库${repo}强推${force2Branch.src}到${force2Branch.dest}完成`);
|
|
340
341
|
}
|
|
341
|
-
const runId = await
|
|
342
|
+
const runId = await createPipelineRun(organizationId, pipelineInfo.id, runParams);
|
|
342
343
|
const piplineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineInfo.id}/builds/${runId}`;
|
|
343
344
|
log(`流水线${pipelineInfo.name}触发成功,流水线执行id: ${runId}, 流水线执行详情: ${piplineRunDetailUrl}`);
|
|
344
345
|
if (pipeline.waitResult) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 云效 Flow OpenAPI 薄封装(仅 deploy-helper 流水线触发/查询所需),不做 Zod 等严格校验。
|
|
3
|
+
* 文档基址与鉴权与官方 OpenAPI 一致。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_BASE = "https://openapi-rdc.aliyuncs.com";
|
|
7
|
+
|
|
8
|
+
function getBaseUrl() {
|
|
9
|
+
return process.env.YUNXIAO_API_BASE_URL || DEFAULT_BASE;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildUrl(path, query = {}) {
|
|
13
|
+
const base = getBaseUrl();
|
|
14
|
+
const full = `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
|
15
|
+
const u = new URL(full);
|
|
16
|
+
for (const [k, v] of Object.entries(query)) {
|
|
17
|
+
if (v !== undefined && v !== null) {
|
|
18
|
+
u.searchParams.append(k, String(v));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return u.pathname + u.search;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function parseBody(response) {
|
|
25
|
+
const ct = response.headers.get("content-type") || "";
|
|
26
|
+
if (ct.includes("application/json")) {
|
|
27
|
+
return response.json();
|
|
28
|
+
}
|
|
29
|
+
return response.text();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} urlPath 相对路径或绝对 URL
|
|
34
|
+
* @param {{ method?: string, body?: object, headers?: Record<string, string> }} [options]
|
|
35
|
+
*/
|
|
36
|
+
export async function yunxiaoRequest(urlPath, options = {}) {
|
|
37
|
+
const isAbs = urlPath.startsWith("http://") || urlPath.startsWith("https://");
|
|
38
|
+
const url = isAbs ? urlPath : `${getBaseUrl()}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`;
|
|
39
|
+
const headers = {
|
|
40
|
+
Accept: "application/json",
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"User-Agent": "@taole/deploy-helper/yunxiao-flow",
|
|
43
|
+
...options.headers,
|
|
44
|
+
};
|
|
45
|
+
if (process.env.YUNXIAO_ACCESS_TOKEN) {
|
|
46
|
+
headers["x-yunxiao-token"] = process.env.YUNXIAO_ACCESS_TOKEN;
|
|
47
|
+
}
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
method: options.method || "GET",
|
|
50
|
+
headers,
|
|
51
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
52
|
+
});
|
|
53
|
+
const data = await parseBody(res);
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const msg =
|
|
56
|
+
data && typeof data === "object" && "message" in data
|
|
57
|
+
? String(data.message)
|
|
58
|
+
: typeof data === "string"
|
|
59
|
+
? data
|
|
60
|
+
: res.statusText;
|
|
61
|
+
throw new Error(`云效 API ${res.status}: ${msg}`);
|
|
62
|
+
}
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} organizationId
|
|
68
|
+
* @param {{ pipelineName?: string, perPage?: number, page?: number }} [query]
|
|
69
|
+
*/
|
|
70
|
+
export async function listPipelines(organizationId, query = {}) {
|
|
71
|
+
const path = `/oapi/v1/flow/organizations/${organizationId}/pipelines`;
|
|
72
|
+
const url = buildUrl(path, {
|
|
73
|
+
pipelineName: query.pipelineName,
|
|
74
|
+
perPage: query.perPage,
|
|
75
|
+
page: query.page,
|
|
76
|
+
});
|
|
77
|
+
const response = await yunxiaoRequest(url, { method: "GET" });
|
|
78
|
+
let items = [];
|
|
79
|
+
if (Array.isArray(response)) {
|
|
80
|
+
items = response.map((item) => ({
|
|
81
|
+
...item,
|
|
82
|
+
id: item.id ?? item.pipelineId,
|
|
83
|
+
name: item.name ?? item.pipelineName,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
return { items };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function getPipeline(organizationId, pipelineId) {
|
|
90
|
+
const url = `/oapi/v1/flow/organizations/${organizationId}/pipelines/${pipelineId}`;
|
|
91
|
+
return yunxiaoRequest(url, { method: "GET" });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function getPipelineRun(organizationId, pipelineId, pipelineRunId) {
|
|
95
|
+
const url = `/oapi/v1/flow/organizations/${organizationId}/pipelines/${pipelineId}/runs/${pipelineRunId}`;
|
|
96
|
+
return yunxiaoRequest(url, { method: "GET" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} organizationId
|
|
101
|
+
* @param {string|number} pipelineId
|
|
102
|
+
* @param {{ params?: string }} options params 为 JSON 字符串(与云效 OpenAPI 一致)
|
|
103
|
+
*/
|
|
104
|
+
export async function createPipelineRun(organizationId, pipelineId, options = {}) {
|
|
105
|
+
const url = `/oapi/v1/flow/organizations/${organizationId}/pipelines/${pipelineId}/runs`;
|
|
106
|
+
const body = {};
|
|
107
|
+
if (options.params !== undefined) {
|
|
108
|
+
body.params = options.params;
|
|
109
|
+
}
|
|
110
|
+
const response = await yunxiaoRequest(url, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
body,
|
|
113
|
+
});
|
|
114
|
+
return Number(response);
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taole/deploy-helper",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "脚本部署工具,用于将项目部署到测试环境或生产环境",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "node index.mjs",
|
|
9
|
-
"test": "node test.mjs"
|
|
9
|
+
"test": "node test.mjs",
|
|
10
|
+
"test:unit": "node --test ./test/yunxiaoFlowApi.test.mjs"
|
|
10
11
|
},
|
|
11
12
|
"bin": {
|
|
12
13
|
"deploy-helper": "index.mjs",
|
|
@@ -18,15 +19,13 @@
|
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"index.mjs",
|
|
21
|
-
"lib"
|
|
22
|
-
"modules/alibabacloud-devops-mcp-server/dist"
|
|
22
|
+
"lib"
|
|
23
23
|
],
|
|
24
24
|
"keywords": [],
|
|
25
25
|
"author": "",
|
|
26
26
|
"license": "ISC",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"ali-oss": "^6.23.0",
|
|
29
|
-
"alibabacloud-devops-mcp-server": "*",
|
|
30
29
|
"archiver": "^7.0.1",
|
|
31
30
|
"form-data": "^4.0.5",
|
|
32
31
|
"md5-file": "^5.0.0",
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
export class YunxiaoError extends Error {
|
|
2
|
-
status;
|
|
3
|
-
response;
|
|
4
|
-
constructor(message, status, response) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.status = status;
|
|
7
|
-
this.response = response;
|
|
8
|
-
this.name = "YunxiaoError";
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
export class YunxiaoValidationError extends YunxiaoError {
|
|
12
|
-
constructor(message, status, response) {
|
|
13
|
-
super(message, status, response);
|
|
14
|
-
this.name = "YunxiaoValidationError";
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export class YunxiaoResourceNotFoundError extends YunxiaoError {
|
|
18
|
-
constructor(resource) {
|
|
19
|
-
super(`Resource not found: ${resource}`, 404, { message: `${resource} not found` });
|
|
20
|
-
this.name = "YunxiaoResourceNotFoundError";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export class YunxiaoAuthenticationError extends YunxiaoError {
|
|
24
|
-
constructor(message = "Authentication failed") {
|
|
25
|
-
super(message, 401, { message });
|
|
26
|
-
this.name = "YunxiaoAuthenticationError";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export class YunxiaoPermissionError extends YunxiaoError {
|
|
30
|
-
constructor(message = "Insufficient permissions") {
|
|
31
|
-
super(message, 403, { message });
|
|
32
|
-
this.name = "YunxiaoPermissionError";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
export class YunxiaoRateLimitError extends YunxiaoError {
|
|
36
|
-
resetAt;
|
|
37
|
-
constructor(message = "Rate limit exceeded", resetAt) {
|
|
38
|
-
super(message, 429, { message, reset_at: resetAt.toISOString() });
|
|
39
|
-
this.resetAt = resetAt;
|
|
40
|
-
this.name = "YunxiaoRateLimitError";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export class YunxiaoConflictError extends YunxiaoError {
|
|
44
|
-
constructor(message) {
|
|
45
|
-
super(message, 409, { message });
|
|
46
|
-
this.name = "YunxiaoConflictError";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
export function isYunxiaoError(error) {
|
|
50
|
-
return error instanceof YunxiaoError;
|
|
51
|
-
}
|
|
52
|
-
export function createYunxiaoError(status, response) {
|
|
53
|
-
switch (status) {
|
|
54
|
-
case 401:
|
|
55
|
-
return new YunxiaoAuthenticationError(response?.message);
|
|
56
|
-
case 403:
|
|
57
|
-
return new YunxiaoPermissionError(response?.message);
|
|
58
|
-
case 404:
|
|
59
|
-
return new YunxiaoResourceNotFoundError(response?.message || "Resource");
|
|
60
|
-
case 409:
|
|
61
|
-
return new YunxiaoConflictError(response?.message || "Conflict occurred");
|
|
62
|
-
case 422:
|
|
63
|
-
return new YunxiaoValidationError(response?.message || "Validation failed", status, response);
|
|
64
|
-
case 429:
|
|
65
|
-
return new YunxiaoRateLimitError(response?.message, new Date(response?.reset_at || Date.now() + 60000));
|
|
66
|
-
default:
|
|
67
|
-
return new YunxiaoError(response?.message || "Yunxiao API error", status, response);
|
|
68
|
-
}
|
|
69
|
-
}
|