diff --git a/desktop/src/bridge/updater.ts b/desktop/src/bridge/updater.ts index 372f69c..9305417 100644 --- a/desktop/src/bridge/updater.ts +++ b/desktop/src/bridge/updater.ts @@ -147,6 +147,11 @@ async function formatUpdateError(e: unknown): Promise { const onWin = isWindowsOs(); if (!msg.trim()) return '업데이트 확인에 실패했습니다.'; + if (/unexpected end of json|eof|empty|content-length.*0|^$/.test(lower) && msg.trim().length < 80) { + return onMac + ? '서버 latest.json 이 비어 있습니다. PC 프로그램 탭에서 dmg를 받아 다시 설치해 주세요. (관리자: Desktop Jenkins publish 필요)' + : '서버 latest.json 이 비어 있습니다. PC 프로그램 탭에서 exe를 받아 다시 설치해 주세요. (관리자: Desktop Jenkins publish 필요)'; + } if (/could not fetch a valid release json|release not found/.test(lower)) { return onMac ? '업데이트 서버 응답을 처리하지 못했습니다. PC 프로그램 탭에서 dmg를 새로 설치해 주세요.' diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 294857b..c671217 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -80,6 +80,20 @@ if [[ -x "$RECONCILE" ]]; then WORK_TREE="$WORK_TREE" "$RECONCILE" || warn "reconcile-desktop-latest-json 실패" fi +ENSURE="$WORK_TREE/scripts/ensure-desktop-latest-json.mjs" +UPDATES_DIR="$WORK_TREE/frontend/public/desktop/updates" +if [[ -f "$ENSURE" && -d "$UPDATES_DIR" ]]; then + DESKTOP_VER="$(node -p "require('$WORK_TREE/desktop/src-tauri/tauri.conf.json').version" 2>/dev/null || echo '0.1.0')" + if ! node "$ENSURE" --dir "$UPDATES_DIR" --version "$DESKTOP_VER" \ + --base-url "${DESKTOP_UPDATE_BASE_URL:-https://exdev.co.kr/desktop/updates}" 2>/dev/null; then + warn "latest.json platforms 비어 있음 — Desktop Jenkins 파이프라인 publish 필요" + fi + LATEST="$UPDATES_DIR/latest.json" + if [[ -f "$LATEST" ]] && [[ "$(wc -c < "$LATEST" | tr -d ' ')" -lt 8 ]]; then + err "latest.json 이 비어 있습니다 — Desktop Jenkins publish 후 재배포하세요" + fi +fi + # Phase 2 — Docker 이미지 빌드 (호스트 npm 사용 안 함 → postcss 홈 디렉터리 간섭 방지) info "[Phase 2] Docker 이미지 빌드 ($SERVICE_NAME)" diff --git a/scripts/ensure-desktop-latest-json.mjs b/scripts/ensure-desktop-latest-json.mjs new file mode 100755 index 0000000..769a601 --- /dev/null +++ b/scripts/ensure-desktop-latest-json.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +/** + * latest.json 무결성 보장 — 빈 파일·깨진 JSON 방지 (Tauri updater JSON parse 오류 예방) + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +function arg(name, fallback) { + const i = process.argv.indexOf(name); + return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback; +} + +const dir = path.resolve(arg('--dir', 'frontend/public/desktop/updates')); +const version = arg('--version', '0.1.0'); +const rawBaseUrl = arg('--base-url', 'https://exdev.co.kr/desktop/updates'); +const baseUrl = rawBaseUrl.replace(/\/$/, '').replace(/^http:\/\//i, 'https://'); +const notes = arg('--notes', `GoldenChart Desktop ${version}`); +const latestPath = path.join(dir, 'latest.json'); +const generator = path.join(__dirname, 'generate-desktop-latest-json.mjs'); + +function isValidManifest(obj) { + return obj + && typeof obj === 'object' + && typeof obj.version === 'string' + && obj.version.trim().length > 0 + && typeof obj.pub_date === 'string' + && obj.platforms + && typeof obj.platforms === 'object' + && !Array.isArray(obj.platforms); +} + +function readManifest() { + if (!fs.existsSync(latestPath)) return null; + if (fs.statSync(latestPath).size < 8) return null; + try { + const obj = JSON.parse(fs.readFileSync(latestPath, 'utf8')); + return isValidManifest(obj) ? obj : null; + } catch { + return null; + } +} + +function writeManifest(manifest) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + const tmp = `${latestPath}.tmp`; + const body = `${JSON.stringify(manifest, null, 2)}\n`; + if (body.length < 8) throw new Error('refusing to write empty latest.json'); + fs.writeFileSync(tmp, body, 'utf8'); + fs.renameSync(tmp, latestPath); +} + +function runGenerate() { + const r = spawnSync( + process.execPath, + [generator, '--version', version, '--dir', dir, '--base-url', baseUrl, '--notes', notes], + { encoding: 'utf8' }, + ); + if (r.status === 0) return readManifest(); + return null; +} + +function fallbackManifest(existing) { + return { + version: existing?.version?.trim() || version, + notes: existing?.notes?.trim() || notes, + pub_date: new Date().toISOString(), + platforms: existing?.platforms && typeof existing.platforms === 'object' ? existing.platforms : {}, + }; +} + +if (!fs.existsSync(dir)) { + console.error('[ensure-latest] directory not found:', dir); + process.exit(1); +} + +let manifest = readManifest(); +const platformKeys = manifest ? Object.keys(manifest.platforms) : []; + +if (manifest && platformKeys.length > 0) { + console.log('[ensure-latest] OK v' + manifest.version + ' platforms=' + platformKeys.join(',')); + process.exit(0); +} + +const generated = runGenerate(); +if (generated && Object.keys(generated.platforms).length > 0) { + console.log('[ensure-latest] regenerated v' + generated.version + ' platforms=' + Object.keys(generated.platforms).join(',')); + process.exit(0); +} + +manifest = fallbackManifest(manifest); +writeManifest(manifest); +console.warn('[ensure-latest] fallback v' + manifest.version + ' (platforms empty — Jenkins desktop publish 필요)'); +process.exit(2); diff --git a/scripts/generate-desktop-latest-json.mjs b/scripts/generate-desktop-latest-json.mjs index 3f7fbf5..0c69de8 100644 --- a/scripts/generate-desktop-latest-json.mjs +++ b/scripts/generate-desktop-latest-json.mjs @@ -129,7 +129,14 @@ const manifest = { }; const outPath = path.join(dir, 'latest.json'); -fs.writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`); +const body = `${JSON.stringify(manifest, null, 2)}\n`; +if (body.length < 8) { + console.error('[error] refusing to write empty latest.json'); + process.exit(3); +} +const tmpPath = `${outPath}.tmp`; +fs.writeFileSync(tmpPath, body, 'utf8'); +fs.renameSync(tmpPath, outPath); console.log('Wrote', outPath); console.log('Platforms:', Object.keys(platforms).join(', ') || '(none — sign updater bundles first)'); diff --git a/scripts/publish-desktop-update.sh b/scripts/publish-desktop-update.sh index baf3568..114a090 100755 --- a/scripts/publish-desktop-update.sh +++ b/scripts/publish-desktop-update.sh @@ -176,6 +176,22 @@ elif [[ "$GEN_RC" -ne 0 ]]; then exit "$GEN_RC" fi +# 빈·손상 latest.json 방지 +node "$WORK_TREE/scripts/ensure-desktop-latest-json.mjs" \ + --version "$VERSION" \ + --dir "$PUBLISH_UPDATES" \ + --base-url "$BASE_URL" \ + --notes "$NOTES" || { + log "ERROR: latest.json 무결성 검증 실패" >&2 + exit 1 +} + +LATEST_SIZE="$(wc -c < "$PUBLISH_UPDATES/latest.json" | tr -d ' ')" +if [[ "${LATEST_SIZE:-0}" -lt 8 ]]; then + log "ERROR: latest.json is empty (${LATEST_SIZE} bytes)" >&2 + exit 1 +fi + log "Published v${VERSION}:" ls -la "$PUBLISH_UPDATES" diff --git a/scripts/reconcile-desktop-latest-json.sh b/scripts/reconcile-desktop-latest-json.sh index 23a04a5..1b125a8 100755 --- a/scripts/reconcile-desktop-latest-json.sh +++ b/scripts/reconcile-desktop-latest-json.sh @@ -67,7 +67,19 @@ set -e if [[ "$RC" -eq 0 ]]; then echo "[reconcile-latest] updated $UPDATES_DIR/latest.json" elif [[ "$RC" -eq 2 ]]; then - echo "[reconcile-latest] no signed bundles — keeping existing latest.json (v${VERSION})" + echo "[reconcile-latest] no signed bundles — ensure valid latest.json (never empty)" + node "$ROOT/scripts/ensure-desktop-latest-json.mjs" \ + --version "$VERSION" \ + --dir "$UPDATES_DIR" \ + --base-url "$BASE_URL" \ + --notes "$NOTES" || true else exit "$RC" fi + +# 빈·손상 latest.json 최종 방어 +node "$ROOT/scripts/ensure-desktop-latest-json.mjs" \ + --version "$VERSION" \ + --dir "$UPDATES_DIR" \ + --base-url "$BASE_URL" \ + --notes "$NOTES" || echo "[reconcile-latest] WARN: ensure failed — Jenkins desktop publish 필요" >&2 diff --git a/scripts/smoke-test-desktop.sh b/scripts/smoke-test-desktop.sh index 2925be4..348f29d 100755 --- a/scripts/smoke-test-desktop.sh +++ b/scripts/smoke-test-desktop.sh @@ -41,7 +41,12 @@ if [[ -f "$LOCAL_LATEST" ]]; then fi if curl -sf "${BASE_URL}/latest.json" >/dev/null 2>&1; then - ok "remote latest.json ${BASE_URL}/latest.json" + REMOTE="$(curl -sf "${BASE_URL}/latest.json" 2>/dev/null || true)" + if [[ -z "$REMOTE" || "${#REMOTE}" -lt 8 ]]; then + echo "[FAIL] remote latest.json empty (0-byte)" + exit 1 + fi + node -e "JSON.parse(process.argv[1])" "$REMOTE" && ok "remote latest.json ${BASE_URL}/latest.json" else echo "[WARN] remote latest.json unreachable (배포 전 정상)" fi diff --git a/scripts/verify-desktop-published-version.sh b/scripts/verify-desktop-published-version.sh index 88fa326..4806baf 100755 --- a/scripts/verify-desktop-published-version.sh +++ b/scripts/verify-desktop-published-version.sh @@ -15,12 +15,22 @@ log "git tauri.conf → v${GIT_VER}" log "remote latest.json → ${LATEST_URL}" REMOTE_VER="" +REMOTE_PLATFORMS="0" if REMOTE_JSON="$(curl -sf --connect-timeout 5 --max-time 12 "$LATEST_URL" 2>/dev/null)"; then - REMOTE_VER="$(printf '%s' "$REMOTE_JSON" | node -e " + if [[ -z "$REMOTE_JSON" || "${#REMOTE_JSON}" -lt 8 ]]; then + log "remote latest.json → EMPTY (0-byte — updater JSON parse 실패 원인)" + else + read -r REMOTE_VER REMOTE_PLATFORMS <<< "$(printf '%s' "$REMOTE_JSON" | node -e " let d=''; process.stdin.on('data',c=>d+=c); -process.stdin.on('end',()=>{ try { console.log(JSON.parse(d).version||''); } catch { console.log(''); } }); -" 2>/dev/null || true)" - log "remote version → v${REMOTE_VER:-?}" +process.stdin.on('end',()=>{ + try { + const j=JSON.parse(d); + console.log((j.version||'')+' '+Object.keys(j.platforms||{}).length); + } catch { console.log(' 0'); } +}); +" 2>/dev/null || echo ' 0')" + log "remote version → v${REMOTE_VER:-?} (platforms: ${REMOTE_PLATFORMS:-0})" + fi else log "remote version → (fetch failed)" fi