UNPKG

long-git-cli

Version:

A CLI tool for Git tag management.

1,023 lines (1,022 loc) 63.7 kB
"use strict"; /** * Web UI 服务器 * 提供配置管理的 Web 界面 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebUIServer = void 0; const koa_1 = __importDefault(require("koa")); const router_1 = __importDefault(require("@koa/router")); const koa_bodyparser_1 = __importDefault(require("koa-bodyparser")); const koa_static_1 = __importDefault(require("koa-static")); const path = __importStar(require("path")); const open_1 = __importDefault(require("open")); const constants_1 = require("../constants"); const config_manager_1 = require("../config/config-manager"); const bitbucket_client_1 = require("../api/bitbucket-client"); const jenkins_client_1 = require("../api/jenkins-client"); /** * Web UI 服务器类 */ class WebUIServer { constructor(configManager, port = constants_1.WEB_UI_PORT, host = constants_1.WEB_UI_HOST) { this.app = new koa_1.default(); this.router = new router_1.default(); this.port = port; this.host = host; this.configManager = configManager || new config_manager_1.ConfigManager(); this.setupMiddleware(); this.setupRoutes(); } /** * 配置中间件 */ setupMiddleware() { /** 错误处理中间件 */ this.app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.status || 500; ctx.body = { error: err.message || "Internal Server Error", }; console.error("Server error:", err); } }); /** 请求日志中间件 */ this.app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); /** Body parser 中间件 */ this.app.use((0, koa_bodyparser_1.default)()); /** 静态文件服务 */ /** 在开发和生产环境中都指向源代码的 public 目录 */ const publicPath = path.join(__dirname, "../../../src/devops/ui/public"); this.app.use((0, koa_static_1.default)(publicPath)); } /** * 配置路由 */ setupRoutes() { /** API 路由前缀 - 必须在注册路由之前设置 */ this.router.prefix("/api"); /** 健康检查端点 */ this.router.get("/health", async (ctx) => { ctx.body = { status: "ok", timestamp: new Date().toISOString(), }; }); /** 获取配置 */ this.router.get("/config", async (ctx) => { try { const config = await this.configManager.loadConfig(); /** 直接返回加密后的 token(前端用于比较是否修改) */ const bitbucketEncryptedToken = config.bitbucket.apiTokenHash || config.bitbucket.appPasswordHash || ''; /** Jenkins tokens */ const jenkinsWithTokens = config.jenkins.map((j) => ({ type: j.type, url: j.url, username: j.username, hasToken: !!j.apiTokenHash, encryptedToken: j.apiTokenHash || '', // 返回加密后的 token })); ctx.body = { version: config.version, bitbucket: { username: config.bitbucket.username, hasPassword: !!bitbucketEncryptedToken, encryptedToken: bitbucketEncryptedToken, // 返回加密后的 token }, jenkins: jenkinsWithTokens, projects: config.projects, }; } catch (error) { ctx.status = 500; ctx.body = { error: error.message }; } }); /** 保存配置 */ this.router.post("/config", async (ctx) => { try { const { bitbucket, jenkins } = ctx.request.body; /** 更新 Bitbucket 配置 */ if (bitbucket?.username && bitbucket?.appPassword) { // appPassword 字段兼容新旧两种方式(API Token 或 App Password) await this.configManager.updateBitbucketConfig(bitbucket.username, bitbucket.appPassword); } /** 更新 Jenkins 配置 */ if (jenkins && Array.isArray(jenkins)) { const jenkinsConfigs = await Promise.all(jenkins.map(async (j) => { /** 缓存明文 token */ if (j.apiToken) { await this.configManager.addJenkinsInstance(j.type, j.url, j.username, j.apiToken); } return { type: j.type, url: j.url, username: j.username, apiTokenHash: await this.configManager.encryptToken(j.apiToken), }; })); await this.configManager.updateJenkinsConfig(jenkinsConfigs); } ctx.body = { success: true, message: "配置保存成功" }; } catch (error) { ctx.status = 500; ctx.body = { error: error.message }; } }); /** 获取项目列表 */ this.router.get("/projects", async (ctx) => { try { const config = await this.configManager.loadConfig(); ctx.body = { projects: config.projects }; } catch (error) { ctx.status = 500; ctx.body = { error: error.message }; } }); /** 添加项目 */ this.router.post("/projects", async (ctx) => { try { const projectConfig = ctx.request.body; console.log('添加项目请求:'); console.log(' 项目路径:', projectConfig.path); console.log(' 项目名称:', projectConfig.name); console.log(' 完整配置:', JSON.stringify(projectConfig, null, 2)); await this.configManager.updateProjectConfig(projectConfig.path, projectConfig); console.log('项目添加成功'); ctx.body = { success: true, message: "项目添加成功" }; } catch (error) { console.error('添加项目失败:', error); console.error('错误堆栈:', error.stack); ctx.status = 500; ctx.body = { error: error.message }; } }); /** 更新项目 */ this.router.put("/projects/:path", async (ctx) => { try { const projectPath = decodeURIComponent(ctx.params.path); const projectConfig = ctx.request.body; console.log('更新项目请求:'); console.log(' 项目路径:', projectPath); console.log(' 完整配置:', JSON.stringify(projectConfig, null, 2)); await this.configManager.updateProjectConfig(projectPath, projectConfig); console.log('项目更新成功'); ctx.body = { success: true, message: "项目更新成功" }; } catch (error) { console.error('更新项目失败:', error); console.error('错误堆栈:', error.stack); ctx.status = 500; ctx.body = { error: error.message }; } }); /** 删除项目 */ this.router.delete("/projects/:path", async (ctx) => { try { const projectPath = decodeURIComponent(ctx.params.path); await this.configManager.deleteProjectConfig(projectPath); ctx.body = { success: true, message: "项目删除成功" }; } catch (error) { ctx.status = 500; ctx.body = { error: error.message }; } }); /** 解析 Git 仓库信息 */ this.router.post("/parse-git-repo", async (ctx) => { try { const { projectPath } = ctx.request.body; if (!projectPath) { ctx.status = 400; ctx.body = { error: "项目路径不能为空" }; return; } const gitInfo = await this.parseGitRepo(projectPath); ctx.body = gitInfo; } catch (error) { ctx.status = 500; ctx.body = { error: error.message }; } }); /** 选择文件夹(使用系统对话框) */ this.router.post("/select-folder", async (ctx) => { try { const folderPath = await this.selectFolder(); ctx.body = { path: folderPath }; } catch (error) { ctx.status = 500; ctx.body = { error: error.message }; } }); /** 测试 Jenkins 部署(不依赖配置文件) */ this.router.post("/test-jenkins-deploy", async (ctx) => { try { const { url, username, token, jobName, parameters } = ctx.request.body; console.log('Jenkins 测试部署请求:'); console.log(' url:', url); console.log(' username:', username); console.log(' jobName:', jobName); console.log(' parameters:', parameters); if (!url || !username || !token || !jobName || !parameters) { ctx.status = 400; ctx.body = { error: "缺少必要参数" }; return; } console.log('创建 Jenkins 客户端...'); /** 创建 Jenkins 客户端 */ const jenkinsClient = new jenkins_client_1.JenkinsClient(url, username, token); console.log('创建 Jenkins 部署器...'); /** 创建 Jenkins 部署器 */ const { JenkinsDeployer } = await Promise.resolve().then(() => __importStar(require("../deployer/jenkins-deployer"))); const deployer = new JenkinsDeployer(jenkinsClient); console.log('开始触发部署...'); /** 触发部署并等待完成 */ const result = await deployer.deploy(jobName, parameters, { pollInterval: 10000, // 10 秒 timeout: 30 * 60 * 1000, // 30 分钟 onProgress: (build) => { console.log(`部署进度: #${build.number} - ${build.result || "BUILDING"}`); }, }); console.log('部署成功:', result); ctx.body = { success: true, buildNumber: result.number, result: result.result, url: result.url, message: "部署成功", }; } catch (error) { console.error('Jenkins 测试部署失败:', error); console.error('错误堆栈:', error.stack); ctx.status = 500; ctx.body = { error: error.message }; } }); /** 触发 Jenkins 部署 */ this.router.post("/deploy", async (ctx) => { try { const { jenkinsType, jobName, parameters } = ctx.request.body; console.log('Jenkins 部署请求:'); console.log(' jenkinsType:', jenkinsType); console.log(' jobName:', jobName); console.log(' parameters:', parameters); if (!jenkinsType || !jobName || !parameters) { ctx.status = 400; ctx.body = { error: "缺少必要参数" }; return; } /** 加载配置 */ const config = await this.configManager.loadConfig(); console.log('已加载配置,Jenkins 实例数量:', config.jenkins.length); /** 查找对应的 Jenkins 实例 */ const jenkinsConfig = config.jenkins.find((j) => j.type === jenkinsType); if (!jenkinsConfig) { console.error('未找到 Jenkins 实例:', jenkinsType); ctx.status = 400; ctx.body = { error: `未找到 Jenkins 实例: ${jenkinsType}` }; return; } console.log('找到 Jenkins 配置:', jenkinsConfig.url); /** 获取缓存的明文 token */ const apiToken = await this.configManager.getJenkinsToken(jenkinsType); if (!apiToken) { console.error('未找到 Jenkins token'); ctx.status = 400; ctx.body = { error: `未找到 Jenkins 凭证,请先在配置页面保存 Jenkins 配置`, }; return; } console.log('已获取 Jenkins token'); /** 创建 Jenkins 客户端 */ const { JenkinsClient } = await Promise.resolve().then(() => __importStar(require("../api/jenkins-client"))); const jenkinsClient = new JenkinsClient(jenkinsConfig.url, jenkinsConfig.username, apiToken); console.log('创建 Jenkins 客户端成功'); /** 创建 Jenkins 部署器 */ const { JenkinsDeployer } = await Promise.resolve().then(() => __importStar(require("../deployer/jenkins-deployer"))); const deployer = new JenkinsDeployer(jenkinsClient); console.log('开始触发部署...'); /** 触发部署并等待完成 */ const result = await deployer.deploy(jobName, parameters, { pollInterval: 10000, // 10 秒 timeout: 30 * 60 * 1000, // 30 分钟 onProgress: (build) => { console.log(`部署进度: #${build.number} - ${build.result || "BUILDING"}`); }, }); console.log('部署成功:', result); ctx.body = { success: true, buildNumber: result.number, result: result.result, url: result.url, message: "部署成功", }; } catch (error) { console.error('Jenkins 部署失败:', error); console.error('错误堆栈:', error.stack); ctx.status = 500; ctx.body = { error: error.message }; } }); /** 一键部署 - SSE 版本 */ this.router.get("/full-deploy-stream", async (ctx) => { const { projectPath, environment } = ctx.query; console.log('一键部署请求 (SSE):'); console.log(' projectPath:', projectPath); console.log(' environment:', environment); if (!projectPath || !environment) { ctx.status = 400; ctx.body = { error: "缺少必要参数" }; return; } // 设置 SSE 响应头 ctx.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); ctx.status = 200; // 发送 SSE 消息的辅助函数 const sendEvent = (event, data) => { ctx.res.write(`event: ${event}\n`); ctx.res.write(`data: ${JSON.stringify(data)}\n\n`); }; try { sendEvent('log', { message: '开始一键部署...', type: 'info' }); sendEvent('log', { message: `项目: ${projectPath}`, type: 'info' }); sendEvent('log', { message: `环境: ${environment}`, type: 'info' }); /** 加载配置 */ const config = await this.configManager.loadConfig(); const projectConfig = config.projects[projectPath]; if (!projectConfig) { sendEvent('error', { message: `未找到项目配置: ${projectPath}` }); ctx.res.end(); return; } const environmentConfig = projectConfig.environments[environment]; if (!environmentConfig) { sendEvent('error', { message: `未找到环境配置: ${environment}` }); ctx.res.end(); return; } /** 获取 Bitbucket 凭证 */ const bitbucketPassword = await this.configManager.getBitbucketPassword(); if (!bitbucketPassword) { sendEvent('error', { message: '未找到 Bitbucket 凭证' }); ctx.res.end(); return; } /** 获取 Jenkins 凭证 */ const jenkinsConfig = config.jenkins.find((j) => j.type === projectConfig.jenkinsInstance); if (!jenkinsConfig) { sendEvent('error', { message: `未找到 Jenkins 实例: ${projectConfig.jenkinsInstance}` }); ctx.res.end(); return; } const jenkinsToken = await this.configManager.getJenkinsToken(projectConfig.jenkinsInstance); if (!jenkinsToken) { sendEvent('error', { message: '未找到 Jenkins 凭证' }); ctx.res.end(); return; } const bitbucketClient = new bitbucket_client_1.BitbucketClient(config.bitbucket.username, bitbucketPassword); const { JenkinsClient } = await Promise.resolve().then(() => __importStar(require("../api/jenkins-client"))); const jenkinsClient = new JenkinsClient(jenkinsConfig.url, jenkinsConfig.username, jenkinsToken); const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process"))); /** 步骤 1: 创建并推送 Tag */ sendEvent('log', { message: '步骤 1/3: 创建并推送 Tag', type: 'info' }); sendEvent('log', { message: '获取最新 tag...', type: 'info' }); const tagFormat = environmentConfig.tagFormat; const tags = execSync('git tag --sort=-version:refname', { cwd: projectPath, encoding: 'utf-8', }).trim().split('\n').filter(t => t); const formatPattern = tagFormat.replace(/0+/g, (match) => `\\d{${match.length}}`); const regex = new RegExp(`^${formatPattern}$`); const matchingTags = tags.filter(tag => regex.test(tag)); let newTag; if (matchingTags.length === 0) { newTag = tagFormat.replace(/0+/, (match) => '1'.padStart(match.length, '0')); sendEvent('log', { message: `没有匹配的 tag,创建第一个: ${newTag}`, type: 'info' }); } else { const latestTag = matchingTags[0]; sendEvent('log', { message: `最新 tag: ${latestTag}`, type: 'info' }); const numbers = latestTag.match(/\d+/g); if (!numbers || numbers.length === 0) { throw new Error('无法从 tag 中提取数字'); } const lastNumber = numbers[numbers.length - 1]; const incrementedNumber = (parseInt(lastNumber) + 1).toString().padStart(lastNumber.length, '0'); let count = 0; newTag = latestTag.replace(/\d+/g, (match) => { count++; return count === numbers.length ? incrementedNumber : match; }); sendEvent('log', { message: `新 tag: ${newTag}`, type: 'info' }); } sendEvent('log', { message: '创建 tag...', type: 'info' }); execSync(`git tag ${newTag}`, { cwd: projectPath }); sendEvent('log', { message: '推送 tag...', type: 'info' }); execSync(`git push origin ${newTag}`, { cwd: projectPath }); sendEvent('log', { message: `Tag 创建并推送成功: ${newTag}`, type: 'success' }); /** 更新部署状态到配置文件 */ await this.configManager.updateDeployStatus(projectPath, environment, { status: 'building', tagName: newTag, startTime: new Date().toISOString(), step: 'pipeline', }); /** 步骤 2: 监听构建状态 */ sendEvent('log', { message: '步骤 2/3: 监听构建状态', type: 'info' }); sendEvent('log', { message: '等待构建状态...', type: 'info' }); const { PipelineMonitor } = await Promise.resolve().then(() => __importStar(require("../monitor/pipeline-monitor"))); const monitor = new PipelineMonitor(bitbucketClient); const buildResult = await monitor.monitorBuildStatus(projectConfig.repository.workspace, projectConfig.repository.repoSlug, newTag, { pollInterval: 15000, // 15 秒轮询一次 timeout: 30 * 60 * 1000, // 30 分钟超时 onProgress: (data) => { const statuses = data.statuses || []; if (statuses.length > 0) { statuses.forEach((status) => { // 计算已用时间 let elapsedTime = ''; if (status.created_on) { const createdTime = new Date(status.created_on).getTime(); const now = Date.now(); const elapsed = Math.floor((now - createdTime) / 1000); const minutes = Math.floor(elapsed / 60); const seconds = elapsed % 60; elapsedTime = ` (${minutes}分${seconds}秒)`; } // 直接使用 status.state,它包含完整信息 const stateStr = typeof status.state === 'object' ? JSON.stringify(status.state) : status.state; sendEvent('log', { message: `${status.name || status.key}: ${stateStr}${elapsedTime}`, type: status.state === 'SUCCESSFUL' ? 'success' : status.state === 'FAILED' ? 'error' : 'info' }); }); } }, }); sendEvent('log', { message: `构建完成`, type: 'success' }); /** 更新部署状态 */ await this.configManager.updateDeployStatus(projectPath, environment, { status: 'deploying', tagName: newTag, step: 'jenkins', }); /** 步骤 3: Jenkins 部署 */ sendEvent('log', { message: '步骤 3/3: Jenkins 部署', type: 'info' }); sendEvent('log', { message: `触发 Jenkins Job: ${environmentConfig.jenkinsJobName}`, type: 'info' }); const { JenkinsDeployer } = await Promise.resolve().then(() => __importStar(require("../deployer/jenkins-deployer"))); const deployer = new JenkinsDeployer(jenkinsClient); const jenkinsResult = await deployer.deploy(environmentConfig.jenkinsJobName, { action: 'approve' }, // 固定传 approve { pollInterval: 10000, timeout: 30 * 60 * 1000, onProgress: (build) => { const resultIcon = build.result === 'SUCCESS' ? '' : build.result === 'FAILURE' ? '' : build.building ? '' : ''; sendEvent('log', { message: `${resultIcon} 构建 #${build.number}: ${build.result || 'BUILDING'} (${new Date().toLocaleTimeString()})`, type: build.result === 'SUCCESS' ? 'success' : build.result === 'FAILURE' ? 'error' : 'info' }); if (build.duration > 0) { const durationMin = Math.floor(build.duration / 60000); const durationSec = Math.floor((build.duration % 60000) / 1000); sendEvent('log', { message: ` 耗时: ${durationMin}分${durationSec}秒`, type: 'info' }); } }, }); sendEvent('log', { message: `Jenkins 部署成功`, type: 'success' }); sendEvent('log', { message: `构建号: #${jenkinsResult.number}`, type: 'info' }); sendEvent('log', { message: `结果: ${jenkinsResult.result}`, type: 'success' }); sendEvent('log', { message: `构建 URL: ${jenkinsResult.url}`, type: 'info' }); /** 更新部署状态为完成 */ await this.configManager.updateDeployStatus(projectPath, environment, { status: 'success', tagName: newTag, step: 'completed', jenkinsBuildNumber: jenkinsResult.number, jenkinsBuildUrl: jenkinsResult.url, completedTime: new Date().toISOString(), }); sendEvent('complete', { tagName: newTag, buildStatuses: buildResult, jenkinsBuildNumber: jenkinsResult.number, jenkinsBuildUrl: jenkinsResult.url, message: '一键部署成功', }); } catch (error) { console.error('一键部署失败:', error); sendEvent('error', { message: error.message }); /** 更新部署状态为失败 */ try { const config = await this.configManager.loadConfig(); const projectConfig = config.projects[projectPath]; if (projectConfig) { const currentStatus = projectConfig.environments[environment]?.deployStatus; await this.configManager.updateDeployStatus(projectPath, environment, { status: 'failed', tagName: currentStatus?.tagName || '', step: currentStatus?.step || 'unknown', error: error.message, failedTime: new Date().toISOString(), }); } } catch (updateError) { console.error('更新失败状态时出错:', updateError); } } ctx.res.end(); }); /** 完整部署流程 */ this.router.post("/full-deploy", async (ctx) => { try { const { projectPath, environment, createNewTag, tagName } = ctx.request.body; if (!projectPath || !environment) { ctx.status = 400; ctx.body = { error: "缺少必要参数" }; return; } if (!createNewTag && !tagName) { ctx.status = 400; ctx.body = { error: "使用现有 tag 时必须提供 tag 名称" }; return; } /** 加载配置 */ const config = await this.configManager.loadConfig(); /** 获取项目配置 */ const projectConfig = config.projects[projectPath]; if (!projectConfig) { ctx.status = 400; ctx.body = { error: `未找到项目配置: ${projectPath}` }; return; } /** 获取环境配置 */ const environmentConfig = projectConfig.environments[environment]; if (!environmentConfig) { ctx.status = 400; ctx.body = { error: `未找到环境配置: ${environment}` }; return; } /** 获取 Bitbucket 凭证 */ const bitbucketPassword = await this.configManager.getBitbucketPassword(); if (!bitbucketPassword) { ctx.status = 400; ctx.body = { error: "未找到 Bitbucket 凭证,请先在配置页面保存配置", }; return; } /** 获取 Jenkins 凭证 */ const jenkinsConfig = config.jenkins.find((j) => j.type === projectConfig.jenkinsInstance); if (!jenkinsConfig) { ctx.status = 400; ctx.body = { error: `未找到 Jenkins 实例: ${projectConfig.jenkinsInstance}`, }; return; } const jenkinsToken = await this.configManager.getJenkinsToken(projectConfig.jenkinsInstance); if (!jenkinsToken) { ctx.status = 400; ctx.body = { error: "未找到 Jenkins 凭证,请先在配置页面保存配置", }; return; } /** 创建客户端 */ const { BitbucketClient } = await Promise.resolve().then(() => __importStar(require("../api/bitbucket-client"))); const { JenkinsClient } = await Promise.resolve().then(() => __importStar(require("../api/jenkins-client"))); const bitbucketClient = new BitbucketClient(config.bitbucket.username, bitbucketPassword); const jenkinsClient = new JenkinsClient(jenkinsConfig.url, jenkinsConfig.username, jenkinsToken); /** 创建完整部署器 */ const { FullDeployer } = await Promise.resolve().then(() => __importStar(require("../deployer/full-deployer"))); const deployer = new FullDeployer(bitbucketClient, jenkinsClient, projectConfig, environmentConfig); /** 执行部署 */ const result = await deployer.deploy({ createNewTag, tagName, onProgress: (progress) => { console.log(`[${progress.step}] ${progress.message}`); }, }); ctx.body = result; } catch (error) { ctx.status = 500; ctx.body = { error: error.message }; } }); /** 查询固定 Tag 的构建状态 - SSE 版本 */ this.router.get("/query-tag-status-stream", async (ctx) => { const { workspace, repoSlug, fixedTag } = ctx.query; console.log('查询固定 Tag 请求 (SSE):'); console.log(' workspace:', workspace); console.log(' repoSlug:', repoSlug); console.log(' fixedTag:', fixedTag); if (!workspace || !repoSlug || !fixedTag) { ctx.status = 400; ctx.body = { error: "缺少必要参数" }; return; } // 设置 SSE 响应头 ctx.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); ctx.status = 200; // 发送 SSE 消息的辅助函数 const sendEvent = (event, data) => { ctx.res.write(`event: ${event}\n`); ctx.res.write(`data: ${JSON.stringify(data)}\n\n`); }; try { sendEvent('log', { message: `查询 Tag: ${fixedTag}`, type: 'info' }); sendEvent('log', { message: `仓库: ${workspace}/${repoSlug}`, type: 'info' }); /** 获取 Bitbucket 凭证 */ const bitbucketPassword = await this.configManager.getBitbucketPassword(); if (!bitbucketPassword) { sendEvent('error', { message: '未找到 Bitbucket 凭证' }); ctx.res.end(); return; } const config = await this.configManager.loadConfig(); const bitbucketClient = new bitbucket_client_1.BitbucketClient(config.bitbucket.username, bitbucketPassword); sendEvent('log', { message: '获取 tag 对应的 commit...', type: 'info' }); /** 获取 tag 对应的 commit hash */ const commitHash = await bitbucketClient.getTagCommit(workspace, repoSlug, fixedTag); sendEvent('log', { message: `Commit: ${commitHash.substring(0, 7)}`, type: 'success' }); sendEvent('log', { message: '查询构建状态...', type: 'info' }); /** 获取 commit 的 build status */ const statuses = await bitbucketClient.getCommitBuildStatus(workspace, repoSlug, commitHash); if (statuses.length === 0) { sendEvent('log', { message: '未找到构建状态', type: 'warning' }); sendEvent('complete', { tagName: fixedTag, commitHash, buildStatuses: [], message: '未找到构建状态', }); } else { sendEvent('log', { message: `找到 ${statuses.length} 个构建状态`, type: 'info' }); sendEvent('log', { message: '', type: 'info' }); // 空行 statuses.forEach((status) => { // 计算已用时间 let elapsedTime = ''; if (status.created_on) { const createdTime = new Date(status.created_on).getTime(); const now = Date.now(); const elapsed = Math.floor((now - createdTime) / 1000); const minutes = Math.floor(elapsed / 60); const seconds = elapsed % 60; elapsedTime = ` (${minutes}分${seconds}秒)`; } // 直接使用 status.state,它包含完整信息 const stateStr = typeof status.state === 'object' ? JSON.stringify(status.state) : status.state; sendEvent('log', { message: `${status.name || status.key}: ${stateStr}${elapsedTime}`, type: status.state === 'SUCCESSFUL' ? 'success' : status.state === 'FAILED' ? 'error' : 'info' }); }); sendEvent('log', { message: '', type: 'info' }); // 空行 const allSuccessful = statuses.every((s) => s.state === 'SUCCESSFUL'); const anyFailed = statuses.some((s) => s.state === 'FAILED'); const anyInProgress = statuses.some((s) => s.state === 'INPROGRESS'); if (allSuccessful) { sendEvent('log', { message: '所有构建已成功完成', type: 'success' }); } else if (anyFailed) { sendEvent('log', { message: '部分构建失败', type: 'error' }); } else if (anyInProgress) { sendEvent('log', { message: '部分构建仍在进行中', type: 'info' }); } else { sendEvent('log', { message: '构建状态未知', type: 'warning' }); } sendEvent('complete', { tagName: fixedTag, commitHash, buildStatuses: statuses, message: '查询完成', }); } } catch (error) { console.error('查询失败:', error); sendEvent('error', { message: error.message }); } ctx.res.end(); }); /** 测试 Pipeline(创建 Tag 并监听)- SSE 版本 */ this.router.get("/test-pipeline-stream", async (ctx) => { const { projectPath, workspace, repoSlug, tagFormat } = ctx.query; console.log('Pipeline 测试请求 (SSE):'); console.log(' projectPath:', projectPath); console.log(' workspace:', workspace); console.log(' repoSlug:', repoSlug); console.log(' tagFormat:', tagFormat); if (!projectPath || !workspace || !repoSlug || !tagFormat) { ctx.status = 400; ctx.body = { error: "缺少必要参数" }; return; } // 设置 SSE 响应头 ctx.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); ctx.status = 200; // 发送 SSE 消息的辅助函数 const sendEvent = (event, data) => { ctx.res.write(`event: ${event}\n`); ctx.res.write(`data: ${JSON.stringify(data)}\n\n`); }; try { sendEvent('log', { message: '开始创建 Tag...', type: 'info' }); sendEvent('log', { message: `项目: ${projectPath}`, type: 'info' }); sendEvent('log', { message: `仓库: ${workspace}/${repoSlug}`, type: 'info' }); sendEvent('log', { message: `格式: ${tagFormat}`, type: 'info' }); /** 获取 Bitbucket 凭证 */ const bitbucketPassword = await this.configManager.getBitbucketPassword(); if (!bitbucketPassword) { sendEvent('error', { message: '未找到 Bitbucket 凭证' }); ctx.res.end(); return; } const config = await this.configManager.loadConfig(); const bitbucketClient = new bitbucket_client_1.BitbucketClient(config.bitbucket.username, bitbucketPassword); const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process"))); sendEvent('log', { message: '获取最新 tag...', type: 'info' }); /** 获取匹配格式的最新 tag */ const tags = execSync('git tag --sort=-version:refname', { cwd: projectPath, encoding: 'utf-8', }).trim().split('\n').filter(t => t); const formatPattern = tagFormat.replace(/0+/g, (match) => `\\d{${match.length}}`); const regex = new RegExp(`^${formatPattern}$`); const matchingTags = tags.filter(tag => regex.test(tag)); let newTag; if (matchingTags.length === 0) { newTag = tagFormat.replace(/0+/, (match) => '1'.padStart(match.length, '0')); sendEvent('log', { message: `没有匹配的 tag,创建第一个: ${newTag}`, type: 'info' }); } else { const latestTag = matchingTags[0]; sendEvent('log', { message: `最新 tag: ${latestTag}`, type: 'info' }); const numbers = latestTag.match(/\d+/g); if (!numbers || numbers.length === 0) { throw new Error('无法从 tag 中提取数字'); } const lastNumber = numbers[numbers.length - 1]; const incrementedNumber = (parseInt(lastNumber) + 1).toString().padStart(lastNumber.length, '0'); let count = 0; newTag = latestTag.replace(/\d+/g, (match) => { count++; return count === numbers.length ? incrementedNumber : match; }); sendEvent('log', { message: `新 tag: ${newTag}`, type: 'info' }); } /** 创建并推送 tag */ sendEvent('log', { message: '创建 tag...', type: 'info' }); execSync(`git tag ${newTag}`, { cwd: projectPath }); sendEvent('log', { message: '推送 tag...', type: 'info' }); execSync(`git push origin ${newTag}`, { cwd: projectPath }); sendEvent('log', { message: `Tag 创建并推送成功: ${newTag}`, type: 'success' }); /** 监听 Build Status */ sendEvent('log', { message: '等待构建状态...', type: 'info' }); const { PipelineMonitor } = await Promise.resolve().then(() => __importStar(require("../monitor/pipeline-monitor"))); const monitor = new PipelineMonitor(bitbucketClient); const result = await monitor.monitorBuildStatus(workspace, repoSlug, newTag, { pollInterval: 15000, // 15 秒 timeout: 30 * 60 * 1000, // 30 分钟 onProgress: (data) => { const statuses = data.statuses || []; statuses.forEach((status) => { sendEvent('log', { message: `${status.name || status.key}: ${status.state}`, type: 'info' }); }); }, }); sendEvent('log', { message: `构建完成`, type: 'success' }); sendEvent('complete', { tagName: newTag, buildStatuses: result, message: 'Tag 创建成功,构建完成', }); } catch (error) { console.error('Pipeline 测试失败:', error); sendEvent('error', { message: error.message }); } ctx.res.end(); }); /** 测试 Pipeline(创建 Tag 并监听) */ this.router.post("/test-pipeline", async (ctx) => { try { const { projectPath, workspace, repoSlug, tagFormat } = ctx.request.body; console.log('Pipeline 测试请求:'); console.log(' projectPath:', projectPath); console.log(' workspace:', workspace); console.log(' repoSlug:', repoSlug); console.log(' tagFormat:', tagFormat); if (!projectPath || !workspace || !repoSlug || !tagFormat) { ctx.status = 400; ctx.body = { error: "缺少必要参数" }; return; } /** 获取 Bitbucket 凭证 */ const bitbucketPassword = await this.configManager.getBitbucketPassword(); if (!bitbucketPassword) { ctx.status = 400; ctx.body = { error: "未找到 Bitbucket 凭证,请先在配置页面保存配置", }; return; } const config = await this.configManager.loadConfig(); const bitbucketClient = new bitbucket_client_1.BitbucketClient(config.bitbucket.username, bitbucketPassword); /** 导入所需模块 */ const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process"))); const path = await Promise.resolve().then(() => __importStar(require("path"))); console.log('获取最新 tag...'); /** 获取匹配格式的最新 tag */ try { const tags = execSync('git tag --sort=-version:refname', { cwd: projectPath, encoding: 'utf-8', }).trim().split('\n').filter(t => t); console.log('所有 tags:', tags.slice(0, 10)); /** 提取格式中的数字部分模式 */ const formatPattern = tagFormat.replace(/0+/g, (match) => `\\d{${match.length}}`); const regex = new RegExp(`^${formatPattern}$`); console.log('匹配模式:', formatPattern); /** 找到匹配格式的最新 tag */ const matchingTags = tags.filter(tag => regex.test(tag)); console.log('匹配的 tags:', matchingTags.slice(0, 5)); let newTag; if (matchingTags.length === 0) { /** 没有匹配的 tag,使用格式本身(将第一个数字序列设为 1) */ newTag = tagFormat.replace(/0+/, (match) => '1'.padStart(match.length, '0')); console.log('没有匹配的 tag,创建第一个:', newTag); } else { /** 获取最新的 tag 并叠加 */ const latestTag = matchingTags[0]; console.log('最新 tag:', latestTag); /** 提取所有数字序列并叠加最后一个 */ const numbers = latestTag.match(/\d+/g); if (!numbers || numbers.length === 0) { throw new Error('无法从 tag 中提取数字'); } /** 叠加最后一个数字序列 */ const lastNumber = numbers[numbers.length - 1]; const incrementedNumber = (parseInt(lastNumber) + 1).toString().padStart(lastNumber.length, '0'); /** 替换最后一个数字序列 */ let count = 0; newTag = latestTag.replace(/\d+/g, (match) => { count++; return count === numbers.length ? incrementedNumber : match; }); console.log('新 tag:', newTag); } /** 创建并推送 tag */ console.log('创建 tag...'); execSync(`git tag ${newTag}`, { cwd: projectPath }); console.log('推送 tag...'); execSync(`git push origin ${newTag}`, { cwd: projectPath }); console.log('Tag 创建并推送成功:', newTag); /** 监听 Pipeline */ console.log('开始监听 Pipeline...'); const { PipelineMonitor } = await Promise.resolve().then(() => __importStar(require("../monitor/pipeline-monitor"))); const monitor = new PipelineMonitor(bitbucketClient); const result = await monitor.monitorPipeline(workspace, repoSlug, newTag, { pollInterval: 15000, // 15 秒 timeout: 30 * 60 * 1000, // 30 分钟 onProgress: (status) => { console.log(`Pipeline 进度: ${status.state.name}`); }, }); console.log('Pipeline 执行成功'); ctx.body = { success: true, tagName: newTag, pipelineStatus: result.state.name, message: "Tag 创建成功,Pipeline 执行完成", }; } catch (error) { console.error('执行失败:', error); throw error; } } catch (error) { console.error('Pipeline 测试失败:', error); console.error('错误堆栈:', error.stack); ctx.status = 500; ctx.body = { error: error.message }; } }); /** 监听 Pipeline */ this.router.post("/monitor-pipeline", async (ctx) => { try { const { workspace, repoSlug, tagName } = ctx.request.body; if (!workspace || !repoSlug || !tagName) { ctx.status = 400; ctx.body = { error: "缺少必要参数" }; return; } /** 加载配置 */ const config = await this.configManager.loadConfig(); /** 获取缓存的明文密码 */ const appPassword = await this.configManager.getBitbucketPassword(); if (!appPassword) { ctx.status = 400; ctx.body = { error: "未找到 Bitbucket 凭证,请先在配置页面保存 Bitbucket 配置", }; return; } /** 创建 Bitbucket 客户端 */ const { BitbucketClient } = await Promise.resolve().then(() => __importStar(require("../api/bitbucket-client"))); const bitbucketClient = new BitbucketClient(config.bitbucket.username, appPassword); /** 创建 Pipeline 监听器 */ const { PipelineMonitor } = await Promise.resolve().then(() => __importStar(require("../monitor/pipeline-monitor"))); const monitor = new PipelineMonitor(bitbucketClient); /** 开始监听 */ const result = await monitor.monitorPipeline(workspace, repoSlug, tagName, { pollInterval: 15000, // 15 秒 timeout: 30 * 60 * 1000, // 30 分钟 onProgress: (status) => { console.log(`Pipeline 进度: ${status.state.name}`); }, }); ctx.body = { success: true,