자동빌드 배포 오류 수정
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.DesktopAppBuildReadinessDto;
|
||||
import com.goldenchart.dto.DesktopAppBuildStatusDto;
|
||||
import com.goldenchart.dto.DesktopAppBuildTriggerDto;
|
||||
import com.goldenchart.dto.DesktopAppReleaseInfoDto;
|
||||
import com.goldenchart.service.DesktopAppBuildReadinessService;
|
||||
import com.goldenchart.service.DesktopAppBuildService;
|
||||
import com.goldenchart.service.DesktopAppReleaseService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -22,6 +24,7 @@ public class DesktopAppReleaseController {
|
||||
|
||||
private final DesktopAppReleaseService releaseService;
|
||||
private final DesktopAppBuildService buildService;
|
||||
private final DesktopAppBuildReadinessService readinessService;
|
||||
|
||||
@GetMapping("/info")
|
||||
public DesktopAppReleaseInfoDto info(HttpServletRequest request) {
|
||||
@@ -33,6 +36,11 @@ public class DesktopAppReleaseController {
|
||||
return buildService.getStatus();
|
||||
}
|
||||
|
||||
@GetMapping("/build/readiness")
|
||||
public DesktopAppBuildReadinessDto buildReadiness() {
|
||||
return readinessService.getReadiness();
|
||||
}
|
||||
|
||||
@PostMapping("/build")
|
||||
public DesktopAppBuildTriggerDto triggerBuild(
|
||||
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
public class DesktopAppBuildReadinessDto {
|
||||
/** 현재 배포·다운로드 가능 버전 */
|
||||
String publishedVersion;
|
||||
/** 다음 빌드 성공 시 예상 버전 (published patch +1) */
|
||||
String nextBuildVersion;
|
||||
boolean buildRequired;
|
||||
/** up_to_date | build_required | web_only | building | unknown */
|
||||
String status;
|
||||
String message;
|
||||
String lastBuiltVersion;
|
||||
String lastBuiltGitShort;
|
||||
String sourceHeadGitShort;
|
||||
int pendingCommitCount;
|
||||
boolean desktopChangesPending;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.goldenchart.dto.DesktopAppBuildReadinessDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class DesktopAppBuildReadinessService {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final DesktopAppBuildService buildService;
|
||||
|
||||
@Value("${goldenchart.desktop-app.release-dir:data/desktop-releases}")
|
||||
private String releaseDir;
|
||||
|
||||
public DesktopAppBuildReadinessDto getReadiness() {
|
||||
Path release = releasePath();
|
||||
JsonNode buildInfo = readJson(release.resolve("build-info.json"));
|
||||
JsonNode sourceState = readJson(release.resolve("source-state.json"));
|
||||
|
||||
String published = text(buildInfo, "version");
|
||||
String lastSha = text(buildInfo, "gitSha");
|
||||
String lastShort = text(buildInfo, "gitShort");
|
||||
String headSha = text(sourceState, "headSha");
|
||||
String headShort = text(sourceState, "headShort");
|
||||
boolean affectsDesktop = sourceState.path("affectsDesktop").asBoolean(true);
|
||||
int pendingCommits = sourceState.path("pendingCommitCount").asInt(0);
|
||||
|
||||
String nextVersion = bumpPatch(published != null ? published : "0.1.0");
|
||||
|
||||
var jenkins = buildService.getStatus();
|
||||
if (jenkins.isBuilding() || "queued".equals(jenkins.getStatus())) {
|
||||
return DesktopAppBuildReadinessDto.builder()
|
||||
.publishedVersion(published)
|
||||
.nextBuildVersion(nextVersion)
|
||||
.buildRequired(true)
|
||||
.status("building")
|
||||
.message("Jenkins 빌드 진행 중 (#" + jenkins.getBuildNumber() + "). 완료 후 v"
|
||||
+ nextVersion + " 설치 파일이 반영됩니다.")
|
||||
.lastBuiltVersion(published)
|
||||
.lastBuiltGitShort(lastShort)
|
||||
.sourceHeadGitShort(headShort)
|
||||
.pendingCommitCount(pendingCommits)
|
||||
.desktopChangesPending(affectsDesktop)
|
||||
.build();
|
||||
}
|
||||
|
||||
boolean shaKnown = headSha != null && !headSha.isBlank() && lastSha != null && !lastSha.isBlank();
|
||||
boolean inSync = shaKnown && headSha.equalsIgnoreCase(lastSha);
|
||||
|
||||
if (inSync || (shaKnown && !affectsDesktop && pendingCommits == 0)) {
|
||||
return DesktopAppBuildReadinessDto.builder()
|
||||
.publishedVersion(published)
|
||||
.nextBuildVersion(nextVersion)
|
||||
.buildRequired(false)
|
||||
.status("up_to_date")
|
||||
.message(published != null
|
||||
? "최신 소스가 반영되어 있습니다 (v" + published + "). 데스크톱 앱 빌드가 필요하지 않습니다."
|
||||
: "설치 파일이 없습니다. 「앱 빌드」로 첫 빌드를 실행하세요.")
|
||||
.lastBuiltVersion(published)
|
||||
.lastBuiltGitShort(lastShort)
|
||||
.sourceHeadGitShort(headShort)
|
||||
.pendingCommitCount(0)
|
||||
.desktopChangesPending(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
if (shaKnown && !affectsDesktop) {
|
||||
return DesktopAppBuildReadinessDto.builder()
|
||||
.publishedVersion(published)
|
||||
.nextBuildVersion(nextVersion)
|
||||
.buildRequired(false)
|
||||
.status("web_only")
|
||||
.message("main에 미반영 커밋이 있으나 데스크톱 앱에 영향 없는 변경입니다. PC 빌드는 불필요합니다.")
|
||||
.lastBuiltVersion(published)
|
||||
.lastBuiltGitShort(lastShort)
|
||||
.sourceHeadGitShort(headShort)
|
||||
.pendingCommitCount(pendingCommits)
|
||||
.desktopChangesPending(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
String pendingMsg = pendingCommits > 0
|
||||
? "데스크톱 영향 변경 " + pendingCommits + "커밋 미반영"
|
||||
: "최신 소스가 아직 PC 빌드에 반영되지 않았습니다";
|
||||
return DesktopAppBuildReadinessDto.builder()
|
||||
.publishedVersion(published)
|
||||
.nextBuildVersion(nextVersion)
|
||||
.buildRequired(true)
|
||||
.status("build_required")
|
||||
.message(pendingMsg + ". 「앱 빌드」 시 v" + nextVersion + " (macOS·Windows)로 빌드됩니다.")
|
||||
.lastBuiltVersion(published)
|
||||
.lastBuiltGitShort(lastShort)
|
||||
.sourceHeadGitShort(headShort)
|
||||
.pendingCommitCount(pendingCommits)
|
||||
.desktopChangesPending(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Path releasePath() {
|
||||
return Paths.get(releaseDir).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private JsonNode readJson(Path path) {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readTree(Files.readString(path));
|
||||
} catch (IOException e) {
|
||||
log.debug("[desktop-readiness] read {} failed: {}", path, e.getMessage());
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(JsonNode node, String field) {
|
||||
if (node == null || node.isMissingNode()) return null;
|
||||
JsonNode v = node.path(field);
|
||||
if (!v.isTextual()) return null;
|
||||
String t = v.asText().trim();
|
||||
return t.isEmpty() ? null : t;
|
||||
}
|
||||
|
||||
static String bumpPatch(String version) {
|
||||
String[] parts = version.replaceAll("^[vV]", "").split("\\.");
|
||||
int major = parts.length > 0 ? parseInt(parts[0]) : 0;
|
||||
int minor = parts.length > 1 ? parseInt(parts[1]) : 0;
|
||||
int patch = parts.length > 2 ? parseInt(parts[2]) : 0;
|
||||
return major + "." + minor + "." + (patch + 1);
|
||||
}
|
||||
|
||||
private static int parseInt(String s) {
|
||||
try {
|
||||
return Integer.parseInt(s.replaceAll("[^0-9].*", ""));
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,19 @@ public class DesktopAppBuildService {
|
||||
.build();
|
||||
}
|
||||
try {
|
||||
return fetchLastBuildStatus();
|
||||
DesktopAppBuildStatusDto last = fetchLastBuildStatus();
|
||||
if (isJenkinsQueued() && !last.isBuilding()) {
|
||||
return DesktopAppBuildStatusDto.builder()
|
||||
.jenkinsEnabled(true)
|
||||
.building(true)
|
||||
.status("queued")
|
||||
.buildNumber(last.getBuildNumber())
|
||||
.message("Jenkins 큐 대기 중…")
|
||||
.buildUrl(last.getBuildUrl())
|
||||
.startedAtMs(last.getStartedAtMs())
|
||||
.build();
|
||||
}
|
||||
return last;
|
||||
} catch (Exception e) {
|
||||
log.warn("[desktop-build] status failed: {}", e.getMessage());
|
||||
return DesktopAppBuildStatusDto.builder()
|
||||
@@ -110,12 +122,13 @@ public class DesktopAppBuildService {
|
||||
|
||||
if (hasBasicAuth()) {
|
||||
try {
|
||||
DesktopAppBuildStatusDto current = fetchLastBuildStatus();
|
||||
if (current.isBuilding()) {
|
||||
if (isJenkinsBusy()) {
|
||||
DesktopAppBuildStatusDto current = fetchLastBuildStatus();
|
||||
return DesktopAppBuildTriggerDto.builder()
|
||||
.accepted(false)
|
||||
.buildNumber(current.getBuildNumber())
|
||||
.message("이미 빌드가 진행 중입니다 (#" + current.getBuildNumber() + ").")
|
||||
.message("이미 빌드가 진행 중이거나 Jenkins 큐에 대기 중입니다 (#"
|
||||
+ current.getBuildNumber() + "). 완료 후 다시 시도하세요.")
|
||||
.build();
|
||||
}
|
||||
} catch (WebClientResponseException e) {
|
||||
@@ -149,6 +162,49 @@ public class DesktopAppBuildService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJenkinsBusy() {
|
||||
try {
|
||||
DesktopAppBuildStatusDto last = fetchLastBuildStatus();
|
||||
if (last.isBuilding()) {
|
||||
return true;
|
||||
}
|
||||
if (last.getStatus() != null && "queued".equalsIgnoreCase(last.getStatus())) {
|
||||
return true;
|
||||
}
|
||||
return isJenkinsQueued();
|
||||
} catch (Exception e) {
|
||||
log.debug("[desktop-build] busy check failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJenkinsQueued() {
|
||||
if (!hasBasicAuth()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
WebClient client = jenkinsClient();
|
||||
JsonNode queue = client.get()
|
||||
.uri("/queue/api/json?tree=items[task[name]]")
|
||||
.retrieve()
|
||||
.bodyToMono(JsonNode.class)
|
||||
.block(Duration.ofSeconds(10));
|
||||
if (queue == null || !queue.path("items").isArray()) {
|
||||
return false;
|
||||
}
|
||||
String jobName = jenkinsJob.trim();
|
||||
for (JsonNode item : queue.path("items")) {
|
||||
String name = textOrNull(item.path("task").path("name"));
|
||||
if (jobName.equals(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("[desktop-build] queue check failed: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private DesktopAppBuildStatusDto fetchLastBuildStatus() {
|
||||
WebClient client = jenkinsClient();
|
||||
String jobPath = jobApiPath();
|
||||
|
||||
Reference in New Issue
Block a user