增加构建通知

This commit is contained in:
dengqichen 2025-11-15 10:21:20 +08:00
parent f9633e23b1
commit 56a9e39bbe
7 changed files with 159 additions and 55 deletions

View File

@ -1,12 +1,23 @@
package com.qqchen.deploy.backend.deploy.enums;
// Jenkins构建状态枚举
import lombok.Getter;
/**
* Jenkins构建状态枚举
*/
@Getter
public enum JenkinsBuildStatus {
SUCCESS, // 构建成功
SUCCESS("构建成功"),
FAILURE("构建失败"),
IN_PROGRESS("构建中"),
ABORTED("构建已取消"),
NOT_FOUND("构建记录不存在"),
UNSTABLE("构建不稳定");
FAILURE, // 构建失败
IN_PROGRESS,// 构建中
ABORTED, // 构建被取消
NOT_FOUND // 构建不存在
private final String description;
JenkinsBuildStatus(String description) {
this.description = description;
}
}

View File

@ -3,6 +3,8 @@ package com.qqchen.deploy.backend.deploy.integration.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.utils.DateUtils;
import com.qqchen.deploy.backend.deploy.entity.ExternalSystem;
import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus;
@ -202,7 +204,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
headers.set("Authorization", "Basic " + new String(encodedAuth));
}
default -> throw new RuntimeException("Unsupported authentication type: " + decryptedSystem.getAuthType());
default -> throw new BusinessException(ResponseCode.JENKINS_AUTH_FAILED, new Object[] {decryptedSystem.getAuthType()});
}
// 设置接受JSON响应
@ -247,12 +249,14 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
// 6. 获取队列ID
String location = response.getHeaders().getFirst("Location");
if (location == null) {
throw new RuntimeException("未获取到构建队列信息");
throw new BusinessException(ResponseCode.JENKINS_API_ERROR, new Object[] {201, "未获取到构建队列信息"});
}
return extractQueueId(location);
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Failed to trigger Jenkins build: job={}, error={}", jobName, e.getMessage(), e);
throw new RuntimeException("触发Jenkins构建失败: " + jobName, e);
throw new BusinessException(ResponseCode.JENKINS_API_ERROR, new Object[] {"POST", e.getMessage()});
}
}
@ -283,7 +287,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
);
if (response.getBody() == null) {
throw new RuntimeException("无法获取Jenkins Job配置");
throw new BusinessException(ResponseCode.JENKINS_JOB_NOT_FOUND, new Object[] {jobName});
}
// 解析XML
@ -396,9 +400,11 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
);
log.info("Successfully added PIPELINE_SCRIPT parameter to Jenkins job: {}", jobName);
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Failed to ensure PIPELINE_SCRIPT parameter for Jenkins job: {}, error: {}", jobName, e.getMessage(), e);
throw new RuntimeException("确保Jenkins Job PIPELINE_SCRIPT参数失败", e);
throw new BusinessException(ResponseCode.JENKINS_API_ERROR, new Object[] {"UPDATE_CONFIG", e.getMessage()});
}
}
@ -443,13 +449,13 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
Map<String, Object> queueInfo = response.getBody();
if (queueInfo == null) {
throw new RuntimeException("Failed to get queue information");
throw new BusinessException(ResponseCode.JENKINS_API_ERROR, new Object[] {200, "队列信息为空"});
}
// 检查是否被取消
Boolean cancelled = (Boolean) queueInfo.get("cancelled");
if (Boolean.TRUE.equals(cancelled)) {
throw new RuntimeException("Build was cancelled in queue");
throw new BusinessException(ResponseCode.JENKINS_BUILD_CANCELLED_IN_QUEUE);
}
// 检查是否已开始执行
@ -495,9 +501,11 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
case "ABORTED" -> JenkinsBuildStatus.ABORTED;
default -> JenkinsBuildStatus.IN_PROGRESS;
};
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Failed to get build status: job={}, buildNumber={}", jobName, buildNumber, e);
throw new RuntimeException("Failed to get build status", e);
throw new BusinessException(ResponseCode.JENKINS_API_ERROR, new Object[] {"GET_STATUS", e.getMessage()});
}
}
@ -615,11 +623,13 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
return buildResponse;
}
throw new RuntimeException("Failed to get build details: empty response");
throw new BusinessException(ResponseCode.JENKINS_API_ERROR, new Object[] {200, "构建详情响应为空"});
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Failed to get build details: job={}, buildNumber={}, error={}",
jobName, buildNumber, e.getMessage(), e);
throw new RuntimeException("获取Jenkins构建详情失败: " + jobName + "#" + buildNumber, e);
throw new BusinessException(ResponseCode.JENKINS_API_ERROR, new Object[] {"GET_BUILD_DETAILS", e.getMessage()});
}
}
@ -635,7 +645,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
try {
jsonNode = objectMapper.readTree(response.getBody());
} catch (JsonProcessingException e) {
throw new RuntimeException("解析Jenkins响应失败", e);
throw new BusinessException(ResponseCode.JENKINS_RESPONSE_PARSE_ERROR, new Object[] {e.getMessage()});
}
// 2. 从响应头中获取cookie
@ -688,10 +698,12 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
mapper.getTypeFactory().constructCollectionType(List.class, responseType));
}
return Collections.emptyList();
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Failed to call Jenkins API: path={}, error={}, responseType={}",
path, e.getMessage(), responseType.getSimpleName(), e);
throw new RuntimeException("调用Jenkins API失败: " + path, e);
throw new BusinessException(ResponseCode.JENKINS_API_ERROR, new Object[] {path, e.getMessage()});
}
}
@ -803,9 +815,11 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
}
return null;
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Failed to get Jenkins job: jobName={}, error={}", jobName, e.getMessage(), e);
throw new RuntimeException("获取Jenkins任务失败: " + jobName, e);
throw new BusinessException(ResponseCode.JENKINS_JOB_NOT_FOUND, new Object[] {jobName});
}
}

View File

@ -241,7 +241,19 @@ public enum ResponseCode {
NOTIFICATION_CHANNEL_NOT_FOUND(3120, "notification.channel.not.found"),
NOTIFICATION_CHANNEL_DISABLED(3121, "notification.channel.disabled"),
NOTIFICATION_CHANNEL_CONFIG_ERROR(3122, "notification.channel.config.error"),
NOTIFICATION_SEND_FAILED(3123, "notification.send.failed");
NOTIFICATION_SEND_FAILED(3123, "notification.send.failed"),
// Jenkins集成错误码 (3200-3219)
JENKINS_SERVER_NOT_FOUND(3200, "jenkins.server.not.found"),
JENKINS_CONNECTION_FAILED(3201, "jenkins.connection.failed"),
JENKINS_AUTH_FAILED(3202, "jenkins.auth.failed"),
JENKINS_JOB_NOT_FOUND(3203, "jenkins.job.not.found"),
JENKINS_SERVER_ERROR(3204, "jenkins.server.error"),
JENKINS_QUEUE_TIMEOUT(3205, "jenkins.queue.timeout"),
JENKINS_BUILD_TIMEOUT(3206, "jenkins.build.timeout"),
JENKINS_API_ERROR(3207, "jenkins.api.error"),
JENKINS_RESPONSE_PARSE_ERROR(3208, "jenkins.response.parse.error"),
JENKINS_BUILD_CANCELLED_IN_QUEUE(3209, "jenkins.build.cancelled.in.queue");
private final int code;
private final String messageKey; // 国际化消息key

View File

@ -7,6 +7,9 @@ import com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutpu
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse;
import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.utils.DateUtils;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.JenkinsBuildInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.JenkinsBuildOutputs;
import com.qqchen.deploy.backend.workflow.enums.LogLevel;
@ -17,8 +20,9 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
import com.qqchen.deploy.backend.framework.utils.DateUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import java.util.HashMap;
import java.util.Map;
@ -53,50 +57,75 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
@Override
protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, JenkinsBuildInputMapping input) {
log.info("Jenkins Build - serverId: {}, jobName: {}", input.getServerId(), input.getJobName());
try {
log.info("Jenkins Build - serverId: {}, jobName: {}", input.getServerId(), input.getJobName());
// 1. 获取外部系统不存在会抛出异常由基类处理
ExternalSystem externalSystem = externalSystemRepository.findById(input.getServerId())
.orElseThrow(() -> new RuntimeException("Jenkins服务器不存在: " + input.getServerId()));
// 1. 获取外部系统配置错误 - 技术异常
ExternalSystem externalSystem = externalSystemRepository.findById(input.getServerId())
.orElseThrow(() -> new BusinessException(ResponseCode.JENKINS_SERVER_NOT_FOUND, new Object[]{input.getServerId()}));
String jobName = input.getJobName();
String jobName = input.getJobName();
// 2. 触发构建
Map<String, String> parameters = new HashMap<>();
// 可以根据需要添加构建参数
// parameters.put("BRANCH", "main");
// 2. 触发构建
Map<String, String> parameters = new HashMap<>();
// 可以根据需要添加构建参数
// parameters.put("BRANCH", "main");
String queueId = jenkinsServiceIntegration.buildWithParameters(externalSystem, jobName, parameters);
String queueId = jenkinsServiceIntegration.buildWithParameters(externalSystem, jobName, parameters);
log.info("Jenkins build queued: queueId={}", queueId);
log.info("Jenkins build queued: queueId={}", queueId);
// 3. 等待构建从队列中开始
JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(externalSystem, queueId);
// 3. 等待构建从队列中开始
JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(externalSystem, queueId);
log.info("Jenkins build started: buildNumber={}", buildInfo.getBuildNumber());
log.info("Jenkins build started: buildNumber={}", buildInfo.getBuildNumber());
// 4. 记录构建启动日志
workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS, String.format("Jenkins 构建已启动: job=%s, buildNumber=%d", jobName, buildInfo.getBuildNumber()));
// 4. 记录构建启动日志
workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS,
String.format("Jenkins 构建已启动: job=%s, buildNumber=%d", jobName, buildInfo.getBuildNumber()));
// 5. 轮询构建状态直到完成
// 只有成功时才会返回到这里
JenkinsBuildStatus buildStatus = pollBuildStatus(execution, externalSystem, jobName, buildInfo.getBuildNumber());
// 5. 轮询构建状态直到完成
JenkinsBuildStatus buildStatus = pollBuildStatus(execution, externalSystem, jobName, buildInfo.getBuildNumber());
// 5. 获取构建详细信息包括 duration, changeSets, artifacts
JenkinsBuildResponse buildDetails = jenkinsServiceIntegration.getBuildDetails(externalSystem, jobName, buildInfo.getBuildNumber());
// 6. 获取构建详细信息包括 duration, changeSets, artifacts
JenkinsBuildResponse buildDetails = jenkinsServiceIntegration.getBuildDetails(externalSystem, jobName, buildInfo.getBuildNumber());
// 打印调试信息仅数量避免大对象噪音
int changeSetsCount = (buildDetails.getChangeSets() != null) ? buildDetails.getChangeSets().size() : 0;
int artifactsCount = (buildDetails.getArtifacts() != null) ? buildDetails.getArtifacts().size() : 0;
log.info("Build details - changeSetsCount={}, artifactsCount={}", changeSetsCount, artifactsCount);
// 打印调试信息仅数量避免大对象噪音
int changeSetsCount = (buildDetails.getChangeSets() != null) ? buildDetails.getChangeSets().size() : 0;
int artifactsCount = (buildDetails.getArtifacts() != null) ? buildDetails.getArtifacts().size() : 0;
log.info("Build details - changeSetsCount={}, artifactsCount={}", changeSetsCount, artifactsCount);
// 6. 设置输出结果执行到这里说明构建成功
fillOutputsFrom(buildInfo, buildDetails, buildStatus, output);
// 7. 设置输出结果包含节点状态
fillOutputsFrom(buildInfo, buildDetails, buildStatus, output);
// 记录完成日志
workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS, "Jenkins 构建任务执行完成");
// 8. 记录完成日志
String logMessage = buildStatus == JenkinsBuildStatus.SUCCESS
? "Jenkins 构建任务执行完成:构建成功"
: String.format("Jenkins 构建任务执行完成:%s", buildStatus.getDescription());
workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS, logMessage);
// 不需要 returnstatus 已经是 SUCCESS
// 正常返回基类会根据 output.status + continueOnFailure 决定是否抛 BpmnError
} catch (ResourceAccessException e) {
// 网络连接异常 - 技术异常强制失败
log.error("Jenkins 连接失败: serverId={}, error={}", input.getServerId(), e.getMessage(), e);
throw new BusinessException(ResponseCode.JENKINS_CONNECTION_FAILED, new Object[]{input.getServerId()});
} catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) {
// 认证失败 - 技术异常强制失败
log.error("Jenkins 认证失败: serverId={}, status={}", input.getServerId(), e.getStatusCode());
throw new BusinessException(ResponseCode.JENKINS_AUTH_FAILED, new Object[]{input.getServerId()});
} catch (HttpClientErrorException.NotFound e) {
// Job不存在 - 技术异常强制失败
log.error("Jenkins Job 不存在: jobName={}", input.getJobName());
throw new BusinessException(ResponseCode.JENKINS_JOB_NOT_FOUND, new Object[]{input.getJobName()});
} catch (HttpServerErrorException e) {
// Jenkins服务器错误 - 技术异常强制失败
log.error("Jenkins 服务器错误: serverId={}, status={}", input.getServerId(), e.getStatusCode());
throw new BusinessException(ResponseCode.JENKINS_SERVER_ERROR, new Object[]{e.getStatusCode().value()});
}
}
private JenkinsQueueBuildInfoResponse waitForBuildToStart(ExternalSystem externalSystem, String queueId) {
@ -119,7 +148,7 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
}
}
throw new RuntimeException(String.format("Build did not start within %d seconds", MAX_QUEUE_POLLS * QUEUE_POLL_INTERVAL));
throw new BusinessException(ResponseCode.JENKINS_QUEUE_TIMEOUT, new Object[]{MAX_QUEUE_POLLS * QUEUE_POLL_INTERVAL});
}
private JenkinsBuildStatus pollBuildStatus(DelegateExecution execution, ExternalSystem externalSystem, String jobName, Integer buildNumber) {
@ -169,7 +198,7 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
throw new RuntimeException("Build status polling was interrupted", e);
}
}
throw new RuntimeException(String.format("Jenkins build timed out after %d minutes: job=%s, buildNumber=%d", MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60, jobName, buildNumber));
throw new BusinessException(ResponseCode.JENKINS_BUILD_TIMEOUT, new Object[]{jobName, buildNumber, MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60});
}
/**
@ -252,11 +281,13 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
// 制品URL服务层已拼接
output.setArtifactUrl(buildDetails.getArtifactUrl() != null ? buildDetails.getArtifactUrl() : "");
// 节点状态Jenkins 构建非 SUCCESS 节点标记为失败
// 节点状态Jenkins 构建非 SUCCESS 节点标记为失败业务失败可通过 continueOnFailure 控制
if (buildStatus == JenkinsBuildStatus.SUCCESS) {
output.setStatus(NodeExecutionStatusEnum.SUCCESS);
output.setMessage("Jenkins 构建成功");
} else {
output.setStatus(NodeExecutionStatusEnum.FAILURE);
output.setMessage(String.format("Jenkins %s", buildStatus.getDescription()));
}
}
}

View File

@ -264,3 +264,15 @@ notification.channel.not.found=通知渠道不存在或已删除
notification.channel.disabled=通知渠道已禁用
notification.channel.config.error=通知渠道配置错误:{0}
notification.send.failed=通知发送失败:{0}
# Jenkins集成相关 (3200-3219)
jenkins.server.not.found=Jenkins服务器不存在{0}
jenkins.connection.failed=无法连接到Jenkins服务器{0}
jenkins.auth.failed=Jenkins认证失败请检查凭据配置{0}
jenkins.job.not.found=Jenkins Job不存在{0}
jenkins.server.error=Jenkins服务器错误HTTP {0}
jenkins.queue.timeout=Jenkins构建队列超时等待时间超过{0}秒
jenkins.build.timeout=Jenkins构建超时job={0}, buildNumber={1}, 超时时间{2}分钟
jenkins.api.error=Jenkins API调用失败HTTP {0}, {1}
jenkins.response.parse.error=Jenkins响应解析失败{0}
jenkins.build.cancelled.in.queue=Jenkins构建在队列中被取消

View File

@ -197,3 +197,15 @@ notification.channel.not.found=Notification channel not found or has been delete
notification.channel.disabled=Notification channel is disabled
notification.channel.config.error=Notification channel configuration error: {0}
notification.send.failed=Notification send failed: {0}
# Jenkins Integration Related (3200-3219)
jenkins.server.not.found=Jenkins server not found: {0}
jenkins.connection.failed=Unable to connect to Jenkins server: {0}
jenkins.auth.failed=Jenkins authentication failed, please check credentials: {0}
jenkins.job.not.found=Jenkins job not found: {0}
jenkins.server.error=Jenkins server error: HTTP {0}
jenkins.queue.timeout=Jenkins build queue timeout, waited more than {0} seconds
jenkins.build.timeout=Jenkins build timeout: job={0}, buildNumber={1}, timeout {2} minutes
jenkins.api.error=Jenkins API call failed: HTTP {0}, {1}
jenkins.response.parse.error=Jenkins response parse error: {0}
jenkins.build.cancelled.in.queue=Jenkins build was cancelled in queue

View File

@ -197,3 +197,15 @@ notification.channel.not.found=通知渠道不存在或已删除
notification.channel.disabled=通知渠道已禁用
notification.channel.config.error=通知渠道配置错误:{0}
notification.send.failed=通知发送失败:{0}
# Jenkins集成相关 (3200-3219)
jenkins.server.not.found=Jenkins服务器不存在{0}
jenkins.connection.failed=无法连接到Jenkins服务器{0}
jenkins.auth.failed=Jenkins认证失败请检查凭据配置{0}
jenkins.job.not.found=Jenkins Job不存在{0}
jenkins.server.error=Jenkins服务器错误HTTP {0}
jenkins.queue.timeout=Jenkins构建队列超时等待时间超过{0}秒
jenkins.build.timeout=Jenkins构建超时job={0}, buildNumber={1}, 超时时间{2}分钟
jenkins.api.error=Jenkins API调用失败HTTP {0}, {1}
jenkins.response.parse.error=Jenkins响应解析失败{0}
jenkins.build.cancelled.in.queue=Jenkins构建在队列中被取消