@taole/deploy-helper 0.2.9 → 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 +35 -9
- 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,7 +5,9 @@ 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
|
|
|
10
|
+
const TEST_SERVER_HOST = "192.168.0.35";
|
|
9
11
|
/**
|
|
10
12
|
* 加载配置
|
|
11
13
|
* @returns 配置对象
|
|
@@ -62,7 +64,7 @@ async function getScpClient() {
|
|
|
62
64
|
let scpClient = null;
|
|
63
65
|
// 不能放密码。。残念
|
|
64
66
|
const scpClientConfig = {
|
|
65
|
-
host:
|
|
67
|
+
host: TEST_SERVER_HOST,
|
|
66
68
|
username: 'root',
|
|
67
69
|
tryKeyboard: true,
|
|
68
70
|
}
|
|
@@ -77,8 +79,13 @@ async function getScpClient() {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
|
|
82
|
+
|
|
83
|
+
|
|
80
84
|
async function main() {
|
|
85
|
+
|
|
86
|
+
// await devTest();
|
|
81
87
|
// console.log(`process.argv: ${process.argv[2]} ${process.argv[3]} ${process.argv[4]}`);
|
|
88
|
+
|
|
82
89
|
// return;
|
|
83
90
|
// other commands
|
|
84
91
|
let command = process.argv[2];
|
|
@@ -107,7 +114,7 @@ async function main() {
|
|
|
107
114
|
process.exit(1);
|
|
108
115
|
}
|
|
109
116
|
const workDir = process.cwd(); // 当前项目根目录
|
|
110
|
-
|
|
117
|
+
|
|
111
118
|
const srcFilePath = join(workDir, file);
|
|
112
119
|
if (!fs.existsSync(srcFilePath)) {
|
|
113
120
|
console.log(`${srcFilePath}不存在`);
|
|
@@ -121,20 +128,20 @@ async function main() {
|
|
|
121
128
|
const fileDir = dirname(srcFilePath);
|
|
122
129
|
const fileName = basename(srcFilePath);
|
|
123
130
|
const dest = process.argv[4] || fileName;
|
|
124
|
-
if(dest.includes("/") || dest.includes("\\")){
|
|
131
|
+
if (dest.includes("/") || dest.includes("\\")) {
|
|
125
132
|
console.log(`dest不能包含/或\\`);
|
|
126
133
|
process.exit(1);
|
|
127
134
|
}
|
|
128
135
|
|
|
129
|
-
if(workDir !== fileDir){
|
|
136
|
+
if (workDir !== fileDir) {
|
|
130
137
|
console.log(`仅支持scp当前目录下的文件(不带子目录)`);
|
|
131
138
|
process.exit(1);
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
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;
|
|
135
142
|
|
|
136
143
|
const scpClient = await getScpClient();
|
|
137
|
-
console.log(`scp: ${srcFilePath} -> ${destPath}`);
|
|
144
|
+
console.log(`scp: ${srcFilePath} -> ${TEST_SERVER_HOST}:${destPath}`);
|
|
138
145
|
await scpClient.uploadFile(srcFilePath, destPath);
|
|
139
146
|
console.log(`scp done.`);
|
|
140
147
|
process.exit(0);
|
|
@@ -161,6 +168,7 @@ async function main() {
|
|
|
161
168
|
process.exit(1);
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
|
|
164
172
|
// 需要处理entry
|
|
165
173
|
const needHandleEntry = mode === 'prod' || (mode === 'test' && !config.entry.onlyProd);
|
|
166
174
|
const entryTestBranch = (config.entry && config.entry.testBranch) || "test";
|
|
@@ -168,6 +176,11 @@ async function main() {
|
|
|
168
176
|
const entryTargetBranch = mode === 'prod' ? entryProdBranch : entryTestBranch;
|
|
169
177
|
const currentProdBranch = config.prodBranch || "master";
|
|
170
178
|
|
|
179
|
+
|
|
180
|
+
// 检查流水线配置
|
|
181
|
+
checkYunxiaoToken(config, mode);
|
|
182
|
+
|
|
183
|
+
|
|
171
184
|
// 检查项目
|
|
172
185
|
// 1. 检查项目是否存在
|
|
173
186
|
const assetsDest = join(workDir, config.assets.dest);
|
|
@@ -305,11 +318,19 @@ async function main() {
|
|
|
305
318
|
|
|
306
319
|
// 7. 推送
|
|
307
320
|
if (canPushAssets) {
|
|
308
|
-
|
|
321
|
+
try {
|
|
322
|
+
await assetsGit.push();
|
|
323
|
+
} catch (error) {
|
|
324
|
+
await assetsGit.push();
|
|
325
|
+
}
|
|
309
326
|
console.log(`assets push done.`);
|
|
310
327
|
}
|
|
311
328
|
if (canPushEntry) {
|
|
312
|
-
|
|
329
|
+
try {
|
|
330
|
+
await entryGit.push();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
await entryGit.push();
|
|
333
|
+
}
|
|
313
334
|
console.log(`entry push done.`);
|
|
314
335
|
}
|
|
315
336
|
|
|
@@ -321,7 +342,7 @@ async function main() {
|
|
|
321
342
|
for (const [src, dest] of Object.entries(syncTestFiles)) {
|
|
322
343
|
const srcPath = join(workDir, src);
|
|
323
344
|
const destPath = "/home/web/website/tuwan_www/templets/static/play/" + dest;
|
|
324
|
-
console.log(`scp: ${srcPath} -> ${destPath}`);
|
|
345
|
+
console.log(`scp: ${srcPath} -> ${TEST_SERVER_HOST}:${destPath}`);
|
|
325
346
|
await scpClient.uploadFile(srcPath, destPath)
|
|
326
347
|
}
|
|
327
348
|
}
|
|
@@ -333,6 +354,11 @@ async function main() {
|
|
|
333
354
|
}
|
|
334
355
|
process.exit(1);
|
|
335
356
|
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
// 处理触发流水线的任务
|
|
360
|
+
await runPipeline(config, mode);
|
|
361
|
+
|
|
336
362
|
console.log(`deploy-helper deploy done.`);
|
|
337
363
|
process.exit(0);
|
|
338
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
|
+
}
|