자동빌드 배포 오류 수정
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();
|
||||
|
||||
@@ -13309,6 +13309,44 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
|
||||
.app-download-build-btn:hover:not(:disabled) { filter: brightness(1.08); }
|
||||
.app-download-build-btn:disabled, .app-download-build-btn.busy { opacity: 0.65; cursor: wait; }
|
||||
.app-download-build-btn.busy svg { animation: tmb-update-spin 0.9s linear infinite; }
|
||||
.app-download-readiness {
|
||||
margin: 0 0 12px; padding: 10px 12px; border-radius: 10px;
|
||||
border: 1px solid var(--border); background: var(--bg2);
|
||||
font-size: 12px; line-height: 1.45;
|
||||
}
|
||||
.app-download-readiness-badge {
|
||||
display: inline-block; font-size: 11px; font-weight: 700;
|
||||
padding: 2px 8px; border-radius: 999px; margin-bottom: 6px;
|
||||
}
|
||||
.app-download-readiness-msg { margin: 0; color: var(--text2); }
|
||||
.app-download-readiness-ver { margin: 6px 0 0; font-size: 11px; color: var(--text3); }
|
||||
.app-download-readiness--up_to_date {
|
||||
border-color: color-mix(in srgb, #22c55e 40%, var(--border));
|
||||
background: color-mix(in srgb, #22c55e 8%, var(--bg2));
|
||||
}
|
||||
.app-download-readiness--up_to_date .app-download-readiness-badge {
|
||||
background: color-mix(in srgb, #22c55e 20%, var(--bg)); color: #4ade80;
|
||||
}
|
||||
.app-download-readiness--web_only {
|
||||
border-color: color-mix(in srgb, #94a3b8 40%, var(--border));
|
||||
}
|
||||
.app-download-readiness--web_only .app-download-readiness-badge {
|
||||
background: color-mix(in srgb, #94a3b8 20%, var(--bg)); color: var(--text2);
|
||||
}
|
||||
.app-download-readiness--build_required {
|
||||
border-color: color-mix(in srgb, #f59e0b 45%, var(--border));
|
||||
background: color-mix(in srgb, #f59e0b 8%, var(--bg2));
|
||||
}
|
||||
.app-download-readiness--build_required .app-download-readiness-badge {
|
||||
background: color-mix(in srgb, #f59e0b 22%, var(--bg)); color: #fbbf24;
|
||||
}
|
||||
.app-download-readiness--building {
|
||||
border-color: color-mix(in srgb, #2196f3 45%, var(--border));
|
||||
background: color-mix(in srgb, #2196f3 8%, var(--bg2));
|
||||
}
|
||||
.app-download-readiness--building .app-download-readiness-badge {
|
||||
background: color-mix(in srgb, #2196f3 20%, var(--bg)); color: #5eb5ff;
|
||||
}
|
||||
.app-download-build-status {
|
||||
margin: -8px 0 12px; font-size: 12px; color: var(--text2); line-height: 1.45;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/**
|
||||
* 모바일·PC 프로그램 설치 — 탭 UI (모바일 QR / PC dmg·exe + Jenkins 빌드)
|
||||
*/
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
import DraggableModalFrame from './DraggableModalFrame';
|
||||
import {
|
||||
loadDesktopAppBuildReadiness,
|
||||
loadDesktopAppBuildStatus,
|
||||
loadDesktopAppReleaseInfo,
|
||||
loadMobileAppReleaseInfo,
|
||||
triggerDesktopAppBuild,
|
||||
type DesktopAppBuildReadinessDto,
|
||||
type DesktopAppBuildStatusDto,
|
||||
type DesktopAppPlatformReleaseDto,
|
||||
type DesktopAppReleaseInfoDto,
|
||||
@@ -351,12 +353,14 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
const [info, setInfo] = useState<DesktopAppReleaseInfoDto | null>(null);
|
||||
const [mobileInfo, setMobileInfo] = useState<MobileAppReleaseInfoDto | null>(null);
|
||||
const [buildStatus, setBuildStatus] = useState<DesktopAppBuildStatusDto | null>(null);
|
||||
const [readiness, setReadiness] = useState<DesktopAppBuildReadinessDto | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [buildBusy, setBuildBusy] = useState(false);
|
||||
const [buildHint, setBuildHint] = useState<string | null>(null);
|
||||
const [buildTracking, setBuildTracking] = useState(false);
|
||||
const [elapsedTick, setElapsedTick] = useState(0);
|
||||
const autoTrackStarted = useRef(false);
|
||||
|
||||
const reloadRelease = useCallback(async () => {
|
||||
const [desktop, mobile] = await Promise.all([
|
||||
@@ -374,19 +378,28 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
return st;
|
||||
}, []);
|
||||
|
||||
const reloadReadiness = useCallback(async () => {
|
||||
const rd = await loadDesktopAppBuildReadiness();
|
||||
setReadiness(rd);
|
||||
return rd;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void Promise.all([reloadRelease(), reloadBuildStatus()])
|
||||
void Promise.all([reloadRelease(), reloadBuildStatus(), reloadReadiness()])
|
||||
.catch(() => { if (!cancelled) setError('PC 프로그램 정보를 불러오지 못했습니다.'); })
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [reloadRelease, reloadBuildStatus]);
|
||||
}, [reloadRelease, reloadBuildStatus, reloadReadiness]);
|
||||
|
||||
const isBuilding = Boolean(buildStatus?.building || buildStatus?.status === 'queued');
|
||||
const showBuildProgress = Boolean(
|
||||
buildBusy || buildTracking || isBuilding
|
||||
|| (buildStatus?.buildNumber && buildStatus.status !== 'idle' && buildStatus.status !== 'ready'),
|
||||
);
|
||||
const showBuildProgress = Boolean(buildBusy || buildTracking || isBuilding);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || autoTrackStarted.current || !isBuilding) return;
|
||||
autoTrackStarted.current = true;
|
||||
setBuildTracking(true);
|
||||
}, [loading, isBuilding]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showBuildProgress || buildStatus?.startedAtMs == null) return;
|
||||
@@ -401,27 +414,38 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
if (!st) return;
|
||||
if (isTerminalBuildStatus(st.status, st.building)) {
|
||||
setBuildTracking(false);
|
||||
void reloadRelease();
|
||||
} else if (!st.building && st.status !== 'queued') {
|
||||
void reloadRelease();
|
||||
void Promise.all([reloadRelease(), reloadReadiness()]).then(([desktop]) => {
|
||||
if (st.status === 'success' && readiness?.nextBuildVersion && desktop?.version) {
|
||||
if (desktop.version === readiness.nextBuildVersion) {
|
||||
setBuildHint(`빌드 완료 — v${desktop.version} 설치 파일이 반영되었습니다.`);
|
||||
} else {
|
||||
setBuildHint(`빌드 완료 — v${desktop.version} (예상 v${readiness.nextBuildVersion})`);
|
||||
}
|
||||
} else if (st.status === 'failure') {
|
||||
setBuildHint(st.message ?? '빌드 실패 — Jenkins 로그를 확인하세요.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
poll();
|
||||
const timer = window.setInterval(poll, BUILD_POLL_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [buildTracking, isBuilding, buildBusy, reloadBuildStatus, reloadRelease]);
|
||||
}, [buildTracking, isBuilding, buildBusy, reloadBuildStatus, reloadRelease, reloadReadiness, readiness?.nextBuildVersion]);
|
||||
|
||||
const handleBuild = useCallback(async () => {
|
||||
if (!canBuildDesktop || buildBusy || isBuilding) return;
|
||||
setBuildBusy(true);
|
||||
setBuildTracking(true);
|
||||
setBuildHint('Jenkins 빌드 요청 중…');
|
||||
const nextVer = readiness?.nextBuildVersion;
|
||||
setBuildHint(nextVer
|
||||
? `Jenkins 빌드 요청 중… (예상 v${nextVer})`
|
||||
: 'Jenkins 빌드 요청 중…');
|
||||
try {
|
||||
const res = await triggerDesktopAppBuild();
|
||||
setBuildHint(res.message ?? (res.accepted ? '빌드 시작됨' : '빌드 요청 거부'));
|
||||
if (!res.accepted) setBuildTracking(false);
|
||||
await reloadBuildStatus();
|
||||
await Promise.all([reloadBuildStatus(), reloadReadiness()]);
|
||||
if (res.accepted) {
|
||||
window.setTimeout(() => { void reloadBuildStatus(); }, 2000);
|
||||
}
|
||||
@@ -431,7 +455,7 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
} finally {
|
||||
setBuildBusy(false);
|
||||
}
|
||||
}, [canBuildDesktop, buildBusy, isBuilding, reloadBuildStatus]);
|
||||
}, [canBuildDesktop, buildBusy, isBuilding, reloadBuildStatus, reloadReadiness, readiness?.nextBuildVersion]);
|
||||
|
||||
const hasAny = info?.mac?.available || info?.windows?.available || mobileInfo?.available;
|
||||
|
||||
@@ -443,10 +467,21 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
),
|
||||
);
|
||||
|
||||
const readinessLabel = useMemo(() => {
|
||||
if (!readiness) return null;
|
||||
switch (readiness.status) {
|
||||
case 'up_to_date': return '최신 버전';
|
||||
case 'web_only': return 'PC 빌드 불필요';
|
||||
case 'build_required': return '빌드 필요';
|
||||
case 'building': return '빌드 진행 중';
|
||||
default: return readiness.buildRequired ? '빌드 필요' : '상태 확인';
|
||||
}
|
||||
}, [readiness]);
|
||||
|
||||
if (loading) return <p className="app-download-muted">PC 프로그램 정보 불러오는 중…</p>;
|
||||
if (error) return <p className="app-download-error">{error}</p>;
|
||||
|
||||
const statusMessage = !showBuildProgress ? (buildHint ?? buildStatus?.message) : null;
|
||||
const statusMessage = !showBuildProgress ? (buildHint ?? (readiness?.status === 'up_to_date' ? readiness.message : buildStatus?.message)) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -467,7 +502,9 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
className={`app-download-build-btn${buildBusy || isBuilding ? ' busy' : ''}`}
|
||||
onClick={() => { void handleBuild(); }}
|
||||
disabled={buildBusy || isBuilding}
|
||||
title="Git main 최신 소스로 macOS·Windows 데스크톱 앱 Jenkins 빌드"
|
||||
title={readiness?.buildRequired
|
||||
? `다음 빌드 예상 버전 v${readiness.nextBuildVersion ?? '?'}`
|
||||
: '현재 최신 소스가 PC 빌드에 반영되어 있습니다'}
|
||||
aria-label="데스크톱 앱 빌드"
|
||||
>
|
||||
<IcDesktopBuild />
|
||||
@@ -476,6 +513,18 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
|
||||
)}
|
||||
</div>
|
||||
|
||||
{readiness && readinessLabel && (
|
||||
<div className={`app-download-readiness app-download-readiness--${readiness.status}`}>
|
||||
<span className="app-download-readiness-badge">{readinessLabel}</span>
|
||||
<p className="app-download-readiness-msg">{readiness.message}</p>
|
||||
{readiness.publishedVersion && readiness.nextBuildVersion && readiness.buildRequired && (
|
||||
<p className="app-download-readiness-ver">
|
||||
현재 v{readiness.publishedVersion} → 빌드 후 v{readiness.nextBuildVersion}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{desktopVersionMismatch && (
|
||||
<p className="app-download-error" style={{ marginBottom: 8 }}>
|
||||
macOS·Windows 설치 파일 버전이 최신 빌드(v{info?.version})와 다릅니다. 「앱 빌드」를 다시 실행하세요.
|
||||
|
||||
@@ -719,6 +719,23 @@ export async function loadDesktopAppBuildStatus(): Promise<DesktopAppBuildStatus
|
||||
return request<DesktopAppBuildStatusDto>('/desktop-app/build/status');
|
||||
}
|
||||
|
||||
export interface DesktopAppBuildReadinessDto {
|
||||
publishedVersion?: string;
|
||||
nextBuildVersion?: string;
|
||||
buildRequired: boolean;
|
||||
status: string;
|
||||
message?: string;
|
||||
lastBuiltVersion?: string;
|
||||
lastBuiltGitShort?: string;
|
||||
sourceHeadGitShort?: string;
|
||||
pendingCommitCount?: number;
|
||||
desktopChangesPending?: boolean;
|
||||
}
|
||||
|
||||
export async function loadDesktopAppBuildReadiness(): Promise<DesktopAppBuildReadinessDto | null> {
|
||||
return request<DesktopAppBuildReadinessDto>('/desktop-app/build/readiness');
|
||||
}
|
||||
|
||||
export async function triggerDesktopAppBuild(): Promise<DesktopAppBuildTriggerDto> {
|
||||
return requestOrThrow<DesktopAppBuildTriggerDto>('/desktop-app/build', { method: 'POST' });
|
||||
}
|
||||
|
||||
@@ -74,6 +74,13 @@ if (releaseDir && fs.existsSync(releaseDir)) {
|
||||
const m = name.match(/GoldenChart[_-](\\d+\\.\\d+\\.\\d+)/i);
|
||||
if (m) base = maxVer(base, m[1]);
|
||||
}
|
||||
const releaseBuildInfo = path.join(releaseDir, 'build-info.json');
|
||||
if (fs.existsSync(releaseBuildInfo)) {
|
||||
try {
|
||||
const bi = JSON.parse(fs.readFileSync(releaseBuildInfo, 'utf8'));
|
||||
if (bi.version) base = maxVer(base, bi.version);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
const parts = parse(base);
|
||||
|
||||
@@ -26,6 +26,9 @@ if [[ -d "$GIT_DIR" ]]; then
|
||||
git --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" checkout -f main 2>/dev/null || true
|
||||
fi
|
||||
|
||||
chmod +x "$WORK_TREE/scripts/"*.sh 2>/dev/null || true
|
||||
"$WORK_TREE/scripts/prepare-desktop-jenkins-build.sh"
|
||||
|
||||
# Jenkins SCM workspace 가 있으면 scripts 를 WORK_TREE 로 동기 (구 스크립트 방지)
|
||||
if [[ -n "${WORKSPACE:-}" && -d "${WORKSPACE}/scripts" && "${WORKSPACE}" != "$WORK_TREE" ]]; then
|
||||
log "Sync scripts: ${WORKSPACE}/scripts → ${WORK_TREE}/scripts"
|
||||
|
||||
+17
-3
@@ -49,17 +49,31 @@ while read -r OLDREV NEWREV REFNAME; do
|
||||
WORK_TREE="$WORK_TREE" "$RELOAD" >> "$DEPLOY_LOG" 2>&1 || true
|
||||
fi
|
||||
|
||||
# Desktop(macOS·Windows) Jenkins 빌드 — git push 성공 후 자동 (웹 배포와 독립)
|
||||
chmod +x "$WORK_TREE/scripts/update-desktop-source-state.sh" 2>/dev/null || true
|
||||
if [[ -x "$WORK_TREE/scripts/update-desktop-source-state.sh" ]]; then
|
||||
WORK_TREE="$WORK_TREE" "$WORK_TREE/scripts/update-desktop-source-state.sh" || true
|
||||
fi
|
||||
|
||||
# Desktop(macOS·Windows) Jenkins 빌드 — 데스크톱 영향 변경 시에만 자동 트리거
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
|
||||
TRIGGER="${WORK_TREE}/scripts/trigger-desktop-jenkins-build.sh"
|
||||
if [ -x "$TRIGGER" ]; then
|
||||
DESKTOP_BUILD_NEEDED=1
|
||||
if [[ -x "$WORK_TREE/scripts/desktop-version-scope.sh" ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$WORK_TREE/scripts/desktop-version-scope.sh"
|
||||
if ! desktop_version_diff_affects_app "$OLDREV..$NEWREV"; then
|
||||
DESKTOP_BUILD_NEEDED=0
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Desktop Jenkins 스kip — 데스크톱 영향 없는 변경"
|
||||
fi
|
||||
fi
|
||||
if [ "$DESKTOP_BUILD_NEEDED" = "1" ] && [ -x "$TRIGGER" ]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Desktop Jenkins 빌드 트리거"
|
||||
if "$TRIGGER" >> "$DEPLOY_LOG" 2>&1; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Desktop 빌드 큐 등록 완료"
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] Desktop Jenkins 트리거 실패 (웹 배포는 성공)"
|
||||
fi
|
||||
else
|
||||
elif [ "$DESKTOP_BUILD_NEEDED" = "1" ]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] trigger-desktop-jenkins-build.sh 없음: $TRIGGER"
|
||||
fi
|
||||
|
||||
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# Jenkins Desktop 빌드 시작 전 — git 동기화·deps·source-state (1회 성공률 향상)
|
||||
set -euo pipefail
|
||||
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
|
||||
ROOT="$WORK_TREE"
|
||||
export WORK_TREE
|
||||
|
||||
export PATH="/opt/homebrew/opt/llvm/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
|
||||
for prefix in "$HOME/homebrew" /opt/homebrew; do
|
||||
[[ -x "$prefix/bin/brew" ]] && eval "$("$prefix/bin/brew" shellenv)"
|
||||
done
|
||||
# shellcheck disable=SC1091
|
||||
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
|
||||
|
||||
log() { echo "[prepare-desktop $(date '+%H:%M:%S')] $*"; }
|
||||
|
||||
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}"
|
||||
if [[ -d "$GIT_DIR" ]]; then
|
||||
log "git fetch + checkout main"
|
||||
if git --git-dir="$GIT_DIR" remote get-url origin >/dev/null 2>&1; then
|
||||
git --git-dir="$GIT_DIR" fetch origin main 2>/dev/null || true
|
||||
fi
|
||||
git --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" checkout -f main 2>/dev/null || true
|
||||
git --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" reset --hard main 2>/dev/null || true
|
||||
fi
|
||||
|
||||
chmod +x "$WORK_TREE/scripts/"*.sh 2>/dev/null || true
|
||||
"$WORK_TREE/scripts/update-desktop-source-state.sh"
|
||||
|
||||
if ! command -v cargo-xwin >/dev/null 2>&1 || ! command -v rustc >/dev/null 2>&1; then
|
||||
log "desktop build deps 설치"
|
||||
"$WORK_TREE/scripts/install-desktop-build-deps.sh"
|
||||
fi
|
||||
|
||||
if [[ -f "$WORK_TREE/desktop/updater.pub" ]]; then
|
||||
"$WORK_TREE/scripts/patch-tauri-updater-pubkey.sh"
|
||||
fi
|
||||
|
||||
log "HEAD $(git -C "$WORK_TREE" rev-parse --short HEAD 2>/dev/null || echo n/a)"
|
||||
log "prepare OK"
|
||||
@@ -180,4 +180,9 @@ log "Published v${VERSION}:"
|
||||
ls -la "$PUBLISH_UPDATES"
|
||||
|
||||
log "nginx: /desktop/updates/ → frontend/public/desktop/updates (Docker rebuild 시 반영)"
|
||||
|
||||
if [[ -x "$WORK_TREE/scripts/update-desktop-source-state.sh" ]]; then
|
||||
WORK_TREE="$WORK_TREE" "$WORK_TREE/scripts/update-desktop-source-state.sh" || true
|
||||
fi
|
||||
|
||||
log "Done."
|
||||
|
||||
@@ -56,6 +56,16 @@ if curl_get "${JOB_API}/lastBuild/api/json?tree=building" 2>/dev/null | grep -q
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if curl_get "${JENKINS_URL}/queue/api/json?tree=items[task[name]]" 2>/dev/null | grep -q "\"name\":\"${JENKINS_JOB}\""; then
|
||||
log "[SKIP] Jenkins 큐에 동일 Job 대기 중"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
|
||||
if [[ -x "$WORK_TREE/scripts/update-desktop-source-state.sh" ]]; then
|
||||
WORK_TREE="$WORK_TREE" "$WORK_TREE/scripts/update-desktop-source-state.sh" || true
|
||||
fi
|
||||
|
||||
parse_crumb_field() {
|
||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('crumbRequestField',''))" 2>/dev/null || true
|
||||
}
|
||||
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# main HEAD vs 마지막 성공 PC 빌드(build-info) 비교 → source-state.json (API·UI용)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK_TREE="${WORK_TREE:-$ROOT}"
|
||||
RELEASE_DIR="${GC_DESKTOP_APP_RELEASE_DIR:-$WORK_TREE/data/desktop-releases}"
|
||||
BUILD_INFO="$RELEASE_DIR/build-info.json"
|
||||
OUT="$RELEASE_DIR/source-state.json"
|
||||
|
||||
mkdir -p "$RELEASE_DIR"
|
||||
|
||||
HEAD_SHA="$(git -C "$WORK_TREE" rev-parse HEAD 2>/dev/null || echo '')"
|
||||
HEAD_SHORT="$(git -C "$WORK_TREE" rev-parse --short HEAD 2>/dev/null || echo '')"
|
||||
|
||||
LAST_SHA=""
|
||||
PUBLISHED_VER=""
|
||||
if [[ -f "$BUILD_INFO" ]]; then
|
||||
read -r LAST_SHA PUBLISHED_VER < <(node -e "
|
||||
const fs=require('fs');
|
||||
try {
|
||||
const j=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));
|
||||
console.log((j.gitSha||'')+' '+(j.version||''));
|
||||
} catch { console.log(' '); }
|
||||
" "$BUILD_INFO")
|
||||
fi
|
||||
|
||||
PENDING=0
|
||||
AFFECTS=0
|
||||
if [[ -n "$HEAD_SHA" && -n "$LAST_SHA" && "$HEAD_SHA" != "$LAST_SHA" ]]; then
|
||||
PENDING="$(git -C "$WORK_TREE" rev-list --count "${LAST_SHA}..${HEAD_SHA}" 2>/dev/null || echo 0)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT/scripts/desktop-version-scope.sh"
|
||||
if desktop_version_diff_affects_app "${LAST_SHA}..${HEAD_SHA}"; then
|
||||
AFFECTS=1
|
||||
fi
|
||||
elif [[ -n "$HEAD_SHA" && -z "$LAST_SHA" ]]; then
|
||||
PENDING=1
|
||||
AFFECTS=1
|
||||
fi
|
||||
|
||||
node -e "
|
||||
const fs=require('fs');
|
||||
const out=process.argv[1];
|
||||
const data={
|
||||
headSha: process.argv[2]||'',
|
||||
headShort: process.argv[3]||'',
|
||||
lastBuiltSha: process.argv[4]||'',
|
||||
publishedVersion: process.argv[5]||'',
|
||||
pendingCommitCount: parseInt(process.argv[6],10)||0,
|
||||
affectsDesktop: process.argv[7]==='1',
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
fs.writeFileSync(out, JSON.stringify(data,null,2)+'\n');
|
||||
console.log('[source-state]', data.headShort||'?','pending',data.pendingCommitCount,'affects',data.affectsDesktop);
|
||||
" "$OUT" "$HEAD_SHA" "$HEAD_SHORT" "$LAST_SHA" "$PUBLISHED_VER" "$PENDING" "$AFFECTS"
|
||||
Reference in New Issue
Block a user