앱 버전 갱신문제
This commit is contained in:
@@ -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<Path> macFile = resolveMacInstaller();
|
||||
Optional<Path> 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<Path> macFile, Optional<Path> 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<Path> resolveMacInstaller() {
|
||||
Path dir = releaseDirectory();
|
||||
if (dir == null) return Optional.empty();
|
||||
if (dir == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try (Stream<Path> 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<Path> resolveWindowsInstaller() {
|
||||
Path dir = releaseDirectory();
|
||||
if (dir == null) return Optional.empty();
|
||||
if (dir == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try (Stream<Path> stream = Files.list(dir)) {
|
||||
return stream
|
||||
.filter(p -> {
|
||||
@@ -144,26 +239,44 @@ public class DesktopAppReleaseService {
|
||||
private Comparator<Path> 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<Path> 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");
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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:}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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[@]}"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user