long-git-cli
Version:
A CLI tool for Git tag management.
1,023 lines (1,022 loc) • 63.7 kB
JavaScript
"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,