From dec812089d7c7b64d7a09d2467142b23c08dd931 Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 14 Jun 2026 20:01:33 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EB=B6=88=EC=9D=BC?= =?UTF-8?q?=EC=B9=98=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DesktopAppReleaseService.java | 11 +-- desktop/package.json | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- frontend/src/components/AppDownloadModal.tsx | 16 ++++- scripts/build-desktop.sh | 69 +++++++++++++------ scripts/desktop-installer-version.sh | 33 +++++++++ scripts/jenkins-desktop-pipeline.sh | 41 +++++++---- scripts/publish-desktop-update.sh | 61 ++++++++++++---- 8 files changed, 180 insertions(+), 55 deletions(-) create mode 100755 scripts/desktop-installer-version.sh 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 시 반영)"