diff --git a/build/ci/upload-release-asset.ps1 b/build/ci/upload-release-asset.ps1 index f65efa5..e733b3a 100644 --- a/build/ci/upload-release-asset.ps1 +++ b/build/ci/upload-release-asset.ps1 @@ -1,35 +1,100 @@ -# Forgejo 릴리스(draft)에 산출물을 업로드한다. 릴리스가 없으면 만들고, 동명 자산은 교체한다(재실행 멱등). -# 사용: ./build/ci/upload-release-asset.ps1 -Tag v1.0.0 -Files out/a.exe, out/b.exe -# 필요 환경: RELEASE_TOKEN + (Actions 기본 제공) GITHUB_SERVER_URL, GITHUB_REPOSITORY -# RELEASE_REPO 를 주면 해당 저장소로 업로드(공개 배포 저장소용), 없으면 현재 저장소 -# RELEASE_PRERELEASE=true 면 prerelease 로 생성(리허설용) +# Forgejo 릴리스에 산출물을 업로드한다. 릴리스가 없으면 만들고, 동명 자산은 교체한다(재실행 멱등). +# 사용: ./build/ci/upload-release-asset.ps1 -Tag v1.0.0 -Files out/a.exe, out/b.exe [-Publish] param( [Parameter(Mandatory)][string]$Tag, - [Parameter(Mandatory)][string[]]$Files + [Parameter(Mandatory)][string[]]$Files, + [switch]$Publish, + [string]$Notes = "", + [string]$Repo = "" ) $ErrorActionPreference = 'Stop' -if (-not $env:RELEASE_TOKEN) { throw "RELEASE_TOKEN 환경변수가 없습니다" } -$repo = if ($env:RELEASE_REPO) { $env:RELEASE_REPO } else { $env:GITHUB_REPOSITORY } -$api = "$($env:GITHUB_SERVER_URL.TrimEnd('/'))/api/v1/repos/$repo" -$hdr = @{ Authorization = "token $env:RELEASE_TOKEN" } + +# .env 자동 로드 (로컬 실행 시) +$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +$envFile = Join-Path $root ".env" +if (Test-Path $envFile) { + foreach ($line in Get-Content $envFile) { + if ($line -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.+)$') { + $k = $Matches[1]; $v = $Matches[2].Trim() + if (-not (Get-Item "env:$k" -ErrorAction SilentlyContinue)) { + Set-Item "env:$k" $v + } + } + } +} + +$token = if ($env:RELEASE_TOKEN) { $env:RELEASE_TOKEN } else { $env:FORGEJO_TOKEN } +$hdr = @{} + +if ($token) { + $hdr["Authorization"] = "token $token" +} elseif ($env:FORGEJO_USER -and $env:FORGEJO_PASSWORD) { + $bytes = [System.Text.Encoding]::UTF8.GetBytes("$($env:FORGEJO_USER):$($env:FORGEJO_PASSWORD)") + $b64 = [Convert]::ToBase64String($bytes) + $hdr["Authorization"] = "Basic $b64" +} else { + throw ".env 에 FORGEJO_TOKEN 또는 FORGEJO_USER/PASSWORD 가 없습니다." +} + +$serverUrl = if ($env:GITHUB_SERVER_URL) { $env:GITHUB_SERVER_URL } + elseif ($env:FORGEJO_URL) { $env:FORGEJO_URL } + else { "https://git.chanpaca.net" } +$serverUrl = $serverUrl.TrimEnd('/') + +$targetRepo = if ($Repo) { $Repo } + elseif ($env:RELEASE_REPO) { $env:RELEASE_REPO } + elseif ($env:GITHUB_REPOSITORY) { $env:GITHUB_REPOSITORY } + else { "Video-Downloader/releases" } + +$api = "$serverUrl/api/v1/repos/$targetRepo" + +$isDraft = -not $Publish +$isPrerelease = ($env:RELEASE_PRERELEASE -eq 'true') # draft 릴리스는 tags/ 단건 조회에 안 잡힐 수 있어 목록에서 tag_name 으로 찾는다 function Find-Release { - (Invoke-RestMethod -Uri "$api/releases?limit=50" -Headers $hdr) | - Where-Object tag_name -eq $Tag | Select-Object -First 1 + try { + (Invoke-RestMethod -Uri "$api/releases?limit=50" -Headers $hdr) | + Where-Object tag_name -eq $Tag | Select-Object -First 1 + } catch { + return $null + } } $rel = Find-Release if (-not $rel) { - $body = @{ tag_name = $Tag; name = $Tag; draft = $true - prerelease = ($env:RELEASE_PRERELEASE -eq 'true') } | ConvertTo-Json - try { $rel = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $hdr -ContentType 'application/json' -Body $body } - catch { $rel = Find-Release } # 병렬 잡 동시 생성 경합 대비 + $bodyObj = @{ + tag_name = $Tag + name = $Tag + draft = $isDraft + prerelease = $isPrerelease + } + if ($Notes) { $bodyObj["body"] = $Notes } + $body = $bodyObj | ConvertTo-Json + + try { + $rel = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $hdr -ContentType 'application/json' -Body $body + } catch { + $rel = Find-Release # 병렬 잡 동시 생성 경합 대비 + } +} else { + # 기존 릴리스가 있고 Publish 옵션이 주어졌는데 현재 draft 상태인 경우 Publish로 갱신 + if ($Publish -and $rel.draft) { + $patchBody = @{ draft = $false } | ConvertTo-Json + $rel = Invoke-RestMethod -Method Patch -Uri "$api/releases/$($rel.id)" -Headers $hdr -ContentType 'application/json' -Body $patchBody + Write-Host "릴리스를 정식 공개(Published) 상태로 전환했습니다." -ForegroundColor Green + } } -if (-not $rel) { throw "릴리스 확보 실패: $Tag" } -Write-Host "릴리스 #$($rel.id) ($Tag)" + +if (-not $rel) { throw "릴리스 확보 실패: $Tag (저장소: $targetRepo)" } +$statusStr = if ($rel.draft) { "Draft" } else { "Published" } +Write-Host "릴리스 #$($rel.id) ($Tag, 상태: $statusStr, 저장소: $targetRepo)" -ForegroundColor Cyan foreach ($f in $Files) { + if (-not (Test-Path $f)) { + Write-Warning "파일이 존재하지 않아 건너뜁니다: $f" + continue + } $item = Get-Item $f (Invoke-RestMethod -Uri "$api/releases/$($rel.id)/assets" -Headers $hdr) | Where-Object name -eq $item.Name | ForEach-Object { @@ -37,5 +102,5 @@ foreach ($f in $Files) { } Invoke-RestMethod -Method Post -Uri "$api/releases/$($rel.id)/assets?name=$($item.Name)" ` -Headers $hdr -Form @{ attachment = $item } | Out-Null - Write-Host ("업로드 완료: {0} ({1:N1} MB)" -f $item.Name, ($item.Length / 1MB)) + Write-Host ("업로드 완료: {0} ({1:N1} MB)" -f $item.Name, ($item.Length / 1MB)) -ForegroundColor Green } diff --git a/build/deploy-site.ps1 b/build/deploy-site.ps1 index a54d59a..79740b3 100644 --- a/build/deploy-site.ps1 +++ b/build/deploy-site.ps1 @@ -11,8 +11,9 @@ $root = Split-Path -Parent $PSScriptRoot $envFile = Join-Path $root ".env" if (-not (Test-Path $envFile)) { throw ".env 가 없습니다. .env.example 을 복사해 채우세요." } foreach ($line in Get-Content $envFile) { - if ($line -match '^\s*(CLOUDFLARE_API_TOKEN|CLOUDFLARE_ACCOUNT_ID)\s*=\s*(.+)$') { - Set-Item "env:$($Matches[1])" $Matches[2].Trim() + if ($line -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.+)$') { + $k = $Matches[1]; $v = $Matches[2].Trim() + Set-Item "env:$k" $v } } if (-not $env:CLOUDFLARE_API_TOKEN) { throw ".env 에 CLOUDFLARE_API_TOKEN 이 없습니다." } @@ -26,4 +27,4 @@ Write-Host "2) Cloudflare Pages 배포 ($Project)" -ForegroundColor Cyan npx wrangler pages deploy (Join-Path $root "site") --project-name $Project --branch main --commit-dirty=true if ($LASTEXITCODE -ne 0) { throw "wrangler 배포 실패" } -Write-Host "완료: https://$Project.pages.dev" -ForegroundColor Green +Write-Host "완료: https://$Project-7r4.pages.dev (또는 대시보드 지정 도메인)" -ForegroundColor Green diff --git a/build/release.ps1 b/build/release.ps1 new file mode 100644 index 0000000..c9f02ca --- /dev/null +++ b/build/release.ps1 @@ -0,0 +1,142 @@ +# MediaBrowser 원클릭 통합 릴리스 & 사이트 배포 스크립트 +# +# 동작: +# 1) Windows 설치자(x64, arm64) 빌드 (+ 선택: Android APK, 원격 Linux/macOS 머신 빌드) +# 2) NAS Git(Forgejo)의 공개 릴리스 저장소(Video-Downloader/releases)에 릴리스 생성 및 자산 업로드 +# 3) (선택: -DeploySite) releases.json 갱신 및 Cloudflare Pages 배포 +# +# 사용 예시: +# ./build/release.ps1 -Version 1.0.0 -Publish -DeploySite +# ./build/release.ps1 -Version 1.0.0-preview -Prerelease +# ./build/release.ps1 -Version 1.0.0 -SkipBuild -Publish -DeploySite + +param( + [Parameter(Mandatory=$true)][string]$Version, + [string]$Config = "Release", + [switch]$Publish, + [switch]$Prerelease, + [switch]$SkipBuild, + [switch]$IncludeAndroid, + [switch]$IncludeRemote, + [switch]$DeploySite, + [string]$Notes = "", + [string]$Repo = "Video-Downloader/releases" +) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$outDir = Join-Path $root "out" + +Write-Host "======================================================" -ForegroundColor Cyan +Write-Host " MediaBrowser 릴리스 파이프라인 (v$Version)" -ForegroundColor Cyan +Write-Host "======================================================" -ForegroundColor Cyan + +# ── 1. .env 환경변수 로드 ──────────────────────────────────── +$envFile = Join-Path $root ".env" +if (Test-Path $envFile) { + foreach ($line in Get-Content $envFile) { + if ($line -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.+)$') { + $k = $Matches[1]; $v = $Matches[2].Trim() + if (-not (Get-Item "env:$k" -ErrorAction SilentlyContinue)) { + Set-Item "env:$k" $v + } + } + } +} + +$hasAuth = ($env:RELEASE_TOKEN) -or ($env:FORGEJO_TOKEN) -or ($env:FORGEJO_USER -and $env:FORGEJO_PASSWORD) +if (-not $hasAuth) { + throw ".env 에 FORGEJO_TOKEN 또는 FORGEJO_USER/PASSWORD 가 없습니다. Forgejo 인증을 설정하세요." +} + +# ── 2. 아티팩트 빌드 ───────────────────────────────────────── +if (-not $SkipBuild) { + Write-Host "`n[1/4] Windows 설치자 빌드 (x64, arm64)" -ForegroundColor Yellow + & (Join-Path $PSScriptRoot "publish.ps1") -Config $Config -Version $Version + if ($LASTEXITCODE -ne 0) { throw "Windows 설치자 빌드 실패" } + + if ($IncludeAndroid) { + Write-Host "`n[2/4] Android APK 빌드" -ForegroundColor Yellow + $mobileCsproj = Join-Path $root "VideoDownloader.Mobile\VideoDownloader.Mobile.csproj" + $androidOut = Join-Path $outDir "android" + dotnet publish $mobileCsproj -f net10.0-android -c $Config -p:AndroidPackageFormat=apk -p:CiPackArm64Only=true -o $androidOut + if ($LASTEXITCODE -eq 0) { + $apk = Get-ChildItem (Join-Path $androidOut "*.apk") | Select-Object -First 1 + if ($apk) { + $targetApk = Join-Path $outDir "MediaBrowser-$Version-arm64.apk" + Copy-Item $apk.FullName $targetApk -Force + Write-Host "Android APK 생성 완료: $targetApk" -ForegroundColor Green + } + } else { + Write-Warning "Android 빌드 실패 (워크로드 미설치 시 건너뜁니다)" + } + } + + if ($IncludeRemote) { + Write-Host "`n[3/4] Linux / macOS 원격 빌드머신 빌드" -ForegroundColor Yellow + $remoteScript = Join-Path $PSScriptRoot "remote\build-remote.sh" + if (Get-Command bash -ErrorAction SilentlyContinue) { + bash $remoteScript all + } else { + Write-Warning "bash 명령어를 찾을 수 없어 원격 빌드를 건너뜁니다." + } + } +} else { + Write-Host "`n[빌드 건너뜀 (-SkipBuild)]" -ForegroundColor DarkGray +} + +# ── 3. 수집된 자산 목록 확인 ────────────────────────────────── +$assetsToUpload = @() +$candPatterns = @( + "MediaBrowser-Setup-*.exe", + "MediaBrowser-*-arm64.apk", + "mediabrowser-*.tar.gz" +) + +foreach ($pat in $candPatterns) { + Get-ChildItem -Path $outDir -Filter $pat -ErrorAction SilentlyContinue | ForEach-Object { + $assetsToUpload += $_.FullName + } +} + +if ($assetsToUpload.Count -eq 0) { + throw "업로드할 릴리스 산출물이 out/ 디렉토리에 없습니다." +} + +Write-Host "`n업로드 대상 자산 ($($assetsToUpload.Count)개):" -ForegroundColor Cyan +foreach ($a in $assetsToUpload) { + $f = Get-Item $a + Write-Host (" - {0,-35} ({1:N1} MB)" -f $f.Name, ($f.Length / 1MB)) +} + +# ── 4. NAS Git (Forgejo) 릴리스 등록 및 업로드 ──────────────── +Write-Host "`n[4/4] NAS Git (Forgejo) 릴리스 업로드 ($Repo)" -ForegroundColor Yellow +$tag = "v$Version" +$uploadScript = Join-Path $PSScriptRoot "ci\upload-release-asset.ps1" + +$uploadArgs = @{ + Tag = $tag + Files = $assetsToUpload + Repo = $Repo +} +if ($Publish) { $uploadArgs["Publish"] = $true } +if ($Notes) { $uploadArgs["Notes"] = $Notes } + +$global:LASTEXITCODE = 0 +& $uploadScript @uploadArgs + +# ── 5. 사이트 배포 (옵션) ──────────────────────────────────── +if ($DeploySite) { + Write-Host "`n[추가] Cloudflare Pages 사이트 배포" -ForegroundColor Yellow + $global:LASTEXITCODE = 0 + & (Join-Path $PSScriptRoot "deploy-site.ps1") +} + +Write-Host "`n======================================================" -ForegroundColor Green +Write-Host " 릴리스 완료: $tag" -ForegroundColor Green +if ($Publish) { + Write-Host " 상태: 정식 공개(Published)" -ForegroundColor Green +} else { + Write-Host " 상태: 초안(Draft) — 웹 UI(https://git.chanpaca.net/$Repo/releases)에서 확인 후 공개 가능" -ForegroundColor Yellow +} +Write-Host "======================================================" -ForegroundColor Green diff --git a/build/remote/build-remote.ps1 b/build/remote/build-remote.ps1 new file mode 100644 index 0000000..171d043 --- /dev/null +++ b/build/remote/build-remote.ps1 @@ -0,0 +1,66 @@ +# MediaBrowser 원격 빌드 수집 스크립트 (PowerShell) +# 빌드머신(vd-linux / vd-mac)에서 Cli + Avalonia 셸을 RID별 self-contained publish 하고 +# 산출물을 out/mediabrowser-.tar.gz 로 수집한다. +# +# 사용: ./build/remote/build-remote.ps1 [-Target mac|linux|all] +param( + [ValidateSet("mac", "linux", "all")] + [string]$Target = "all" +) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +$outDir = Join-Path $root "out" +New-Item -ItemType Directory -Path $outDir -Force | Out-Null + +$remoteDir = "build/videodownloader" + +function Push-Source { + param([string]$HostName) + Write-Host "==> [$HostName] 소스 코드 아카이브 전송" -ForegroundColor Cyan + $tempTar = Join-Path $outDir "source_sync.tar" + if (Test-Path $tempTar) { Remove-Item $tempTar -Force } + + # git archive 생성 + git archive --format=tar -o $tempTar HEAD + if ($LASTEXITCODE -ne 0) { throw "git archive 생성 실패" } + + # 원격 폴더 초기화 및 tar 전송/풀기 + & ssh.exe $HostName "rm -rf $remoteDir && mkdir -p $remoteDir" + & scp.exe $tempTar "${HostName}:${remoteDir}/source_sync.tar" + & ssh.exe $HostName "tar -xf $remoteDir/source_sync.tar -C $remoteDir && rm -f $remoteDir/source_sync.tar" + Remove-Item $tempTar -Force -ErrorAction SilentlyContinue +} + +function Build-And-Collect { + param([string]$HostName, [string]$Rid) + Write-Host "==> [$HostName] publish $Rid" -ForegroundColor Cyan + $cmd = "cd $remoteDir && export DOTNET_ROOT=`$HOME/.dotnet && export PATH=`$HOME/.dotnet:`$PATH && " + + "dotnet publish VideoDownloader.Cli -c Release -r $Rid --self-contained -o out/cli-$Rid && " + + "dotnet publish VideoDownloader.Avalonia -c Release -r $Rid --self-contained -o out/avalonia-$Rid && " + + "cd out && tar -czf mediabrowser-$Rid.tar.gz cli-$Rid avalonia-$Rid" + + & ssh.exe $HostName $cmd + if ($LASTEXITCODE -ne 0) { throw "[$HostName] 빌드 실패: $Rid" } + + $localTarget = Join-Path $outDir "mediabrowser-$Rid.tar.gz" + Write-Host "==> [$HostName] 수집: mediabrowser-$Rid.tar.gz" -ForegroundColor Green + & scp.exe "${HostName}:${remoteDir}/out/mediabrowser-$Rid.tar.gz" $localTarget + if ($LASTEXITCODE -ne 0) { throw "[$HostName] 산출물 수집 실패: $Rid" } +} + +if ($Target -eq "mac" -or $Target -eq "all") { + Push-Source "vd-mac" + Build-And-Collect "vd-mac" "osx-arm64" + Build-And-Collect "vd-mac" "osx-x64" +} + +if ($Target -eq "linux" -or $Target -eq "all") { + Push-Source "vd-linux" + Build-And-Collect "vd-linux" "linux-x64" + Build-And-Collect "vd-linux" "linux-arm64" +} + +Write-Host "`n원격 빌드 및 수집 완료:" -ForegroundColor Green +Get-ChildItem (Join-Path $outDir "mediabrowser-*.tar.gz") | + ForEach-Object { Write-Host (" {0,-35} {1:N1} MB" -f $_.Name, ($_.Length / 1MB)) } diff --git a/build/remote/build-remote.sh b/build/remote/build-remote.sh index f7b8eb5..1bcd7a3 100644 --- a/build/remote/build-remote.sh +++ b/build/remote/build-remote.sh @@ -1,43 +1,46 @@ #!/usr/bin/env bash -# 빌드머신(ChanpacaLaptop=linux-x64 / Mac mini=osx-arm64)에서 -# git HEAD 소스를 전송해 Cli + Avalonia 셸을 RID별 self-contained publish 하고 -# 산출물을 로컬 out/remote// 로 수집한다. +# 빌드머신(ChanpacaLaptop=linux / Mac mini=macos)에서 +# Cli + Avalonia 셸을 RID별 self-contained publish 하고 +# 산출물을 로컬 out/mediabrowser-.tar.gz 로 수집한다. # -# 사용 (Windows Git Bash): bash build/remote/build-remote.sh [linux|mac|all] -# 사전 조건: ssh 별칭 vd-linux / vd-mac (키 인증), 각 머신 ~/.dotnet SDK 8 +# 사용: bash build/remote/build-remote.sh [linux|mac|all] set -euo pipefail cd "$(dirname "$0")/../.." TARGET="${1:-all}" REMOTE_DIR="build/videodownloader" +mkdir -p out + push_src() { # $1 = ssh 대상 + echo "==> [$1] 소스 코드 전송" git archive HEAD | ssh -o BatchMode=yes "$1" "rm -rf $REMOTE_DIR && mkdir -p $REMOTE_DIR && tar -x -C $REMOTE_DIR" } -build_on() { # $1 = ssh 대상, $2 = RID - echo "==> [$1] publish $2" - ssh -o BatchMode=yes "$1" "cd $REMOTE_DIR && export DOTNET_ROOT=\$HOME/.dotnet && export PATH=\$HOME/.dotnet:\$PATH && \ - dotnet publish VideoDownloader.Cli -c Release -r $2 --self-contained -o out/cli-$2 && \ - dotnet publish VideoDownloader.Avalonia -c Release -r $2 --self-contained -o out/avalonia-$2 && \ - cd out && tar -czf artifacts-$2.tar.gz cli-$2 avalonia-$2" +build_and_collect() { # $1 = ssh 대상, $2 = RID + local host="$1" + local rid="$2" + echo "==> [$host] publish $rid" + ssh -o BatchMode=yes "$host" "cd $REMOTE_DIR && export DOTNET_ROOT=\$HOME/.dotnet && export PATH=\$HOME/.dotnet:\$PATH && \ + dotnet publish VideoDownloader.Cli -c Release -r $rid --self-contained -o out/cli-$rid && \ + dotnet publish VideoDownloader.Avalonia -c Release -r $rid --self-contained -o out/avalonia-$rid && \ + cd out && tar -czf mediabrowser-$rid.tar.gz cli-$rid avalonia-$rid" + + echo "==> [$host] 수집: out/mediabrowser-$rid.tar.gz" + scp -o BatchMode=yes "$host:$REMOTE_DIR/out/mediabrowser-$rid.tar.gz" "out/mediabrowser-$rid.tar.gz" } -collect() { # $1 = ssh 대상, $2 = RID - mkdir -p "out/remote/$2" - scp -o BatchMode=yes "$1:$REMOTE_DIR/out/artifacts-$2.tar.gz" "out/remote/$2/" && \ - tar -xzf "out/remote/$2/artifacts-$2.tar.gz" -C "out/remote/$2" && \ - echo " 수집: out/remote/$2/{cli-$2,avalonia-$2}" -} +if [[ "$TARGET" == "mac" || "$TARGET" == "all" ]]; then + push_src vd-mac + build_and_collect vd-mac osx-arm64 + build_and_collect vd-mac osx-x64 +fi if [[ "$TARGET" == "linux" || "$TARGET" == "all" ]]; then push_src vd-linux - build_on vd-linux linux-x64 - collect vd-linux linux-x64 + build_and_collect vd-linux linux-x64 + build_and_collect vd-linux linux-arm64 fi -if [[ "$TARGET" == "mac" || "$TARGET" == "all" ]]; then - push_src vd-mac - build_on vd-mac osx-arm64 - collect vd-mac osx-arm64 -fi -echo "완료." + +echo "==> 원격 빌드 완료:" +ls -lh out/mediabrowser-*.tar.gz 2>/dev/null || true diff --git a/docs/DEPLOYMENT_PLAN.md b/docs/DEPLOYMENT_PLAN.md index d1d4fa2..dc05a95 100644 --- a/docs/DEPLOYMENT_PLAN.md +++ b/docs/DEPLOYMENT_PLAN.md @@ -169,8 +169,9 @@ jobs: - **워크플로**: `.forgejo/workflows/release.yml` — 태그 `v*` 푸시 → test(전체 xUnit) 관문 → 빌드 잡 병렬 → 각 잡이 draft 릴리스에 자산을 직접 업로드. host 러너에 Node 가 없어도 되도록 `uses:` 액션 없이 순수 `run:` 스텝으로만 구성(체크아웃도 git 직접). -- **업로드 헬퍼**: `build/ci/upload-release-asset.ps1`/`.sh` — draft 릴리스 확보(병렬 잡 경합 대응) + - 동명 자산 교체(재실행 멱등). Windows Git Bash 의 CRLF 오염 방지 처리 포함. +- **원클릭 통합 릴리스 도구**: `build/release.ps1 -Version [-Publish] [-DeploySite] [-IncludeAndroid] [-IncludeRemote]` — 로컬 빌드(Windows 설치자/Android APK/원격 Linux/macOS)부터 NAS Git(Forgejo) 릴리스 생성, 자산 업로드, Cloudflare Pages 사이트 배포까지 한 번에 자동화. +- **업로드 헬퍼**: `build/ci/upload-release-asset.ps1`/`.sh` — draft/published 릴리스 확보(병렬 잡 경합 대응) + + 동명 자산 교체(재실행 멱등), `.env` 자동 로드 및 Publish 전환 지원. Windows Git Bash 의 CRLF 오염 방지 처리 포함. - **리허설**: workflow_dispatch → `ci-rehearsal` draft prerelease 로 태그 없이 전 파이프라인 검증. - **시크릿**: `RELEASE_TOKEN`(Forgejo PAT, `write:repository`) — 릴리스 생성·자산 업로드. 체크아웃은 잡 자동 토큰(`github.token`). `ANDROID_KEYSTORE/ALIAS/PASS` 등록 전까지 APK 는 디버그 서명. diff --git a/site/fetch-releases.mjs b/site/fetch-releases.mjs index d507aa7..99bfc0e 100644 --- a/site/fetch-releases.mjs +++ b/site/fetch-releases.mjs @@ -1,35 +1,80 @@ // 배포 전에 공개 릴리스 저장소의 릴리스 목록을 releases.json 으로 굽는다. // (브라우저에서 Forgejo API 를 직접 호출하면 CORS 에 막히므로 빌드 시점에 정적 생성) // 사용: node site/fetch-releases.mjs -import { writeFileSync } from "node:fs"; +import { writeFileSync, readFileSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; -const API = "https://git.chanpaca.net/api/v1/repos/Video-Downloader/releases/releases?limit=50"; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, ".."); +const envPath = resolve(root, ".env"); -const res = await fetch(API, { headers: { Accept: "application/json" } }); -if (!res.ok) throw new Error(`릴리스 API 실패: HTTP ${res.status}`); -const raw = await res.json(); +// .env 로드 (환경변수가 없을 경우 보완) +if (existsSync(envPath)) { + const content = readFileSync(envPath, "utf8"); + for (const line of content.split("\n")) { + const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*)$/); + if (match && !process.env[match[1]]) { + process.env[match[1]] = match[2].trim(); + } + } +} -const releases = raw - .filter((r) => !r.draft) - .sort((a, b) => new Date(b.published_at) - new Date(a.published_at)) - .map((r) => ({ - tag: r.tag_name, - name: r.name || r.tag_name, - published_at: r.published_at, - prerelease: r.prerelease, - notes: r.body || "", - assets: (r.assets || []).map((a) => ({ - name: a.name, - size: a.size, - url: a.browser_download_url, - })), - })); +const serverUrl = (process.env.FORGEJO_URL || "https://git.chanpaca.net").replace(/\/$/, ""); +const repo = process.env.RELEASE_REPO || "Video-Downloader/releases"; +const API = `${serverUrl}/api/v1/repos/${repo}/releases?limit=50`; -const out = { - generated_at: new Date().toISOString(), - repo_url: "https://git.chanpaca.net/Video-Downloader/releases", - releases, -}; +const headers = { Accept: "application/json" }; +if (process.env.FORGEJO_TOKEN) { + headers.Authorization = `token ${process.env.FORGEJO_TOKEN}`; +} else if (process.env.FORGEJO_USER && process.env.FORGEJO_PASSWORD) { + const b64 = Buffer.from(`${process.env.FORGEJO_USER}:${process.env.FORGEJO_PASSWORD}`).toString("base64"); + headers.Authorization = `Basic ${b64}`; +} -writeFileSync(new URL("./releases.json", import.meta.url), JSON.stringify(out, null, 2)); -console.log(`releases.json 생성: 릴리스 ${releases.length}건`); +const includeDraft = process.env.INCLUDE_DRAFT === "true"; + +try { + const res = await fetch(API, { headers }); + if (!res.ok) throw new Error(`릴리스 API 실패: HTTP ${res.status}`); + const raw = await res.json(); + + const releases = (Array.isArray(raw) ? raw : []) + .filter((r) => includeDraft || !r.draft) + .sort((a, b) => new Date(b.published_at || b.created_at) - new Date(a.published_at || a.created_at)) + .map((r) => ({ + tag: r.tag_name, + name: r.name || r.tag_name, + published_at: r.published_at || r.created_at, + prerelease: r.prerelease, + draft: r.draft, + notes: r.body || "", + assets: (r.assets || []).map((a) => ({ + name: a.name, + size: a.size, + url: a.browser_download_url, + })), + })); + + const publicServerUrl = process.env.FORGEJO_PUBLIC_URL || "https://git.chanpaca.net"; + const out = { + generated_at: new Date().toISOString(), + repo_url: `${publicServerUrl.replace(/\/$/, "")}/${repo}`, + releases, + }; + + writeFileSync(new URL("./releases.json", import.meta.url), JSON.stringify(out, null, 2)); + console.log(`releases.json 생성 완료: 릴리스 ${releases.length}건 (저장소: ${repo})`); +} catch (err) { + console.error(`릴리스 정보 로드 실패: ${err.message}`); + // 기존 releases.json 이 없으면 빈 파일 생성 + const outPath = resolve(__dirname, "releases.json"); + if (!existsSync(outPath)) { + const fallback = { + generated_at: new Date().toISOString(), + repo_url: `${serverUrl}/${repo}`, + releases: [], + }; + writeFileSync(outPath, JSON.stringify(fallback, null, 2)); + } +} diff --git a/site/guide.html b/site/guide.html index cb3abf5..41e5d36 100644 --- a/site/guide.html +++ b/site/guide.html @@ -7,7 +7,7 @@ - + @@ -31,6 +31,7 @@