본문으로 건너뛰기

Tauri v2 자동 업데이트 가이드

작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Tauri v2 데스크탑 앱 자동 업데이트 (Windows / macOS / Linux) 공통 참조: ../common/update.md (업데이트 전략, 강제 업데이트, 롤아웃, UX) 공통 참조: ../common/ci-cd.md (CI/CD 파이프라인, 버전 관리)


목차

  1. tauri-plugin-updater 설정
  2. 업데이트 서버 구성
  3. Ed25519 서명 키 관리
  4. 업데이트 매니페스트 JSON
  5. 플랫폼별 업데이트 흐름
  6. 점진적 롤아웃
  7. 강제 업데이트 + 최소 버전 체크
  8. CI/CD 파이프라인 연동

1. tauri-plugin-updater 설정

1.1 설치

# Rust 의존성
cargo add tauri-plugin-updater

# Frontend 의존성
npm install @tauri-apps/plugin-updater @tauri-apps/plugin-process

1.2 플러그인 등록 (Rust)

// src-tauri/src/lib.rs
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
// ... 기타 플러그인
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

1.3 tauri.conf.json 설정

{
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"endpoints": [
"https://releases.example.com/tauri/{{target}}/{{arch}}/{{current_version}}"
],
"windows": {
"installMode": "passive"
}
}
}
}

설정 필드 상세

필드타입필수설명
pubkeystringOEd25519 공개 키 (업데이트 서명 검증)
endpointsstring[]O업데이트 매니페스트 URL (복수 가능, fallback)
windows.installModestringXpassive (기본, 자동) / basicUi (진행률 UI) / quiet (완전 무음)

엔드포인트 URL 템플릿 변수

변수설명예시 값
{{target}}OS 타겟linux, darwin, windows
{{arch}}CPU 아키텍처x86_64, aarch64
{{current_version}}현재 앱 버전1.2.3

1.4 Capability 설정

// src-tauri/capabilities/default.json
{
"permissions": [
"core:default",
"updater:default",
"process:allow-restart"
]
}

2. 업데이트 서버 구성

2.1 서버 옵션 비교

옵션비용난이도롤아웃분석추천 대상
GitHub Releases무료쉬움XX오픈소스, 소규모
Cloudflare R2 + Worker거의 무료중간OO중규모
S3 + CloudFront + Lambda저렴중간OO중~대규모
자체 서버 (Laravel/Go)서버 비용높음OO완전 제어 필요 시

2.2 GitHub Releases (가장 간단)

// tauri.conf.json
{
"plugins": {
"updater": {
"endpoints": [
"https://github.com/{owner}/{repo}/releases/latest/download/latest.json"
]
}
}
}

tauri-action GitHub Action이 빌드 시 latest.json 매니페스트를 자동 생성하여 Release에 첨부한다.

2.3 자체 업데이트 서버 (Laravel)

// routes/api.php
Route::get('/tauri/{target}/{arch}/{current_version}', [TauriUpdateController::class, 'check']);
// app/Http/Controllers/TauriUpdateController.php
class TauriUpdateController extends Controller
{
public function check(
string $target,
string $arch,
string $currentVersion
): JsonResponse {
$release = TauriRelease::query()
->where('target', $target)
->where('arch', $arch)
->where('active', true)
->latest('version')
->first();

// 업데이트 없음
if (!$release || version_compare($currentVersion, $release->version, '>=')) {
return response()->json(null, 204);
}

// 점진적 롤아웃 체크
if ($release->rollout_percentage < 100) {
$bucket = crc32(request()->ip()) % 100;
if ($bucket >= $release->rollout_percentage) {
return response()->json(null, 204);
}
}

return response()->json([
'version' => $release->version,
'notes' => $release->release_notes,
'pub_date' => $release->published_at->toIso8601String(),
'url' => $release->download_url,
'signature' => $release->signature,
]);
}
}
// Database Migration
Schema::create('tauri_releases', function (Blueprint $table) {
$table->id();
$table->string('version'); // "2.4.0"
$table->string('target'); // "darwin", "linux", "windows"
$table->string('arch'); // "x86_64", "aarch64"
$table->string('download_url'); // 바이너리 다운로드 URL
$table->text('signature'); // Ed25519 서명
$table->text('release_notes')->nullable();
$table->timestamp('published_at');
$table->boolean('active')->default(true);
$table->unsignedTinyInteger('rollout_percentage')->default(100);
$table->timestamps();

$table->unique(['version', 'target', 'arch']);
$table->index(['target', 'arch', 'active']);
});

2.4 Cloudflare R2 + Worker

// Cloudflare Worker - 업데이트 체크 엔드포인트
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const [, target, arch, currentVersion] =
url.pathname.match(/\/tauri\/(\w+)\/([\w_]+)\/([\d.]+)/) ?? [];

if (!target || !arch || !currentVersion) {
return new Response('Bad Request', { status: 400 });
}

// R2에서 매니페스트 읽기
const manifestKey = `releases/latest/${target}-${arch}.json`;
const manifest = await env.R2_BUCKET.get(manifestKey);

if (!manifest) {
return new Response(null, { status: 204 });
}

const release = JSON.parse(await manifest.text());

// 현재 버전이 최신이면 204
if (compareVersions(currentVersion, release.version) >= 0) {
return new Response(null, { status: 204 });
}

return new Response(JSON.stringify(release), {
headers: { 'Content-Type': 'application/json' },
});
},
};

3. Ed25519 서명 키 관리

3.1 키 생성

# Tauri CLI로 키 쌍 생성
npx tauri signer generate -w ~/.tauri/myapp.key

# 출력:
# 비밀 키 → ~/.tauri/myapp.key (절대 공개 금지)
# 공개 키 → 콘솔에 출력됨 → tauri.conf.json의 pubkey에 입력

3.2 키 저장소 관리

저장 위치접근 권한
비밀 키 (signing key)GitHub Secrets TAURI_SIGNING_PRIVATE_KEYCI/CD만
비밀 키 패스워드GitHub Secrets TAURI_SIGNING_PRIVATE_KEY_PASSWORDCI/CD만
공개 키 (verify key)tauri.conf.json > plugins.updater.pubkey앱에 내장 (공개)

3.3 환경 변수 설정

# 로컬 빌드 시
export TAURI_SIGNING_PRIVATE_KEY=$(cat ~/.tauri/myapp.key)
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your-password"

# GitHub Actions
# Settings > Secrets > Actions에 등록

3.4 키 로테이션 절차

1. 새 키 쌍 생성 (tauri signer generate)
2. tauri.conf.json의 pubkey를 새 공개 키로 변경
3. 새 키로 서명된 업데이트 배포
4. 이전 키로 서명된 바이너리는 검증 실패 → 사용자가 수동 다운로드 필요

키 로테이션은 Breaking Change이다. 기존 사용자는 자동 업데이트가 실패하므로, 앱 내에서 "수동 다운로드" 링크를 안내해야 한다.

키 관리 체크리스트

  • Ed25519 키 쌍 생성
  • 비밀 키를 GitHub Secrets에 등록
  • 비밀 키 패스워드를 GitHub Secrets에 등록
  • 공개 키를 tauri.conf.json에 설정
  • 비밀 키 백업 (안전한 장소, 예: 1Password/Vault)
  • 키 로테이션 시 사용자 안내 방안 준비

4. 업데이트 매니페스트 JSON

4.1 매니페스트 구조

{
"version": "2.4.0",
"notes": "Bug fixes and performance improvements\n\n- Fixed crash on startup\n- Improved rendering speed",
"pub_date": "2026-04-06T12:00:00Z",
"platforms": {
"darwin-aarch64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"url": "https://releases.example.com/v2.4.0/myapp-2.4.0-aarch64.app.tar.gz"
},
"darwin-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"url": "https://releases.example.com/v2.4.0/myapp-2.4.0-x86_64.app.tar.gz"
},
"linux-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"url": "https://releases.example.com/v2.4.0/myapp-2.4.0-x86_64.AppImage"
},
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"url": "https://releases.example.com/v2.4.0/myapp-2.4.0-x64-setup.nsis.zip"
}
}
}

4.2 플랫폼 키 명명 규칙

플랫폼 키OS아키텍처빌드 출력
darwin-aarch64macOSApple Silicon.app.tar.gz
darwin-x86_64macOSIntel.app.tar.gz
linux-x86_64Linuxx64.AppImage
linux-aarch64LinuxARM64.AppImage
windows-x86_64Windowsx64.nsis.zip 또는 .msi.zip
windows-aarch64WindowsARM64.nsis.zip

4.3 단일 엔드포인트 vs 플랫폼별 엔드포인트

단일 엔드포인트 (권장 - 모든 플랫폼을 하나의 JSON에):

// tauri.conf.json
"endpoints": [
"https://releases.example.com/tauri/latest.json"
]

플랫폼별 엔드포인트 (동적 라우팅):

// tauri.conf.json
"endpoints": [
"https://releases.example.com/tauri/{{target}}/{{arch}}/{{current_version}}"
]

단일 엔드포인트: CDN 캐싱 용이, 관리 간단. 플랫폼별 엔드포인트: 서버에서 롤아웃/A-B 테스트 등 세밀한 제어 가능.

4.4 서명 파일 구조

Tauri 빌드 시 서명 파일이 바이너리와 함께 생성된다:

target/release/bundle/
+-- nsis/
| +-- myapp_2.4.0_x64-setup.exe # 설치 프로그램
| +-- myapp_2.4.0_x64-setup.nsis.zip # 업데이트용 (압축)
| +-- myapp_2.4.0_x64-setup.nsis.zip.sig # Ed25519 서명
+-- macos/
| +-- myapp.app.tar.gz # 업데이트용 (압축)
| +-- myapp.app.tar.gz.sig # Ed25519 서명
+-- appimage/
+-- myapp_2.4.0_amd64.AppImage # AppImage
+-- myapp_2.4.0_amd64.AppImage.sig # Ed25519 서명

5. 플랫폼별 업데이트 흐름

5.1 공통 업데이트 플로우 (Frontend)

// src/lib/services/updater.ts
import { check, type Update } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';

export interface UpdateProgress {
status: 'checking' | 'available' | 'downloading' | 'installing' | 'up-to-date' | 'error';
version?: string;
notes?: string;
downloadedBytes?: number;
totalBytes?: number;
error?: string;
}

export async function checkAndInstallUpdate(
onProgress: (progress: UpdateProgress) => void,
): Promise<void> {
onProgress({ status: 'checking' });

try {
const update = await check();

if (!update) {
onProgress({ status: 'up-to-date' });
return;
}

onProgress({
status: 'available',
version: update.version,
notes: update.body ?? undefined,
});

// 사용자 확인 대기 (UI에서 처리)
// 아래 코드는 사용자가 "업데이트" 버튼 클릭 후 호출

await downloadAndInstall(update, onProgress);
} catch (error) {
onProgress({
status: 'error',
error: error instanceof Error ? error.message : String(error),
});
}
}

export async function downloadAndInstall(
update: Update,
onProgress: (progress: UpdateProgress) => void,
): Promise<void> {
let totalBytes = 0;
let downloadedBytes = 0;

await update.downloadAndInstall((event) => {
if (event.event === 'Started') {
totalBytes = event.data.contentLength ?? 0;
onProgress({
status: 'downloading',
version: update.version,
downloadedBytes: 0,
totalBytes,
});
} else if (event.event === 'Progress') {
downloadedBytes += event.data.chunkLength;
onProgress({
status: 'downloading',
version: update.version,
downloadedBytes,
totalBytes,
});
} else if (event.event === 'Finished') {
onProgress({ status: 'installing', version: update.version });
}
});

// 재시작
await relaunch();
}

5.2 백그라운드 자동 체크

// src/lib/services/auto-updater.ts
import { check } from '@tauri-apps/plugin-updater';

const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4시간

let checkInterval: ReturnType<typeof setInterval> | null = null;

export function startAutoUpdateCheck(
onUpdateAvailable: (version: string, notes: string | null) => void,
): void {
// 앱 시작 후 30초 뒤 첫 체크 (시작 성능 영향 최소화)
setTimeout(async () => {
await performCheck(onUpdateAvailable);
}, 30_000);

// 이후 주기적 체크
checkInterval = setInterval(async () => {
await performCheck(onUpdateAvailable);
}, CHECK_INTERVAL_MS);
}

export function stopAutoUpdateCheck(): void {
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
}

async function performCheck(
onUpdateAvailable: (version: string, notes: string | null) => void,
): Promise<void> {
try {
const update = await check();
if (update) {
onUpdateAvailable(update.version, update.body);
}
} catch {
// 네트워크 에러 등은 무시 (자동 체크이므로)
}
}

5.3 플랫폼별 특이사항

OS업데이트 바이너리설치 방식재시작 필요비고
Windows (NSIS).nsis.zipNSIS 인스톨러 실행OinstallMode: passive/basicUi/quiet
Windows (MSI).msi.zipMSI 자동 실행OWiX 기반
macOS.app.tar.gz앱 번들 교체OGatekeeper 재검증 없음 (동일 서명)
Linux.AppImage파일 교체OAppImage 자체 업데이트

Windows installMode 비교

모드설명사용자 경험
passive자동 설치 (진행 바 표시)기본값, 대부분 상황에 적합
basicUiNSIS 기본 UI 표시사용자가 설치 과정 확인 가능
quiet완전 무음 설치백그라운드, 사용자 인지 없음

6. 점진적 롤아웃

공통 롤아웃 전략: ../common/update.md 참조.

자체 업데이트 서버에서 롤아웃 구현

// TauriUpdateController.php - 롤아웃 로직
public function check(string $target, string $arch, string $currentVersion): JsonResponse
{
$release = TauriRelease::query()
->where('target', $target)
->where('arch', $arch)
->where('active', true)
->latest('version')
->first();

if (!$release || version_compare($currentVersion, $release->version, '>=')) {
return response()->json(null, 204);
}

// 롤아웃 퍼센트 체크
if (!$this->isInRolloutGroup($release->rollout_percentage)) {
return response()->json(null, 204); // 아직 대상 아님
}

return response()->json([
'version' => $release->version,
'notes' => $release->release_notes,
'pub_date' => $release->published_at->toIso8601String(),
'url' => $release->download_url,
'signature' => $release->signature,
]);
}

private function isInRolloutGroup(int $percentage): bool
{
if ($percentage >= 100) return true;

// IP 또는 요청 헤더 기반 결정론적 버킷팅
$identifier = request()->ip() . request()->header('X-Machine-Id', '');
$bucket = crc32($identifier) % 100;
return $bucket < $percentage;
}

롤아웃 관리 API

// 관리자가 롤아웃 퍼센트 조절
Route::put('/admin/releases/{id}/rollout', function (Request $request, int $id) {
$release = TauriRelease::findOrFail($id);
$release->update([
'rollout_percentage' => $request->input('percentage'), // 0~100
]);
return response()->json(['message' => 'Rollout updated']);
});

7. 강제 업데이트 + 최소 버전 체크

공통 강제 업데이트 패턴: ../common/update.md 참조.

Tauri 전용 강제 업데이트 (Rust 측)

// src-tauri/src/commands/update_check.rs
use tauri_plugin_updater::UpdaterExt;
use serde::Serialize;

#[derive(Serialize)]
pub struct VersionCheckResult {
pub update_available: bool,
pub update_required: bool,
pub current_version: String,
pub latest_version: Option<String>,
pub minimum_version: Option<String>,
pub download_url: Option<String>,
pub release_notes: Option<String>,
}

#[tauri::command]
pub async fn check_version(
app: tauri::AppHandle,
) -> Result<VersionCheckResult, String> {
let current_version = app.package_info().version.to_string();

// 1. 서버에서 최소 버전 정보 가져오기
let client = reqwest::Client::new();
let server_check = client
.get(format!(
"{}/api/client/version-check",
std::env::var("API_BASE_URL").unwrap_or_default()
))
.query(&[
("platform", "tauri"),
("currentVersion", &current_version),
])
.send()
.await
.ok();

let minimum_version = server_check
.and_then(|r| r.json::<serde_json::Value>().ok())
.and_then(|v| v["minimumVersion"].as_str().map(String::from));

// 2. Tauri Updater로 최신 버전 체크
let updater = app.updater_builder().build().map_err(|e| e.to_string())?;
let update = updater.check().await.map_err(|e| e.to_string())?;

// 3. 강제 업데이트 여부 판단
let update_required = minimum_version
.as_ref()
.map(|min| {
semver::Version::parse(&current_version)
.ok()
.zip(semver::Version::parse(min).ok())
.map(|(current, min)| current < min)
.unwrap_or(false)
})
.unwrap_or(false);

Ok(VersionCheckResult {
update_available: update.is_some(),
update_required,
current_version,
latest_version: update.as_ref().map(|u| u.version.clone()),
minimum_version,
download_url: None,
release_notes: update.and_then(|u| u.body),
})
}

프론트엔드 강제 업데이트 UI

// src/lib/components/ForceUpdateModal.svelte 의 로직
import { invoke } from '@tauri-apps/api/core';

interface VersionCheck {
update_available: boolean;
update_required: boolean;
current_version: string;
latest_version: string | null;
minimum_version: string | null;
release_notes: string | null;
}

async function onAppStart(): Promise<void> {
const result = await invoke<VersionCheck>('check_version');

if (result.update_required) {
// 강제 업데이트 모달 표시 (닫기 불가)
showForceUpdateModal(result);
} else if (result.update_available) {
// 선택 업데이트 알림 배너
showUpdateBanner(result);
}
}

8. CI/CD 파이프라인 연동

CI/CD 공통 설정: ../common/ci-cd.md 참조.

8.1 GitHub Actions 릴리즈 워크플로우

# .github/workflows/release-tauri.yml
name: Release Tauri App

on:
push:
tags: ['v*']

jobs:
create-release:
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create.outputs.result }}
steps:
- uses: actions/create-release@v1
id: create
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
draft: true

build-and-upload:
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- platform: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- platform: macos-latest
target: aarch64-apple-darwin
- platform: macos-latest
target: x86_64-apple-darwin
- platform: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

# Linux 의존성
- name: Install Linux dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf

- name: Install frontend dependencies
run: npm ci

- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 코드 서명
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# 업데이트 서명
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: ${{ github.ref_name }}
releaseName: "v__VERSION__"
releaseBody: "See CHANGELOG.md for details."
releaseDraft: true
prerelease: false
args: --target ${{ matrix.target }}

8.2 업데이트 매니페스트 자동 생성

tauri-action을 사용하면 latest.json이 자동 생성되어 GitHub Release에 첨부된다.

자체 서버 사용 시 수동 생성 스크립트:

#!/bin/bash
# scripts/generate-update-manifest.sh
VERSION=$1
RELEASE_DIR="releases/${VERSION}"

generate_manifest() {
cat <<EOF
{
"version": "${VERSION}",
"notes": "$(cat CHANGELOG.md | head -20)",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-aarch64": {
"signature": "$(cat ${RELEASE_DIR}/myapp.app.tar.gz.sig)",
"url": "https://releases.example.com/${VERSION}/myapp-${VERSION}-aarch64.app.tar.gz"
},
"darwin-x86_64": {
"signature": "$(cat ${RELEASE_DIR}/myapp-x86_64.app.tar.gz.sig)",
"url": "https://releases.example.com/${VERSION}/myapp-${VERSION}-x86_64.app.tar.gz"
},
"linux-x86_64": {
"signature": "$(cat ${RELEASE_DIR}/myapp.AppImage.sig)",
"url": "https://releases.example.com/${VERSION}/myapp-${VERSION}-x86_64.AppImage"
},
"windows-x86_64": {
"signature": "$(cat ${RELEASE_DIR}/myapp-setup.nsis.zip.sig)",
"url": "https://releases.example.com/${VERSION}/myapp-${VERSION}-x64-setup.nsis.zip"
}
}
}
EOF
}

generate_manifest > "${RELEASE_DIR}/latest.json"
echo "Manifest generated: ${RELEASE_DIR}/latest.json"

8.3 필요한 GitHub Secrets

Secret 이름용도필수
TAURI_SIGNING_PRIVATE_KEYEd25519 비밀 키 (업데이트 서명)O
TAURI_SIGNING_PRIVATE_KEY_PASSWORD비밀 키 패스워드O
APPLE_CERTIFICATEmacOS 코드 서명 인증서 (Base64)macOS
APPLE_CERTIFICATE_PASSWORD인증서 패스워드macOS
APPLE_SIGNING_IDENTITY서명 ID (예: "Developer ID Application: ...")macOS
APPLE_IDApple ID 이메일macOS
APPLE_APP_SPECIFIC_PASSWORD앱 전용 패스워드 (Notarization)macOS
APPLE_TEAM_IDApple 팀 IDmacOS
WINDOWS_PFX_BASE64Windows 코드 서명 인증서 (Base64)Windows
WINDOWS_PFX_PASSWORD인증서 패스워드Windows

자동 업데이트 체크리스트

설정

  • tauri-plugin-updater + tauri-plugin-process 설치
  • lib.rs에 플러그인 등록
  • Ed25519 키 쌍 생성 + 안전한 보관
  • tauri.conf.json에 pubkey + endpoints 설정
  • Capability에 updater:default + process:allow-restart 추가

업데이트 서버

  • 업데이트 서버 선택 (GitHub Releases / 자체 서버)
  • 매니페스트 JSON 생성 자동화
  • 서명 파일(.sig) 업로드 자동화
  • 엔드포인트 HTTPS 확인
  • 204 (업데이트 없음) 응답 정상 확인

프론트엔드

  • 업데이트 체크 UI 구현 (자동 + 수동)
  • 다운로드 진행률 표시
  • 강제 업데이트 모달 구현
  • 오프라인 시 에러 무시 처리
  • 업데이트 후 재시작 안내

CI/CD

  • GitHub Actions 릴리즈 워크플로우 작성
  • 모든 플랫폼 빌드 매트릭스 구성
  • 코드 서명 Secrets 등록
  • 업데이트 서명 Secrets 등록
  • 릴리즈 후 매니페스트 자동 배포