增加构建通知

This commit is contained in:
dengqichen 2025-11-14 15:54:35 +08:00
parent d8e65d8855
commit c81006177f
7 changed files with 136 additions and 33 deletions

View File

@ -3,6 +3,7 @@ package com.qqchen.deploy.backend.deploy.integration.impl;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.utils.DateUtils;
import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; import com.qqchen.deploy.backend.deploy.entity.ExternalSystem;
import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus; import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus;
import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration; import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration;
@ -505,8 +506,12 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
try { try {
// 构建 tree 参数只获取我们需要的字段 // 构建 tree 参数只获取我们需要的字段
String treeQuery = "number,url,result,duration,timestamp,building," + String treeQuery = "number,url,result,duration,timestamp,building," +
"changeSets[items[commitId,author,message]]," + // 变更集字段更精确author.fullName msg
"artifacts[fileName,relativePath,displayPath]"; "changeSets[kind,items[commitId,author[fullName],msg,timestamp,affectedPaths]]," +
// 制品
"artifacts[fileName,relativePath,displayPath]," +
// Git 插件 BuildData兜底的 commit 信息
"actions[_class,lastBuiltRevision[SHA1,branch[name,SHA1]]]";
// 直接使用原始系统信息构建URLURL不需要解密 // 直接使用原始系统信息构建URLURL不需要解密
String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl())
@ -537,11 +542,76 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
JenkinsBuildResponse buildResponse = mapper.readValue(response.getBody(), JenkinsBuildResponse.class); JenkinsBuildResponse buildResponse = mapper.readValue(response.getBody(), JenkinsBuildResponse.class);
// 清理URL移除基础URL部分 // 清理URL移除基础URL部分
String baseUrl = StringUtils.removeEnd(externalSystem.getUrl(), "/");
if (buildResponse.getUrl() != null) { if (buildResponse.getUrl() != null) {
String baseUrl = StringUtils.removeEnd(externalSystem.getUrl(), "/");
buildResponse.setUrl(buildResponse.getUrl().replace(baseUrl, "")); buildResponse.setUrl(buildResponse.getUrl().replace(baseUrl, ""));
} }
// 计算结束时间毫秒与格式化 timestamp duration 均存在
if (buildResponse.getTimestamp() != null && buildResponse.getDuration() != null) {
long endMillis = buildResponse.getTimestamp() + buildResponse.getDuration();
buildResponse.setEndTimeMillis(endMillis);
try {
buildResponse.setEndTime(DateUtils.formatMillis(endMillis));
} catch (Exception ignore) {
// 忽略格式化异常保留 endTimeMillis
}
}
// 提取 gitCommitId优先 changeSets.items[0].commitId兜底 actions.BuildData.lastBuiltRevision.SHA1
String commitId = null;
try {
// 先尝试从 changeSets
if (buildResponse.getChangeSets() != null && !buildResponse.getChangeSets().isEmpty()) {
var first = buildResponse.getChangeSets().get(0);
if (first.getItems() != null && !first.getItems().isEmpty()) {
commitId = first.getItems().get(0).getCommitId();
}
}
if (commitId == null) {
// 兜底解析原始 JSON actions 查找 BuildData
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(response.getBody());
com.fasterxml.jackson.databind.JsonNode actions = root.get("actions");
if (actions != null && actions.isArray()) {
for (com.fasterxml.jackson.databind.JsonNode act : actions) {
com.fasterxml.jackson.databind.JsonNode clazz = act.get("_class");
if (clazz != null && "hudson.plugins.git.util.BuildData".equals(clazz.asText())) {
com.fasterxml.jackson.databind.JsonNode lbr = act.get("lastBuiltRevision");
if (lbr != null && lbr.get("SHA1") != null) {
commitId = lbr.get("SHA1").asText();
break;
}
}
}
}
}
} catch (Exception ex) {
log.warn("Failed to extract git commit id from build details: {}", ex.getMessage());
}
buildResponse.setGitCommitId(commitId);
// 拼接制品完整URL串逗号分隔
try {
if (buildResponse.getArtifacts() != null && !buildResponse.getArtifacts().isEmpty()) {
String buildPath = buildResponse.getUrl() != null ? buildResponse.getUrl() : (
"/job/" + jobName + "/" + buildNumber + "/");
String buildPageUrl = baseUrl + (buildPath.startsWith("/") ? buildPath : "/" + buildPath);
if (!buildPageUrl.endsWith("/")) {
buildPageUrl = buildPageUrl + "/";
}
final String artifactBaseUrl = buildPageUrl;
String joined = buildResponse.getArtifacts().stream()
.map(a -> artifactBaseUrl + "artifact/" + a.getRelativePath())
.collect(java.util.stream.Collectors.joining(","));
buildResponse.setArtifactUrl(joined);
} else {
buildResponse.setArtifactUrl("");
}
} catch (Exception ex) {
log.warn("Failed to build artifact urls: {}", ex.getMessage());
buildResponse.setArtifactUrl("");
}
return buildResponse; return buildResponse;
} }

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.deploy.integration.response; package com.qqchen.deploy.backend.deploy.integration.response;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -9,6 +10,7 @@ import java.util.Map;
* Jenkins构建响应对象 * Jenkins构建响应对象
*/ */
@Data @Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class JenkinsBuildResponse { public class JenkinsBuildResponse {
/** /**
* 构建编号 * 构建编号
@ -45,6 +47,16 @@ public class JenkinsBuildResponse {
* 构建持续时间毫秒 * 构建持续时间毫秒
*/ */
private Long duration; private Long duration;
/**
* 构建结束时间毫秒= timestamp + duration
*/
private Long endTimeMillis;
/**
* 构建结束时间格式化字符串例如yyyy-MM-dd HH:mm:ss
*/
private String endTime;
/** /**
* 预计持续时间 * 预计持续时间
@ -100,13 +112,23 @@ public class JenkinsBuildResponse {
* 构建制品 * 构建制品
*/ */
private List<Artifact> artifacts; private List<Artifact> artifacts;
/**
* 构建制品URL逗号分隔的完整URL串由服务层拼接
*/
private String artifactUrl;
/** /**
* 构建控制台输出 * 构建控制台输出
*/ */
private String consoleLog; private String consoleLog;
/**
* 提取的Git提交ID兜底自 actions.BuildData.lastBuiltRevision.SHA1 changeSets
*/
private String gitCommitId;
@Data @Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class BuildParameter { public static class BuildParameter {
@JsonProperty("_class") @JsonProperty("_class")
private String type; private String type;
@ -143,4 +165,4 @@ public class JenkinsBuildResponse {
private String fileName; private String fileName;
private String relativePath; private String relativePath;
} }
} }

View File

@ -0,0 +1,24 @@
package com.qqchen.deploy.backend.framework.utils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class DateUtils {
public static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static String formatMillis(long millis) {
return formatMillis(millis, DEFAULT_PATTERN);
}
public static String formatMillis(long millis, String pattern) {
LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault());
return time.format(DateTimeFormatter.ofPattern(pattern));
}
public static LocalDateTime toLocalDateTime(long millis) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault());
}
}

View File

@ -92,8 +92,8 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter<WeworkN
// 根据消息类型合并标题和内容 // 根据消息类型合并标题和内容
return switch (request.getMessageType()) { return switch (request.getMessageType()) {
case MARKDOWN -> "## " + title + "\n\n" + content; case MARKDOWN -> title + "\n\n" + content;
case TEXT -> "" + title + "\n" + content; case TEXT -> title + "\n" + content;
case FILE -> content; // 文件类型不处理标题 case FILE -> content; // 文件类型不处理标题
}; };
} }

View File

@ -102,7 +102,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
log.info("Stored NodeContext for: {}", currentNodeId); log.info("Stored NodeContext for: {}", currentNodeId);
} catch (Exception e) { } catch (Exception e) {
// 业务异常根据 continueOnFailure 配置决定行为 // 业务异常根据 continueOnFailure 配置决定行为
log.error("Business exception in node: {}", currentNodeId, e); log.error("Business exception in node: {}", currentNodeId, e);
boolean continueOnFailure = WorkflowUtils.getContinueOnFailure(currentInputMapping); boolean continueOnFailure = WorkflowUtils.getContinueOnFailure(currentInputMapping);

View File

@ -24,6 +24,7 @@ import org.flowable.engine.delegate.BpmnError;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -83,7 +84,6 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS, String.format("Jenkins 构建已启动: job=%s, buildNumber=%d", jobName, buildInfo.getBuildNumber())); workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS, String.format("Jenkins 构建已启动: job=%s, buildNumber=%d", jobName, buildInfo.getBuildNumber()));
// 5. 轮询构建状态直到完成 // 5. 轮询构建状态直到完成
// 注意如果构建失败或被取消pollBuildStatus 会抛出 BpmnError触发错误边界事件
// 只有成功时才会返回到这里 // 只有成功时才会返回到这里
JenkinsBuildStatus buildStatus = pollBuildStatus(execution, externalSystem, jobName, buildInfo.getBuildNumber()); JenkinsBuildStatus buildStatus = pollBuildStatus(execution, externalSystem, jobName, buildInfo.getBuildNumber());
@ -104,32 +104,15 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
// 从构建详情中提取信息 // 从构建详情中提取信息
output.setBuildDuration(buildDetails.getDuration() != null ? buildDetails.getDuration().intValue() : 0); output.setBuildDuration(buildDetails.getDuration() != null ? buildDetails.getDuration().intValue() : 0);
// 提取 Git Commit ID changeSets 中获取第一个 // 直接使用服务层计算与提取的结束时间与提交ID
if (buildDetails.getChangeSets() != null && !buildDetails.getChangeSets().isEmpty()) { output.setDeployEndTimeMillis(buildDetails.getEndTimeMillis());
log.info("Found {} changeSets", buildDetails.getChangeSets().size()); output.setDeployEndTime(buildDetails.getEndTime());
var changeSet = buildDetails.getChangeSets().get(0);
if (changeSet.getItems() != null && !changeSet.getItems().isEmpty()) {
log.info("Found {} items in changeSet", changeSet.getItems().size());
output.setGitCommitId(changeSet.getItems().get(0).getCommitId());
}
} else {
log.warn("No changeSets found in build details");
}
if (output.getGitCommitId() == null) {
output.setGitCommitId("");
}
// 提取构建制品URL如果有多个制品拼接成逗号分隔的列表 // 提取 Git Commit ID由服务层兜底填充
if (buildDetails.getArtifacts() != null && !buildDetails.getArtifacts().isEmpty()) { output.setGitCommitId(buildDetails.getGitCommitId() != null ? buildDetails.getGitCommitId() : "");
log.info("Found {} artifacts", buildDetails.getArtifacts().size());
String artifactUrls = buildDetails.getArtifacts().stream() // 直接使用服务层拼接的制品URL串
.map(artifact -> buildInfo.getBuildUrl() + "artifact/" + artifact.getRelativePath()) output.setArtifactUrl(buildDetails.getArtifactUrl() != null ? buildDetails.getArtifactUrl() : "");
.collect(java.util.stream.Collectors.joining(","));
output.setArtifactUrl(artifactUrls);
} else {
log.warn("No artifacts found in build details");
output.setArtifactUrl("");
}
// 记录完成日志 // 记录完成日志
workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS, "Jenkins 构建任务执行完成"); workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS, "Jenkins 构建任务执行完成");

View File

@ -43,5 +43,9 @@ public class JenkinsBuildOutputs extends BaseNodeOutputs {
* 构建时长 * 构建时长
*/ */
private Integer buildDuration; private Integer buildDuration;
private Long deployEndTimeMillis;
private String deployEndTime;
} }