daodou-command 1.4.6 → 1.4.8
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/lib/commands/build.js +157 -161
- package/lib/utils/browser.js +236 -25
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -20
- package/.idea/UniappTool.xml +0 -10
- package/.idea/copilot.data.migration.agent.xml +0 -6
- package/.idea/copilot.data.migration.ask.xml +0 -6
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/copilot.data.migration.edit.xml +0 -6
- package/.idea/daodou-command.iml +0 -12
- package/.idea/editorJumperProjectSettings.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.idea/workspace.xml +0 -84
- package/AI_QUICK_REFERENCE.md +0 -202
- package/CHANGELOG.md +0 -201
- package/COMMAND_DEVELOPMENT_GUIDE.md +0 -504
package/lib/commands/build.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const ora = require('ora');
|
|
3
|
-
const inquirer = require('inquirer');
|
|
4
3
|
const path = require('path');
|
|
5
4
|
const fs = require('fs');
|
|
6
5
|
const { getCurrentBranch } = require('../utils/git');
|
|
@@ -18,20 +17,19 @@ class BuildConfigManager {
|
|
|
18
17
|
|
|
19
18
|
checkAndCreateConfig() {
|
|
20
19
|
try {
|
|
21
|
-
// 检查是否有配置文件
|
|
22
20
|
if (!fs.existsSync(this.configManager.projectConfigFile) && !fs.existsSync(this.configManager.configFile)) {
|
|
23
21
|
this.configManager.createDefaultConfig();
|
|
24
|
-
console.log('
|
|
25
|
-
console.log('
|
|
26
|
-
console.log('
|
|
27
|
-
console.log('
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log(chalk.yellow(' ⚠ 已创建默认配置文件'));
|
|
24
|
+
console.log(chalk.dim(' 请编辑后重新运行:'));
|
|
25
|
+
console.log(' ' + this.configManager.projectConfigFile);
|
|
28
26
|
process.exit(0);
|
|
29
27
|
}
|
|
30
|
-
|
|
28
|
+
|
|
31
29
|
this.loadConfig();
|
|
32
30
|
this.checkTemplateValues();
|
|
33
31
|
} catch (e) {
|
|
34
|
-
console.error('
|
|
32
|
+
console.error(chalk.red(' ✖ 读取配置文件失败,请检查文件格式'));
|
|
35
33
|
process.exit(1);
|
|
36
34
|
}
|
|
37
35
|
}
|
|
@@ -50,14 +48,14 @@ class BuildConfigManager {
|
|
|
50
48
|
];
|
|
51
49
|
const hasTemplateValues = Object.values(this.localConfig).some(value => templateValues.includes(value));
|
|
52
50
|
if (hasTemplateValues) {
|
|
53
|
-
console.log(
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(chalk.yellow(' ⚠ 配置文件包含模板值,请修改:'));
|
|
54
53
|
Object.entries(this.localConfig).forEach(([key, value]) => {
|
|
55
54
|
if (templateValues.includes(value)) {
|
|
56
|
-
console.log(chalk.
|
|
55
|
+
console.log(chalk.dim(` ${key}`) + ' = ' + chalk.cyan(value));
|
|
57
56
|
}
|
|
58
57
|
});
|
|
59
|
-
console.log(chalk.
|
|
60
|
-
console.log(chalk.cyan('🔄 修改完成后请重新运行命令'));
|
|
58
|
+
console.log(chalk.dim(' 文件: ') + this.configManager.projectConfigFile);
|
|
61
59
|
process.exit(0);
|
|
62
60
|
}
|
|
63
61
|
}
|
|
@@ -65,34 +63,30 @@ class BuildConfigManager {
|
|
|
65
63
|
validateConfig() {
|
|
66
64
|
const requiredKeys = ['jenkinsBase', 'jenkinsToken', 'jenkinsUrl', 'jobName'];
|
|
67
65
|
const missingKeys = [];
|
|
68
|
-
|
|
66
|
+
|
|
69
67
|
for (const key of requiredKeys) {
|
|
70
68
|
if (!this.localConfig[key]) {
|
|
71
69
|
missingKeys.push(key);
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
|
-
|
|
72
|
+
|
|
75
73
|
if (missingKeys.length > 0) {
|
|
76
|
-
console.
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(chalk.red(' ✖ 缺少必要配置:'));
|
|
77
76
|
missingKeys.forEach(key => {
|
|
78
|
-
console.
|
|
77
|
+
console.log(chalk.dim(` - ${key}`));
|
|
79
78
|
});
|
|
80
|
-
console.
|
|
81
|
-
console.
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(chalk.dim(' 请在 .daodourc 或 ~/.daodou/config.json 中补充'));
|
|
82
81
|
if (missingKeys.includes('jobName')) {
|
|
83
|
-
console.
|
|
84
|
-
} else {
|
|
85
|
-
console.error(' 2. 或者确保全局配置文件 (~/.daodou/config.json) 中包含这些配置');
|
|
82
|
+
console.log(chalk.dim(' jobName 必须在项目配置 (.daodourc) 中设置'));
|
|
86
83
|
}
|
|
87
|
-
console.error(' 3. 项目配置优先于全局配置');
|
|
88
84
|
process.exit(1);
|
|
89
85
|
}
|
|
90
|
-
|
|
91
|
-
// 验证构建参数配置
|
|
86
|
+
|
|
92
87
|
if (this.localConfig.buildParams) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.error('❌ buildParams 必须是对象格式');
|
|
88
|
+
if (typeof this.localConfig.buildParams !== 'object') {
|
|
89
|
+
console.log(chalk.red(' ✖ buildParams 必须是对象格式'));
|
|
96
90
|
process.exit(1);
|
|
97
91
|
}
|
|
98
92
|
}
|
|
@@ -107,6 +101,40 @@ class BuildCommand {
|
|
|
107
101
|
baseUrl: null,
|
|
108
102
|
auth: null
|
|
109
103
|
};
|
|
104
|
+
|
|
105
|
+
// 创建 axios 实例
|
|
106
|
+
this.axios = axios.create({
|
|
107
|
+
maxRedirects: 0,
|
|
108
|
+
validateStatus: s => s < 400 || s === 401 || s === 403 || s === 302
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 请求拦截器:自动添加 Cookie
|
|
112
|
+
this.axios.interceptors.request.use(config => {
|
|
113
|
+
const cookieString = this.browserAuth.getCookieString();
|
|
114
|
+
if (cookieString) {
|
|
115
|
+
config.headers.Cookie = cookieString;
|
|
116
|
+
}
|
|
117
|
+
// 确保有 User-Agent
|
|
118
|
+
if (!config.headers['User-Agent']) {
|
|
119
|
+
config.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
|
|
120
|
+
}
|
|
121
|
+
return config;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// 响应拦截器:自动更新 Cookie
|
|
125
|
+
this.axios.interceptors.response.use(async response => {
|
|
126
|
+
const setCookie = response.headers['set-cookie'];
|
|
127
|
+
if (setCookie) {
|
|
128
|
+
await this.browserAuth.updateCookiesFromSetCookie(setCookie);
|
|
129
|
+
}
|
|
130
|
+
return response;
|
|
131
|
+
}, async error => {
|
|
132
|
+
// 即使是错误响应,也可能包含 set-cookie
|
|
133
|
+
if (error.response && error.response.headers && error.response.headers['set-cookie']) {
|
|
134
|
+
await this.browserAuth.updateCookiesFromSetCookie(error.response.headers['set-cookie']);
|
|
135
|
+
}
|
|
136
|
+
return Promise.reject(error);
|
|
137
|
+
});
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
async execute(options = {}) {
|
|
@@ -117,48 +145,37 @@ class BuildCommand {
|
|
|
117
145
|
this.jenkinsConfig.baseUrl = config.jenkinsUrl;
|
|
118
146
|
this.jenkinsBase = config.jenkinsBase;
|
|
119
147
|
this.jenkinsToken = config.jenkinsToken;
|
|
120
|
-
console.log(chalk.blue('🔧 刀豆构建工具启动中...\n'));
|
|
121
148
|
|
|
122
|
-
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(chalk.bold(' 🔧 刀豆构建工具'));
|
|
151
|
+
console.log(chalk.dim(' ─────────────────────────'));
|
|
152
|
+
|
|
153
|
+
// 1. 获取分支名称
|
|
123
154
|
const branch = await this.getBranch(options);
|
|
124
|
-
console.log(chalk.green(`✅ 当前分支: ${chalk.cyan(branch)}\n`));
|
|
125
155
|
|
|
126
|
-
// 2.
|
|
156
|
+
// 2. 登录 & 认证
|
|
127
157
|
await this.browserAuth.ensureLogin();
|
|
128
|
-
|
|
129
|
-
// 3. 设置Jenkins认证
|
|
130
158
|
this.setupJenkinsAuth();
|
|
131
|
-
|
|
132
|
-
// 4. 检查 Jenkins 会话有效性
|
|
133
159
|
await this.ensureJenkinsSession();
|
|
134
160
|
|
|
135
|
-
//
|
|
161
|
+
// 3. 构建参数
|
|
136
162
|
const jobName = this.buildJobName(config);
|
|
137
163
|
const jenkinsUrl = this.buildJobUrl(config, jobName);
|
|
138
164
|
const params = this.buildParams(config, branch);
|
|
139
165
|
|
|
140
|
-
|
|
141
|
-
console.log(chalk.
|
|
142
|
-
console.log(
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(chalk.dim(' ─────────────────────────'));
|
|
168
|
+
console.log(' ' + chalk.dim('任务') + ' ' + chalk.cyan(jobName));
|
|
169
|
+
console.log(' ' + chalk.dim('分支') + ' ' + chalk.cyan(branch));
|
|
143
170
|
Object.entries(params).forEach(([key, value]) => {
|
|
144
|
-
if (key === 'token') return;
|
|
145
|
-
console.log(
|
|
171
|
+
if (key === 'token' || key === 'GIT_BRANCH') return;
|
|
172
|
+
console.log(' ' + chalk.dim(key) + ' ' + value);
|
|
146
173
|
});
|
|
174
|
+
console.log(chalk.dim(' ─────────────────────────'));
|
|
175
|
+
console.log('');
|
|
147
176
|
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
type: 'input',
|
|
151
|
-
name: 'confirm',
|
|
152
|
-
message: chalk.yellow('按回车确认开始构建,Ctrl+C 取消...'),
|
|
153
|
-
default: undefined,
|
|
154
|
-
transformer: () => ''
|
|
155
|
-
}
|
|
156
|
-
]);
|
|
157
|
-
// 只要不是 Ctrl+C 终止就继续
|
|
158
|
-
|
|
159
|
-
// 8. 记录本地计时起点
|
|
177
|
+
// 4. 触发构建
|
|
160
178
|
const buildStartTime = Date.now();
|
|
161
|
-
// 9. 触发Jenkins构建并监听进度
|
|
162
179
|
await this.triggerAndMonitorBuild(jobName, params, buildStartTime);
|
|
163
180
|
}
|
|
164
181
|
|
|
@@ -199,33 +216,19 @@ class BuildCommand {
|
|
|
199
216
|
* @returns {Promise<string>} 分支名称
|
|
200
217
|
*/
|
|
201
218
|
async getBranch(options) {
|
|
202
|
-
// 如果命令行指定了分支,优先使用命令行参数
|
|
203
219
|
if (options.branch) {
|
|
204
|
-
console.log(chalk.yellow(
|
|
220
|
+
console.log(chalk.yellow(' ⚠ 使用指定分支 ') + chalk.cyan(options.branch));
|
|
205
221
|
return options.branch;
|
|
206
222
|
}
|
|
207
223
|
|
|
208
|
-
|
|
209
|
-
const spinner = ora('检测当前Git分支...').start();
|
|
224
|
+
const spinner = ora({ text: '检测 Git 分支...', indent: 2 }).start();
|
|
210
225
|
try {
|
|
211
226
|
const branch = await getCurrentBranch();
|
|
212
|
-
spinner.succeed('
|
|
227
|
+
spinner.succeed('分支 ' + chalk.cyan(branch));
|
|
213
228
|
return branch;
|
|
214
229
|
} catch (error) {
|
|
215
230
|
spinner.fail('分支检测失败');
|
|
216
|
-
throw new Error(
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async detectBranch() {
|
|
221
|
-
const spinner = ora('检测当前分支...').start();
|
|
222
|
-
try {
|
|
223
|
-
const branch = await getCurrentBranch();
|
|
224
|
-
spinner.succeed('分支检测完成');
|
|
225
|
-
return branch;
|
|
226
|
-
} catch (error) {
|
|
227
|
-
spinner.fail('分支检测失败');
|
|
228
|
-
throw new Error(`无法检测当前分支: ${error.message}`);
|
|
231
|
+
throw new Error(`无法检测 Git 分支: ${error.message}\n 请确保当前目录是 Git 仓库,或使用 --branch 指定`);
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
234
|
|
|
@@ -242,27 +245,24 @@ class BuildCommand {
|
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
async ensureJenkinsSession() {
|
|
245
|
-
|
|
246
|
-
const cookieString = this.browserAuth.cookies
|
|
247
|
-
? this.browserAuth.cookies.map(c => `${c.name}=${c.value}`).join('; ')
|
|
248
|
-
: '';
|
|
248
|
+
const spinner = ora({ text: '验证 Jenkins 会话...', indent: 2 }).start();
|
|
249
249
|
try {
|
|
250
|
-
const resp = await axios.get(this.jenkinsConfig.baseUrl
|
|
251
|
-
headers: {
|
|
252
|
-
Cookie: cookieString,
|
|
253
|
-
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
|
254
|
-
},
|
|
255
|
-
maxRedirects: 0,
|
|
256
|
-
validateStatus: s => s < 400 || s === 401 || s === 403 || s === 302
|
|
257
|
-
});
|
|
258
|
-
// 302 跳转到登录页、401/403 都视为未登录
|
|
250
|
+
const resp = await this.axios.get(this.jenkinsConfig.baseUrl);
|
|
259
251
|
if (resp.status === 401 || resp.status === 403) throw new Error('未登录');
|
|
260
252
|
if (resp.status === 302 && resp.headers.location && resp.headers.location.includes('casdoor')) throw new Error('未登录');
|
|
261
|
-
|
|
253
|
+
spinner.succeed('认证有效');
|
|
262
254
|
return true;
|
|
263
255
|
} catch (e) {
|
|
264
|
-
//
|
|
265
|
-
|
|
256
|
+
// 优先用 Casdoor cookie 静默刷新
|
|
257
|
+
spinner.text = 'Session 已过期,正在刷新...';
|
|
258
|
+
const refreshed = await this.browserAuth.refreshSessionViaCasdoor();
|
|
259
|
+
if (refreshed) {
|
|
260
|
+
this.setupJenkinsAuth();
|
|
261
|
+
spinner.succeed('Session 已刷新');
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
// Casdoor 也过期,启动浏览器重新登录
|
|
265
|
+
spinner.warn('Session 已过期,需要重新登录');
|
|
266
266
|
await this.browserAuth.login();
|
|
267
267
|
this.setupJenkinsAuth();
|
|
268
268
|
return true;
|
|
@@ -270,27 +270,24 @@ class BuildCommand {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
async triggerAndMonitorBuild(jobName, params, buildStartTime) {
|
|
273
|
-
|
|
274
|
-
const spinner = ora('正在触发Jenkins构建...').start();
|
|
273
|
+
const spinner = ora({ text: '触发构建...', indent: 2 }).start();
|
|
275
274
|
try {
|
|
276
275
|
const queueId = await this.triggerBuild(jobName, params);
|
|
277
|
-
spinner.succeed(chalk.
|
|
278
|
-
// 2. 监听队列,获取build number
|
|
276
|
+
spinner.succeed('构建已触发 ' + chalk.dim(`队列 #${queueId}`));
|
|
279
277
|
await this.monitorQueueAndBuild(jobName, queueId, buildStartTime);
|
|
280
278
|
} catch (error) {
|
|
281
|
-
spinner.fail('
|
|
282
|
-
console.error(chalk.red('
|
|
279
|
+
spinner.fail('构建触发失败');
|
|
280
|
+
console.error(chalk.red(' ' + error.message));
|
|
283
281
|
}
|
|
284
282
|
}
|
|
285
283
|
|
|
286
284
|
async triggerBuild(jobName, params) {
|
|
287
285
|
try {
|
|
288
286
|
const url = `${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/buildWithParameters`;
|
|
289
|
-
let headers = {
|
|
287
|
+
let headers = {};
|
|
290
288
|
// 1. 获取 crumb(防止 403)
|
|
291
289
|
try {
|
|
292
|
-
const crumbResp = await axios.get(`${this.jenkinsConfig.baseUrl}crumbIssuer/api/json`, {
|
|
293
|
-
headers,
|
|
290
|
+
const crumbResp = await this.axios.get(`${this.jenkinsConfig.baseUrl}crumbIssuer/api/json`, {
|
|
294
291
|
timeout: 10000
|
|
295
292
|
});
|
|
296
293
|
const { crumb, crumbRequestField } = crumbResp.data;
|
|
@@ -299,7 +296,7 @@ class BuildCommand {
|
|
|
299
296
|
// 如果 crumb 获取失败,继续尝试(部分 Jenkins 关闭了 CSRF 防护)
|
|
300
297
|
}
|
|
301
298
|
// 2. 触发构建
|
|
302
|
-
const response = await axios.post(url, null, {
|
|
299
|
+
const response = await this.axios.post(url, null, {
|
|
303
300
|
headers,
|
|
304
301
|
params: params,
|
|
305
302
|
timeout: 30000
|
|
@@ -309,113 +306,112 @@ class BuildCommand {
|
|
|
309
306
|
const location = response.headers.location;
|
|
310
307
|
const queueId = location ? location.replace(/\/$/, '').split('/').pop() : null;
|
|
311
308
|
if (!queueId) {
|
|
312
|
-
console.log(chalk.yellow('
|
|
309
|
+
console.log(chalk.yellow(' ⚠ 未能获取队列号'));
|
|
313
310
|
}
|
|
314
311
|
return queueId;
|
|
315
312
|
} else {
|
|
316
313
|
throw new Error('触发构建失败');
|
|
317
314
|
}
|
|
318
315
|
} catch (error) {
|
|
319
|
-
console.log(chalk.red('❌ Jenkins构建触发失败: ' + error.message));
|
|
320
|
-
console.log(chalk.yellow('💡 请检查Jenkins权限、Token、参数设置,或联系管理员。'));
|
|
321
316
|
throw new Error(`触发构建失败: ${error.message}`);
|
|
322
317
|
}
|
|
323
318
|
}
|
|
324
319
|
|
|
325
320
|
async monitorQueueAndBuild(jobName, queueId, buildStartTime) {
|
|
326
|
-
const spinner = ora('
|
|
321
|
+
const spinner = ora({ text: '等待分配构建号...', indent: 2 }).start();
|
|
327
322
|
let buildNumber = null;
|
|
328
323
|
let count = 0;
|
|
329
|
-
while (!buildNumber && count < 60) {
|
|
324
|
+
while (!buildNumber && count < 60) {
|
|
330
325
|
try {
|
|
331
|
-
const response = await axios.get(`${this.jenkinsConfig.baseUrl}queue/item/${queueId}/api/json`, {
|
|
332
|
-
...this.jenkinsConfig.auth,
|
|
326
|
+
const response = await this.axios.get(`${this.jenkinsConfig.baseUrl}queue/item/${queueId}/api/json`, {
|
|
333
327
|
timeout: 10000
|
|
334
328
|
});
|
|
335
329
|
const item = response.data;
|
|
336
330
|
if (item.executable && item.executable.number) {
|
|
337
331
|
buildNumber = item.executable.number;
|
|
338
|
-
spinner.succeed(chalk.
|
|
332
|
+
spinner.succeed('构建号 ' + chalk.cyan(`#${buildNumber}`));
|
|
339
333
|
break;
|
|
340
334
|
}
|
|
341
|
-
spinner.text = '等待Jenkins分配构建号...';
|
|
342
335
|
} catch (e) {
|
|
343
|
-
|
|
336
|
+
// 队列查询失败不影响主流程
|
|
344
337
|
}
|
|
345
338
|
await new Promise(r => setTimeout(r, 2000));
|
|
346
339
|
count++;
|
|
347
340
|
}
|
|
348
341
|
if (!buildNumber) {
|
|
349
|
-
spinner.fail('
|
|
342
|
+
spinner.fail('超时未分配构建号');
|
|
350
343
|
return;
|
|
351
344
|
}
|
|
352
|
-
// 监听构建进度
|
|
353
345
|
await this.monitorBuild(jobName, buildNumber, buildStartTime);
|
|
354
346
|
}
|
|
355
347
|
|
|
356
348
|
async monitorBuild(jobName, buildNumber, buildStartTime) {
|
|
349
|
+
const formatTime = (ms) => {
|
|
350
|
+
const s = Math.floor(ms / 1000);
|
|
351
|
+
return s >= 60 ? `${Math.floor(s / 60)}m${s % 60}s` : `${s}s`;
|
|
352
|
+
};
|
|
353
|
+
|
|
357
354
|
const spinner = ora({
|
|
358
|
-
text: '
|
|
355
|
+
text: '构建中 ' + chalk.dim('0s'),
|
|
356
|
+
indent: 2,
|
|
359
357
|
spinner: 'dots'
|
|
360
358
|
}).start();
|
|
359
|
+
|
|
360
|
+
// 独立定时器刷新计时,不受 API 请求阻塞
|
|
361
|
+
const timer = setInterval(() => {
|
|
362
|
+
spinner.text = '构建中 ' + chalk.dim(formatTime(Date.now() - buildStartTime));
|
|
363
|
+
}, 200);
|
|
364
|
+
|
|
361
365
|
let building = true;
|
|
362
366
|
let lastLog = '';
|
|
363
367
|
let lastLogTime = 0;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
spinner.text = `Jenkins构建中... [${buildInfo.result || 'BUILDING'}] ${duration}s`;
|
|
393
|
-
lastLog = lastLines;
|
|
368
|
+
try {
|
|
369
|
+
while (building) {
|
|
370
|
+
try {
|
|
371
|
+
const response = await this.axios.get(`${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/${buildNumber}/api/json`, {
|
|
372
|
+
timeout: 10000
|
|
373
|
+
});
|
|
374
|
+
const buildInfo = response.data;
|
|
375
|
+
building = buildInfo.building;
|
|
376
|
+
|
|
377
|
+
// 实时输出日志片段
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
if (now - lastLogTime > 2900) {
|
|
380
|
+
try {
|
|
381
|
+
const logResponse = await this.axios.get(`${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/${buildNumber}/consoleText`, {
|
|
382
|
+
timeout: 10000
|
|
383
|
+
});
|
|
384
|
+
const log = logResponse.data;
|
|
385
|
+
const lines = log.split('\n');
|
|
386
|
+
const lastLines = lines.slice(-5).join('\n');
|
|
387
|
+
if (lastLines !== lastLog) {
|
|
388
|
+
spinner.stop();
|
|
389
|
+
process.stdout.write(chalk.dim(lastLines) + '\n');
|
|
390
|
+
spinner.start();
|
|
391
|
+
lastLog = lastLines;
|
|
392
|
+
}
|
|
393
|
+
lastLogTime = now;
|
|
394
|
+
} catch (logError) {
|
|
395
|
+
// 日志获取失败不影响主流程
|
|
394
396
|
}
|
|
395
|
-
lastLogTime = now;
|
|
396
|
-
} catch (logError) {
|
|
397
|
-
// 日志获取失败不影响主流程
|
|
398
397
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
console.log(chalk.green(`⏱️ 总用时: ${timeStr}`));
|
|
409
|
-
} else {
|
|
410
|
-
console.log(chalk.red(`❌ Jenkins构建失败: ${buildInfo.result}`));
|
|
411
|
-
console.log(chalk.red(`⏱️ 总用时: ${timeStr}`));
|
|
398
|
+
if (!building) {
|
|
399
|
+
clearInterval(timer);
|
|
400
|
+
const total = formatTime(Date.now() - buildStartTime);
|
|
401
|
+
if (buildInfo.result === 'SUCCESS') {
|
|
402
|
+
spinner.succeed(chalk.green('构建成功') + chalk.dim(` ${total}`));
|
|
403
|
+
} else {
|
|
404
|
+
spinner.fail(chalk.red(`构建失败 ${buildInfo.result}`) + chalk.dim(` ${total}`));
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
412
407
|
}
|
|
413
|
-
|
|
408
|
+
} catch (e) {
|
|
409
|
+
// 请求失败不影响主流程
|
|
414
410
|
}
|
|
415
|
-
|
|
416
|
-
spinner.text = 'Jenkins构建中...';
|
|
411
|
+
await new Promise(r => setTimeout(r, 500));
|
|
417
412
|
}
|
|
418
|
-
|
|
413
|
+
} finally {
|
|
414
|
+
clearInterval(timer);
|
|
419
415
|
}
|
|
420
416
|
}
|
|
421
417
|
}
|