desktop 앱 적용

This commit is contained in:
Macbook
2026-06-14 10:15:54 +09:00
parent 7a2f65088c
commit 86b99d8c10
81 changed files with 8164 additions and 37 deletions
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# GoldenChart Desktop — macOS Jenkins 빌드 (macOS dmg + Windows NSIS cross-build)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# shellcheck disable=SC1091
source "$ROOT/scripts/desktop-dev-path.sh"
# shellcheck disable=SC1091
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
BUILD_MAC=1
BUILD_WIN=1
BUMP_VERSION="${BUMP_VERSION:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--mac) BUILD_WIN=0; shift ;;
--win) BUILD_MAC=0; shift ;;
--all) BUILD_MAC=1; BUILD_WIN=1; shift ;;
--bump-version) BUMP_VERSION="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
echo "=== GoldenChart Desktop Build ==="
echo "Root: $ROOT"
echo "Git: $(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo 'n/a')"
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing: $1 — run ./scripts/install-desktop-build-deps.sh"
exit 1
fi
}
need_cmd node
need_cmd npm
need_cmd cargo
need_cmd rustc
if [[ "$BUILD_WIN" -eq 1 ]]; then
need_cmd cargo-xwin
rustup target add x86_64-pc-windows-msvc 2>/dev/null || true
fi
CONF="$ROOT/desktop/src-tauri/tauri.conf.json"
if [[ -n "$BUMP_VERSION" ]]; then
node -e "
const fs=require('fs'); const p=process.argv[1]; const b=process.argv[2];
const c=JSON.parse(fs.readFileSync(p,'utf8'));
const [a,x,y]=c.version.split('.').map(Number);
let nv=c.version;
if(b==='patch') nv=[a,x,y+1].join('.');
else if(b==='minor') nv=[a,x+1,0].join('.');
else if(b==='major') nv=[a+1,0,0].join('.');
else nv=b;
c.version=nv; fs.writeFileSync(p, JSON.stringify(c,null,2)+'\n');
console.log('version', nv);
" "$CONF" "$BUMP_VERSION"
fi
VERSION="$(node -p "require('$CONF').version")"
echo "Version: $VERSION"
if [[ -f "$ROOT/scripts/patch-tauri-updater-pubkey.sh" ]]; then
chmod +x "$ROOT/scripts/patch-tauri-updater-pubkey.sh"
fi
chmod +x "$ROOT/scripts/prepare-tauri-build.sh" 2>/dev/null || true
# shellcheck disable=SC1091
source "$ROOT/scripts/prepare-tauri-build.sh"
npm install
echo "--- Vite build (desktop) ---"
npm run build -w @goldenchart/desktop
DESKTOP_DIR="$ROOT/desktop"
TAURI_TARGET="$DESKTOP_DIR/src-tauri/target"
ARTIFACT_DIR="$ROOT/dist-desktop"
UPDATES_DIR="$ARTIFACT_DIR/updates"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR" "$UPDATES_DIR"
collect_updater_artifacts() {
find "$TAURI_TARGET" \( \
-name '*.app.tar.gz' -o \
-name '*.tar.gz' -path '*/bundle/macos/*' -o \
-name '*.nsis.zip' -o \
-name '*-setup.nsis.zip' \
\) -type f 2>/dev/null | while read -r f; do
cp -f "$f" "$UPDATES_DIR/"
echo " updater bundle: $(basename "$f")"
done
}
if [[ "$BUILD_MAC" -eq 1 ]]; then
echo "--- Tauri build (macOS) ---"
(cd "$DESKTOP_DIR" && npm run tauri:build:mac)
find "$TAURI_TARGET/release/bundle" -name '*.dmg' -exec cp -f {} "$ARTIFACT_DIR/" \; 2>/dev/null || true
collect_updater_artifacts
fi
if [[ "$BUILD_WIN" -eq 1 ]]; then
echo "--- Tauri build (Windows NSIS cross) ---"
(cd "$DESKTOP_DIR" && npm run tauri:build:win)
find "$TAURI_TARGET" -path '*x86_64-pc-windows-msvc*' -name '*-setup.exe' -exec cp -f {} "$ARTIFACT_DIR/" \; 2>/dev/null || true
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 '')"
cat > "$ARTIFACT_DIR/build-info.json" <<EOF
{
"version": "$VERSION",
"gitSha": "$GIT_SHA",
"gitShort": "$GIT_SHORT",
"builtAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"buildNumber": "${BUILD_NUMBER:-local}"
}
EOF
echo "--- Artifacts ($ARTIFACT_DIR) ---"
ls -la "$ARTIFACT_DIR" || true
echo "--- Updater bundles ($UPDATES_DIR) ---"
ls -la "$UPDATES_DIR" 2>/dev/null || true
echo "Done."
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# GoldenChart desktop 빌드용 PATH — source scripts/desktop-dev-path.sh
prepend_path() {
case ":${PATH}:" in
*":$1:"*) ;;
*) export PATH="$1:$PATH" ;;
esac
}
prepend_path "$HOME/.cargo/bin"
[[ -x "$HOME/homebrew/bin/brew" ]] && eval "$("$HOME/homebrew/bin/brew" shellenv)"
[[ -x /opt/homebrew/bin/brew ]] && eval "$(/opt/homebrew/bin/brew shellenv)"
for llvm in /opt/homebrew/opt/llvm/bin "$HOME/homebrew/opt/llvm/bin"; do
[[ -d "$llvm" ]] && prepend_path "$llvm"
done
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env node
/**
* dist-desktop/updates → latest.json (Tauri updater v2)
*
* Usage:
* node scripts/generate-desktop-latest-json.mjs \
* --version 0.1.0 \
* --dir dist-desktop/updates \
* --base-url http://exdev.co.kr/desktop/updates \
* --notes "Release notes"
*/
import fs from 'node:fs';
import path from 'node:path';
function arg(name, fallback) {
const i = process.argv.indexOf(name);
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
}
const version = arg('--version', '0.1.0');
const dir = path.resolve(arg('--dir', 'dist-desktop/updates'));
const baseUrl = (arg('--base-url', 'http://exdev.co.kr/desktop/updates')).replace(/\/$/, '');
const notes = arg('--notes', `GoldenChart Desktop ${version}`);
if (!fs.existsSync(dir)) {
console.error('Directory not found:', dir);
process.exit(1);
}
const files = fs.readdirSync(dir);
/** @type {Record<string, { url: string; signature: string }>} */
const platforms = {};
function addPlatform(platformKey, bundleName) {
const bundlePath = path.join(dir, bundleName);
if (!fs.existsSync(bundlePath)) return;
const sigPath = `${bundlePath}.sig`;
let signature = '';
if (fs.existsSync(sigPath)) {
signature = fs.readFileSync(sigPath, 'utf8').trim();
} else {
console.warn(`[warn] missing signature: ${sigPath}`);
return;
}
platforms[platformKey] = {
url: `${baseUrl}/${encodeURIComponent(bundleName)}`,
signature,
};
}
for (const f of files) {
if (f.endsWith('.sig')) continue;
const lower = f.toLowerCase();
if (lower.includes('aarch64') && (lower.endsWith('.tar.gz') || lower.endsWith('.app.tar.gz'))) {
addPlatform('darwin-aarch64', f);
} else if ((lower.includes('x64') || lower.includes('x86_64')) && lower.endsWith('.tar.gz')) {
addPlatform('darwin-x86_64', f);
} else if (lower.includes('aarch64') && lower.endsWith('.app.tar.gz')) {
addPlatform('darwin-aarch64', f);
} else if (lower.endsWith('.nsis.zip') || (lower.includes('setup') && lower.endsWith('.zip'))) {
addPlatform('windows-x86_64', f);
} else if (lower.endsWith('.app.tar.gz')) {
// universal mac fallback
if (!platforms['darwin-aarch64']) addPlatform('darwin-aarch64', f);
}
}
const manifest = {
version,
notes,
pub_date: new Date().toISOString(),
platforms,
};
const outPath = path.join(dir, 'latest.json');
fs.writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`);
console.log('Wrote', outPath);
console.log('Platforms:', Object.keys(platforms).join(', ') || '(none — sign updater bundles first)');
if (Object.keys(platforms).length === 0) {
process.exit(2);
}
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# macOS Jenkins / 개발 Mac — GoldenChart Desktop 빌드 의존성 설치
set -euo pipefail
MAC_ONLY=0
while [[ $# -gt 0 ]]; do
case "$1" in
--mac-only) MAC_ONLY=1; shift ;;
*) echo "Unknown arg: $1 (supported: --mac-only)"; exit 1 ;;
esac
done
log() { echo "[install-desktop-deps] $*"; }
if [[ "$(uname -s)" != "Darwin" ]]; then
log "이 스크립트는 macOS용입니다."
exit 1
fi
# Homebrew / Rust PATH (표준 prefix + 사용자 prefix)
prepend_path() {
case ":${PATH}:" in
*":$1:"*) ;;
*) export PATH="$1:$PATH" ;;
esac
}
for prefix in /opt/homebrew "$HOME/homebrew" /usr/local; do
[[ -x "$prefix/bin/brew" ]] && prepend_path "$prefix/bin" && prepend_path "$prefix/sbin"
[[ -d "$prefix/opt/llvm/bin" ]] && prepend_path "$prefix/opt/llvm/bin"
done
prepend_path "$HOME/.cargo/bin"
install_homebrew_user() {
local prefix="$HOME/homebrew"
if [[ -x "$prefix/bin/brew" ]]; then
return 0
fi
log "Homebrew 사용자 설치 ($prefix) — sudo 불필요"
mkdir -p "$prefix"
curl -fsSL https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C "$prefix"
prepend_path "$prefix/bin"
prepend_path "$prefix/sbin"
}
install_homebrew_system() {
if [[ -x /opt/homebrew/bin/brew ]] || [[ -x /usr/local/bin/brew ]]; then
return 0
fi
if [[ "${NONINTERACTIVE:-}" == "1" ]] && ! sudo -n true 2>/dev/null; then
log "sudo 없음 — $HOME/homebrew 로 설치합니다."
install_homebrew_user
return 0
fi
log "Homebrew 시스템 설치 (/opt/homebrew)..."
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
}
ensure_homebrew() {
if command -v brew >/dev/null 2>&1; then
return 0
fi
install_homebrew_system
if ! command -v brew >/dev/null 2>&1; then
install_homebrew_user
fi
if ! command -v brew >/dev/null 2>&1; then
log "Homebrew 설치 실패. https://brew.sh 수동 설치 후 재실행."
exit 1
fi
# shellcheck disable=SC1091
eval "$(brew shellenv)"
}
ensure_rust() {
if command -v rustc >/dev/null 2>&1; then
log "Rust 이미 설치됨: $(rustc --version)"
return 0
fi
log "Rust 설치 (rustup)..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
# shellcheck disable=SC1091
source "$HOME/.cargo/env"
}
setup_shell_profile() {
local marker="# GoldenChart desktop build PATH"
local profile="${ZDOTDIR:-$HOME}/.zprofile"
if [[ -f "$profile" ]] && grep -qF "$marker" "$profile" 2>/dev/null; then
log "shell profile 이미 설정됨: $profile"
return 0
fi
log "shell profile에 PATH 추가: $profile"
{
echo ""
echo "$marker"
echo 'export PATH="$HOME/.cargo/bin:$PATH"'
if [[ -x "$HOME/homebrew/bin/brew" ]]; then
echo 'eval "$("$HOME/homebrew/bin/brew" shellenv)"'
elif [[ -x /opt/homebrew/bin/brew ]]; then
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"'
fi
echo 'export PATH="/opt/homebrew/opt/llvm/bin:$HOME/homebrew/opt/llvm/bin:$PATH"'
} >> "$profile"
}
ensure_homebrew
setup_shell_profile
if [[ "$MAC_ONLY" -eq 1 ]]; then
log "macOS 전용 모드 — NSIS/LLVM/cargo-xwin 생략"
else
log "Homebrew 패키지 (NSIS, LLVM)..."
brew install nsis llvm 2>/dev/null || brew upgrade nsis llvm 2>/dev/null || true
fi
ensure_rust
# shellcheck disable=SC1091
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
if [[ "$MAC_ONLY" -eq 0 ]]; then
log "Windows 크로스 컴파일 타겟..."
rustup target add x86_64-pc-windows-msvc 2>/dev/null || true
if ! command -v cargo-xwin >/dev/null 2>&1; then
log "cargo-xwin 설치 (시간 소요)..."
cargo install cargo-xwin --locked
else
log "cargo-xwin 이미 설치됨"
fi
fi
log "완료. 검증:"
node --version
npm --version
rustc --version
cargo --version
command -v makensis && makensis -VERSION || log "[INFO] makensis — Windows 빌드 시 필요"
command -v cargo-xwin && cargo-xwin --version || log "[INFO] cargo-xwin — Windows 빌드 시 필요"
log "새 터미널을 열거나: source ~/.zprofile"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# @deprecated — scripts/jenkins-desktop-pipeline.sh + scripts/server-install-desktop-jenkins.sh 사용
exec "$(dirname "$0")/jenkins-desktop-pipeline.sh" "$@"
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Jenkins goldenChart-Desktop-Pipeline — 전체 빌드·배포 진입점
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
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)"
done
# shellcheck disable=SC1091
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
log() { echo "[desktop-pipeline $(date '+%H:%M:%S')] $*"; }
log "=== goldenChart Desktop Pipeline #${BUILD_NUMBER:-local} ==="
log "WORK_TREE=$WORK_TREE"
cd "$WORK_TREE"
# Git 동기화 (Jenkins SCM checkout 후에도 file:// bare 와 맞춤)
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
# 의존성 (최초 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"
fi
# updater pubkey (repo에 desktop/updater.pub 있으면 patch만)
if [[ -f "$ROOT/desktop/updater.pub" ]]; then
"$ROOT/scripts/patch-tauri-updater-pubkey.sh"
elif [[ -f "${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}" ]]; then
"$ROOT/scripts/setup-desktop-updater-keys.sh"
fi
BUILD_ARGS=(--all)
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"); fi
"$ROOT/scripts/build-desktop.sh" "${BUILD_ARGS[@]}"
WORK_TREE="$WORK_TREE" \
ARTIFACT_DIR="$ROOT/dist-desktop" \
DESKTOP_UPDATE_BASE_URL="${DESKTOP_UPDATE_BASE_URL:-http://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"
# Jenkins artifact archive 경로
if [[ -d "$ROOT/dist-desktop" ]]; then
log "Artifacts ready: $ROOT/dist-desktop"
fi
# 웹 frontend Docker 재배포 (latest.json static 포함) — 선택
if [[ "${DESKTOP_DEPLOY_WEB:-0}" == "1" ]] && [[ -x "${DEPLOY_SCRIPT:-/Volumes/ADATA/git/deploy.sh}" ]]; then
log "Trigger web deploy (frontend only)..."
SERVICE_NAME=frontend "${DEPLOY_SCRIPT:-/Volumes/ADATA/git/deploy.sh}"
fi
log "Pipeline complete."
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PUB_PATH="${TAURI_SIGNER_PUB:-$ROOT/desktop/updater.pub}"
CONF="$ROOT/desktop/src-tauri/tauri.conf.json"
if [[ ! -f "$PUB_PATH" ]]; then
echo "[patch-updater-pubkey] skip — no $PUB_PATH (run setup-desktop-updater-keys.sh)"
exit 0
fi
PUBKEY="$(grep -v '^[[:space:]]*#' "$PUB_PATH" | grep -v '^[[:space:]]*$' | tr -d '\n\r' | head -c 500)"
if [[ -z "$PUBKEY" || "$PUBKEY" == "REPLACE_WITH_TAURI_SIGNER_PUBKEY" ]]; then
echo "[patch-updater-pubkey] skip — invalid or placeholder pubkey in $PUB_PATH"
exit 0
fi
CONF="$CONF" PUBKEY="$PUBKEY" node -e "
const fs = require('fs');
const confPath = process.env.CONF;
const pubkey = process.env.PUBKEY;
const conf = JSON.parse(fs.readFileSync(confPath, 'utf8'));
conf.plugins = conf.plugins || {};
conf.plugins.updater = conf.plugins.updater || {};
conf.plugins.updater.pubkey = pubkey;
fs.writeFileSync(confPath, JSON.stringify(conf, null, 2) + '\n');
console.log('[patch-updater-pubkey] updated', confPath);
"
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# tauri.conf.json — updater artifacts / pubkey (키 있을 때만 활성화)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CONF="$ROOT/desktop/src-tauri/tauri.conf.json"
KEY_PATH="${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}"
PUB_PATH="${TAURI_SIGNER_PUB:-$ROOT/desktop/updater.pub}"
enable="false"
if [[ -f "$KEY_PATH" && -f "$PUB_PATH" ]]; then
enable="true"
if [[ -x "$ROOT/scripts/patch-tauri-updater-pubkey.sh" ]]; then
"$ROOT/scripts/patch-tauri-updater-pubkey.sh"
fi
fi
node -e "
const fs = require('fs');
const confPath = process.argv[1];
const enable = process.argv[2] === 'true';
const conf = JSON.parse(fs.readFileSync(confPath, 'utf8'));
conf.bundle = conf.bundle || {};
conf.bundle.createUpdaterArtifacts = enable;
conf.plugins = conf.plugins || {};
conf.plugins.updater = conf.plugins.updater || {};
if (!conf.plugins.updater.endpoints?.length) {
conf.plugins.updater.endpoints = ['https://exdev.co.kr/desktop/updates/latest.json'];
}
if (!conf.plugins.updater.pubkey) {
conf.plugins.updater.pubkey = 'REPLACE_WITH_TAURI_SIGNER_PUBKEY';
}
fs.writeFileSync(confPath, JSON.stringify(conf, null, 2) + '\n');
console.log('[prepare-tauri-build] createUpdaterArtifacts=' + enable);
" "$CONF" "$enable"
if [[ "$enable" == "true" ]]; then
export TAURI_SIGNING_PRIVATE_KEY="$(tr -d '\n\r' < "$KEY_PATH")"
echo "[prepare-tauri-build] updater signing enabled"
else
unset TAURI_SIGNING_PRIVATE_KEY 2>/dev/null || true
echo "[prepare-tauri-build] updater signing skipped (no key)"
fi
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# GoldenChart Desktop — updater 서명 + latest.json + 웹 static 배포
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT/dist-desktop}"
UPDATES_SRC="${UPDATES_SRC:-$ARTIFACT_DIR/updates}"
UPDATES_DIR="${UPDATES_DIR:-${WORK_TREE:-$ROOT}/frontend/public/desktop/updates}"
WEB_STATIC_DIR="${WEB_STATIC_DIR:-$UPDATES_DIR}"
BASE_URL="${DESKTOP_UPDATE_BASE_URL:-http://exdev.co.kr/desktop/updates}"
KEY_PATH="${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}"
DESKTOP_DIR="$ROOT/desktop"
CONF="$DESKTOP_DIR/src-tauri/tauri.conf.json"
VERSION="$(node -p "require('$CONF').version")"
NOTES="${DESKTOP_RELEASE_NOTES:-GoldenChart Desktop $VERSION}"
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"
log() { echo "[publish-desktop] $*"; }
log "=== Publish Desktop v$VERSION ==="
log "Updates src: $UPDATES_SRC"
log "Web static: $WEB_STATIC_DIR"
mkdir -p "$WEB_STATIC_DIR"
# 설치 파일(dist-desktop/*.dmg, *setup.exe) 복사
if [[ -d "$ARTIFACT_DIR" ]]; then
find "$ARTIFACT_DIR" -maxdepth 1 -type f \( -name '*.dmg' -o -name '*setup.exe' \) -exec cp -f {} "$WEB_STATIC_DIR/" \;
fi
PUBLISH_UPDATES="$WEB_STATIC_DIR"
mkdir -p "$PUBLISH_UPDATES"
if [[ -d "$UPDATES_SRC" ]]; then
cp -f "$UPDATES_SRC"/* "$PUBLISH_UPDATES/" 2>/dev/null || true
fi
# updater 번들 서명
if [[ -f "$KEY_PATH" ]]; then
shopt -s nullglob
for bundle in "$PUBLISH_UPDATES"/*.tar.gz "$PUBLISH_UPDATES"/*.nsis.zip "$PUBLISH_UPDATES"/*.zip; do
[[ -f "$bundle" ]] || continue
[[ "$bundle" == *.sig ]] && continue
log "Signing $(basename "$bundle")"
(cd "$DESKTOP_DIR" && npx tauri signer sign -f "$KEY_PATH" "$bundle")
done
shopt -u nullglob
else
log "[WARN] TAURI_SIGNER_KEY 없음 ($KEY_PATH) — updater 서명 생략"
log " ./scripts/setup-desktop-updater-keys.sh 실행 후 Jenkins Credential 등록"
fi
# build-info 복사
if [[ -f "$BUILD_INFO" ]]; then
cp -f "$BUILD_INFO" "$PUBLISH_UPDATES/build-info.json"
fi
# latest.json
set +e
node "$ROOT/scripts/generate-desktop-latest-json.mjs" \
--version "$VERSION" \
--dir "$PUBLISH_UPDATES" \
--base-url "$BASE_URL" \
--notes "$NOTES"
GEN_RC=$?
set -e
if [[ "$GEN_RC" -eq 2 ]]; then
log "[WARN] signed updater bundle 없음 — placeholder latest.json 작성"
cat > "$PUBLISH_UPDATES/latest.json" <<EOF
{
"version": "$VERSION",
"notes": "$NOTES",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {}
}
EOF
elif [[ "$GEN_RC" -ne 0 ]]; then
exit "$GEN_RC"
fi
log "Published:"
ls -la "$PUBLISH_UPDATES"
log "nginx: /desktop/updates/ → frontend/public/desktop/updates (Docker rebuild 시 반영)"
log "Done."
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# exdev Jenkins — goldenChart-Desktop-Pipeline Job 생성
set -euo pipefail
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
JENKINS_HOME="${JENKINS_HOME:-$HOME/.jenkins}"
JENKINS_JOB="goldenChart-Desktop-Pipeline"
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}"
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
log() { echo "[install-desktop-jenkins] $*"; }
log "Jenkins job 생성: $JENKINS_JOB"
mkdir -p "${JENKINS_HOME}/jobs/${JENKINS_JOB}"
cat > "${JENKINS_HOME}/jobs/${JENKINS_JOB}/config.xml" << JENKINS_XML
<?xml version='1.1' encoding='UTF-8'?>
<project>
<description>GoldenChart Desktop — Tauri macOS dmg + Windows NSIS + updater</description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.plugins.git.GitSCM" plugin="git@5.0.0">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>file://${GIT_DIR}</url>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/main</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="empty-list"/>
<extensions/>
</scm>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers>
<hudson.triggers.SCMTrigger>
<spec>H/30 * * * *</spec>
</hudson.triggers.SCMTrigger>
</triggers>
<concurrentBuild>false</concurrentBuild>
<builders>
<hudson.tasks.Shell>
<command>#!/bin/bash
set -euo pipefail
export PATH="/opt/homebrew/opt/llvm/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:\$PATH"
export WORK_TREE="${WORK_TREE}"
export GIT_DIR="${GIT_DIR}"
export BUILD_NUMBER="\${BUILD_NUMBER}"
export TAURI_SIGNER_KEY="\${TAURI_SIGNER_KEY:-\$HOME/.tauri/goldenchart.key}"
export DESKTOP_DEPLOY_WEB="\${DESKTOP_DEPLOY_WEB:-1}"
"${REPO_ROOT}/scripts/jenkins-desktop-pipeline.sh"
</command>
</hudson.tasks.Shell>
</builders>
<publishers>
<hudson.tasks.ArtifactArchiver>
<artifacts>dist-desktop/**</artifacts>
<allowEmptyArchive>true</allowEmptyArchive>
<onlyIfSuccessful>false</onlyIfSuccessful>
<fingerprint>false</fingerprint>
<defaultExcludes>true</defaultExcludes>
</hudson.tasks.ArtifactArchiver>
</publishers>
<buildWrappers/>
</project>
JENKINS_XML
if curl -sf -o /dev/null "http://127.0.0.1:8090/login" 2>/dev/null; then
curl -sf -X POST "http://127.0.0.1:8090/reload" 2>/dev/null || true
fi
log "완료: http://127.0.0.1:8090/job/${JENKINS_JOB}/"
log ""
log "Jenkins Credentials (Secret file):"
log " ID: tauri-signer-key → \$HOME/.tauri/goldenchart.key"
log ""
log "최초 1회 (서버):"
log " ./scripts/install-desktop-build-deps.sh"
log " ./scripts/setup-desktop-updater-keys.sh"
log " git add desktop/updater.pub && git commit"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Tauri updater minisign 키쌍 생성 — 1회 (Jenkins secret 또는 서버 로컬)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
KEY_PATH="${TAURI_SIGNER_KEY:-$HOME/.tauri/goldenchart.key}"
PUB_PATH="${TAURI_SIGNER_PUB:-$ROOT/desktop/updater.pub}"
DESKTOP_DIR="$ROOT/desktop"
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"
mkdir -p "$(dirname "$KEY_PATH")"
if [[ -f "$KEY_PATH" ]]; then
echo "기존 private key: $KEY_PATH"
else
echo "새 updater 키 생성: $KEY_PATH"
GEN_OUT="$(cd "$DESKTOP_DIR" && npx tauri signer generate -w "$KEY_PATH" -f 2>&1)" || true
echo "$GEN_OUT"
if [[ ! -f "$KEY_PATH" ]]; then
echo "키 생성 실패. Rust/tauri-cli 확인 후 재시도."
exit 1
fi
fi
if [[ ! -f "$PUB_PATH" ]]; then
echo "Public key 추출 시도..."
if echo "$GEN_OUT" | grep -qi 'public key'; then
echo "$GEN_OUT" | grep -i 'public key' | tail -1 | sed -E 's/^[^:]*:[[:space:]]*//' > "$PUB_PATH"
fi
if [[ ! -s "$PUB_PATH" ]] && [[ -f "${KEY_PATH}.pub" ]]; then
cp "${KEY_PATH}.pub" "$PUB_PATH"
fi
if [[ ! -s "$PUB_PATH" ]]; then
echo "desktop/updater.pub 를 수동 생성하세요 (tauri signer generate 출력의 public key)."
exit 1
fi
fi
chmod +x "$ROOT/scripts/patch-tauri-updater-pubkey.sh"
"$ROOT/scripts/patch-tauri-updater-pubkey.sh"
echo ""
echo "=== updater 키 설정 완료 ==="
echo " Private: $KEY_PATH (git/Jenkins secret — 커밋 금지)"
echo " Public: $PUB_PATH"
echo "Jenkins: TAURI_SIGNER_KEY=$KEY_PATH"
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# GoldenChart Desktop — smoke test (CI·로컬)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BASE_URL="${DESKTOP_UPDATE_BASE_URL:-http://exdev.co.kr/desktop/updates}"
FAIL=0
ok() { echo "[OK] $*"; }
fail() { echo "[FAIL] $*"; FAIL=1; }
echo "=== Desktop Smoke Test ==="
# 1) Vite 번들
if npm run build -w @goldenchart/desktop >/dev/null 2>&1; then
ok "desktop vite build"
else
fail "desktop vite build"
fi
# 2) updater pubkey
if [[ -f "$ROOT/desktop/updater.pub" ]]; then
ok "desktop/updater.pub exists"
else
echo "[WARN] desktop/updater.pub 없음 — setup-desktop-updater-keys.sh 필요"
fi
# 3) tauri.conf pubkey not placeholder
PUB="$(node -p "require('$ROOT/desktop/src-tauri/tauri.conf.json').plugins?.updater?.pubkey || ''")"
if [[ -n "$PUB" && "$PUB" != "REPLACE_WITH_TAURI_SIGNER_PUBKEY" ]]; then
ok "tauri.conf updater pubkey configured"
else
echo "[WARN] tauri.conf pubkey 미설정"
fi
# 4) latest.json (local or remote)
LOCAL_LATEST="$ROOT/frontend/public/desktop/updates/latest.json"
if [[ -f "$LOCAL_LATEST" ]]; then
ok "local latest.json"
node -e "JSON.parse(require('fs').readFileSync('$LOCAL_LATEST','utf8'))" && ok "latest.json valid JSON"
fi
if curl -sf "${BASE_URL}/latest.json" >/dev/null 2>&1; then
ok "remote latest.json ${BASE_URL}/latest.json"
else
echo "[WARN] remote latest.json unreachable (배포 전 정상)"
fi
# 5) 필수 스크립트
for s in build-desktop.sh publish-desktop-update.sh jenkins-desktop-pipeline.sh; do
if [[ -x "$ROOT/scripts/$s" ]]; then ok "scripts/$s"; else fail "scripts/$s missing"; fi
done
if [[ "$FAIL" -eq 0 ]]; then
echo "=== Smoke test passed ==="
exit 0
fi
echo "=== Smoke test failed ==="
exit 1