diff --git a/backend/src/main/java/com/goldenchart/service/DesktopAppReleaseService.java b/backend/src/main/java/com/goldenchart/service/DesktopAppReleaseService.java
index f81b5a5..82680e3 100644
--- a/backend/src/main/java/com/goldenchart/service/DesktopAppReleaseService.java
+++ b/backend/src/main/java/com/goldenchart/service/DesktopAppReleaseService.java
@@ -59,14 +59,17 @@ public class DesktopAppReleaseService {
String publishedVer = readLatestJsonVersion();
String buildInfoVer = readBuildInfoVersion(releaseDirectory());
- // 헤더 버전: 실제 설치 파일 > build-info > 설정값 (latest.json은 업데이터용이라 다운로드와 어긋날 수 있음)
+ // 헤더 = build-info(마지막 성공 Jenkins 빌드) → mac/win 설치 파일 → 설정값
+ // latest.json 은 업데이터용 — 설치 파일보다 높으면 헤더에 쓰지 않음 (UI 불일치 방지)
String installerVer = maxSemver(macVer, winVer);
String version = maxSemver(
- installerVer,
buildInfoVer,
- nullIfBlank(configuredVersion),
- publishedVer
+ installerVer,
+ nullIfBlank(configuredVersion)
);
+ if (version == null) {
+ version = publishedVer;
+ }
Instant latestUpdated = Stream.of(macFile, winFile)
.flatMap(Optional::stream)
diff --git a/desktop/package.json b/desktop/package.json
index 99d389a..4d61ad5 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@goldenchart/desktop",
- "version": "0.1.4",
+ "version": "0.1.18",
"private": true,
"type": "module",
"scripts": {
diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json
index d618565..3314619 100644
--- a/desktop/src-tauri/tauri.conf.json
+++ b/desktop/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "GoldenChart",
- "version": "0.1.4",
+ "version": "0.1.18",
"identifier": "com.goldenchart.desktop",
"build": {
"beforeDevCommand": "npm run dev",
diff --git a/frontend/src/components/AppDownloadModal.tsx b/frontend/src/components/AppDownloadModal.tsx
index 8c7a2c5..f1f01ca 100644
--- a/frontend/src/components/AppDownloadModal.tsx
+++ b/frontend/src/components/AppDownloadModal.tsx
@@ -161,7 +161,7 @@ const PlatformCard: React.FC<{
const configs = {
mac: { label: 'macOS (Apple Silicon · Intel)', icon: '🍎', ext: '.dmg', isLink: false },
windows: { label: 'Windows (64-bit)', icon: '🪟', ext: '.exe', isLink: false },
- android: { label: 'Android (패드 · 폰)', icon: '🤖', ext: '.apk', isLink: false },
+ android: { label: 'Android (패드 · 폰 · 별도 버전)', icon: '🤖', ext: '.apk', isLink: false },
} as const;
const { label, icon, ext } = configs[platform];
@@ -435,6 +435,14 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
const hasAny = info?.mac?.available || info?.windows?.available || mobileInfo?.available;
+ const desktopVersionMismatch = Boolean(
+ info?.version
+ && (
+ (info.mac?.available && info.mac.version && info.mac.version !== info.version)
+ || (info.windows?.available && info.windows.version && info.windows.version !== info.version)
+ ),
+ );
+
if (loading) return
PC 프로그램 정보 불러오는 중…
;
if (error) return {error}
;
@@ -468,6 +476,12 @@ const DesktopTab: React.FC<{ canBuildDesktop: boolean }> = ({ canBuildDesktop })
)}
+ {desktopVersionMismatch && (
+
+ macOS·Windows 설치 파일 버전이 최신 빌드(v{info?.version})와 다릅니다. 「앱 빌드」를 다시 실행하세요.
+
+ )}
+
{showBuildProgress && (
/dev/null || echo 'n/a')"
+echo "Work tree: $WORK_TREE"
+echo "Git: $(git -C "$WORK_TREE" rev-parse --short HEAD 2>/dev/null || echo 'n/a')"
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
@@ -45,14 +48,16 @@ if [[ "$BUILD_WIN" -eq 1 ]]; then
rustup target add x86_64-pc-windows-msvc 2>/dev/null || true
fi
-CONF="$ROOT/desktop/src-tauri/tauri.conf.json"
+CONF="$WORK_TREE/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" ]] \
+# CI patch bump (jenkins-desktop-pipeline 에서 선행 bump 시 DESKTOP_SKIP_VERSION_BUMP=1)
+if [[ "${DESKTOP_SKIP_VERSION_BUMP:-}" != "1" ]] \
+ && [[ -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")"
+ chmod +x "$WORK_TREE/scripts/bump-desktop-version-from-release.sh"
+ export WORK_TREE
+ BUMPED="$("$WORK_TREE/scripts/bump-desktop-version-from-release.sh")"
echo "Desktop CI version bump → $BUMPED"
fi
@@ -85,12 +90,12 @@ fs.writeFileSync(confPath, JSON.stringify(conf, null, 2) + '\n');
console.log('window title →', title);
" "$CONF" "$VERSION"
-if [[ -f "$ROOT/scripts/patch-tauri-updater-pubkey.sh" ]]; then
- chmod +x "$ROOT/scripts/patch-tauri-updater-pubkey.sh"
+if [[ -f "$WORK_TREE/scripts/patch-tauri-updater-pubkey.sh" ]]; then
+ chmod +x "$WORK_TREE/scripts/patch-tauri-updater-pubkey.sh"
fi
-chmod +x "$ROOT/scripts/prepare-tauri-build.sh" 2>/dev/null || true
+chmod +x "$WORK_TREE/scripts/prepare-tauri-build.sh" 2>/dev/null || true
# shellcheck disable=SC1091
-source "$ROOT/scripts/prepare-tauri-build.sh"
+source "$WORK_TREE/scripts/prepare-tauri-build.sh"
npm install
@@ -98,9 +103,9 @@ echo "--- Vite build (desktop) ---"
export DESKTOP_APP_VERSION="$VERSION"
npm run build -w @goldenchart/desktop
-DESKTOP_DIR="$ROOT/desktop"
+DESKTOP_DIR="$WORK_TREE/desktop"
TAURI_TARGET="$DESKTOP_DIR/src-tauri/target"
-ARTIFACT_DIR="$ROOT/dist-desktop"
+ARTIFACT_DIR="${ARTIFACT_DIR:-$WORK_TREE/dist-desktop}"
UPDATES_DIR="$ARTIFACT_DIR/updates"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR" "$UPDATES_DIR"
@@ -125,7 +130,7 @@ copy_installer_matching_version() {
local found=""
while IFS= read -r f; do
[[ -z "$f" ]] && continue
- if [[ "$f" == *"${VERSION}"* ]]; then
+ if desktop_installer_matches_version "$(basename "$f")" "$VERSION"; then
found="$f"
break
fi
@@ -157,8 +162,32 @@ if [[ "$BUILD_WIN" -eq 1 ]]; then
collect_updater_artifacts
fi
-GIT_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo '')"
-GIT_SHORT="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo '')"
+# mac·win 설치 파일 semver 일치 검증
+MAC_VER="" WIN_VER=""
+shopt -s nullglob
+for f in "$ARTIFACT_DIR"/*.dmg; do
+ MAC_VER="$(desktop_extract_version_from_name "$(basename "$f")")"
+done
+for f in "$ARTIFACT_DIR"/*setup.exe; do
+ WIN_VER="$(desktop_extract_version_from_name "$(basename "$f")")"
+done
+shopt -u nullglob
+
+if [[ "$BUILD_MAC" -eq 1 && -n "$MAC_VER" && "$MAC_VER" != "$VERSION" ]]; then
+ echo "ERROR: macOS installer v${MAC_VER} != build v${VERSION}" >&2
+ exit 1
+fi
+if [[ "$BUILD_WIN" -eq 1 && -n "$WIN_VER" && "$WIN_VER" != "$VERSION" ]]; then
+ echo "ERROR: Windows installer v${WIN_VER} != build v${VERSION}" >&2
+ exit 1
+fi
+if [[ "$BUILD_MAC" -eq 1 && "$BUILD_WIN" -eq 1 && -n "$MAC_VER" && -n "$WIN_VER" && "$MAC_VER" != "$WIN_VER" ]]; then
+ echo "ERROR: macOS v${MAC_VER} != Windows v${WIN_VER}" >&2
+ exit 1
+fi
+
+GIT_SHA="$(git -C "$WORK_TREE" rev-parse HEAD 2>/dev/null || echo '')"
+GIT_SHORT="$(git -C "$WORK_TREE" rev-parse --short HEAD 2>/dev/null || echo '')"
cat > "$ARTIFACT_DIR/build-info.json" < | extract " >&2
+ exit 1
+ ;;
+ esac
+fi
diff --git a/scripts/jenkins-desktop-pipeline.sh b/scripts/jenkins-desktop-pipeline.sh
index 5ba7375..e35c14c 100755
--- a/scripts/jenkins-desktop-pipeline.sh
+++ b/scripts/jenkins-desktop-pipeline.sh
@@ -2,8 +2,8 @@
# Jenkins goldenChart-Desktop-Pipeline — 전체 빌드·배포 진입점
set -euo pipefail
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
+ROOT="$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)"
@@ -18,25 +18,25 @@ log "WORK_TREE=$WORK_TREE"
cd "$WORK_TREE"
-# Git 동기화 (Jenkins SCM checkout 후에도 file:// bare 와 맞춤)
+# Git 동기화 (checkout 후 tauri.conf 가 git 버전으로 리셋됨 → bump 필수)
if [[ -d "${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}" ]]; then
git --git-dir="${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}" \
--work-tree="$WORK_TREE" checkout -f main 2>/dev/null || true
fi
-chmod +x "$ROOT/scripts/"*.sh 2>/dev/null || true
+chmod +x "$WORK_TREE/scripts/"*.sh 2>/dev/null || true
# 의존성 (최초 1회 — 이미 있으면 빠르게 통과)
if ! command -v cargo-xwin >/dev/null 2>&1 || ! command -v rustc >/dev/null 2>&1; then
log "Desktop build deps 설치..."
- "$ROOT/scripts/install-desktop-build-deps.sh"
+ "$WORK_TREE/scripts/install-desktop-build-deps.sh"
fi
# updater pubkey (repo에 desktop/updater.pub 있으면 patch만)
-if [[ -f "$ROOT/desktop/updater.pub" ]]; then
- "$ROOT/scripts/patch-tauri-updater-pubkey.sh"
+if [[ -f "$WORK_TREE/desktop/updater.pub" ]]; then
+ "$WORK_TREE/scripts/patch-tauri-updater-pubkey.sh"
elif [[ -f "${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}" ]]; then
- "$ROOT/scripts/setup-desktop-updater-keys.sh"
+ "$WORK_TREE/scripts/setup-desktop-updater-keys.sh"
fi
BUILD_ARGS=(--all)
@@ -45,25 +45,36 @@ if [[ -n "${DESKTOP_BUILD_WIN_ONLY:-}" ]]; then BUILD_ARGS=(--win); fi
if [[ -n "${BUMP_VERSION:-}" ]]; then
BUILD_ARGS+=(--bump-version "$BUMP_VERSION")
fi
-# 버전 bump 는 scripts/build-desktop.sh (CI BUILD_NUMBER) 에서 처리
-"$ROOT/scripts/build-desktop.sh" "${BUILD_ARGS[@]}"
+export WORK_TREE
+export BUILD_NUMBER="${BUILD_NUMBER:-}"
+export DESKTOP_BUMP_ON_CI="${DESKTOP_BUMP_ON_CI:-1}"
+
+# mobile pipeline 과 동일: git checkout 직후 semver bump (0.1.4 고정 tauri.conf 방지)
+if [[ -z "${BUMP_VERSION:-}" ]] && [[ -n "${BUILD_NUMBER:-}" ]] && [[ "${DESKTOP_BUMP_ON_CI:-1}" == "1" ]]; then
+ log "CI Desktop version bump..."
+ BUMPED="$("$WORK_TREE/scripts/bump-desktop-version-from-release.sh")"
+ export DESKTOP_SKIP_VERSION_BUMP=1
+ log "Desktop version → $BUMPED"
+fi
+
+"$WORK_TREE/scripts/build-desktop.sh" "${BUILD_ARGS[@]}"
WORK_TREE="$WORK_TREE" \
-ARTIFACT_DIR="$ROOT/dist-desktop" \
+ARTIFACT_DIR="$WORK_TREE/dist-desktop" \
DESKTOP_UPDATE_BASE_URL="${DESKTOP_UPDATE_BASE_URL:-https://exdev.co.kr/desktop/updates}" \
TAURI_SIGNER_KEY="${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}" \
DESKTOP_RELEASE_NOTES="${DESKTOP_RELEASE_NOTES:-Build ${BUILD_NUMBER:-local}}" \
-"$ROOT/scripts/publish-desktop-update.sh"
+"$WORK_TREE/scripts/publish-desktop-update.sh"
# latest.json·설치 파일 nginx 즉시 반영 (볼륨 마운트)
-if [[ -x "$ROOT/scripts/reload-frontend-desktop-static.sh" ]]; then
- WORK_TREE="$WORK_TREE" "$ROOT/scripts/reload-frontend-desktop-static.sh" || true
+if [[ -x "$WORK_TREE/scripts/reload-frontend-desktop-static.sh" ]]; then
+ WORK_TREE="$WORK_TREE" "$WORK_TREE/scripts/reload-frontend-desktop-static.sh" || true
fi
# Jenkins artifact archive 경로
-if [[ -d "$ROOT/dist-desktop" ]]; then
- log "Artifacts ready: $ROOT/dist-desktop"
+if [[ -d "$WORK_TREE/dist-desktop" ]]; then
+ log "Artifacts ready: $WORK_TREE/dist-desktop"
fi
# 웹 frontend Docker 재배포 (latest.json static 포함) — 선택
diff --git a/scripts/publish-desktop-update.sh b/scripts/publish-desktop-update.sh
index 7ab1944..1818c8e 100755
--- a/scripts/publish-desktop-update.sh
+++ b/scripts/publish-desktop-update.sh
@@ -2,26 +2,29 @@
# GoldenChart Desktop — updater 서명 + latest.json + 웹 static 배포
set -euo pipefail
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT/dist-desktop}"
+SCRIPT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+WORK_TREE="${WORK_TREE:-$SCRIPT_ROOT}"
+ARTIFACT_DIR="${ARTIFACT_DIR:-$WORK_TREE/dist-desktop}"
UPDATES_SRC="${UPDATES_SRC:-$ARTIFACT_DIR/updates}"
-UPDATES_DIR="${UPDATES_DIR:-${WORK_TREE:-$ROOT}/frontend/public/desktop/updates}"
+UPDATES_DIR="${UPDATES_DIR:-$WORK_TREE/frontend/public/desktop/updates}"
WEB_STATIC_DIR="${WEB_STATIC_DIR:-$UPDATES_DIR}"
BASE_URL="${DESKTOP_UPDATE_BASE_URL:-https://exdev.co.kr/desktop/updates}"
BASE_URL="${BASE_URL/http:\/\//https:\/\/}"
-DESKTOP_RELEASE_DIR="${GC_DESKTOP_APP_RELEASE_DIR:-$ROOT/data/desktop-releases}"
+DESKTOP_RELEASE_DIR="${GC_DESKTOP_APP_RELEASE_DIR:-$WORK_TREE/data/desktop-releases}"
KEY_PATH="${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}"
-DESKTOP_DIR="$ROOT/desktop"
+DESKTOP_DIR="$WORK_TREE/desktop"
CONF="$DESKTOP_DIR/src-tauri/tauri.conf.json"
BUILD_INFO="$ARTIFACT_DIR/build-info.json"
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
# shellcheck disable=SC1091
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
+# shellcheck disable=SC1091
+source "$WORK_TREE/scripts/desktop-installer-version.sh"
log() { echo "[publish-desktop] $*"; }
-# CI bump 후 deploy checkout 등으로 tauri.conf가 되돌아갈 수 있음 — build-info 우선
+# build-info 가 단일 버전 소스 (tauri.conf 는 git checkout 으로 구버전일 수 있음)
VERSION="$(node -p "require('$CONF').version" 2>/dev/null || echo '0.1.0')"
if [[ -f "$BUILD_INFO" ]]; then
VERSION="$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version||'')}catch{}" "$BUILD_INFO" 2>/dev/null || echo "$VERSION")"
@@ -30,8 +33,10 @@ fi
NOTES="${DESKTOP_RELEASE_NOTES:-GoldenChart Desktop $VERSION}"
log "=== Publish Desktop v$VERSION ==="
+log "Work tree: $WORK_TREE"
log "Updates src: $UPDATES_SRC"
log "Web static: $WEB_STATIC_DIR"
+log "Release dir: $DESKTOP_RELEASE_DIR"
mkdir -p "$WEB_STATIC_DIR"
@@ -46,16 +51,18 @@ prune_old_installers() {
}
# 설치 파일(dist-desktop/*.dmg, *setup.exe) 복사 — nginx static + backend API 디렉터리
+MAC_VER="" WIN_VER=""
+INSTALLER_COUNT=0
+
if [[ -d "$ARTIFACT_DIR" ]]; then
mkdir -p "$DESKTOP_RELEASE_DIR"
prune_old_installers "$DESKTOP_RELEASE_DIR"
prune_old_installers "$WEB_STATIC_DIR"
- INSTALLER_COUNT=0
while IFS= read -r f; do
[[ -z "$f" ]] && continue
base="$(basename "$f")"
- if [[ "$base" != *"${VERSION}"* ]]; then
+ if ! desktop_installer_matches_version "$base" "$VERSION"; then
log "[WARN] skip installer (version mismatch): $base (expected v${VERSION})"
continue
fi
@@ -63,10 +70,28 @@ if [[ -d "$ARTIFACT_DIR" ]]; then
cp -f "$f" "$DESKTOP_RELEASE_DIR/"
log "Installer: $base"
INSTALLER_COUNT=$((INSTALLER_COUNT + 1))
+ case "$base" in
+ *.dmg) MAC_VER="$(desktop_extract_version_from_name "$base")" ;;
+ *setup.exe) WIN_VER="$(desktop_extract_version_from_name "$base")" ;;
+ esac
done < <(find "$ARTIFACT_DIR" -maxdepth 1 -type f \( -name '*.dmg' -o -name '*setup.exe' \) 2>/dev/null)
if [[ "$INSTALLER_COUNT" -eq 0 ]]; then
- log "[WARN] v${VERSION} 설치 파일(dmg/exe) 없음 — latest.json 만 갱신될 수 있음"
+ log "ERROR: v${VERSION} 설치 파일(dmg/exe) 없음 — latest.json 만 갱신하면 UI 버전이 어긋납니다." >&2
+ exit 1
+ fi
+
+ if [[ -n "$MAC_VER" && "$MAC_VER" != "$VERSION" ]]; then
+ log "ERROR: macOS installer v${MAC_VER} != publish v${VERSION}" >&2
+ exit 1
+ fi
+ if [[ -n "$WIN_VER" && "$WIN_VER" != "$VERSION" ]]; then
+ log "ERROR: Windows installer v${WIN_VER} != publish v${VERSION}" >&2
+ exit 1
+ fi
+ if [[ -n "$MAC_VER" && -n "$WIN_VER" && "$MAC_VER" != "$WIN_VER" ]]; then
+ log "ERROR: macOS v${MAC_VER} != Windows v${WIN_VER}" >&2
+ exit 1
fi
if [[ -f "$BUILD_INFO" ]]; then
@@ -102,9 +127,9 @@ if [[ -f "$BUILD_INFO" ]]; then
cp -f "$BUILD_INFO" "$PUBLISH_UPDATES/build-info.json"
fi
-# latest.json
+# latest.json — 설치 파일과 동일 semver
set +e
-node "$ROOT/scripts/generate-desktop-latest-json.mjs" \
+node "$WORK_TREE/scripts/generate-desktop-latest-json.mjs" \
--version "$VERSION" \
--dir "$PUBLISH_UPDATES" \
--base-url "$BASE_URL" \
@@ -113,12 +138,22 @@ GEN_RC=$?
set -e
if [[ "$GEN_RC" -eq 2 ]]; then
- log "[WARN] signed updater bundle 없음 — latest.json 유지 (placeholder 덮어쓰기 안 함)"
+ log "[WARN] signed updater bundle 없음 — latest.json 은 v${VERSION} build-info 기준 유지"
+ if [[ -f "$BUILD_INFO" ]]; then
+ node -e "
+const fs=require('fs'); const p=process.argv[1]; const v=process.argv[2]; const u=process.argv[3]; const n=process.argv[4];
+const latestPath=p+'/latest.json';
+let cur={version:v,notes:n,pub_date:new Date().toISOString(),platforms:{}};
+if(fs.existsSync(latestPath)){try{cur=JSON.parse(fs.readFileSync(latestPath,'utf8'));cur.version=v;cur.notes=n;}catch{}}
+fs.writeFileSync(latestPath, JSON.stringify(cur,null,2)+'\n');
+console.log('latest.json version →', v);
+" "$PUBLISH_UPDATES" "$VERSION" "$BASE_URL" "$NOTES"
+ fi
elif [[ "$GEN_RC" -ne 0 ]]; then
exit "$GEN_RC"
fi
-log "Published:"
+log "Published v${VERSION}:"
ls -la "$PUBLISH_UPDATES"
log "nginx: /desktop/updates/ → frontend/public/desktop/updates (Docker rebuild 시 반영)"