@taole/deploy-helper 0.2.10 → 0.3.1
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 +7 -1
- package/index.mjs +31 -6
- package/lib/pipelineApi.mjs +183 -0
- package/modules/alibabacloud-devops-mcp-server/dist/common/errors.js +69 -0
- package/modules/alibabacloud-devops-mcp-server/dist/common/modularTemplates.js +483 -0
- package/modules/alibabacloud-devops-mcp-server/dist/common/pipelineTemplates.js +19 -0
- package/modules/alibabacloud-devops-mcp-server/dist/common/types.js +1119 -0
- package/modules/alibabacloud-devops-mcp-server/dist/common/utils.js +353 -0
- package/modules/alibabacloud-devops-mcp-server/dist/common/version.js +1 -0
- package/modules/alibabacloud-devops-mcp-server/dist/index.js +1067 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/branches.js +144 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/changeRequestComments.js +89 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/changeRequests.js +203 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/compare.js +26 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/files.js +233 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/codeup/repositories.js +64 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/flow/hostGroup.js +48 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/flow/pipeline.js +507 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/flow/pipelineJob.js +113 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/flow/serviceConnection.js +23 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/organization/members.js +94 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/organization/organization.js +73 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/packages/artifacts.js +64 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/packages/repositories.js +35 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/projex/project.js +206 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/projex/sprint.js +30 -0
- package/modules/alibabacloud-devops-mcp-server/dist/operations/projex/workitem.js +264 -0
- package/package.json +5 -3
package/README.md
CHANGED
package/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'fs';
|
|
|
5
5
|
import { join, basename, dirname } from "path";
|
|
6
6
|
import { simpleGit } from 'simple-git';
|
|
7
7
|
import { homedir } from 'os'
|
|
8
|
+
import { runPipeline, checkYunxiaoToken } from './lib/pipelineApi.mjs';
|
|
8
9
|
|
|
9
10
|
const TEST_SERVER_HOST = "192.168.0.35";
|
|
10
11
|
/**
|
|
@@ -78,8 +79,13 @@ async function getScpClient() {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
|
|
82
|
+
|
|
83
|
+
|
|
81
84
|
async function main() {
|
|
85
|
+
|
|
86
|
+
// await devTest();
|
|
82
87
|
// console.log(`process.argv: ${process.argv[2]} ${process.argv[3]} ${process.argv[4]}`);
|
|
88
|
+
|
|
83
89
|
// return;
|
|
84
90
|
// other commands
|
|
85
91
|
let command = process.argv[2];
|
|
@@ -108,7 +114,7 @@ async function main() {
|
|
|
108
114
|
process.exit(1);
|
|
109
115
|
}
|
|
110
116
|
const workDir = process.cwd(); // 当前项目根目录
|
|
111
|
-
|
|
117
|
+
|
|
112
118
|
const srcFilePath = join(workDir, file);
|
|
113
119
|
if (!fs.existsSync(srcFilePath)) {
|
|
114
120
|
console.log(`${srcFilePath}不存在`);
|
|
@@ -122,17 +128,17 @@ async function main() {
|
|
|
122
128
|
const fileDir = dirname(srcFilePath);
|
|
123
129
|
const fileName = basename(srcFilePath);
|
|
124
130
|
const dest = process.argv[4] || fileName;
|
|
125
|
-
if(dest.includes("/") || dest.includes("\\")){
|
|
131
|
+
if (dest.includes("/") || dest.includes("\\")) {
|
|
126
132
|
console.log(`dest不能包含/或\\`);
|
|
127
133
|
process.exit(1);
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
if(workDir !== fileDir){
|
|
136
|
+
if (workDir !== fileDir) {
|
|
131
137
|
console.log(`仅支持scp当前目录下的文件(不带子目录)`);
|
|
132
138
|
process.exit(1);
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
const destPath = "/home/web/website/tuwan_www/templets/static/play/" + (command === 'scp' ? '' : 'events/')
|
|
141
|
+
const destPath = "/home/web/website/tuwan_www/templets/static/play/" + (command === 'scp' ? '' : 'events/') + dest;
|
|
136
142
|
|
|
137
143
|
const scpClient = await getScpClient();
|
|
138
144
|
console.log(`scp: ${srcFilePath} -> ${TEST_SERVER_HOST}:${destPath}`);
|
|
@@ -162,6 +168,7 @@ async function main() {
|
|
|
162
168
|
process.exit(1);
|
|
163
169
|
}
|
|
164
170
|
|
|
171
|
+
|
|
165
172
|
// 需要处理entry
|
|
166
173
|
const needHandleEntry = mode === 'prod' || (mode === 'test' && !config.entry.onlyProd);
|
|
167
174
|
const entryTestBranch = (config.entry && config.entry.testBranch) || "test";
|
|
@@ -169,6 +176,11 @@ async function main() {
|
|
|
169
176
|
const entryTargetBranch = mode === 'prod' ? entryProdBranch : entryTestBranch;
|
|
170
177
|
const currentProdBranch = config.prodBranch || "master";
|
|
171
178
|
|
|
179
|
+
|
|
180
|
+
// 检查流水线配置
|
|
181
|
+
checkYunxiaoToken(config, mode);
|
|
182
|
+
|
|
183
|
+
|
|
172
184
|
// 检查项目
|
|
173
185
|
// 1. 检查项目是否存在
|
|
174
186
|
const assetsDest = join(workDir, config.assets.dest);
|
|
@@ -306,11 +318,19 @@ async function main() {
|
|
|
306
318
|
|
|
307
319
|
// 7. 推送
|
|
308
320
|
if (canPushAssets) {
|
|
309
|
-
|
|
321
|
+
try {
|
|
322
|
+
await assetsGit.push();
|
|
323
|
+
} catch (error) {
|
|
324
|
+
await assetsGit.push();
|
|
325
|
+
}
|
|
310
326
|
console.log(`assets push done.`);
|
|
311
327
|
}
|
|
312
328
|
if (canPushEntry) {
|
|
313
|
-
|
|
329
|
+
try {
|
|
330
|
+
await entryGit.push();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
await entryGit.push();
|
|
333
|
+
}
|
|
314
334
|
console.log(`entry push done.`);
|
|
315
335
|
}
|
|
316
336
|
|
|
@@ -334,6 +354,11 @@ async function main() {
|
|
|
334
354
|
}
|
|
335
355
|
process.exit(1);
|
|
336
356
|
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
// 处理触发流水线的任务
|
|
360
|
+
await runPipeline(config, mode);
|
|
361
|
+
|
|
337
362
|
console.log(`deploy-helper deploy done.`);
|
|
338
363
|
process.exit(0);
|
|
339
364
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import simpleGit from 'simple-git';
|
|
5
|
+
|
|
6
|
+
import { createPipelineRunFunc, getLatestPipelineRunFunc, listPipelinesFunc } from '../modules/alibabacloud-devops-mcp-server/dist/operations/flow/pipeline.js'
|
|
7
|
+
|
|
8
|
+
export const organizationId = "5ec8bb7bd1d1abe63b55cd33";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
let isFirstFoundToken = true;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function getPipelineConfig(config, mode) {
|
|
16
|
+
const pipelineCfgName = mode === "test" ? "testPipeline" : "prodPipeline";
|
|
17
|
+
let pipelineConfig = config[pipelineCfgName];
|
|
18
|
+
if (!pipelineConfig) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(pipelineConfig.pipelines) || pipelineConfig.pipelines.length === 0) {
|
|
22
|
+
throw new Error(`${pipelineCfgName}配置中请至少配置一个流水线`);
|
|
23
|
+
}
|
|
24
|
+
for (const [index, pipeline] of pipelineConfig.pipelines.entries()) {
|
|
25
|
+
if (!pipeline.id && !pipeline.name) {
|
|
26
|
+
throw new Error(`${pipelineCfgName}[${index}]配置中请至少配置id或name中的一个`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return pipelineConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let devToken = "";
|
|
33
|
+
|
|
34
|
+
export function getYunxiaoToken(pipelineConfig) {
|
|
35
|
+
if (devToken) {
|
|
36
|
+
if (isFirstFoundToken) {
|
|
37
|
+
isFirstFoundToken = false;
|
|
38
|
+
console.log(`将使用devToken`);
|
|
39
|
+
}
|
|
40
|
+
return devToken;
|
|
41
|
+
}
|
|
42
|
+
let token = "";
|
|
43
|
+
if (pipelineConfig.useEnvToken !== false) {
|
|
44
|
+
token = process.env.YUNXIAO_ACCESS_TOKEN || ""
|
|
45
|
+
if (token) {
|
|
46
|
+
if (isFirstFoundToken) {
|
|
47
|
+
isFirstFoundToken = false;
|
|
48
|
+
console.log(`将使用来自环境变量YUNXIAO_ACCESS_TOKEN的云效token`);
|
|
49
|
+
}
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const userDeployHelperDir = join(homedir(), "deploy-helper.config.json");
|
|
55
|
+
if (fs.existsSync(userDeployHelperDir)) {
|
|
56
|
+
try {
|
|
57
|
+
const userDeployHelperConfig = JSON.parse(fs.readFileSync(userDeployHelperDir, "utf-8"));
|
|
58
|
+
token = userDeployHelperConfig.token || "";
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`读取${userDeployHelperDir}配置失败: ${error}`);
|
|
61
|
+
}
|
|
62
|
+
if (token) {
|
|
63
|
+
if (isFirstFoundToken) {
|
|
64
|
+
isFirstFoundToken = false;
|
|
65
|
+
console.log(`将使用来自${userDeployHelperDir}的云效token`);
|
|
66
|
+
}
|
|
67
|
+
return token;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return token;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function setDevToken(token) {
|
|
75
|
+
devToken = token;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getPipelineInfoByName(name) {
|
|
79
|
+
let pipeline = null;
|
|
80
|
+
try {
|
|
81
|
+
const pipelines = await listPipelinesFunc(organizationId, { pipelineName: name, perPage: 1, page: 1 });
|
|
82
|
+
if (pipelines && Array.isArray(pipelines.items) && pipelines.items.length > 0) {
|
|
83
|
+
pipeline = pipelines.items[0];
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`获取流水线信息失败, 请确认流水线名称: ${name} 是否正确或检查云效token的权限是否足够: ${error}`);
|
|
87
|
+
}
|
|
88
|
+
if (!pipeline) {
|
|
89
|
+
throw new Error(`未找到对应的流水线, 请确认流水线名称: ${name} 是否正确或检查云效token的权限是否足够`);
|
|
90
|
+
}
|
|
91
|
+
return pipeline;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 从配置中确认是否需要使用云效流水线,如果需要,则检查是否配置了云效流水线token
|
|
98
|
+
* @param {Object} config
|
|
99
|
+
*/
|
|
100
|
+
export function checkYunxiaoToken(config, mode) {
|
|
101
|
+
let token = "";
|
|
102
|
+
let pipelineConfig = null;
|
|
103
|
+
try {
|
|
104
|
+
pipelineConfig = getPipelineConfig(config, mode);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// pass
|
|
107
|
+
}
|
|
108
|
+
if (!pipelineConfig) {
|
|
109
|
+
// 无相关配置, 检查通过
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
if (pipelineConfig) {
|
|
114
|
+
token = getYunxiaoToken(pipelineConfig);
|
|
115
|
+
if (token) {
|
|
116
|
+
// 有相关配置,且token存在, 检查通过
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`检查云效token失败: ${error}`);
|
|
122
|
+
}
|
|
123
|
+
console.warn(`未设置云效token, 将不会自动触发云效流水线`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function runSinglePipeline(pipeline, index, total) {
|
|
128
|
+
console.log(`开始处理流水线: ${pipeline.name}, 当前是第${index + 1}个, 总共${total}个`);
|
|
129
|
+
// 获取流水线信息
|
|
130
|
+
const pipelineInfo = await getPipelineInfoByName(pipeline.name);
|
|
131
|
+
console.log(`流水线${pipeline.name}的id: ${pipelineInfo.id}`);
|
|
132
|
+
|
|
133
|
+
// 处理分支强推的问题
|
|
134
|
+
const force2Branch = pipeline.force2Branch;
|
|
135
|
+
if (force2Branch && force2Branch.src && force2Branch.dest) {
|
|
136
|
+
let repo = pipeline.repo || "./";
|
|
137
|
+
repo = join(process.cwd(), repo);
|
|
138
|
+
if (!fs.existsSync(repo)) {
|
|
139
|
+
console.log(`仓库${repo}有未提交的修改, 跳过流水线处理`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// 强推到指定分支
|
|
143
|
+
const git = simpleGit(repo);
|
|
144
|
+
const gitStatus = await git.status();
|
|
145
|
+
if (!gitStatus.isClean()) {
|
|
146
|
+
console.log(`仓库${repo}有未提交的修改, 跳过流水线处理`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (gitStatus.current !== force2Branch.src) {
|
|
150
|
+
console.log(`仓库${repo}当前分支不是${force2Branch.src}, 切换到${force2Branch.src}`);
|
|
151
|
+
await git.checkout(force2Branch.src);
|
|
152
|
+
console.log(`仓库${repo}切换到${force2Branch.src}完成`);
|
|
153
|
+
}
|
|
154
|
+
await git.pull();
|
|
155
|
+
await git.push();
|
|
156
|
+
// git push origin master-test-0513:test --force
|
|
157
|
+
await git.raw("push", "origin", `${force2Branch.src}:${force2Branch.dest}`, "--force");
|
|
158
|
+
console.log(`仓库${repo}强推${force2Branch.src}到${force2Branch.dest}完成`);
|
|
159
|
+
}
|
|
160
|
+
// TODO: PERF: 获取流水线信息后, 可以缓存下来, 避免每次都重新获取
|
|
161
|
+
// TODO: 触发流水线时, 可以传入参数, 比如分支, 比如环境, 但是不知道为什么覆盖不了。这里先不传了
|
|
162
|
+
const runId = await createPipelineRunFunc(organizationId, pipelineInfo.id, {});
|
|
163
|
+
const piplineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineInfo.id}/builds/${runId}`;
|
|
164
|
+
console.log(`流水线${pipelineInfo.name}触发成功,流水线执行id: ${runId}, 流水线执行详情: ${piplineRunDetailUrl}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function runPipeline(config, mode) {
|
|
168
|
+
const pipelineConfig = getPipelineConfig(config, mode);
|
|
169
|
+
if (!pipelineConfig) {
|
|
170
|
+
// 无相关配置, 不执行
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const token = getYunxiaoToken(pipelineConfig);
|
|
174
|
+
if (!token) {
|
|
175
|
+
console.log(`未设置云效token, 此次流水线未自动执行: ${JSON.stringify(pipelineConfig)}`);
|
|
176
|
+
}
|
|
177
|
+
process.env.YUNXIAO_ACCESS_TOKEN = token;
|
|
178
|
+
|
|
179
|
+
for (const [index, pipeline] of pipelineConfig.pipelines.entries()) {
|
|
180
|
+
await runSinglePipeline(pipeline, index, pipelineConfig.pipelines.length);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
}
|