From 7d9bc73cad57bdedb4b2401bcaece35e715dc42b Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Mon, 17 Aug 2026 23:17:07 +0900 Subject: [PATCH] =?UTF-8?q?Forgejo=20=EC=9E=90=EC=B2=B4=20=EB=9F=AC?= =?UTF-8?q?=EB=84=88=203=EB=8C=80(windows/linux-builder/macos,=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20host=20=EB=AA=A8=EB=93=9C)=EC=9A=A9=20=EB=A6=B4?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=20CI=20=EC=8B=A0=EC=84=A4=20=E2=80=94=20.for?= =?UTF-8?q?gejo/workflows/release.yml:=20=ED=83=9C=EA=B7=B8=20v*=20?= =?UTF-8?q?=E2=86=92=20test=20=EA=B4=80=EB=AC=B8(=EC=A0=84=EC=B2=B4=20xUni?= =?UTF-8?q?t)=20=E2=86=92=20Windows=20=EC=84=A4=EC=B9=98=EC=9E=90(x64/arm6?= =?UTF-8?q?4)=C2=B7Android=20APK=C2=B7linux/mac=20tar.gz(=EA=B0=81=202=20R?= =?UTF-8?q?ID)=20=EB=B3=91=EB=A0=AC=20=EB=B9=8C=EB=93=9C=20=E2=86=92=20dra?= =?UTF-8?q?ft=20=EB=A6=B4=EB=A6=AC=EC=8A=A4=EC=97=90=20=EC=9E=90=EC=82=B0?= =?UTF-8?q?=20=EC=A7=81=EC=A0=91=20=EC=97=85=EB=A1=9C=EB=93=9C.=20host=20?= =?UTF-8?q?=EB=9F=AC=EB=84=88=EC=97=90=20Node=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=98=EB=8F=84=EB=A1=9D=20uses:=20=EC=95=A1?= =?UTF-8?q?=EC=85=98=20=EC=97=86=EC=9D=B4=20=EC=88=9C=EC=88=98=20run:=20?= =?UTF-8?q?=EC=8A=A4=ED=85=9D(=EC=B2=B4=ED=81=AC=EC=95=84=EC=9B=83?= =?UTF-8?q?=EB=8F=84=20git=20=EC=A7=81=EC=A0=91),=20workflow=5Fdispatch=20?= =?UTF-8?q?=EB=A6=AC=ED=97=88=EC=84=A4=EC=9D=80=20ci-rehearsal=20draft=20p?= =?UTF-8?q?rerelease.=20build/ci/upload-release-asset.{ps1,sh}:=20draft=20?= =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4=20=ED=99=95=EB=B3=B4(=EB=B3=91?= =?UTF-8?q?=EB=A0=AC=20=EA=B2=BD=ED=95=A9=20=EB=8C=80=EC=9D=91)+=EB=8F=99?= =?UTF-8?q?=EB=AA=85=20=EC=9E=90=EC=82=B0=20=EA=B5=90=EC=B2=B4=20=EB=A9=B1?= =?UTF-8?q?=EB=93=B1=20=E2=80=94=20Git=20Bash=20CRLF=20=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=90=EC=82=B0=20id=20=EA=B0=80=20=EC=98=A4=EC=97=BC?= =?UTF-8?q?=EB=90=98=EC=96=B4=20=EC=82=AD=EC=A0=9C=EA=B0=80=20=EC=A1=B0?= =?UTF-8?q?=EC=9A=A9=ED=9E=88=20=EC=8B=A4=ED=8C=A8=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EA=B2=B0=ED=95=A8=20=EC=88=98=EC=A0=95,=20=EC=8B=A4=20Forgejo?= =?UTF-8?q?=20API=20=EB=8C=80=EC=83=81=202=ED=9A=8C=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EC=99=84=EB=A3=8C.=20RELEASE=5FTOKEN?= =?UTF-8?q?=20=EC=8B=9C=ED=81=AC=EB=A6=BF(PAT=20write:repository)=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D,=20=EC=9C=A0=EB=A0=B9=20Inno=20Setup=20?= =?UTF-8?q?=EC=9E=AC=EC=84=A4=EC=B9=98(ALPACA-HOME),=20DEPLOYMENT=5FPLAN?= =?UTF-8?q?=20=C2=A75-bis=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .forgejo/workflows/release.yml | 203 ++++++++++++++++++++++++++++++ build/ci/upload-release-asset.ps1 | 39 ++++++ build/ci/upload-release-asset.sh | 55 ++++++++ docs/DEPLOYMENT_PLAN.md | 29 ++++- 4 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 .forgejo/workflows/release.yml create mode 100644 build/ci/upload-release-asset.ps1 create mode 100644 build/ci/upload-release-asset.sh diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..8c01e53 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,203 @@ +# Forgejo Actions 릴리스 파이프라인 (DEPLOYMENT_PLAN §5-bis) +# 자체 러너 3대(모두 host 모드): yunchan-win(windows) · kd-desktop(linux-builder) · yunchan-mac(macos) +# - 태그 v* 푸시 → 해당 태그의 draft 릴리스 생성 + 전 아티팩트 첨부 (QA 후 수동 Publish 승격) +# - workflow_dispatch → 'ci-rehearsal' draft 릴리스로 전체 파이프라인 리허설 +# host 러너에는 Node 가 없어도 되도록 uses: 액션을 쓰지 않는다(체크아웃 포함 전부 run: 스텝). +# 빌드머신 전제: docs/DEPLOYMENT_PLAN.md §0 — win(SDK8+MAUI+Inno) · linux/mac(~/.dotnet SDK 8+10) +name: release + +on: + push: + tags: ["v*"] + workflow_dispatch: # 수동 실행(리허설) + +env: + DOTNET_NOLOGO: "true" + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + +jobs: + # ── 관문: 전 플랫폼 빌드 전에 단위+E2E 통과 ── + test: + runs-on: windows + defaults: { run: { shell: pwsh } } + steps: + - name: checkout + env: { CI_TOKEN: "${{ github.token }}" } + run: | + $u = [Uri]$env:GITHUB_SERVER_URL + $url = "$($u.Scheme)://actions:$($env:CI_TOKEN)@$($u.Authority)/$($env:GITHUB_REPOSITORY).git" + if (-not (Test-Path .git)) { git init -q . } + if (git remote | Select-String -Quiet '^origin$') { git remote set-url origin $url } else { git remote add origin $url } + git fetch -q --depth 1 origin $env:GITHUB_REF + if ($LASTEXITCODE -ne 0) { throw "fetch 실패: $env:GITHUB_REF" } + git checkout -q -f FETCH_HEAD + if ($LASTEXITCODE -ne 0) { throw "checkout 실패" } + git clean -qfdx + - name: 환경 정보 + run: dotnet --list-sdks + - name: dotnet test + run: dotnet test VideoDownloader.Tests -c Release + + # ── Windows 설치자 (x64 / arm64 교차 publish + Inno Setup) ── + windows: + needs: test + runs-on: windows + defaults: { run: { shell: pwsh } } + steps: + - name: checkout + env: { CI_TOKEN: "${{ github.token }}" } + run: | + $u = [Uri]$env:GITHUB_SERVER_URL + $url = "$($u.Scheme)://actions:$($env:CI_TOKEN)@$($u.Authority)/$($env:GITHUB_REPOSITORY).git" + if (-not (Test-Path .git)) { git init -q . } + if (git remote | Select-String -Quiet '^origin$') { git remote set-url origin $url } else { git remote add origin $url } + git fetch -q --depth 1 origin $env:GITHUB_REF + if ($LASTEXITCODE -ne 0) { throw "fetch 실패: $env:GITHUB_REF" } + git checkout -q -f FETCH_HEAD + if ($LASTEXITCODE -ne 0) { throw "checkout 실패" } + git clean -qfdx + - name: 버전 결정 (태그 v1.2.3 → 1.2.3 / 리허설 → 0.0.0) + run: | + if ($env:GITHUB_REF -match '^refs/tags/v(\d+\.\d+\.\d+.*)$') { + "RELTAG=$($env:GITHUB_REF -replace '^refs/tags/','')" >> $env:GITHUB_ENV + "APP_VERSION=$($Matches[1])" >> $env:GITHUB_ENV + "RELEASE_PRERELEASE=false" >> $env:GITHUB_ENV + } else { + "RELTAG=ci-rehearsal" >> $env:GITHUB_ENV + "APP_VERSION=0.0.0" >> $env:GITHUB_ENV + "RELEASE_PRERELEASE=true" >> $env:GITHUB_ENV + } + - name: publish + 설치자 (x64 + arm64) + run: ./build/publish.ps1 -Config Release -Version $env:APP_VERSION + - name: 릴리스 업로드 + env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" } + run: ./build/ci/upload-release-asset.ps1 -Tag $env:RELTAG -Files out/VideoDownloader-Setup-x64.exe, out/VideoDownloader-Setup-arm64.exe + + # ── Android APK (키스토어 시크릿 없으면 디버그 서명 APK) ── + android: + needs: test + runs-on: windows + defaults: { run: { shell: pwsh } } + steps: + - name: checkout + env: { CI_TOKEN: "${{ github.token }}" } + run: | + $u = [Uri]$env:GITHUB_SERVER_URL + $url = "$($u.Scheme)://actions:$($env:CI_TOKEN)@$($u.Authority)/$($env:GITHUB_REPOSITORY).git" + if (-not (Test-Path .git)) { git init -q . } + if (git remote | Select-String -Quiet '^origin$') { git remote set-url origin $url } else { git remote add origin $url } + git fetch -q --depth 1 origin $env:GITHUB_REF + if ($LASTEXITCODE -ne 0) { throw "fetch 실패: $env:GITHUB_REF" } + git checkout -q -f FETCH_HEAD + if ($LASTEXITCODE -ne 0) { throw "checkout 실패" } + git clean -qfdx + - name: 버전 결정 + run: | + if ($env:GITHUB_REF -match '^refs/tags/v(\d+\.\d+\.\d+.*)$') { + "RELTAG=$($env:GITHUB_REF -replace '^refs/tags/','')" >> $env:GITHUB_ENV + "APP_VERSION=$($Matches[1])" >> $env:GITHUB_ENV + "RELEASE_PRERELEASE=false" >> $env:GITHUB_ENV + } else { + "RELTAG=ci-rehearsal" >> $env:GITHUB_ENV + "APP_VERSION=0.0.0" >> $env:GITHUB_ENV + "RELEASE_PRERELEASE=true" >> $env:GITHUB_ENV + } + - name: publish APK + env: + ANDROID_KEYSTORE_B64: "${{ secrets.ANDROID_KEYSTORE }}" + ANDROID_KEY_ALIAS: "${{ secrets.ANDROID_KEY_ALIAS }}" + ANDROID_KEY_PASS: "${{ secrets.ANDROID_KEY_PASS }}" + run: | + $sign = @() + if ($env:ANDROID_KEYSTORE_B64) { + [IO.File]::WriteAllBytes("vd.keystore", [Convert]::FromBase64String($env:ANDROID_KEYSTORE_B64)) + $sign = "-p:AndroidKeyStore=true", "-p:AndroidSigningKeyStore=$PWD/vd.keystore", + "-p:AndroidSigningKeyAlias=$env:ANDROID_KEY_ALIAS", + "-p:AndroidSigningKeyPass=$env:ANDROID_KEY_PASS", "-p:AndroidSigningStorePass=$env:ANDROID_KEY_PASS" + } else { Write-Host "ANDROID_KEYSTORE 시크릿 없음 — 디버그 서명 APK 생성" } + dotnet publish VideoDownloader.Mobile -f net10.0-android -c Release ` + -p:AndroidPackageFormat=apk -p:RuntimeIdentifiers=android-arm64 @sign -o out/android + if ($LASTEXITCODE -ne 0) { throw "publish 실패" } + $apk = Get-ChildItem out/android/*-Signed.apk | Select-Object -First 1 + if (-not $apk) { $apk = Get-ChildItem out/android/*.apk | Select-Object -First 1 } + if (-not $apk) { throw "APK 산출물 없음" } + Copy-Item $apk.FullName "out/VideoDownloader-$env:APP_VERSION-arm64.apk" + - name: 릴리스 업로드 + env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" } + run: ./build/ci/upload-release-asset.ps1 -Tag $env:RELTAG -Files "out/VideoDownloader-$env:APP_VERSION-arm64.apk" + + # ── Linux 셸 (tar.gz self-contained — Cli + Avalonia, x64 네이티브 + arm64 교차) ── + linux: + needs: test + runs-on: linux-builder + steps: + - name: checkout + env: { CI_TOKEN: "${{ github.token }}" } + run: | + proto="${GITHUB_SERVER_URL%%://*}"; host="${GITHUB_SERVER_URL#*://}" + url="$proto://actions:${CI_TOKEN}@${host%/}/${GITHUB_REPOSITORY}.git" + [ -d .git ] || git init -q . + git remote remove origin 2>/dev/null || true + git remote add origin "$url" + git fetch -q --depth 1 origin "$GITHUB_REF" + git checkout -q -f FETCH_HEAD + git clean -qfdx + - name: 환경 준비 (~/.dotnet SDK) + run: | + echo "DOTNET_ROOT=$HOME/.dotnet" >> "$GITHUB_ENV" + echo "$HOME/.dotnet" >> "$GITHUB_PATH" + case "$GITHUB_REF" in + refs/tags/v*) echo "RELTAG=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV" + echo "RELEASE_PRERELEASE=false" >> "$GITHUB_ENV" ;; + *) echo "RELTAG=ci-rehearsal" >> "$GITHUB_ENV" + echo "RELEASE_PRERELEASE=true" >> "$GITHUB_ENV" ;; + esac + - name: publish (Cli + Avalonia, linux-x64 + linux-arm64) + run: | + dotnet --list-sdks + for rid in linux-x64 linux-arm64; do + 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" + tar -C out -czf "out/videodownloader-$rid.tar.gz" "cli-$rid" "avalonia-$rid" + done + - name: 릴리스 업로드 + env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" } + run: ./build/ci/upload-release-asset.sh "$RELTAG" out/videodownloader-linux-x64.tar.gz out/videodownloader-linux-arm64.tar.gz + + # ── macOS 셸 (tar.gz — arm64 네이티브 + x64 교차. 서명·공증은 D4, Apple Developer Program 후) ── + macos: + needs: test + runs-on: macos + steps: + - name: checkout + env: { CI_TOKEN: "${{ github.token }}" } + run: | + proto="${GITHUB_SERVER_URL%%://*}"; host="${GITHUB_SERVER_URL#*://}" + url="$proto://actions:${CI_TOKEN}@${host%/}/${GITHUB_REPOSITORY}.git" + [ -d .git ] || git init -q . + git remote remove origin 2>/dev/null || true + git remote add origin "$url" + git fetch -q --depth 1 origin "$GITHUB_REF" + git checkout -q -f FETCH_HEAD + git clean -qfdx + - name: 환경 준비 (~/.dotnet SDK) + run: | + echo "DOTNET_ROOT=$HOME/.dotnet" >> "$GITHUB_ENV" + echo "$HOME/.dotnet" >> "$GITHUB_PATH" + case "$GITHUB_REF" in + refs/tags/v*) echo "RELTAG=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV" + echo "RELEASE_PRERELEASE=false" >> "$GITHUB_ENV" ;; + *) echo "RELTAG=ci-rehearsal" >> "$GITHUB_ENV" + echo "RELEASE_PRERELEASE=true" >> "$GITHUB_ENV" ;; + esac + - name: publish (Cli + Avalonia, osx-arm64 + osx-x64) + run: | + dotnet --list-sdks + for rid in osx-arm64 osx-x64; do + 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" + tar -C out -czf "out/videodownloader-$rid.tar.gz" "cli-$rid" "avalonia-$rid" + done + - name: 릴리스 업로드 + env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" } + run: ./build/ci/upload-release-asset.sh "$RELTAG" out/videodownloader-osx-arm64.tar.gz out/videodownloader-osx-x64.tar.gz diff --git a/build/ci/upload-release-asset.ps1 b/build/ci/upload-release-asset.ps1 new file mode 100644 index 0000000..483a3e3 --- /dev/null +++ b/build/ci/upload-release-asset.ps1 @@ -0,0 +1,39 @@ +# 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_PRERELEASE=true 면 prerelease 로 생성(리허설용) +param( + [Parameter(Mandatory)][string]$Tag, + [Parameter(Mandatory)][string[]]$Files +) +$ErrorActionPreference = 'Stop' +if (-not $env:RELEASE_TOKEN) { throw "RELEASE_TOKEN 환경변수가 없습니다" } +$api = "$($env:GITHUB_SERVER_URL.TrimEnd('/'))/api/v1/repos/$($env:GITHUB_REPOSITORY)" +$hdr = @{ Authorization = "token $env:RELEASE_TOKEN" } + +# 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 +} + +$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 } # 병렬 잡 동시 생성 경합 대비 +} +if (-not $rel) { throw "릴리스 확보 실패: $Tag" } +Write-Host "릴리스 #$($rel.id) ($Tag)" + +foreach ($f in $Files) { + $item = Get-Item $f + (Invoke-RestMethod -Uri "$api/releases/$($rel.id)/assets" -Headers $hdr) | + Where-Object name -eq $item.Name | ForEach-Object { + Invoke-RestMethod -Method Delete -Uri "$api/releases/$($rel.id)/assets/$($_.id)" -Headers $hdr | Out-Null + } + 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)) +} diff --git a/build/ci/upload-release-asset.sh b/build/ci/upload-release-asset.sh new file mode 100644 index 0000000..54ece8c --- /dev/null +++ b/build/ci/upload-release-asset.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Forgejo 릴리스(draft)에 산출물을 업로드한다. 릴리스가 없으면 만들고, 동명 자산은 교체한다(재실행 멱등). +# 사용: build/ci/upload-release-asset.sh <릴리스태그> <파일...> +# 필요 환경: RELEASE_TOKEN + (Actions 기본 제공) GITHUB_SERVER_URL, GITHUB_REPOSITORY +# RELEASE_PRERELEASE=true 면 prerelease 로 생성(리허설용) +set -euo pipefail +[ $# -ge 2 ] || { echo "사용법: $0 <태그> <파일...>" >&2; exit 2; } +[ -n "${RELEASE_TOKEN:-}" ] || { echo "RELEASE_TOKEN 환경변수가 없습니다" >&2; exit 2; } +tag="$1"; shift +api="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}" +auth="Authorization: token ${RELEASE_TOKEN}" + +# draft 릴리스는 tags/ 단건 조회에 안 잡힐 수 있어 목록에서 tag_name 으로 찾는다 +# tr -d '\r': Windows Git Bash 에서 python 출력이 CRLF 로 나와 id 가 오염되는 것 방지 +find_release() { + curl -sf -H "$auth" "$api/releases?limit=50" | python3 -c ' +import json, sys +tag = sys.argv[1] +for r in json.load(sys.stdin): + if r.get("tag_name") == tag: + print(r["id"]); break' "$tag" | tr -d '\r' +} + +rid="$(find_release || true)" +if [ -z "$rid" ]; then + body=$(printf '{"tag_name":"%s","name":"%s","draft":true,"prerelease":%s}' \ + "$tag" "$tag" "${RELEASE_PRERELEASE:-false}") + rid=$(curl -s -X POST -H "$auth" -H 'Content-Type: application/json' -d "$body" "$api/releases" \ + | python3 -c 'import json,sys +d = json.load(sys.stdin) +print(d["id"] if isinstance(d, dict) and "id" in d else "")' | tr -d '\r' || true) + [ -n "$rid" ] || rid="$(find_release || true)" # 병렬 잡 동시 생성 경합 대비 +fi +[ -n "$rid" ] || { echo "릴리스 확보 실패: $tag" >&2; exit 1; } +echo "릴리스 #$rid ($tag)" + +for f in "$@"; do + name=$(basename "$f") + # 재실행 멱등성: 동명 자산 제거 후 업로드 + curl -sf -H "$auth" "$api/releases/$rid/assets" | python3 -c ' +import json, sys +for a in json.load(sys.stdin): + if a.get("name") == sys.argv[1]: + print(a["id"])' "$name" | tr -d '\r' | while read -r aid; do + [ -n "$aid" ] && curl -sf -X DELETE -H "$auth" "$api/releases/$rid/assets/$aid" || true + done + resp=$(mktemp) + code=$(curl -s -o "$resp" -w '%{http_code}' -X POST -H "$auth" \ + -F "attachment=@$f" "$api/releases/$rid/assets?name=$name") + if [ "$code" != 201 ]; then + echo "업로드 실패($code): $name" >&2; cat "$resp" >&2; rm -f "$resp"; exit 1 + fi + rm -f "$resp" + echo "업로드 완료: $name ($(du -h "$f" | cut -f1))" +done diff --git a/docs/DEPLOYMENT_PLAN.md b/docs/DEPLOYMENT_PLAN.md index 145c88a..5bd0668 100644 --- a/docs/DEPLOYMENT_PLAN.md +++ b/docs/DEPLOYMENT_PLAN.md @@ -2,6 +2,7 @@ > **범위**: Windows · macOS · Linux 데스크톱 + Android · iOS 모바일의 **빌드→서명→패키징→배포→업데이트** 전 체계. > GitHub Releases 를 단일 배포 소스로, GitHub Actions 로 전 아티팩트 자동 빌드. +> **현행 운영(2026-08-17~)**: 자체 Forgejo + 자체 러너 3대가 주 채널 — §5-bis. GitHub 는 공개 시 백업 채널. > > **작성일**: 2026-08-15 · **관련 문서**: `BROWSER_FEATURES_PLAN.md`(v2) · `MOBILE_PLAN.md`(v1) > **조사 근거**: 전문 조사팀 20회 검색 + GitHub Releases API 로 툴체인 바이너리 실물 검증 (2026-08 기준) @@ -31,7 +32,8 @@ GitHub Actions 대신(또는 보완으로) **사용자 소유 빌드머신 2대* - **빌드 명령**: `bash build/remote/build-remote.sh [linux|mac|all]` — git HEAD 소스를 머신에 전송 → Cli+Avalonia RID별 self-contained publish → 산출물 tar.gz 수집(`out/remote//`). - **검증(2026-08-15)**: linux-x64·osx-arm64 양쪽 4산출물 생성(ELF/Mach-O arm64 매직 확인), **양 머신에서 네이티브 실행 E2E 통과** — HLS 테스트 스트림 64세그먼트 다운로드+병합 EXIT=0. - **운영 지식**: Avalonia 12.1 게터는 .NET 10 SDK 필요(8만 있으면 CS0103). 빌드머신 절전 시 SSH 끊김 → 재연결 감지 폴러 패턴으로 자동 재개 검증됨. Mac 무헤드 운용 시 "네트워크 접근 시 깨우기" 활성화 권장. -- GitHub Actions(§5)는 리포지토리 공개 시 **자동 백업 채널**로 병행. +- GitHub Actions(§5)는 리포지토리 공개 시 **자동 백업 채널**로 병행. 두 머신 모두 2026-08-17 부터 + Forgejo 러너(host 모드)로도 등록되어 CI 잡을 직접 수행한다(§5-bis). ## 1. 제품 × 아티팩트 매트릭스 @@ -152,6 +154,31 @@ jobs: - 공개 저장소 기준 러너 무료(macOS 포함). 비공개 전환 시 macOS 10× 과금 유의. - QA 통과 후 Draft → Publish 수동 승격. +## 5-bis. CI/CD 현행 채널 — Forgejo Actions 자체 러너 (2026-08-17 가동) + +저장소가 자체 Forgejo(`https://git.chanpaca.net/Video-Downloader/client`)로 이관되고 org 러너 3대가 +등록되어, §5(GitHub) 대신 **자체 러너로 전 아티팩트를 빌드·배포**한다. §5 는 저장소 GitHub 공개 시 +백업 채널로 유지(`.github/workflows/release.yml` 존치). + +| 러너 | 머신 | 라벨(모두 host 모드) | 담당 잡 | +|---|---|---|---| +| `yunchan-win` | ALPACA-HOME (Windows 11, 개발 PC) | `windows`, `windows-amd64` | test 관문 · Windows 설치자(x64/arm64) · Android APK | +| `kd-desktop` | ChanpacaLaptop (§0.5 `vd-linux`) | `linux-builder` | linux-x64/arm64 tar.gz | +| `yunchan-mac` | Mac mini M2 (§0.5 `vd-mac`) | `macos`, `macos-arm64` | osx-arm64/x64 tar.gz | + +- **워크플로**: `.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 오염 방지 처리 포함. +- **리허설**: workflow_dispatch → `ci-rehearsal` draft prerelease 로 태그 없이 전 파이프라인 검증. +- **시크릿**: `RELEASE_TOKEN`(Forgejo PAT, `write:repository`) — 릴리스 생성·자산 업로드. 체크아웃은 잡 + 자동 토큰(`github.token`). `ANDROID_KEYSTORE/ALIAS/PASS` 등록 전까지 APK 는 디버그 서명. +- **머신 전제**: win = SDK 8/10 + `maui-android` 워크로드 + JDK 17 + Inno Setup 6 · + linux/mac = `~/.dotnet` SDK 8.0.424 + 10.0.400 (§0.5). +- **갭**: §6 자동 업데이트 v1(`UpdateChecker`)은 GitHub Releases API 를 폴링하므로 GitHub 공개 전까지 + 미동작 — Forgejo Releases API(`/repos/{owner}/{repo}/releases/latest`, 동일 형태) 대응은 후속 항목. + ## 6. 업데이트 체계 | 대상 | 방식 |