[优化] 步骤2改为并行触发。Common/API 完成后,同时触发 Bill/Customer/System/Report。
// ==UserScript==
// @name Jenkins 联合构建 (v7.3 - 全并行模式)
// @namespace http://tampermonkey.net/
// @version 7.3
// @description [优化] 步骤2改为并行触发。Common/API 完成后,同时触发 Bill/Customer/System/Report。
// @author Tandy (修改 by Gemini)
// @match http://10.9.31.83:9001/job/sz-newcis-dev/*
// @grant none
// @license MIT
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// -----------------------------------------------------------------
// v7.3 更新日志:
// 1. [流程] 步骤 2 改为并行触发 (Bill, Customer, System, Report 同时开始)。
// 2. [UI] 优化了步骤日志显示,现在支持多个并行阶段的动态显示。
// -----------------------------------------------------------------
// =================================================================
// ⚙️ [配置区] ⚙️
// =================================================================
// 重试配置
const TRIGGER_MAX_RETRIES = 3;
const TRIGGER_RETRY_DELAY = 2000;
const JOB_DEFINITIONS = {
'common': {
name: 'Common',
url: 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-common/build?delay=0sec'
},
'api': {
name: 'API',
url: 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-api/build?delay=0sec'
},
'web': {
name: 'Web',
url: 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-web/build?delay=0sec'
},
'bill': {
name: 'Bill Service',
url: 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-bill-service/build?delay=0sec'
},
'customer': {
name: 'Customer Service',
url: 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-customer-service/build?delay=0sec'
},
'system': {
name: 'System Service',
url: 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-system-service/build?delay=0sec'
},
'report': {
name: 'Report Service',
url: 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-report-service/build?delay=0sec'
}
};
const PIPELINE_STEPS = [
// 步骤 1: 基础服务 (需要等待构建完成)
{
type: 'parallel-wait',
jobs: [
{ key: 'common', wait: true }, // 阻塞
{ key: 'api', wait: true }, // 阻塞
{ key: 'web', wait: false } // 不阻塞
]
},
// 步骤 2: 业务服务 (改为并行触发,不等待结果)
{
type: 'parallel-wait',
jobs: [
{ key: 'bill', wait: false },
{ key: 'customer', wait: false },
{ key: 'system', wait: false },
{ key: 'report', wait: false }
]
}
];
// =================================================================
// 🔚 [配置区结束]
// =================================================================
// --- 1. 定义全局 UI 元素和状态标志 ---
let panelTitle, progressBar, progressContainer, stepContainer;
let combinedButton, cancelButton;
let isBuildCancelled = false;
const PANEL_TITLE_DEFAULT = '🚀 联合构建 (v7.3)';
class BuildChainError extends Error {
constructor(message) {
super(message);
this.name = 'BuildChainError';
}
}
// --- 2. 辅助函数 ---
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function addStyles() {
const style = document.createElement('style');
style.textContent = `
@keyframes gm-progress-bar-stripes {
from { background-position: 40px 0; }
to { background-position: 0 0; }
}
#gm-build-panel { margin-top: 1em; }
#gm-build-panel-title {
display: block; font-size: 1.17em; font-weight: bold;
color: #000; margin-bottom: 0.5em; padding-left: 5px;
}
#gm-build-panel .gm-button {
width: 100%; box-sizing: border-box; padding: 8px 12px;
font-size: 13px; border: none; border-radius: 4px;
cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
#gm-build-panel .gm-button:disabled { background-color: #aaa; cursor: not-allowed; }
#gm-build-panel #gm-start-btn { background-color: #f0ad4e; color: white; }
#gm-build-panel #gm-start-btn:hover:not(:disabled) { background-color: #ec971f; }
#gm-build-panel #gm-cancel-btn { background-color: #d9534f; color: white; }
#gm-build-panel #gm-cancel-btn:hover:not(:disabled) { background-color: #c9302c; }
#gm-step-container {
width: 100%; background: #fff; border: 1px solid #ccc;
border-radius: 4px; margin-top: 8px; max-height: 250px;
overflow-y: auto; font-size: 12px; box-sizing: border-box;
display: none;
}
.gm-step-strong {
min-width: 90px; display: inline-block; margin-right: 5px;
}
.gm-step-status { color: #555; }
`;
document.head.appendChild(style);
}
function getMyJenkinsCrumb() {
const crumbInput = document.querySelector('input[name="Jenkins-Crumb"]');
if (crumbInput) {
return crumbInput.value;
}
console.error("未能找到 Jenkins Crumb input 元素。");
return null;
}
// --- 3. UI 更新函数 ---
function updateStatus(message, isError = false) {
if (!panelTitle) return;
console.log(message);
panelTitle.innerText = message;
panelTitle.style.color = isError ? 'red' : 'black';
}
function updateStepStatus(jobKey, message, icon, color = 'info') {
const el = document.getElementById(`gm-step-${jobKey}`);
if (!el) return;
const iconEl = el.querySelector('.gm-step-icon');
const statusEl = el.querySelector('.gm-step-status');
if (icon) iconEl.innerText = icon;
if (message) statusEl.innerText = message;
switch (color) {
case 'success': el.style.backgroundColor = '#dff0d8'; break;
case 'warning': el.style.backgroundColor = '#fcf8e3'; break;
case 'error': el.style.backgroundColor = '#f2dede'; break;
case 'skipped': el.style.backgroundColor = '#f5f5f5'; break;
case 'info': default: el.style.backgroundColor = '#fff'; break;
}
}
function populateStepUI() {
if (!stepContainer) return;
stepContainer.innerHTML = '';
stepContainer.style.display = 'block';
for (const [key, jobData] of Object.entries(JOB_DEFINITIONS)) {
const el = document.createElement('div');
el.id = `gm-step-${key}`;
el.style = 'padding: 5px 8px; border-bottom: 1px solid #eee;';
el.innerHTML = `
<span class="gm-step-icon">⚪</span>
<strong class="gm-step-strong">${jobData.name}</strong>
<span class="gm-step-status">未开始</span>
`;
stepContainer.appendChild(el);
}
}
function skipPendingSteps() {
for (const key of Object.keys(JOB_DEFINITIONS)) {
const el = document.getElementById(`gm-step-${key}`);
if (el && el.querySelector('.gm-step-status').innerText === '未开始') {
updateStepStatus(key, '已跳过', '⏩', 'skipped');
}
}
}
function setProgressActive(show, text) {
if (progressContainer) {
progressContainer.style.display = show ? 'block' : 'none';
}
if (show && text) {
updateStatus(text);
}
}
function setBuildInProgressUI(inProgress) {
if (!combinedButton || !cancelButton || !stepContainer) return;
if (inProgress) {
combinedButton.disabled = true;
combinedButton.innerText = '▶ 正在构建...';
combinedButton.style.display = 'none';
cancelButton.style.display = 'block';
populateStepUI();
} else {
combinedButton.disabled = false;
combinedButton.innerText = '▶ 启动联合构建';
combinedButton.style.display = 'block';
cancelButton.style.display = 'none';
setProgressActive(false);
}
}
// --- 4. Jenkins API 核心函数 ---
async function triggerSingleBuild(jobKey, crumb) {
const jobData = JOB_DEFINITIONS[jobKey];
if (!jobData) throw new BuildChainError(`Job key "${jobKey}" 未在 JOB_DEFINITIONS 中定义。`);
updateStepStatus(jobKey, '正在请求...', '⏳', 'warning');
for (let attempt = 0; attempt < TRIGGER_MAX_RETRIES; attempt++) {
if (isBuildCancelled) throw new BuildChainError('构建已取消');
try {
const response = await fetch(jobData.url, {
method: 'POST',
headers: { 'Jenkins-Crumb': crumb },
body: null
});
if (response.status === 201) {
const queueUrl = response.headers.get('Location');
if (!queueUrl) {
updateStepStatus(jobKey, '触发成功,但未找到 Queue URL!', '❌', 'error');
throw new BuildChainError(`[${jobData.name}] 未找到 Queue URL`);
}
let successMsg = '已进入队列';
if (attempt > 0) successMsg = `重试成功 (第 ${attempt + 1} 次),已入队`;
updateStepStatus(jobKey, successMsg, '⏳', 'warning');
return queueUrl;
}
if (response.status >= 400 && response.status < 500) {
updateStepStatus(jobKey, `请求失败 (状态: ${response.status})`, '❌', 'error');
throw new BuildChainError(`[${jobData.name}] 构建请求失败 (状态: ${response.status})`);
}
throw new Error(`服务器状态: ${response.status}`);
} catch (error) {
console.warn(`[${jobData.name}] 触发失败 (第 ${attempt + 1} 次): ${error.message}`);
if (attempt < TRIGGER_MAX_RETRIES - 1) {
const retryMsg = `触发失败 (第 ${attempt + 1} 次),${TRIGGER_RETRY_DELAY / 1000}s 后重试...`;
updateStepStatus(jobKey, retryMsg, '⏳', 'warning');
await sleep(TRIGGER_RETRY_DELAY);
} else {
updateStepStatus(jobKey, `请求失败: ${error.message}`, '❌', 'error');
throw new BuildChainError(`[${jobData.name}] 触发失败`);
}
}
}
throw new BuildChainError(`[${jobData.name}] 未知的触发错误`);
}
async function getBuildNumberFromQueue(jobKey, queueUrl, crumb) {
const jobData = JOB_DEFINITIONS[jobKey];
if (!queueUrl) throw new BuildChainError(`[${jobData.name}] 队列 URL 为空`);
updateStepStatus(jobKey, '等待构建编号...', '⏳', 'warning');
const pollInterval = 2000;
let attempts = 0;
const maxAttempts = 30;
while (attempts < maxAttempts) {
if (isBuildCancelled) throw new BuildChainError('构建已取消');
try {
const response = await fetch(`${queueUrl}api/json`, { headers: { 'Jenkins-Crumb': crumb } });
if (!response.ok) throw new Error(`Queue API 状态: ${response.status}`);
const data = await response.json();
if (data.cancelled) {
updateStepStatus(jobKey, '任务被取消', '❌', 'error');
throw new BuildChainError(`[${jobData.name}] 队列任务被取消`);
}
if (data.executable) {
const buildNumber = data.executable.number;
const buildUrl = data.executable.url;
updateStepStatus(jobKey, `已获取: #${buildNumber}`, '⏳', 'warning');
return { number: buildNumber, url: buildUrl };
}
await sleep(pollInterval);
attempts++;
} catch (error) {
updateStepStatus(jobKey, `轮询队列失败`, '❌', 'error');
throw error;
}
}
updateStepStatus(jobKey, `等待构建编号超时`, '❌', 'error');
throw new BuildChainError(`[${jobData.name}] 等待构建编号超时`);
}
async function pollBuildStatus(jobKey, buildInfo, crumb) {
const jobData = JOB_DEFINITIONS[jobKey];
if (!buildInfo || !buildInfo.url) {
updateStepStatus(jobKey, '缺少 Build 信息', '❌', 'error');
throw new BuildChainError(`[${jobData.name}] 无法轮询`);
}
const buildUrl = buildInfo.url.endsWith('/') ? buildInfo.url : buildInfo.url + '/';
const buildNumber = buildInfo.number;
const pollInterval = 5000;
let isBuilding = true;
updateStepStatus(jobKey, `正在构建 #${buildNumber}`, '⏳', 'warning');
setProgressActive(true, `正在构建 ${jobData.name} #${buildNumber}...`);
while (isBuilding) {
if (isBuildCancelled) throw new BuildChainError('构建已取消');
await sleep(pollInterval);
try {
const response = await fetch(`${buildUrl}api/json`, { headers: { 'Jenkins-Crumb': crumb } });
if (!response.ok) {
if (response.status === 404) continue;
throw new Error(`Build API 状态: ${response.status}`);
}
const data = await response.json();
if (data.building === false) {
isBuilding = false;
const result = data.result;
if (result === 'SUCCESS') {
updateStepStatus(jobKey, `构建成功 (#${buildNumber})`, '✅', 'success');
} else {
updateStepStatus(jobKey, `构建 ${result} (#${buildNumber})`, '❌', 'error');
throw new BuildChainError(`[${jobData.name}] 构建失败`);
}
return result;
}
} catch (error) {
updateStepStatus(jobKey, `轮询状态失败`, '❌', 'error');
throw error;
}
}
}
/**
* 启动联合构建链 (核心逻辑)
*/
async function startCombinedChain() {
isBuildCancelled = false;
const crumb = getMyJenkinsCrumb();
if (!crumb) {
updateStatus("错误:无法获取 Crumb。", true);
return;
}
setBuildInProgressUI(true);
updateStatus('联合构建已启动...');
const jobBuilds = {};
let stepIndex = 0;
try {
// --- 循环执行流水线步骤 ---
for (const step of PIPELINE_STEPS) {
if (isBuildCancelled) throw new BuildChainError('构建已取消');
stepIndex++;
// --- 处理 Parallel Wait 类型 (包含 wait=true 和 wait=false) ---
if (step.type === 'parallel-wait') {
updateStatus(`步骤 ${stepIndex}: 正在并行触发...`);
// 1. 并行触发所有 Job
const triggerPromises = step.jobs.map(job =>
triggerSingleBuild(job.key, crumb)
);
const queueUrls = await Promise.all(triggerPromises);
// 2. 区分需要等待的 Job 和不需要等待的 Job
const buildInfoPromises = [];
for (let i = 0; i < step.jobs.length; i++) {
const job = step.jobs[i];
if (job.wait) {
// 如果需要等待,获取 Build Number
buildInfoPromises.push(
getBuildNumberFromQueue(job.key, queueUrls[i], crumb)
.then(buildInfo => {
jobBuilds[job.key] = buildInfo;
return buildInfo;
})
);
} else {
// 如果不需要等待,直接标记为完成
updateStepStatus(job.key, '已触发 (不等待)', '▶️', 'success');
}
}
// 等待所有需要获取 Build Number 的请求完成
await Promise.all(buildInfoPromises);
// 3. 并行轮询状态 (只轮询 wait=true 的)
const pollPromises = [];
for (const job of step.jobs) {
if (job.wait) {
pollPromises.push(
pollBuildStatus(job.key, jobBuilds[job.key], crumb)
);
}
}
await Promise.all(pollPromises);
updateStatus(`步骤 ${stepIndex}: 本阶段构建完成!`);
}
// --- 处理 Sequential (如果以后还有需要的话,目前配置里已经没了) ---
else if (step.type === 'sequential-trigger') {
updateStatus(`步骤 ${stepIndex}: 正在串行触发...`);
for (const job of step.jobs) {
if (isBuildCancelled) throw new BuildChainError('构建已取消');
await triggerSingleBuild(job.key, crumb);
updateStepStatus(job.key, '已触发', '▶️', 'success');
}
}
}
// --- 所有步骤成功 ---
updateStatus('✅ 联合构建链全部完成!', false);
setProgressActive(false);
} catch (error) {
// --- 捕获任何步骤中的失败 ---
setProgressActive(false);
if (error instanceof BuildChainError) {
updateStatus(`❌ 构建链中止: ${error.message}`, true);
} else {
updateStatus(`❌ 发生意外错误: ${error.message}`, true);
console.error(error);
}
skipPendingSteps();
} finally {
setBuildInProgressUI(false);
if (panelTitle.style.color !== 'red') {
setTimeout(() => {
if (!isBuildCancelled && combinedButton.disabled === false) {
updateStatus(PANEL_TITLE_DEFAULT, false);
}
}, 5000);
}
}
}
// --- 5. UI 创建与初始化 ---
function createUI() {
const sidePanel = document.getElementById('side-panel');
if (!sidePanel) return;
addStyles();
const mainPanel = document.createElement('div');
mainPanel.id = 'gm-build-panel';
mainPanel.className = 'task';
panelTitle = document.createElement('div');
panelTitle.id = 'gm-build-panel-title';
panelTitle.innerText = PANEL_TITLE_DEFAULT;
const controlsContainer = document.createElement('div');
controlsContainer.style = 'padding: 0 5px;';
combinedButton = document.createElement('button');
combinedButton.id = 'gm-start-btn';
combinedButton.className = 'gm-button';
combinedButton.innerText = '▶ 启动联合构建';
combinedButton.onclick = startCombinedChain;
cancelButton = document.createElement('button');
cancelButton.id = 'gm-cancel-btn';
cancelButton.className = 'gm-button';
cancelButton.innerText = '■ 取消';
cancelButton.style.display = 'none';
cancelButton.onclick = function() {
isBuildCancelled = true;
updateStatus('正在取消,请稍候...', true);
};
progressContainer = document.createElement('div');
progressContainer.id = 'gm-progress-container';
progressContainer.style = `
width: 100%; height: 10px; background-color: #e9ecef;
border: 1px solid #ced4da; border-radius: 4px;
box-sizing: border-box; display: none; overflow: hidden;
margin: 8px 0;
`;
progressBar = document.createElement('div');
progressBar.id = 'gm-progress-bar';
progressBar.style = `
height: 100%; width: 100%; background-color: #007bff;
border-radius: 2px;
background-size: 40px 40px;
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
animation: gm-progress-bar-stripes 1s linear infinite;
`;
progressContainer.appendChild(progressBar);
stepContainer = document.createElement('div');
stepContainer.id = 'gm-step-container';
controlsContainer.appendChild(combinedButton);
controlsContainer.appendChild(cancelButton);
mainPanel.appendChild(panelTitle);
mainPanel.appendChild(controlsContainer);
mainPanel.appendChild(progressContainer);
mainPanel.appendChild(stepContainer);
sidePanel.appendChild(mainPanel);
const oldUpdateStatus = updateStatus;
updateStatus = (message, isError = false) => {
if (panelTitle) oldUpdateStatus(message, isError);
};
console.log('Jenkins 联合构建 (v7.3 - 全并行模式) 已加载。');
}
if (document.body) createUI();
else window.addEventListener('load', createUI);
})();