From ac2b0eb7f11ed5bbc4895ae7709528512e739e6a Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 14 Jun 2026 19:49:40 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=B1=20=EB=B2=84=EC=A0=84=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DesktopAppReleaseService.java | 175 +++++++++++++++--- .../src/main/resources/application-docker.yml | 1 + backend/src/main/resources/application.yml | 2 +- scripts/build-desktop.sh | 10 + scripts/bump-desktop-version-from-release.sh | 11 +- scripts/jenkins-desktop-pipeline.sh | 6 +- scripts/publish-desktop-update.sh | 5 + 7 files changed, 180 insertions(+), 30 deletions(-) diff --git a/backend/src/main/java/com/goldenchart/service/DesktopAppReleaseService.java b/backend/src/main/java/com/goldenchart/service/DesktopAppReleaseService.java index db0fe24..93f227e 100644 --- a/backend/src/main/java/com/goldenchart/service/DesktopAppReleaseService.java +++ b/backend/src/main/java/com/goldenchart/service/DesktopAppReleaseService.java @@ -1,7 +1,10 @@ package com.goldenchart.service; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.goldenchart.dto.DesktopAppPlatformReleaseDto; import com.goldenchart.dto.DesktopAppReleaseInfoDto; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -14,18 +17,29 @@ import java.time.format.DateTimeFormatter; import java.util.Comparator; import java.util.Locale; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Stream; @Service @Slf4j +@RequiredArgsConstructor public class DesktopAppReleaseService { private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.of("Asia/Seoul")); + private static final Pattern VERSION_IN_NAME = + Pattern.compile("GoldenChart[_-](\\d+\\.\\d+\\.\\d+)", Pattern.CASE_INSENSITIVE); + + private final ObjectMapper objectMapper; + @Value("${goldenchart.desktop-app.release-dir:data/desktop-releases}") private String releaseDir; + @Value("${goldenchart.desktop-app.updates-dir:}") + private String updatesDir; + @Value("${goldenchart.desktop-app.version:}") private String configuredVersion; @@ -40,9 +54,18 @@ public class DesktopAppReleaseService { Optional macFile = resolveMacInstaller(); Optional winFile = resolveWindowsInstaller(); - String version = configuredVersion != null && !configuredVersion.isBlank() - ? configuredVersion.trim() - : resolveVersion(macFile, winFile); + String macVer = macFile.map(p -> extractVersionFromFileName(p.getFileName().toString())).orElse(null); + String winVer = winFile.map(p -> extractVersionFromFileName(p.getFileName().toString())).orElse(null); + String publishedVer = readLatestJsonVersion(); + String buildInfoVer = readBuildInfoVersion(releaseDirectory()); + + String version = maxSemver( + nullIfBlank(configuredVersion), + publishedVer, + buildInfoVer, + macVer, + winVer + ); Instant latestUpdated = Stream.of(macFile, winFile) .flatMap(Optional::stream) @@ -53,8 +76,8 @@ public class DesktopAppReleaseService { return DesktopAppReleaseInfoDto.builder() .version(version) .updatedAt(latestUpdated.equals(Instant.EPOCH) ? null : DT_FMT.format(latestUpdated)) - .mac(buildPlatform(macFile, staticBase, version)) - .windows(buildPlatform(winFile, staticBase, version)) + .mac(buildPlatform(macFile, staticBase, macVer != null ? macVer : version)) + .windows(buildPlatform(winFile, staticBase, winVer != null ? winVer : version)) .build(); } @@ -93,27 +116,97 @@ public class DesktopAppReleaseService { } } - private String resolveVersion(Optional macFile, Optional winFile) { - return Stream.of(macFile, winFile) - .flatMap(Optional::stream) - .map(p -> p.getFileName().toString()) - .map(this::extractVersionFromFileName) - .filter(v -> v != null && !v.isBlank()) - .findFirst() - .orElse(null); + private String readLatestJsonVersion() { + Path latest = resolveUpdatesDirectory() != null + ? resolveUpdatesDirectory().resolve("latest.json") + : null; + if (latest == null || !Files.isRegularFile(latest)) { + return null; + } + try { + JsonNode root = objectMapper.readTree(Files.readString(latest)); + JsonNode ver = root.path("version"); + return ver.isTextual() ? ver.asText().trim() : null; + } catch (Exception e) { + log.debug("[desktop-app] latest.json read failed: {}", e.getMessage()); + return null; + } + } + + private String readBuildInfoVersion(Path release) { + if (release == null) { + return null; + } + Path info = release.resolve("build-info.json"); + if (!Files.isRegularFile(info)) { + return null; + } + try { + JsonNode root = objectMapper.readTree(Files.readString(info)); + JsonNode ver = root.path("version"); + return ver.isTextual() ? ver.asText().trim() : null; + } catch (Exception e) { + log.debug("[desktop-app] build-info read failed: {}", e.getMessage()); + return null; + } } private String extractVersionFromFileName(String fileName) { - // GoldenChart_0.1.0_aarch64.dmg, GoldenChart_0.1.0_x64-setup.exe - var m = java.util.regex.Pattern - .compile("GoldenChart[_-](\\d+\\.\\d+\\.\\d+)", java.util.regex.Pattern.CASE_INSENSITIVE) - .matcher(fileName); + Matcher m = VERSION_IN_NAME.matcher(fileName); return m.find() ? m.group(1) : null; } + private String maxSemver(String... candidates) { + String best = null; + for (String c : candidates) { + if (c == null || c.isBlank()) { + continue; + } + if (best == null || compareSemver(c, best) > 0) { + best = c.trim(); + } + } + return best; + } + + private int compareSemver(String a, String b) { + int[] pa = parseSemverParts(a); + int[] pb = parseSemverParts(b); + for (int i = 0; i < 3; i++) { + int d = pa[i] - pb[i]; + if (d != 0) { + return d; + } + } + return 0; + } + + private int[] parseSemverParts(String v) { + String[] parts = v.replaceAll("^[vV]", "").split("\\."); + int[] out = new int[3]; + for (int i = 0; i < 3; i++) { + out[i] = i < parts.length ? parseIntSafe(parts[i]) : 0; + } + return out; + } + + private int parseIntSafe(String s) { + try { + return Integer.parseInt(s.replaceAll("[^0-9].*", "")); + } catch (NumberFormatException e) { + return 0; + } + } + + private static String nullIfBlank(String s) { + return s == null || s.isBlank() ? null : s.trim(); + } + private Optional resolveMacInstaller() { Path dir = releaseDirectory(); - if (dir == null) return Optional.empty(); + if (dir == null) { + return Optional.empty(); + } try (Stream stream = Files.list(dir)) { return stream .filter(p -> Files.isRegularFile(p) && p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".dmg")) @@ -126,7 +219,9 @@ public class DesktopAppReleaseService { private Optional resolveWindowsInstaller() { Path dir = releaseDirectory(); - if (dir == null) return Optional.empty(); + if (dir == null) { + return Optional.empty(); + } try (Stream stream = Files.list(dir)) { return stream .filter(p -> { @@ -144,26 +239,44 @@ public class DesktopAppReleaseService { private Comparator macComparator() { return Comparator .comparingInt((Path p) -> scoreMac(p.getFileName().toString())) + .thenComparing((Path p) -> semverScore(extractVersionFromFileName(p.getFileName().toString()))) .thenComparing(p -> lastModified(p).toEpochMilli()); } private Comparator windowsComparator() { return Comparator .comparingInt((Path p) -> scoreWindows(p.getFileName().toString())) + .thenComparing((Path p) -> semverScore(extractVersionFromFileName(p.getFileName().toString()))) .thenComparing(p -> lastModified(p).toEpochMilli()); } + private long semverScore(String version) { + if (version == null) { + return 0; + } + int[] p = parseSemverParts(version); + return p[0] * 1_000_000L + p[1] * 1_000L + p[2]; + } + private static int scoreMac(String name) { String lower = name.toLowerCase(Locale.ROOT); - if (lower.contains("aarch64") || lower.contains("arm64")) return 3; - if (lower.contains("universal")) return 2; - if (lower.contains("x64") || lower.contains("x86_64")) return 1; + if (lower.contains("aarch64") || lower.contains("arm64")) { + return 3; + } + if (lower.contains("universal")) { + return 2; + } + if (lower.contains("x64") || lower.contains("x86_64")) { + return 1; + } return 0; } private static int scoreWindows(String name) { String lower = name.toLowerCase(Locale.ROOT); - if (lower.contains("setup")) return 2; + if (lower.contains("setup")) { + return 2; + } return 1; } @@ -176,6 +289,22 @@ public class DesktopAppReleaseService { return dir; } + private Path resolveUpdatesDirectory() { + if (updatesDir != null && !updatesDir.isBlank()) { + Path dir = Paths.get(updatesDir).toAbsolutePath().normalize(); + if (Files.isDirectory(dir)) { + return dir; + } + } + Path fallback = releaseDirectory() != null + ? releaseDirectory().getParent().resolve("desktop-updates") + : null; + if (fallback != null && Files.isDirectory(fallback)) { + return fallback; + } + return null; + } + private static String encodeFileName(String fileName) { return fileName.replace(" ", "%20"); } diff --git a/backend/src/main/resources/application-docker.yml b/backend/src/main/resources/application-docker.yml index 2369f3f..a261b65 100644 --- a/backend/src/main/resources/application-docker.yml +++ b/backend/src/main/resources/application-docker.yml @@ -28,6 +28,7 @@ goldenchart: - "*" desktop-app: release-dir: ${GC_DESKTOP_APP_RELEASE_DIR:/app/data/desktop-releases} + updates-dir: ${GC_DESKTOP_UPDATES_DIR:/app/data/desktop-updates} public-base-url: ${GC_DESKTOP_APP_PUBLIC_BASE_URL:http://exdev.co.kr} jenkins: enabled: ${GC_DESKTOP_JENKINS_ENABLED:true} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 14880b4..4404451 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -122,7 +122,7 @@ goldenchart: trigger-token: ${GC_MOBILE_JENKINS_TRIGGER_TOKEN:goldenchart-mobile-build} desktop-app: release-dir: ${GC_DESKTOP_APP_RELEASE_DIR:data/desktop-releases} - version: ${GC_DESKTOP_APP_VERSION:} + updates-dir: ${GC_DESKTOP_UPDATES_DIR:} public-base-url: ${GC_DESKTOP_APP_PUBLIC_BASE_URL:http://exdev.co.kr} git-bare-dir: ${GC_GIT_BARE_DIR:} work-tree: ${GC_WORK_TREE:} diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh index c4ef041..511b985 100755 --- a/scripts/build-desktop.sh +++ b/scripts/build-desktop.sh @@ -46,6 +46,16 @@ if [[ "$BUILD_WIN" -eq 1 ]]; then fi CONF="$ROOT/desktop/src-tauri/tauri.conf.json" + +# CI: 매 Jenkins 빌드마다 patch 버전 bump (git tauri.conf 0.1.4 고정이어도 산출물 버전 증가) +if [[ -z "${BUMP_VERSION:-}" ]] && [[ "${DESKTOP_BUMP_ON_CI:-1}" == "1" ]] \ + && [[ -n "${BUILD_NUMBER:-}${DESKTOP_AUTO_BUMP:-}" ]]; then + chmod +x "$ROOT/scripts/bump-desktop-version-from-release.sh" + export WORK_TREE="$ROOT" + BUMPED="$("$ROOT/scripts/bump-desktop-version-from-release.sh")" + echo "Desktop CI version bump → $BUMPED" +fi + if [[ -n "$BUMP_VERSION" ]]; then node -e " const fs=require('fs'); const p=process.argv[1]; const b=process.argv[2]; diff --git a/scripts/bump-desktop-version-from-release.sh b/scripts/bump-desktop-version-from-release.sh index 07e4cf9..4c0ecd0 100755 --- a/scripts/bump-desktop-version-from-release.sh +++ b/scripts/bump-desktop-version-from-release.sh @@ -8,6 +8,7 @@ CONF="${WORK_TREE}/desktop/src-tauri/tauri.conf.json" LATEST="${WORK_TREE}/frontend/public/desktop/updates/latest.json" UPDATES_DIR="${WORK_TREE}/frontend/public/desktop/updates" BUILD_INFO="${WORK_TREE}/dist-desktop/build-info.json" +RELEASE_DIR="${WORK_TREE}/data/desktop-releases" node -e " const fs = require('fs'); @@ -16,6 +17,7 @@ const confPath = process.argv[1]; const latestPath = process.argv[2]; const updatesDir = process.argv[3]; const buildInfoPath = process.argv[4]; +const releaseDir = process.argv[5]; const parse = v => String(v || '0.0.0').trim().replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0); const cmp = (a, b) => { @@ -53,6 +55,13 @@ if (fs.existsSync(updatesDir)) { } } +if (releaseDir && fs.existsSync(releaseDir)) { + for (const name of fs.readdirSync(releaseDir)) { + const m = name.match(/GoldenChart[_-](\\d+\\.\\d+\\.\\d+)/i); + if (m) base = maxVer(base, m[1]); + } +} + const parts = parse(base); while (parts.length < 3) parts.push(0); parts[2] += 1; @@ -66,4 +75,4 @@ try { fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); } catch { /* ignore */ } console.log(nv); -" "$CONF" "$LATEST" "$UPDATES_DIR" "$BUILD_INFO" +" "$CONF" "$LATEST" "$UPDATES_DIR" "$BUILD_INFO" "$RELEASE_DIR" diff --git a/scripts/jenkins-desktop-pipeline.sh b/scripts/jenkins-desktop-pipeline.sh index 61157a4..5ba7375 100755 --- a/scripts/jenkins-desktop-pipeline.sh +++ b/scripts/jenkins-desktop-pipeline.sh @@ -44,12 +44,8 @@ if [[ -n "${DESKTOP_BUILD_MAC_ONLY:-}" ]]; then BUILD_ARGS=(--mac); fi if [[ -n "${DESKTOP_BUILD_WIN_ONLY:-}" ]]; then BUILD_ARGS=(--win); fi if [[ -n "${BUMP_VERSION:-}" ]]; then BUILD_ARGS+=(--bump-version "$BUMP_VERSION") -elif [[ -n "${BUILD_NUMBER:-}" && "${DESKTOP_BUMP_ON_CI:-1}" == "1" ]]; then - log "CI patch bump (latest.json 기준)..." - chmod +x "$ROOT/scripts/bump-desktop-version-from-release.sh" - NEW_VER="$("$ROOT/scripts/bump-desktop-version-from-release.sh")" - log "Desktop version → $NEW_VER" fi +# 버전 bump 는 scripts/build-desktop.sh (CI BUILD_NUMBER) 에서 처리 "$ROOT/scripts/build-desktop.sh" "${BUILD_ARGS[@]}" diff --git a/scripts/publish-desktop-update.sh b/scripts/publish-desktop-update.sh index ff09d08..853e658 100755 --- a/scripts/publish-desktop-update.sh +++ b/scripts/publish-desktop-update.sh @@ -38,11 +38,16 @@ mkdir -p "$WEB_STATIC_DIR" # 설치 파일(dist-desktop/*.dmg, *setup.exe) 복사 — nginx static + backend API 디렉터리 if [[ -d "$ARTIFACT_DIR" ]]; then mkdir -p "$DESKTOP_RELEASE_DIR" + # 구버전 설치 파일 제거 (Windows exe 0.1.3 잔존 등 방지) + find "$DESKTOP_RELEASE_DIR" -maxdepth 1 -type f \( -name 'GoldenChart_*.dmg' -o -name 'GoldenChart_*setup.exe' -o -name 'GoldenChart_*-setup.exe' \) -delete 2>/dev/null || true find "$ARTIFACT_DIR" -maxdepth 1 -type f \( -name '*.dmg' -o -name '*setup.exe' \) | while read -r f; do cp -f "$f" "$WEB_STATIC_DIR/" cp -f "$f" "$DESKTOP_RELEASE_DIR/" log "Installer: $(basename "$f")" done + if [[ -f "$BUILD_INFO" ]]; then + cp -f "$BUILD_INFO" "$DESKTOP_RELEASE_DIR/build-info.json" + fi fi PUBLISH_UPDATES="$WEB_STATIC_DIR"