style:优化代码格式,提高可读性
This commit is contained in:
@@ -5,11 +5,11 @@ const j2m = require('j2m');
|
||||
//返回null表示类型未配置,不应同步
|
||||
function buildJiraFields(title, body, labels, milestone, repoConfig) {
|
||||
const { jira, priorities, types, sprints } = repoConfig;
|
||||
|
||||
|
||||
let issueTypeId = null;
|
||||
let priorityId = "3";
|
||||
let typeFound = false;
|
||||
|
||||
|
||||
//处理标签
|
||||
if (labels && labels.length > 0) {
|
||||
for (const label of labels) {
|
||||
@@ -21,7 +21,7 @@ function buildJiraFields(title, body, labels, milestone, repoConfig) {
|
||||
if (priorities[name]) priorityId = priorities[name];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//如果没有找到匹配的类型标签,检查是否使用默认类型
|
||||
if (!typeFound) {
|
||||
if (jira.defaultType) {
|
||||
@@ -32,13 +32,13 @@ function buildJiraFields(title, body, labels, milestone, repoConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
let sprintId = null;
|
||||
|
||||
let sprintId = null;
|
||||
|
||||
if (milestone && sprints[milestone.title]) {
|
||||
sprintId = sprints[milestone.title];
|
||||
}
|
||||
|
||||
// 如果body为空,给默认值,否则将Markdown转换为Jira Wiki Markup
|
||||
//如果body为空,给默认值,否则将Markdown转换为Jira Wiki Markup
|
||||
let description = "No description";
|
||||
if (body) {
|
||||
description = j2m.toJ(body);
|
||||
|
||||
@@ -53,26 +53,26 @@ function findLabelByTypeName(typeName, typesMap) {
|
||||
//处理Java对象toString格式: "com.atlassian...Sprint@xxx[id=37,name=xxx,...]"
|
||||
function parseSprintId(sprintData) {
|
||||
if (!sprintData) return null;
|
||||
|
||||
// 如果是数组,取最后一个(当前活动Sprint)
|
||||
|
||||
//如果是数组,取最后一个(当前活动Sprint)
|
||||
if (Array.isArray(sprintData)) {
|
||||
if (sprintData.length === 0) return null;
|
||||
return parseSprintId(sprintData[sprintData.length - 1]);
|
||||
}
|
||||
|
||||
// 如果已经是对象,直接返回id
|
||||
|
||||
//如果已经是对象,直接返回id
|
||||
if (typeof sprintData === 'object' && sprintData.id) {
|
||||
return parseInt(sprintData.id);
|
||||
}
|
||||
|
||||
// 如果是字符串(Java对象toString),用正则提取id
|
||||
|
||||
//如果是字符串(Java对象toString),用正则提取id
|
||||
if (typeof sprintData === 'string') {
|
||||
const match = sprintData.match(/\bid=(\d+)/);
|
||||
if (match) {
|
||||
return parseInt(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,16 +95,16 @@ async function handleJiraHook(payload) {
|
||||
logger.warn(`[JIRA->GITEA] Invalid payload: missing issue or issue.key`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//检测是否为/resync评论命令
|
||||
//Jira的评论事件通常作为 jira:issue_updated 发送,但会包含comment字段
|
||||
const hasComment = !!comment;
|
||||
const isResyncCommand = (hasComment && comment.body && comment.body.trim() === '/resync');
|
||||
|
||||
|
||||
if (hasComment) {
|
||||
logger.info(`[JIRA->GITEA] Comment detected in ${issue.key}, body: "${comment.body?.trim()}", isResync: ${isResyncCommand}`);
|
||||
}
|
||||
|
||||
|
||||
//检查标题是否包含#不同步标记
|
||||
if (issue.fields && issue.fields.summary && issue.fields.summary.includes("#不同步")) {
|
||||
if (!isResyncCommand) {
|
||||
@@ -115,28 +115,28 @@ async function handleJiraHook(payload) {
|
||||
|
||||
//查找映射关系
|
||||
const mapping = dbMap.getGiteaInfo(issue.key);
|
||||
|
||||
|
||||
logger.info(`[JIRA->GITEA] Processing ${issue.key}, event: ${webhookEvent}, isResync: ${isResyncCommand}, hasMapping: ${!!mapping}`);
|
||||
|
||||
|
||||
//处理Jira工单创建事件 - 在Gitea创建对应工单
|
||||
if (webhookEvent === 'jira:issue_created' || (isResyncCommand && !mapping)) {
|
||||
logger.info(`[JIRA->GITEA] Entering create logic for ${issue.key}`);
|
||||
|
||||
|
||||
if (mapping && !isResyncCommand) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//根据Jira项目Key查找对应的Gitea仓库
|
||||
const projectKey = issue.fields.project.key;
|
||||
const repoInfo = getRepoByJiraProject(projectKey);
|
||||
|
||||
|
||||
if (!repoInfo) {
|
||||
logger.warn(`[JIRA->GITEA] No repo configured for project ${projectKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const [owner, repo] = repoInfo.repoKey.split('/');
|
||||
|
||||
|
||||
//检查类型是否在配置中
|
||||
//优先检查标题中的[类型名],如果存在且能匹配则允许创建
|
||||
//否则检查Jira问题类型是否配置
|
||||
@@ -154,30 +154,30 @@ async function handleJiraHook(payload) {
|
||||
const typeLabel = findGiteaLabel(issueTypeId, repoInfo.config.types);
|
||||
hasValidType = !!typeLabel;
|
||||
}
|
||||
|
||||
|
||||
if (!hasValidType) {
|
||||
logger.info(`[${repoInfo.repoKey}] [JIRA->GITEA] Skipped ${issue.key}: no valid type found in title or issue type`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const issueData = {
|
||||
title: issue.fields.summary || 'Untitled',
|
||||
body: issue.fields.description ? j2m.toM(issue.fields.description) : ''
|
||||
};
|
||||
|
||||
|
||||
//创建Gitea工单
|
||||
const giteaIssue = await giteaService.createIssue(owner, repo, issueData);
|
||||
|
||||
|
||||
//保存映射关系
|
||||
dbMap.saveMapping(repoInfo.repoKey, giteaIssue.number, issue.key, issue.id);
|
||||
|
||||
|
||||
logger.sync(`[${repoInfo.repoKey}] [JIRA->GITEA] Created #${giteaIssue.number} from ${issue.key}`);
|
||||
|
||||
//同步标签(类型和优先级)
|
||||
try {
|
||||
const labels = [];
|
||||
|
||||
|
||||
//添加类型标签:优先从标题中提取[类型名]
|
||||
let typeLabel = null;
|
||||
const titleType = extractTypeFromTitle(issue.fields.summary);
|
||||
@@ -194,7 +194,7 @@ async function handleJiraHook(payload) {
|
||||
if (typeLabel) {
|
||||
labels.push(typeLabel);
|
||||
}
|
||||
|
||||
|
||||
//添加优先级标签
|
||||
if (issue.fields.priority) {
|
||||
const priorityLabel = findGiteaLabel(issue.fields.priority.id, repoInfo.config.priorities);
|
||||
@@ -202,7 +202,7 @@ async function handleJiraHook(payload) {
|
||||
labels.push(priorityLabel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//如果有标签则设置
|
||||
if (labels.length > 0) {
|
||||
await giteaService.replaceLabels(owner, repo, giteaIssue.number, labels);
|
||||
@@ -214,28 +214,28 @@ async function handleJiraHook(payload) {
|
||||
//通过评论记录来源链接
|
||||
const jiraIssueUrl = `${config.jira.baseUrl}/browse/${issue.key}`;
|
||||
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
||||
// 使用repoInfo中的owner和repo,确保使用当前配置的仓库名
|
||||
//使用repoInfo中的owner和repo,确保使用当前配置的仓库名
|
||||
const giteaIssueUrl = `${giteaWebUrl}/${owner}/${repo}/issues/${giteaIssue.number}`;
|
||||
|
||||
const successMsg = isResyncCommand
|
||||
|
||||
const successMsg = isResyncCommand
|
||||
? `手动同步:已补建Gitea工单 [#${giteaIssue.number}](${giteaIssueUrl})`
|
||||
: `Gitea来源: ${giteaIssueUrl}\n由工单机器人创建`;
|
||||
|
||||
|
||||
Promise.all([
|
||||
//在Gitea工单上添加评论,格式与Gitea->Jira一致
|
||||
giteaService.addComment(owner, repo, giteaIssue.number,
|
||||
giteaService.addComment(owner, repo, giteaIssue.number,
|
||||
`已由工单机器人同步至Jira:[${issue.key}](${jiraIssueUrl})`
|
||||
),
|
||||
//在Jira工单上添加评论
|
||||
jiraService.addComment(issue.key, successMsg)
|
||||
]).catch(err => logger.error('Comment write-back failed', err.message));
|
||||
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[${repoInfo.repoKey}] [JIRA->GITEA] Failed to create issue: ${error.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//其他事件需要已存在的映射关系
|
||||
if (!mapping) {
|
||||
logger.info(`[JIRA->GITEA] No mapping found for ${issue.key}, skipping non-create/non-resync event`);
|
||||
@@ -251,36 +251,36 @@ async function handleJiraHook(payload) {
|
||||
logger.warn(`[${mapping.repo_key}] [JIRA->GITEA] Repository not configured`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//处理工单更新(状态/标题/描述/优先级/类型/迭代/经办人)
|
||||
if ((webhookEvent === 'jira:issue_updated' && changelog) || isResyncCommand) {
|
||||
logger.info(`[JIRA->GITEA] Entering update logic for ${issue.key}, isResync: ${isResyncCommand}`);
|
||||
const updateData = {};
|
||||
const changes = isResyncCommand ? [] : (changelog.items || []);
|
||||
let needsUpdate = isResyncCommand; // resync时强制更新
|
||||
let needsLabelUpdate = isResyncCommand; // resync时强制更新标签
|
||||
let needsMilestoneUpdate = isResyncCommand; // resync时强制更新里程碑
|
||||
|
||||
let needsUpdate = isResyncCommand; //resync时强制更新
|
||||
let needsLabelUpdate = isResyncCommand; //resync时强制更新标签
|
||||
let needsMilestoneUpdate = isResyncCommand; //resync时强制更新里程碑
|
||||
|
||||
//获取当前Gitea工单详情以处理标签
|
||||
let currentGiteaIssue = null;
|
||||
|
||||
|
||||
//检查是否配置了状态流转映射
|
||||
const hasTransitions = repoConfig.transitions && (repoConfig.transitions.close || repoConfig.transitions.reopen);
|
||||
|
||||
|
||||
//如果是resync命令,强制同步所有字段
|
||||
if (isResyncCommand) {
|
||||
//同步标题
|
||||
if (issue.fields.summary) {
|
||||
updateData.title = issue.fields.summary;
|
||||
}
|
||||
|
||||
|
||||
//同步描述
|
||||
if (issue.fields.description) {
|
||||
updateData.body = j2m.toM(issue.fields.description);
|
||||
} else {
|
||||
updateData.body = '';
|
||||
}
|
||||
|
||||
|
||||
//同步状态(仅在配置了状态流转时)
|
||||
if (hasTransitions) {
|
||||
const statusCategory = issue.fields.status?.statusCategory?.key;
|
||||
@@ -290,7 +290,7 @@ async function handleJiraHook(payload) {
|
||||
updateData.state = 'open';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//同步经办人
|
||||
if (issue.fields.assignee) {
|
||||
updateData.assignees = [issue.fields.assignee.name];
|
||||
@@ -357,18 +357,18 @@ async function handleJiraHook(payload) {
|
||||
const priorityLabels = Object.keys(repoConfig.priorities);
|
||||
const typeLabels = Object.keys(repoConfig.types);
|
||||
const mappedLabels = [...priorityLabels, ...typeLabels];
|
||||
|
||||
|
||||
//保留非映射标签
|
||||
let newLabels = currentGiteaIssue.labels
|
||||
.map(l => l.name)
|
||||
.filter(name => !mappedLabels.includes(name));
|
||||
|
||||
|
||||
//添加新的优先级标签
|
||||
if (issue.fields.priority) {
|
||||
const priorityLabel = findGiteaLabel(issue.fields.priority.id, repoConfig.priorities);
|
||||
if (priorityLabel) newLabels.push(priorityLabel);
|
||||
}
|
||||
|
||||
|
||||
//添加新的类型标签:优先从标题中提取[类型名]
|
||||
let typeLabel = null;
|
||||
const titleType = extractTypeFromTitle(issue.fields.summary);
|
||||
@@ -387,7 +387,7 @@ async function handleJiraHook(payload) {
|
||||
} else if (issue.fields.issuetype) {
|
||||
logger.info(`[${mapping.repo_key}] [JIRA->GITEA] Type ${issue.fields.issuetype.id} not configured, skipping type label`);
|
||||
}
|
||||
|
||||
|
||||
//使用专门的标签API替换标签
|
||||
await giteaService.replaceLabels(owner, repo, giteaId, newLabels);
|
||||
}
|
||||
@@ -402,13 +402,13 @@ async function handleJiraHook(payload) {
|
||||
//获取Sprint字段值
|
||||
const sprintField = repoConfig.jira.sprintField || 'customfield_10105';
|
||||
const sprintData = issue.fields[sprintField];
|
||||
|
||||
|
||||
if (sprintData) {
|
||||
const sprintId = parseSprintId(sprintData);
|
||||
|
||||
|
||||
if (sprintId) {
|
||||
const milestoneName = findGiteaMilestone(sprintId, repoConfig.sprints);
|
||||
|
||||
|
||||
if (milestoneName) {
|
||||
const milestones = await giteaService.getMilestones(owner, repo);
|
||||
const milestone = milestones.find(m => m.title === milestoneName);
|
||||
@@ -432,12 +432,12 @@ async function handleJiraHook(payload) {
|
||||
if (needsUpdate) {
|
||||
await giteaService.updateIssue(owner, repo, giteaId, updateData);
|
||||
logger.sync(`[${mapping.repo_key}] [JIRA->GITEA] Updated #${giteaId}`);
|
||||
|
||||
|
||||
//如果是resync命令,添加反馈评论
|
||||
if (isResyncCommand) {
|
||||
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
||||
const giteaIssueUrl = `${giteaWebUrl}/${owner}/${repo}/issues/${giteaId}`;
|
||||
await jiraService.addComment(issue.key,
|
||||
await jiraService.addComment(issue.key,
|
||||
`手动同步:工单已存在,已更新 [Gitea #${giteaId}](${giteaIssueUrl})`
|
||||
).catch(err => logger.error('Comment write-back failed', err.message));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const logger = require('../utils/logger');
|
||||
const config = require('../config/env');
|
||||
const { checkCircuitBreaker } = require('../utils/circuitBreaker');
|
||||
|
||||
// 处理Gitea Issue事件的主逻辑
|
||||
//处理Gitea Issue事件的主逻辑
|
||||
const processingIds = new Set();
|
||||
const LOCK_TIMEOUT = 10000;
|
||||
const RETRY_DELAY = 1500;
|
||||
@@ -31,13 +31,13 @@ function isGiteaBot(sender) {
|
||||
|
||||
async function handleIssueEvent(payload, retryCount = 0) {
|
||||
const { action, issue, repository, comment, sender } = payload;
|
||||
|
||||
|
||||
//如果操作者是机器人,直接忽略
|
||||
if (isGiteaBot(sender)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证payload完整性
|
||||
//验证payload完整性
|
||||
if (!issue || !issue.number || !repository) {
|
||||
logger.error('Invalid payload: missing required fields');
|
||||
return;
|
||||
@@ -47,10 +47,10 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
if (!checkCircuitBreaker()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//构建仓库标识 (owner/repo格式)
|
||||
const repoKey = `${repository.owner.username}/${repository.name}`;
|
||||
|
||||
|
||||
//获取该仓库的配置
|
||||
const repoConfig = getRepoConfig(repoKey);
|
||||
if (!repoConfig) {
|
||||
@@ -58,7 +58,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
logger.error(errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
|
||||
//检测是否为/resync
|
||||
const isResyncCommand = (action === 'created' && comment && comment.body.trim() === '/resync');
|
||||
const giteaId = issue.number;
|
||||
@@ -101,7 +101,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
|
||||
//只有已存在的工单才能进行更新/关闭/重开操作
|
||||
if (mapping) {
|
||||
|
||||
|
||||
//处理关闭事件
|
||||
if (action === 'closed') {
|
||||
if (transitions && transitions.close) {
|
||||
@@ -132,14 +132,14 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
repository.name,
|
||||
giteaId
|
||||
);
|
||||
|
||||
|
||||
let assigneeToSync = null;
|
||||
if (fullIssue.assignees && fullIssue.assignees.length > 0) {
|
||||
assigneeToSync = fullIssue.assignees[0];
|
||||
} else if (fullIssue.assignee) {
|
||||
assigneeToSync = fullIssue.assignee;
|
||||
}
|
||||
|
||||
|
||||
if (assigneeToSync) {
|
||||
const jiraUser = await jiraService.findUser(assigneeToSync.username);
|
||||
if (jiraUser) {
|
||||
@@ -147,7 +147,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
await jiraService.updateIssue(mapping.jira_key, { assignee: { name: jiraUser.name } });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (transitions && transitions.in_progress) {
|
||||
await jiraService.transitionIssue(mapping.jira_key, transitions.in_progress);
|
||||
}
|
||||
@@ -165,7 +165,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
repository.name,
|
||||
giteaId
|
||||
);
|
||||
|
||||
|
||||
if (fullIssue.assignees && fullIssue.assignees.length > 0) {
|
||||
const firstAssignee = fullIssue.assignees[0];
|
||||
const jiraUser = await jiraService.findUser(firstAssignee.username);
|
||||
@@ -190,12 +190,12 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
issue.milestone,
|
||||
repoConfig
|
||||
);
|
||||
|
||||
|
||||
//已有映射的工单,即使类型未配置也允许同步(只是不更新类型字段)
|
||||
//这样当类型从未配置变为已配置时,也能正常同步
|
||||
if (jiraFields) {
|
||||
// 处理指派人同步(resync 时)
|
||||
// Gitea 支持多个指派人,Jira 只支持一个经办人,取第一个
|
||||
//处理指派人同步(resync 时)
|
||||
//Gitea 支持多个指派人,Jira 只支持一个经办人,取第一个
|
||||
if (isResyncCommand) {
|
||||
if (issue.assignees && issue.assignees.length > 0) {
|
||||
const firstAssignee = issue.assignees[0];
|
||||
@@ -230,9 +230,9 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
//只有opened事件和resync命令可以创建新工单
|
||||
if (!isResyncCommand && action !== 'opened') {
|
||||
logger.info(`[${repoKey}] [GITEA->JIRA] Skipped #${giteaId}: no mapping exists and action is '${action}' (only 'opened' or resync can create)`);
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const jiraFields = buildJiraFields(
|
||||
issue.title,
|
||||
issue.body,
|
||||
@@ -240,7 +240,7 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
issue.milestone,
|
||||
repoConfig
|
||||
);
|
||||
|
||||
|
||||
//类型未配置,跳过同步
|
||||
if (!jiraFields) {
|
||||
logger.info(`[${repoKey}] [GITEA->JIRA] Skipped #${giteaId}: type not configured in mappings`);
|
||||
@@ -258,9 +258,9 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
jiraFields.assignee = { name: jiraUser.name };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newIssue = await jiraService.createIssue(jiraFields);
|
||||
|
||||
|
||||
dbMap.saveMapping(repoKey, giteaId, newIssue.key, newIssue.id);
|
||||
logger.sync(`[${repoKey}] [GITEA->JIRA] Created ${newIssue.key} from #${giteaId}`);
|
||||
|
||||
@@ -269,11 +269,11 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
await jiraService.transitionIssue(newIssue.key, transitions.in_progress)
|
||||
.catch(e => logger.error('Initial transition failed', e.message));
|
||||
}
|
||||
|
||||
|
||||
const giteaWebUrl = config.gitea.baseUrl.replace(/\/api\/v1\/?$/, '');
|
||||
const giteaIssueUrl = `${giteaWebUrl}/${repository.owner.username}/${repository.name}/issues/${giteaId}`;
|
||||
|
||||
const successMsg = isResyncCommand
|
||||
|
||||
const successMsg = isResyncCommand
|
||||
? `手动同步:已补建Jira工单 [${newIssue.key}](${config.jira.baseUrl}/browse/${newIssue.key})`
|
||||
: `Jira来源:[${newIssue.key}](${config.jira.baseUrl}/browse/${newIssue.key})\n由工单机器人创建`;
|
||||
|
||||
@@ -290,12 +290,12 @@ async function handleIssueEvent(payload, retryCount = 0) {
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[${repoKey}] [GITEA->JIRA] Failed to sync #${giteaId}: ${error.message}`);
|
||||
|
||||
|
||||
if (isResyncCommand) {
|
||||
await giteaService.addComment(
|
||||
repository.owner.username,
|
||||
repository.name,
|
||||
giteaId,
|
||||
repository.owner.username,
|
||||
repository.name,
|
||||
giteaId,
|
||||
`同步失败: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user