增加同步锁
This commit is contained in:
parent
2a6785d97d
commit
a5276ca524
@ -554,39 +554,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取 gitCommitId:优先 changeSet/changeSets,兜底 actions.BuildData.lastBuiltRevision.SHA1
|
|
||||||
String commitId = null;
|
|
||||||
try {
|
|
||||||
// 1. 先尝试从 changeSet(单数,FreeStyle 任务)获取
|
|
||||||
if (buildResponse.getChangeSet() != null &&
|
|
||||||
buildResponse.getChangeSet().getItems() != null &&
|
|
||||||
!buildResponse.getChangeSet().getItems().isEmpty()) {
|
|
||||||
// 取最后一个 commit(最新的)
|
|
||||||
var items = buildResponse.getChangeSet().getItems();
|
|
||||||
commitId = items.get(items.size() - 1).getCommitId();
|
|
||||||
}
|
|
||||||
// 2. 兜底:解析原始 JSON 的 actions 查找 BuildData
|
|
||||||
if (commitId == null) {
|
|
||||||
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串(逗号分隔)
|
// 拼接制品完整URL串(逗号分隔)
|
||||||
try {
|
try {
|
||||||
if (buildResponse.getArtifacts() != null && !buildResponse.getArtifacts().isEmpty()) {
|
if (buildResponse.getArtifacts() != null && !buildResponse.getArtifacts().isEmpty()) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -22,12 +23,6 @@ public class JenkinsBuildResponse {
|
|||||||
*/
|
*/
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建类型
|
|
||||||
*/
|
|
||||||
@JsonProperty("_class")
|
|
||||||
private String buildClass;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建结果
|
* 构建结果
|
||||||
*/
|
*/
|
||||||
@ -88,11 +83,6 @@ public class JenkinsBuildResponse {
|
|||||||
*/
|
*/
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
/**
|
|
||||||
* 保持永久
|
|
||||||
*/
|
|
||||||
private Boolean keepLog;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建队列ID
|
* 构建队列ID
|
||||||
*/
|
*/
|
||||||
@ -108,6 +98,11 @@ public class JenkinsBuildResponse {
|
|||||||
*/
|
*/
|
||||||
private ChangeSet changeSet;
|
private ChangeSet changeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建变更集(Jenkins Pipeline 任务返回 changeSets 复数形式)
|
||||||
|
*/
|
||||||
|
private List<ChangeSet> changeSets;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建制品
|
* 构建制品
|
||||||
*/
|
*/
|
||||||
@ -118,14 +113,33 @@ public class JenkinsBuildResponse {
|
|||||||
private String artifactUrl;
|
private String artifactUrl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建控制台输出
|
* 获取所有变更集(统一处理 FreeStyle 的 changeSet 和 Pipeline 的 changeSets)
|
||||||
|
* 调用方无需关心底层差异,直接使用此方法获取所有变更集
|
||||||
*/
|
*/
|
||||||
private String consoleLog;
|
public List<ChangeSet> getAllChangeSets() {
|
||||||
|
List<ChangeSet> result = new ArrayList<>();
|
||||||
|
if (changeSet != null) {
|
||||||
|
result.add(changeSet);
|
||||||
|
}
|
||||||
|
if (changeSets != null) {
|
||||||
|
result.addAll(changeSets);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取的Git提交ID(兜底自 actions.BuildData.lastBuiltRevision.SHA1 或 changeSets)
|
* 获取所有变更集中的提交记录(统一处理 FreeStyle 和 Pipeline)
|
||||||
|
* 调用方无需关心底层差异,直接使用此方法获取所有 commit 信息
|
||||||
*/
|
*/
|
||||||
private String gitCommitId;
|
public List<ChangeSetItem> getAllChangeSetItems() {
|
||||||
|
List<ChangeSetItem> items = new ArrayList<>();
|
||||||
|
for (ChangeSet cs : getAllChangeSets()) {
|
||||||
|
if (cs.getItems() != null) {
|
||||||
|
items.addAll(cs.getItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
|||||||
@ -75,4 +75,27 @@ public interface IJenkinsBuildRepository extends IBaseRepository<JenkinsBuild, L
|
|||||||
*/
|
*/
|
||||||
List<JenkinsBuild> findByExternalSystemIdAndStarttimeAfter(
|
List<JenkinsBuild> findByExternalSystemIdAndStarttimeAfter(
|
||||||
Long externalSystemId, LocalDateTime starttime);
|
Long externalSystemId, LocalDateTime starttime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询多个任务的最新构建记录(用于解决N+1查询问题)
|
||||||
|
*
|
||||||
|
* @param externalSystemId 外部系统ID
|
||||||
|
* @param jobIds 任务ID集合
|
||||||
|
* @return 最新构建记录列表
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT b FROM JenkinsBuild b
|
||||||
|
WHERE b.externalSystemId = :externalSystemId
|
||||||
|
AND b.jobId IN :jobIds
|
||||||
|
AND (b.jobId, b.buildNumber) IN (
|
||||||
|
SELECT b2.jobId, MAX(b2.buildNumber)
|
||||||
|
FROM JenkinsBuild b2
|
||||||
|
WHERE b2.externalSystemId = :externalSystemId
|
||||||
|
AND b2.jobId IN :jobIds
|
||||||
|
GROUP BY b2.jobId
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
List<JenkinsBuild> findLatestBuildsByJobIds(
|
||||||
|
@Param("externalSystemId") Long externalSystemId,
|
||||||
|
@Param("jobIds") Collection<Long> jobIds);
|
||||||
}
|
}
|
||||||
@ -40,6 +40,7 @@ import org.springframework.http.HttpEntity;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -55,6 +56,7 @@ import java.time.Instant;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -183,7 +185,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
|
|
||||||
// 4. 更新同步历史
|
// 4. 更新同步历史
|
||||||
updateSyncHistorySuccess(context.getSyncHistory());
|
updateSyncHistorySuccess(context.getSyncHistory());
|
||||||
|
|
||||||
log.info("Successfully synchronized total {} builds for external system: {}",
|
log.info("Successfully synchronized total {} builds for external system: {}",
|
||||||
totalSyncedBuilds, context.getExternalSystem().getId());
|
totalSyncedBuilds, context.getExternalSystem().getId());
|
||||||
return totalSyncedBuilds;
|
return totalSyncedBuilds;
|
||||||
@ -207,15 +208,23 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 创建任务名称到响应的映射
|
// 3. 批量查询所有任务的最新构建(解决N+1查询问题)
|
||||||
|
List<Long> jobIds = jobs.stream().map(JenkinsJob::getId).toList();
|
||||||
|
List<JenkinsBuild> latestBuilds = jenkinsBuildRepository.findLatestBuildsByJobIds(
|
||||||
|
externalSystem.getId(), jobIds);
|
||||||
|
Map<Long, JenkinsBuild> latestBuildMap = latestBuilds.stream()
|
||||||
|
.collect(Collectors.toMap(JenkinsBuild::getJobId, build -> build));
|
||||||
|
|
||||||
|
// 4. 创建任务名称到响应的映射
|
||||||
Map<String, JenkinsJobResponse> jobResponseMap = jobResponses.stream()
|
Map<String, JenkinsJobResponse> jobResponseMap = jobResponses.stream()
|
||||||
.collect(Collectors.toMap(JenkinsJobResponse::getName, job -> job));
|
.collect(Collectors.toMap(JenkinsJobResponse::getName, job -> job));
|
||||||
|
|
||||||
// 4. 同步每个任务的构建信息
|
// 5. 同步每个任务的构建信息
|
||||||
return jobs.stream()
|
return jobs.stream()
|
||||||
.map(job -> {
|
.map(job -> {
|
||||||
JenkinsJobResponse jobResponse = jobResponseMap.get(job.getJobName());
|
JenkinsJobResponse jobResponse = jobResponseMap.get(job.getJobName());
|
||||||
return syncJob(externalSystem, job, jobResponse);
|
JenkinsBuild latestBuild = latestBuildMap.get(job.getId());
|
||||||
|
return syncJob(externalSystem, job, jobResponse, latestBuild);
|
||||||
})
|
})
|
||||||
.mapToInt(Integer::intValue)
|
.mapToInt(Integer::intValue)
|
||||||
.sum();
|
.sum();
|
||||||
@ -227,6 +236,11 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
protected Integer syncJob(ExternalSystem externalSystem, JenkinsJob job, JenkinsJobResponse jobResponse) {
|
protected Integer syncJob(ExternalSystem externalSystem, JenkinsJob job, JenkinsJobResponse jobResponse) {
|
||||||
|
return syncJob(externalSystem, job, jobResponse, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
protected Integer syncJob(ExternalSystem externalSystem, JenkinsJob job, JenkinsJobResponse jobResponse, JenkinsBuild latestBuild) {
|
||||||
try {
|
try {
|
||||||
if (jobResponse == null || jobResponse.getLastBuild() == null) {
|
if (jobResponse == null || jobResponse.getLastBuild() == null) {
|
||||||
log.info("No build information available for job: {}", job.getJobName());
|
log.info("No build information available for job: {}", job.getJobName());
|
||||||
@ -234,21 +248,30 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
}
|
}
|
||||||
|
|
||||||
Integer latestBuildNumber = jobResponse.getLastBuild().getNumber();
|
Integer latestBuildNumber = jobResponse.getLastBuild().getNumber();
|
||||||
Integer lastSyncedBuildNumber = job.getLastBuildNumber();
|
|
||||||
|
|
||||||
// 1. 判断是否有新构建(对比本地缓存的 lastBuildNumber 和 Jenkins 最新的)
|
// 1. 使用预查询的最新构建或查询Build表中该Job的实际最大构建号
|
||||||
|
Integer lastSyncedBuildNumber;
|
||||||
|
if (latestBuild != null) {
|
||||||
|
lastSyncedBuildNumber = latestBuild.getBuildNumber();
|
||||||
|
} else {
|
||||||
|
Optional<JenkinsBuild> build = jenkinsBuildRepository
|
||||||
|
.findTopByExternalSystemIdAndJobIdOrderByBuildNumberDesc(externalSystem.getId(), job.getId());
|
||||||
|
lastSyncedBuildNumber = build.map(JenkinsBuild::getBuildNumber).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 判断是否有新构建(基于Build表实际数据判断)
|
||||||
if (lastSyncedBuildNumber != null && latestBuildNumber <= lastSyncedBuildNumber) {
|
if (lastSyncedBuildNumber != null && latestBuildNumber <= lastSyncedBuildNumber) {
|
||||||
log.info("No new builds to sync for job: {} (last synced: {}, latest: {})", job.getJobName(), lastSyncedBuildNumber, latestBuildNumber);
|
log.info("No new builds to sync for job: {} (last synced: {}, latest: {})", job.getJobName(), lastSyncedBuildNumber, latestBuildNumber);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 确定同步范围
|
// 3. 确定同步范围
|
||||||
Integer fromBuildNumber = (lastSyncedBuildNumber != null) ? lastSyncedBuildNumber + 1 : 1;
|
Integer fromBuildNumber = (lastSyncedBuildNumber != null) ? lastSyncedBuildNumber + 1 : 1;
|
||||||
|
|
||||||
// 3. 获取构建信息
|
// 4. 获取构建信息
|
||||||
List<JenkinsBuildResponse> builds = jenkinsServiceIntegration.listBuilds(externalSystem, job.getJobName());
|
List<JenkinsBuildResponse> builds = jenkinsServiceIntegration.listBuilds(externalSystem, job.getJobName());
|
||||||
|
|
||||||
// 4. 过滤出需要同步的构建
|
// 5. 过滤出需要同步的构建
|
||||||
List<JenkinsBuildResponse> newBuilds = builds.stream()
|
List<JenkinsBuildResponse> newBuilds = builds.stream()
|
||||||
.filter(build -> build.getNumber() >= fromBuildNumber && build.getNumber() <= latestBuildNumber)
|
.filter(build -> build.getNumber() >= fromBuildNumber && build.getNumber() <= latestBuildNumber)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@ -258,10 +281,10 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 保存新的构建信息
|
// 6. 保存新的构建信息
|
||||||
saveNewBuilds(externalSystem, job, newBuilds);
|
saveNewBuilds(externalSystem, job, newBuilds);
|
||||||
|
|
||||||
// 6. 更新任务的最新构建信息
|
// 7. 更新任务的最新构建信息
|
||||||
updateJobLastBuild(job, jobResponse);
|
updateJobLastBuild(job, jobResponse);
|
||||||
|
|
||||||
log.info("Successfully synchronized {} builds for job: {} (from: {}, to: {})",
|
log.info("Successfully synchronized {} builds for job: {} (from: {}, to: {})",
|
||||||
@ -277,16 +300,29 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
ExternalSystem externalSystem,
|
ExternalSystem externalSystem,
|
||||||
JenkinsJob job,
|
JenkinsJob job,
|
||||||
List<JenkinsBuildResponse> builds) {
|
List<JenkinsBuildResponse> builds) {
|
||||||
List<JenkinsBuild> jenkinsBuilds = builds.stream()
|
List<JenkinsBuild> jenkinsBuilds = new ArrayList<>();
|
||||||
.map(buildResponse -> {
|
|
||||||
|
for (JenkinsBuildResponse buildResponse : builds) {
|
||||||
JenkinsBuild build = new JenkinsBuild();
|
JenkinsBuild build = new JenkinsBuild();
|
||||||
build.setExternalSystemId(externalSystem.getId());
|
build.setExternalSystemId(externalSystem.getId());
|
||||||
build.setJobId(job.getId());
|
build.setJobId(job.getId());
|
||||||
build.setBuildNumber(buildResponse.getNumber());
|
build.setBuildNumber(buildResponse.getNumber());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用 getBuildDetails 获取完整信息(包含 changeSets 等)
|
||||||
|
JenkinsBuildResponse detailedBuild = jenkinsServiceIntegration.getBuildDetails(externalSystem, job.getJobName(), buildResponse.getNumber());
|
||||||
|
if (detailedBuild != null) {
|
||||||
|
updateBuildFromResponse(build, detailedBuild);
|
||||||
|
} else {
|
||||||
updateBuildFromResponse(build, buildResponse);
|
updateBuildFromResponse(build, buildResponse);
|
||||||
return build;
|
}
|
||||||
})
|
} catch (Exception e) {
|
||||||
.collect(Collectors.toList());
|
log.warn("获取构建详情失败,使用基础信息: job={}, build={}", job.getJobName(), buildResponse.getNumber());
|
||||||
|
updateBuildFromResponse(build, buildResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
jenkinsBuilds.add(build);
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Saving {} builds for job: {}", jenkinsBuilds.size(), job.getJobName());
|
log.info("Saving {} builds for job: {}", jenkinsBuilds.size(), job.getJobName());
|
||||||
jenkinsBuildRepository.saveAll(jenkinsBuilds);
|
jenkinsBuildRepository.saveAll(jenkinsBuilds);
|
||||||
@ -304,8 +340,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleSyncException(JenkinsSyncContext context, Exception e) {
|
private void handleSyncException(JenkinsSyncContext context, Exception e) {
|
||||||
log.error("Failed to sync Jenkins builds for external system: {}",
|
log.error("Failed to sync Jenkins builds for external system: {}", context.getExternalSystem().getId(), e);
|
||||||
context.getExternalSystem().getId(), e);
|
|
||||||
|
|
||||||
context.getSyncHistory().setStatus(ExternalSystemSyncStatus.FAILED);
|
context.getSyncHistory().setStatus(ExternalSystemSyncStatus.FAILED);
|
||||||
context.getSyncHistory().setErrorMessage(e.getMessage());
|
context.getSyncHistory().setErrorMessage(e.getMessage());
|
||||||
@ -319,7 +354,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
|
|
||||||
private void updateBuildFromResponse(JenkinsBuild jenkinsBuild, JenkinsBuildResponse response) {
|
private void updateBuildFromResponse(JenkinsBuild jenkinsBuild, JenkinsBuildResponse response) {
|
||||||
jenkinsBuild.setBuildUrl(response.getUrl());
|
jenkinsBuild.setBuildUrl(response.getUrl());
|
||||||
// Jenkins API 返回的 result 在构建进行中时为 null,设置默认值 "BUILDING"
|
|
||||||
jenkinsBuild.setBuildStatus(response.getResult() != null ? response.getResult() : "BUILDING");
|
jenkinsBuild.setBuildStatus(response.getResult() != null ? response.getResult() : "BUILDING");
|
||||||
jenkinsBuild.setDuration(response.getDuration());
|
jenkinsBuild.setDuration(response.getDuration());
|
||||||
|
|
||||||
@ -332,16 +366,11 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 保存 actions 和 changeSet 到 JSON
|
String changeSetsJson = objectMapper.writeValueAsString(response.getAllChangeSets());
|
||||||
Map<String, Object> buildData = new HashMap<>();
|
jenkinsBuild.setActions(changeSetsJson);
|
||||||
buildData.put("actions", response.getActions());
|
|
||||||
buildData.put("changeSet", response.getChangeSet());
|
|
||||||
buildData.put("gitCommitId", response.getGitCommitId());
|
|
||||||
String actionsJson = objectMapper.writeValueAsString(buildData);
|
|
||||||
jenkinsBuild.setActions(actionsJson);
|
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
log.error("Failed to serialize build actions", e);
|
log.error("Failed to serialize changeSets", e);
|
||||||
jenkinsBuild.setActions("{}");
|
jenkinsBuild.setActions("[]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,25 +572,26 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
JenkinsBuild latestBuild = latestBuildByJobId.get(job.getId());
|
JenkinsBuild latestBuild = latestBuildByJobId.get(job.getId());
|
||||||
if (latestBuild == null) continue;
|
if (latestBuild == null) continue;
|
||||||
|
|
||||||
// 从 Jenkins API 实时获取最新状态(每个Job只调用一次)
|
// 获取构建状态:只有 BUILDING 状态才需要调用 API 刷新
|
||||||
JenkinsBuildResponse latestBuildInfo = null;
|
String latestStatus = latestBuild.getBuildStatus();
|
||||||
String latestStatus = "BUILDING";
|
|
||||||
|
if ("BUILDING".equals(latestStatus)) {
|
||||||
|
// 构建进行中,需要从 Jenkins API 获取最新状态
|
||||||
try {
|
try {
|
||||||
latestBuildInfo = jenkinsServiceIntegration.getBuildDetails(externalSystem, job.getJobName(), latestBuild.getBuildNumber());
|
JenkinsBuildResponse latestBuildInfo = jenkinsServiceIntegration.getBuildDetails(externalSystem, job.getJobName(), latestBuild.getBuildNumber());
|
||||||
if (latestBuildInfo != null && latestBuildInfo.getResult() != null) {
|
if (latestBuildInfo != null && latestBuildInfo.getResult() != null) {
|
||||||
latestStatus = latestBuildInfo.getResult();
|
latestStatus = latestBuildInfo.getResult();
|
||||||
}
|
// 状态有变化,更新数据库
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("从Jenkins获取构建信息失败,跳过本次处理,等待下次重试: job={}, buildNumber={}, error={}",
|
|
||||||
job.getJobName(), latestBuild.getBuildNumber(), e.getMessage());
|
|
||||||
continue; // 跳过这个 Job,下次定时任务重试
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据库中的构建状态(如果有变化)
|
|
||||||
if (latestBuildInfo != null && !latestStatus.equals(latestBuild.getBuildStatus())) {
|
|
||||||
updateBuildFromResponse(latestBuild, latestBuildInfo);
|
updateBuildFromResponse(latestBuild, latestBuildInfo);
|
||||||
jenkinsBuildRepository.save(latestBuild);
|
jenkinsBuildRepository.save(latestBuild);
|
||||||
|
log.info("构建状态已更新: job={}, build={}, status={}", job.getJobName(), latestBuild.getBuildNumber(), latestStatus);
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("从Jenkins获取构建信息失败,跳过本次处理,等待下次重试: job={}, buildNumber={}, error={}", job.getJobName(), latestBuild.getBuildNumber(), e.getMessage());
|
||||||
|
continue; // 跳过这个 Job,下次定时任务重试
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 已完成的构建(SUCCESS/FAILURE/ABORTED)直接使用数据库中的状态,不调用 API
|
||||||
|
|
||||||
// 处理每个关联的 TeamApplication
|
// 处理每个关联的 TeamApplication
|
||||||
for (TeamApplication teamApp : relatedTeamApps) {
|
for (TeamApplication teamApp : relatedTeamApps) {
|
||||||
@ -599,20 +629,11 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
* <li>有记录但buildEndNotice=false:检查是否完成,发送结束通知</li>
|
* <li>有记录但buildEndNotice=false:检查是否完成,发送结束通知</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
private void processBuildNotification(
|
private void processBuildNotification(TeamEnvironmentNotificationConfig config, NotificationChannel channel, JenkinsJob job, JenkinsBuild build, String latestStatus, ExternalSystem externalSystem, Application application, Environment environment, JenkinsBuildNotification existingRecord) {
|
||||||
TeamEnvironmentNotificationConfig config,
|
|
||||||
NotificationChannel channel,
|
|
||||||
JenkinsJob job,
|
|
||||||
JenkinsBuild build,
|
|
||||||
String latestStatus,
|
|
||||||
ExternalSystem externalSystem,
|
|
||||||
Application application,
|
|
||||||
Environment environment,
|
|
||||||
JenkinsBuildNotification existingRecord) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 计算构建开始到现在的分钟数
|
// 计算构建开始到现在的分钟数
|
||||||
long minutesAgo = java.time.temporal.ChronoUnit.MINUTES.between(build.getStarttime(), LocalDateTime.now());
|
long minutesAgo = ChronoUnit.MINUTES.between(build.getStarttime(), LocalDateTime.now());
|
||||||
|
|
||||||
// 1. 新构建(通知记录不存在)
|
// 1. 新构建(通知记录不存在)
|
||||||
if (existingRecord == null) {
|
if (existingRecord == null) {
|
||||||
@ -625,8 +646,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
record.setBuildStartNotice(true);
|
record.setBuildStartNotice(true);
|
||||||
record.setBuildEndNotice(true); // 直接标记结束
|
record.setBuildEndNotice(true); // 直接标记结束
|
||||||
jenkinsBuildNotificationRepository.save(record);
|
jenkinsBuildNotificationRepository.save(record);
|
||||||
log.info("构建超时,直接标记完成: job={}, build={}, minutesAgo={}",
|
log.info("构建超时,直接标记完成: job={}, build={}, minutesAgo={}", job.getJobName(), build.getBuildNumber(), minutesAgo);
|
||||||
job.getJobName(), build.getBuildNumber(), minutesAgo);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -650,26 +670,22 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
sendNotification(config, channel, job, build, latestStatus, externalSystem, application, environment);
|
sendNotification(config, channel, job, build, latestStatus, externalSystem, application, environment);
|
||||||
existingRecord.setBuildEndNotice(true);
|
existingRecord.setBuildEndNotice(true);
|
||||||
jenkinsBuildNotificationRepository.save(existingRecord);
|
jenkinsBuildNotificationRepository.save(existingRecord);
|
||||||
log.info("发送构建结束通知: job={}, build={}, status={}",
|
log.info("发送构建结束通知: job={}, build={}, status={}", job.getJobName(), build.getBuildNumber(), latestStatus);
|
||||||
job.getJobName(), build.getBuildNumber(), latestStatus);
|
|
||||||
} else {
|
} else {
|
||||||
// 兜底:超过2小时仍未完成,强制标记结束(不发通知)
|
// 兜底:超过2小时仍未完成,强制标记结束(不发通知)
|
||||||
long hoursAgo = java.time.temporal.ChronoUnit.HOURS.between(build.getStarttime(), LocalDateTime.now());
|
long hoursAgo = ChronoUnit.HOURS.between(build.getStarttime(), LocalDateTime.now());
|
||||||
if (hoursAgo >= 2) {
|
if (hoursAgo >= 2) {
|
||||||
log.warn("构建超时未完成,强制标记结束: job={}, build={}, startTime={}",
|
log.warn("构建超时未完成,强制标记结束: job={}, build={}, startTime={}", job.getJobName(), build.getBuildNumber(), build.getStarttime());
|
||||||
job.getJobName(), build.getBuildNumber(), build.getStarttime());
|
|
||||||
existingRecord.setBuildEndNotice(true);
|
existingRecord.setBuildEndNotice(true);
|
||||||
jenkinsBuildNotificationRepository.save(existingRecord);
|
jenkinsBuildNotificationRepository.save(existingRecord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (org.springframework.orm.ObjectOptimisticLockingFailureException e) {
|
} catch (ObjectOptimisticLockingFailureException e) {
|
||||||
log.warn("构建通知记录乐观锁冲突,跳过: teamId={}, envId={}, buildId={}",
|
log.warn("构建通知记录乐观锁冲突,跳过: teamId={}, envId={}, buildId={}", config.getTeamId(), config.getEnvironmentId(), build.getId());
|
||||||
config.getTeamId(), config.getEnvironmentId(), build.getId());
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("处理构建通知失败: teamId={}, envId={}, buildId={}",
|
log.error("处理构建通知失败: teamId={}, envId={}, buildId={}", config.getTeamId(), config.getEnvironmentId(), build.getId(), e);
|
||||||
config.getTeamId(), config.getEnvironmentId(), build.getId(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -680,14 +696,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
return "SUCCESS".equals(status) || "FAILURE".equals(status) || "ABORTED".equals(status);
|
return "SUCCESS".equals(status) || "FAILURE".equals(status) || "ABORTED".equals(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断构建是否已结束
|
|
||||||
*/
|
|
||||||
private boolean isBuildFinished(JenkinsBuild build) {
|
|
||||||
String status = build.getBuildStatus();
|
|
||||||
return "SUCCESS".equals(status) || "FAILURE".equals(status) || "ABORTED".equals(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送通知(使用模板)
|
* 发送通知(使用模板)
|
||||||
*/
|
*/
|
||||||
@ -696,8 +704,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
try {
|
try {
|
||||||
// 1. 检查是否配置了构建通知模板
|
// 1. 检查是否配置了构建通知模板
|
||||||
if (config.getBuildNotificationTemplateId() == null) {
|
if (config.getBuildNotificationTemplateId() == null) {
|
||||||
log.warn("未配置构建通知模板,跳过通知: teamId={}, envId={}",
|
log.warn("未配置构建通知模板,跳过通知: teamId={}, envId={}", config.getTeamId(), config.getEnvironmentId());
|
||||||
config.getTeamId(), config.getEnvironmentId());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -823,83 +830,43 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 actions JSON 中解析 commit 信息
|
* 从 changeSets JSON 中解析 commit 信息
|
||||||
*/
|
*/
|
||||||
private void parseCommitInfoFromActions(String actionsJson, Map<String, Object> templateParams) {
|
private void parseCommitInfoFromActions(String changeSetsJson, Map<String, Object> templateParams) {
|
||||||
if (actionsJson == null || actionsJson.isEmpty()) {
|
if (changeSetsJson == null || changeSetsJson.isEmpty()) {
|
||||||
log.debug("actionsJson 为空,跳过解析");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
log.debug("开始解析 actionsJson: {}", actionsJson);
|
// 直接反序列化为 List<ChangeSet>
|
||||||
JsonNode root = objectMapper.readTree(actionsJson);
|
List<JenkinsBuildResponse.ChangeSet> changeSets = objectMapper.readValue(
|
||||||
|
changeSetsJson,
|
||||||
|
new com.fasterxml.jackson.core.type.TypeReference<List<JenkinsBuildResponse.ChangeSet>>() {
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 解析 gitCommitId
|
if (changeSets == null || changeSets.isEmpty()) {
|
||||||
if (root.has("gitCommitId") && !root.get("gitCommitId").isNull()) {
|
return;
|
||||||
templateParams.put("gitCommitId", root.get("gitCommitId").asText());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 changeSets 中的 commit 信息
|
|
||||||
JsonNode changeSets = root.get("changeSets");
|
|
||||||
log.debug("changeSets 节点: {}", changeSets);
|
|
||||||
|
|
||||||
if (changeSets != null && changeSets.isArray() && changeSets.size() > 0) {
|
|
||||||
log.debug("changeSets 数组大小: {}", changeSets.size());
|
|
||||||
StringBuilder commitMessages = new StringBuilder();
|
StringBuilder commitMessages = new StringBuilder();
|
||||||
for (JsonNode changeSet : changeSets) {
|
for (JenkinsBuildResponse.ChangeSet changeSet : changeSets) {
|
||||||
JsonNode items = changeSet.get("items");
|
if (changeSet.getItems() == null) continue;
|
||||||
log.debug("items 节点: {}", items);
|
for (JenkinsBuildResponse.ChangeSetItem item : changeSet.getItems()) {
|
||||||
|
if (item.getMsg() == null || item.getMsg().isEmpty()) continue;
|
||||||
|
|
||||||
if (items != null && items.isArray()) {
|
|
||||||
log.debug("items 数组大小: {}", items.size());
|
|
||||||
for (JsonNode item : items) {
|
|
||||||
log.debug("处理 item: {}", item);
|
|
||||||
|
|
||||||
// Jenkins API 返回的字段名是 msg
|
|
||||||
String msg = item.has("msg") ? item.get("msg").asText() : "";
|
|
||||||
|
|
||||||
// author 是一个对象,需要提取 fullName 或其他字段
|
|
||||||
String author = "";
|
|
||||||
if (item.has("author")) {
|
|
||||||
JsonNode authorNode = item.get("author");
|
|
||||||
log.debug("author 节点: {}", authorNode);
|
|
||||||
|
|
||||||
if (authorNode.isTextual()) {
|
|
||||||
// 如果是字符串,直接使用
|
|
||||||
author = authorNode.asText();
|
|
||||||
} else if (authorNode.isObject()) {
|
|
||||||
// 如果是对象,尝试提取 fullName
|
|
||||||
if (authorNode.has("fullName")) {
|
|
||||||
author = authorNode.get("fullName").asText();
|
|
||||||
} else if (authorNode.has("name")) {
|
|
||||||
author = authorNode.get("name").asText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String commitId = item.has("commitId") ? item.get("commitId").asText() : "";
|
|
||||||
|
|
||||||
log.debug("解析结果 - msg: {}, author: {}, commitId: {}", msg, author, commitId);
|
|
||||||
|
|
||||||
if (!msg.isEmpty()) {
|
|
||||||
if (commitMessages.length() > 0) {
|
if (commitMessages.length() > 0) {
|
||||||
commitMessages.append("\n");
|
commitMessages.append("\n");
|
||||||
}
|
}
|
||||||
// 截取 commitId 前8位
|
String commitId = item.getCommitId() != null ? item.getCommitId() : "";
|
||||||
String shortCommitId = commitId.length() > 8 ? commitId.substring(0, 8) : commitId;
|
String shortCommitId = commitId.length() > 8 ? commitId.substring(0, 8) : commitId;
|
||||||
commitMessages.append(String.format("[%s] %s - %s", shortCommitId, msg.trim(), author));
|
String author = item.getAuthor() != null && item.getAuthor().getFullName() != null
|
||||||
}
|
? item.getAuthor().getFullName() : "";
|
||||||
}
|
commitMessages.append(String.format("[%s] %s - %s", shortCommitId, item.getMsg().trim(), author));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commitMessages.length() > 0) {
|
if (commitMessages.length() > 0) {
|
||||||
log.info("成功解析 commitMessage: {}", commitMessages.toString());
|
|
||||||
templateParams.put("commitMessage", commitMessages.toString());
|
templateParams.put("commitMessage", commitMessages.toString());
|
||||||
} else {
|
|
||||||
log.warn("commitMessages 为空");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.warn("changeSets 为空或不是数组");
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解析 commit 信息失败", e);
|
log.error("解析 commit 信息失败", e);
|
||||||
@ -1073,7 +1040,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
|||||||
*/
|
*/
|
||||||
private void cleanupTempFile(String filePath) {
|
private void cleanupTempFile(String filePath) {
|
||||||
try {
|
try {
|
||||||
java.nio.file.Files.deleteIfExists(java.nio.file.Paths.get(filePath));
|
Files.deleteIfExists(Paths.get(filePath));
|
||||||
log.debug("临时文件已清理: {}", filePath);
|
log.debug("临时文件已清理: {}", filePath);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("清理临时文件失败: {}", filePath, e);
|
log.warn("清理临时文件失败: {}", filePath, e);
|
||||||
|
|||||||
@ -276,8 +276,14 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
|
|||||||
output.setBuildEndTimeMillis(buildDetails.getEndTimeMillis());
|
output.setBuildEndTimeMillis(buildDetails.getEndTimeMillis());
|
||||||
output.setBuildEndTime(buildDetails.getEndTime());
|
output.setBuildEndTime(buildDetails.getEndTime());
|
||||||
|
|
||||||
// Git 提交ID(服务层已兜底)
|
// Git 提交ID(从 changeSets 中获取最后一个 commit)
|
||||||
output.setGitCommitId(buildDetails.getGitCommitId() != null ? buildDetails.getGitCommitId() : "");
|
String gitCommitId = "";
|
||||||
|
var changeSetItems = buildDetails.getAllChangeSetItems();
|
||||||
|
if (changeSetItems != null && !changeSetItems.isEmpty()) {
|
||||||
|
var lastItem = changeSetItems.get(changeSetItems.size() - 1);
|
||||||
|
gitCommitId = lastItem.getCommitId() != null ? lastItem.getCommitId() : "";
|
||||||
|
}
|
||||||
|
output.setGitCommitId(gitCommitId);
|
||||||
|
|
||||||
// 制品URL(服务层已拼接)
|
// 制品URL(服务层已拼接)
|
||||||
output.setArtifactUrl(buildDetails.getArtifactUrl() != null ? buildDetails.getArtifactUrl() : "");
|
output.setArtifactUrl(buildDetails.getArtifactUrl() != null ? buildDetails.getArtifactUrl() : "");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user