智能轮询 common 和 api 的构建状态,成功后再执行后续步骤
当前为
// ==UserScript==
// @name Jenkins 联合构建 (v5.0 - 智能轮询)
// @namespace http://tampermonkey.net/
// @version 5.0
// @description 智能轮询 common 和 api 的构建状态,成功后再执行后续步骤
// @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';
// -----------------------------------------------------------------
// v5.0 更新日志 (by Gemini):
// 1. [核心] 移除了 startVisibleTimer(60000) 固定时间等待。
// 2. [核心] 增加了 getBuildNumberFromQueue 和 pollBuildStatus 新函数。
// 3. [核心] startCombinedChain 现在会智能轮询 Common 和 API 的真实构建状态。
// 4. [核心] triggerSingleBuild 现在会返回 Jenkins 队列 URL (Queue URL)。
// 5. [UI] 进度条被保留,用于在轮询期间显示“处理中”的动画,而不是倒计时。
// -----------------------------------------------------------------
// 1. 定义所有函数
let statusDisplay, progressBar, progressContainer;
/**
* 辅助函数:异步等待
* @param {number} ms - 等待的毫秒数
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 更新底部悬浮窗的状态文本
* @param {string} message - 显示的消息
* @param {boolean} [isError=false] - 是否为错误消息 (红色)
*/
function updateStatus(message, isError = false) {
if (!statusDisplay) return;
console.log(message);
statusDisplay.innerText = message;
statusDisplay.style.color = isError ? 'red' : 'black';
statusDisplay.style.borderColor = isError ? 'red' : '#ccc';
}
/**
* (新增) 控制进度条的显示
* @param {boolean} show - true: 显示, false: 隐藏
* @param {string} [text] - (可选) 显示时顺便更新状态
*/
function setProgressActive(show, text) {
if (progressContainer) {
progressContainer.style.display = show ? 'block' : 'none';
}
if (show && text) {
updateStatus(text);
} else if (!show) {
// 隐藏时,如果进度条是满的 (100%),重置它
if (progressBar.style.width === '100%') {
progressBar.style.width = '0%';
}
}
}
/**
* (新增) 注入CSS动画,用于实现不确定的进度条
*/
function addStyles() {
const style = document.createElement('style');
style.textContent = `
@keyframes gm-progress-bar-stripes {
from { background-position: 40px 0; }
to { background-position: 0 0; }
}
`;
document.head.appendChild(style);
}
/**
* 获取 Jenkins Crumb (用于 POST 请求认证)
*/
function getMyJenkinsCrumb() {
const crumbInput = document.querySelector('input[name="Jenkins-Crumb"]');
if (crumbInput) {
return crumbInput.value;
}
console.error("未能找到 Jenkins Crumb input 元素。");
return null;
}
/**
* (修改) 触发单个构建
* @param {string} jobName - 任务名称 (用于日志)
* @param {string} buildUrl - 任务的 build URL
* @param {string} crumb - Jenkins Crumb
* @returns {Promise<string|null>} - 成功时返回 队列URL (Queue URL),失败时返回 null
*/
async function triggerSingleBuild(jobName, buildUrl, crumb) {
updateStatus(`[${jobName}] 正在请求构建...`);
try {
const response = await fetch(buildUrl, {
method: 'POST',
headers: { 'Jenkins-Crumb': crumb },
body: null
});
// 201 Created 是 Jenkins 接受构建请求的正确状态码
if (response.status === 201) {
const queueUrl = response.headers.get('Location');
if (!queueUrl) {
updateStatus(`[${jobName}] 构建已触发,但未找到 Queue URL!`, true);
return null;
}
updateStatus(`[${jobName}] 构建已进入队列。`);
return queueUrl;
} else {
updateStatus(`[${jobName}] 构建请求失败!状态: ${response.status}`, true);
return null;
}
} catch (error) {
updateStatus(`[${jobName}] 发送请求时发生网络错误: ${error}`, true);
return null;
}
}
/**
* (新增) 从队列中轮询获取真实的 Build 编号和 URL
* @param {string} jobName - 任务名称
* @param {string} queueUrl - triggerSingleBuild 返回的队列 URL
* @param {string} crumb - Jenkins Crumb
* @returns {Promise<object|null>} - 成功时返回 { number, url },失败时 null
*/
async function getBuildNumberFromQueue(jobName, queueUrl, crumb) {
if (!queueUrl) return null;
updateStatus(`[${jobName}] 正在等待分配构建编号...`);
const pollInterval = 2000; // 2 秒轮询一次
let attempts = 0;
const maxAttempts = 30; // 最多等待 60 秒
while (attempts < maxAttempts) {
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) {
updateStatus(`[${jobName}] 队列中的任务被取消。`, true);
return null;
}
if (data.executable) {
const buildNumber = data.executable.number;
const buildUrl = data.executable.url;
updateStatus(`[${jobName}] 已获取构建编号: #${buildNumber}`);
return { number: buildNumber, url: buildUrl };
}
// 任务仍在队列中,等待
await sleep(pollInterval);
attempts++;
} catch (error) {
updateStatus(`[${jobName}] 轮询队列失败: ${error}`, true);
return null;
}
}
updateStatus(`[${jobName}] 等待构建编号超时。`, true);
return null;
}
/**
* (新增) 轮询特定 Build 的状态,直到它完成
* @param {string} jobName - 任务名称
* @param {object} buildInfo - 包含 { number, url } 的对象
* @param {string} crumb - Jenkins Crumb
* @returns {Promise<string>} - "SUCCESS", "FAILURE", 或 "ABORTED"
*/
async function pollBuildStatus(jobName, buildInfo, crumb) {
if (!buildInfo || !buildInfo.url) {
updateStatus(`[${jobName}] 无法轮询:缺少 Build 信息。`, true);
return "FAILURE";
}
const buildUrl = buildInfo.url.endsWith('/') ? buildInfo.url : buildInfo.url + '/';
const buildNumber = buildInfo.number;
const pollInterval = 5000; // 5 秒轮询一次
let isBuilding = true;
updateStatus(`[${jobName} #${buildNumber}] 正在构建中... (每 5s 检查)`);
while (isBuilding) {
await sleep(pollInterval);
try {
const response = await fetch(`${buildUrl}api/json`, {
headers: { 'Jenkins-Crumb': crumb }
});
if (!response.ok) {
// 如果是 404,可能是 Build 仍在初始化,再试一次
if (response.status === 404) {
updateStatus(`[${jobName} #${buildNumber}] API 尚未就绪 (404),重试中...`);
continue;
}
throw new Error(`Build API 状态: ${response.status}`);
}
const data = await response.json();
if (data.building === false) {
isBuilding = false;
const result = data.result; // "SUCCESS", "FAILURE", "ABORTED"
updateStatus(`[${jobName} #${buildNumber}] 构建完成!结果: ${result}`, result !== "SUCCESS");
return result;
}
// 仍在构建中,继续循环
updateStatus(`[${jobName} #${buildNumber}] 仍在构建中...`);
} catch (error) {
updateStatus(`[${jobName} #${buildNumber}] 轮询构建状态失败: ${error}`, true);
return "FAILURE";
}
}
}
/**
* (重构) 启动联合构建链
*/
async function startCombinedChain() {
const crumb = getMyJenkinsCrumb();
if (!crumb) {
updateStatus("错误:无法获取 Crumb。", true);
return;
}
// --- 步骤 1: 同时触发 Common, API 和 Web ---
updateStatus('步骤 1: 正在同时触发 Common, API 和 Web...');
const commonUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-common/build?delay=0sec';
const apiUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-api/build?delay=0sec';
const webUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-web/build?delay=0sec';
// 注意:我们只关心 common 和 api 的队列 URL,web 触发后不管
const [commonQueueUrl, apiQueueUrl] = await Promise.all([
triggerSingleBuild('Common', commonUrl, crumb),
triggerSingleBuild('API', apiUrl, crumb),
triggerSingleBuild('Web', webUrl, crumb) // Web 也触发,但不捕获其 URL
]);
if (!commonQueueUrl || !apiQueueUrl) {
updateStatus("步骤 1 失败:Common 或 API 未能成功进入队列。构建链中止。", true);
return;
}
updateStatus('Common, API, Web 已全部触发。');
// --- 步骤 2: (新增) 从队列获取 Build 编号 ---
setProgressActive(true, '步骤 2: 正在获取 Common 和 API 的构建编号...');
const [commonBuild, apiBuild] = await Promise.all([
getBuildNumberFromQueue('Common', commonQueueUrl, crumb),
getBuildNumberFromQueue('API', apiQueueUrl, crumb)
]);
if (!commonBuild || !apiBuild) {
updateStatus("步骤 2 失败:无法获取 Common 或 API 的构建编号。构建链中止。", true);
setProgressActive(false);
return;
}
// --- 步骤 3: (新增) 轮询等待 Common 和 API 构建完成 ---
updateStatus('步骤 3: 正在等待 Common 和 API 构建完成...');
const [commonResult, apiResult] = await Promise.all([
pollBuildStatus('Common', commonBuild, crumb),
pollBuildStatus('API', apiBuild, crumb)
]);
// 轮询结束,隐藏进度条
setProgressActive(false);
// --- 步骤 4: (新增) 检查构建结果 ---
if (commonResult !== 'SUCCESS' || apiResult !== 'SUCCESS') {
updateStatus(`步骤 4 失败:Common (结果: ${commonResult}) 或 API (结果: ${apiResult}) 构建失败。构建链中止。`, true);
return;
}
updateStatus('步骤 4: Common 和 API 均已构建成功!');
// --- 步骤 5: (沿用) 触发后续构建 ---
// 注意:这里沿用旧逻辑,只是依次触发,并不等待它们完成
// 如果你也想让它们按顺序等待完成,这里的逻辑需要改成
// const billBuild = await getBuildNumberFromQueue(...);
// const billResult = await pollBuildStatus(...);
// 但目前,我们只按原样触发
updateStatus('步骤 5: 正在触发 Bill Service ...');
const billUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-bill-service/build?delay=0sec';
// (注意:这里使用 triggerSingleBuild 只是为了"触发",并不等待它完成)
let queueUrl = await triggerSingleBuild('Bill Service', billUrl, crumb);
if (!queueUrl) {
updateStatus("构建链因 Bill Service 触发失败而中止。", true);
return;
}
updateStatus('步骤 6: 正在触发 Customer Service ...');
const customerUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-customer-service/build?delay=0sec';
queueUrl = await triggerSingleBuild('Customer Service', customerUrl, crumb);
if (!queueUrl) {
updateStatus("Customer Service 触发失败。构建链结束。", true);
return;
}
updateStatus('步骤 7: 正在触发 System Service ...');
const systemUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-system-service/build?delay=0sec';
queueUrl = await triggerSingleBuild('System Service', systemUrl, crumb);
if (!queueUrl) {
updateStatus("System Service 触发失败。构建链结束。", true);
return;
}
updateStatus("联合构建链所有步骤已触发!(Common/API 已轮询,后续任务已触发)");
}
// 2. (已修改) 定义我们的 "主" 函数,用于创建和附加 UI 元素
function createUI() {
// 注入动画样式
addStyles();
// --- 1. 创建主悬浮容器 (页脚工具栏) ---
const footerBar = document.createElement('div');
footerBar.id = 'gm-footer-bar';
footerBar.style = `
position: fixed; bottom: 0; left: 0; width: 100%;
padding: 8px 12px; background-color: #f0f0f0;
border-top: 1px solid #ccc; z-index: 9997;
display: flex; justify-content: space-between;
align-items: center; box-sizing: border-box;
font-family: Arial, sans-serif;
`;
// --- 2. 创建左侧容器 (用于按钮) ---
const leftControls = document.createElement('div');
leftControls.id = 'gm-left-controls';
// --- 3. 创建右侧容器 (用于状态和进度条) ---
const rightControls = document.createElement('div');
rightControls.id = 'gm-right-controls';
rightControls.style = `
display: flex; flex-direction: column;
width: 300px; /* 稍微加宽以显示更长的状态 */
`;
// --- 状态显示 ---
statusDisplay = document.createElement('div');
statusDisplay.id = 'gm-status-display';
statusDisplay.style = `
padding: 8px; background-color: #f0f0f0;
border: 1px solid #ccc; border-radius: 4px;
width: 100%; font-size: 13px; box-sizing: border-box;
margin-bottom: 5px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
`;
statusDisplay.innerText = '准备就绪。';
statusDisplay.title = '准备就绪。'; // 鼠标悬停显示完整消息
// 重写 updateStatus 以便同时更新 title
const oldUpdateStatus = updateStatus;
updateStatus = (message, isError = false) => {
oldUpdateStatus(message, isError);
if(statusDisplay) statusDisplay.title = message;
};
// --- 进度条 (修改样式为不确定动画) ---
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;
`;
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);
// --- 按钮 ---
const combinedButton = document.createElement('button');
combinedButton.innerText = '▶ 启动联合构建';
combinedButton.style = `
padding: 8px 12px; color: white; border: none; border-radius: 4px;
cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
text-align: left; background-color: #f0ad4e;
box-sizing: border-box; font-size: 13px;
`;
combinedButton.onmouseover = function() { combinedButton.style.backgroundColor = '#ec971f'; };
combinedButton.onmouseout = function() { combinedButton.style.backgroundColor = '#f0ad4e'; };
combinedButton.onclick = startCombinedChain;
// --- 4. 组装 DOM ---
leftControls.appendChild(combinedButton);
rightControls.appendChild(statusDisplay);
rightControls.appendChild(progressContainer);
footerBar.appendChild(leftControls);
footerBar.appendChild(rightControls);
document.body.appendChild(footerBar);
updateStatus('v5.0 智能轮询已加载。');
}
// 3. 确保在 document.body 可用后才执行 UI 创建
if (document.body) {
createUI();
} else {
window.addEventListener('load', createUI);
}
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址