Jenkins 联合构建 (v5.0 - 智能轮询)

智能轮询 common 和 api 的构建状态,成功后再执行后续步骤

当前为 2025-10-30 提交的版本,查看 最新版本

// ==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或关注我们的公众号极客氢云获取最新地址