앱 업데이트 실패 문제
This commit is contained in:
@@ -147,6 +147,11 @@ async function formatUpdateError(e: unknown): Promise<string> {
|
|||||||
const onWin = isWindowsOs();
|
const onWin = isWindowsOs();
|
||||||
|
|
||||||
if (!msg.trim()) return '업데이트 확인에 실패했습니다.';
|
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)) {
|
if (/could not fetch a valid release json|release not found/.test(lower)) {
|
||||||
return onMac
|
return onMac
|
||||||
? '업데이트 서버 응답을 처리하지 못했습니다. PC 프로그램 탭에서 dmg를 새로 설치해 주세요.'
|
? '업데이트 서버 응답을 처리하지 못했습니다. PC 프로그램 탭에서 dmg를 새로 설치해 주세요.'
|
||||||
|
|||||||
@@ -80,6 +80,20 @@ if [[ -x "$RECONCILE" ]]; then
|
|||||||
WORK_TREE="$WORK_TREE" "$RECONCILE" || warn "reconcile-desktop-latest-json 실패"
|
WORK_TREE="$WORK_TREE" "$RECONCILE" || warn "reconcile-desktop-latest-json 실패"
|
||||||
fi
|
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 홈 디렉터리 간섭 방지)
|
# Phase 2 — Docker 이미지 빌드 (호스트 npm 사용 안 함 → postcss 홈 디렉터리 간섭 방지)
|
||||||
info "[Phase 2] Docker 이미지 빌드 ($SERVICE_NAME)"
|
info "[Phase 2] Docker 이미지 빌드 ($SERVICE_NAME)"
|
||||||
|
|
||||||
|
|||||||
Executable
+97
@@ -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);
|
||||||
@@ -129,7 +129,14 @@ const manifest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const outPath = path.join(dir, 'latest.json');
|
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('Wrote', outPath);
|
||||||
console.log('Platforms:', Object.keys(platforms).join(', ') || '(none — sign updater bundles first)');
|
console.log('Platforms:', Object.keys(platforms).join(', ') || '(none — sign updater bundles first)');
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,22 @@ elif [[ "$GEN_RC" -ne 0 ]]; then
|
|||||||
exit "$GEN_RC"
|
exit "$GEN_RC"
|
||||||
fi
|
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}:"
|
log "Published v${VERSION}:"
|
||||||
ls -la "$PUBLISH_UPDATES"
|
ls -la "$PUBLISH_UPDATES"
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,19 @@ set -e
|
|||||||
if [[ "$RC" -eq 0 ]]; then
|
if [[ "$RC" -eq 0 ]]; then
|
||||||
echo "[reconcile-latest] updated $UPDATES_DIR/latest.json"
|
echo "[reconcile-latest] updated $UPDATES_DIR/latest.json"
|
||||||
elif [[ "$RC" -eq 2 ]]; then
|
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
|
else
|
||||||
exit "$RC"
|
exit "$RC"
|
||||||
fi
|
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
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ if [[ -f "$LOCAL_LATEST" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if curl -sf "${BASE_URL}/latest.json" >/dev/null 2>&1; then
|
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
|
else
|
||||||
echo "[WARN] remote latest.json unreachable (배포 전 정상)"
|
echo "[WARN] remote latest.json unreachable (배포 전 정상)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -15,12 +15,22 @@ log "git tauri.conf → v${GIT_VER}"
|
|||||||
log "remote latest.json → ${LATEST_URL}"
|
log "remote latest.json → ${LATEST_URL}"
|
||||||
|
|
||||||
REMOTE_VER=""
|
REMOTE_VER=""
|
||||||
|
REMOTE_PLATFORMS="0"
|
||||||
if REMOTE_JSON="$(curl -sf --connect-timeout 5 --max-time 12 "$LATEST_URL" 2>/dev/null)"; then
|
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);
|
let d=''; process.stdin.on('data',c=>d+=c);
|
||||||
process.stdin.on('end',()=>{ try { console.log(JSON.parse(d).version||''); } catch { console.log(''); } });
|
process.stdin.on('end',()=>{
|
||||||
" 2>/dev/null || true)"
|
try {
|
||||||
log "remote version → v${REMOTE_VER:-?}"
|
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
|
else
|
||||||
log "remote version → (fetch failed)"
|
log "remote version → (fetch failed)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user