name: windows-exe-build on: push: tags: - "v*" jobs: build: runs-on: [window, pyinstaller] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Python venv shell: powershell run: | $ErrorActionPreference = "Stop" python -m venv .venv .\.venv\Scripts\python -m pip install --upgrade pip if (Test-Path requirements.txt) { .\.venv\Scripts\pip install -r requirements.txt } .\.venv\Scripts\pip install pyinstaller - name: Build (onedir) with optional --add-data shell: powershell env: APP: ${{ vars.APP_NAME }} ENTRY: ${{ vars.ENTRY_FILE }} EXTRA: ${{ vars.PYINSTALLER_ARGS }} ADD_DATA: ${{ vars.ADD_DATA }} # 예: "config.ini -> .\n dashboard/templates/dashboard -> templates/dashboard" run: | $ErrorActionPreference = "Stop" if ([string]::IsNullOrWhiteSpace($env:APP)) { throw "Missing var: APP_NAME" } if ([string]::IsNullOrWhiteSpace($env:ENTRY)) { throw "Missing var: ENTRY_FILE" } # ------------------------- # 1) EXTRA (단순 split) # ------------------------- $extraArgs = @() if (-not [string]::IsNullOrWhiteSpace($env:EXTRA)) { $extraArgs = $env:EXTRA -split "\s+" } # ------------------------- # 2) ADD_DATA: "src -> dest" 를 "--add-data src;dest" 로 변환 (Windows는 ;) # - 비어있으면 아무 것도 추가 안 함 # - 여러 줄/쉼표 구분 모두 허용 # ------------------------- $addArgs = @() if (-not [string]::IsNullOrWhiteSpace($env:ADD_DATA)) { # 줄바꿈/쉼표로 분리 (사용자가 어느 방식으로 넣어도 되게) $rules = $env:ADD_DATA ` -split "(\r?\n|,)" ` | ForEach-Object { $_.Trim() } ` | Where-Object { $_ -and $_ -ne "-" } foreach ($r in $rules) { # 지원 포맷: "src -> dest" 또는 "src=dest" 또는 "src|dest" $src = $null $dst = $null if ($r -match "^\s*(.+?)\s*->\s*(.+?)\s*$") { $src = $Matches[1].Trim() $dst = $Matches[2].Trim() } elseif ($r -match "^\s*(.+?)\s*[=|]\s*(.+?)\s*$") { $src = $Matches[1].Trim() $dst = $Matches[2].Trim() } else { throw "ADD_DATA rule format invalid: '$r' (use 'src -> dest')" } if ([string]::IsNullOrWhiteSpace($src) -or [string]::IsNullOrWhiteSpace($dst)) { throw "ADD_DATA rule has empty src/dest: '$r'" } # PyInstaller Windows delimiter is ';' $pair = "$src;$dst" # 인자 추가: --add-data "src;dest" $addArgs += @("--add-data", $pair) } } Write-Host "APP : $env:APP" Write-Host "ENTRY : $env:ENTRY" Write-Host "EXTRA : $env:EXTRA" Write-Host "ADD_DATA : $env:ADD_DATA" Write-Host "AddArgs : $($addArgs -join ' ')" Write-Host "ExtraArgs: $($extraArgs -join ' ')" .\.venv\Scripts\pyinstaller ` --name "$env:APP" ` @addArgs ` @extraArgs ` "$env:ENTRY" - name: Package zip (onedir dist -> out) shell: powershell env: APP: ${{ vars.APP_NAME }} run: | $ErrorActionPreference = "Stop" if ([string]::IsNullOrWhiteSpace($env:APP)) { throw "Missing var: APP_NAME" } $distDir = Join-Path $PWD "dist\$($env:APP)" if (!(Test-Path $distDir)) { throw "Dist folder not found: $distDir" } New-Item -ItemType Directory -Force -Path "out" | Out-Null $zipName = "$($env:APP)-$($env:GITHUB_REF_NAME).zip" $zipPath = Join-Path $PWD "out\$zipName" if (Test-Path $zipPath) { Remove-Item -Force $zipPath } Compress-Archive -Path "$distDir\*" -DestinationPath $zipPath -Force if (!(Test-Path $zipPath)) { throw "Zip not created: $zipPath" } Write-Host "Created: $zipPath" $zipName | Out-File -FilePath zip_name.txt -Encoding ascii # ------------------------- # Gitea Release 생성/업로드 # ------------------------- - name: Create Gitea release (if not exists) shell: powershell env: TOKEN: ${{ secrets.GITEA_TOKEN }} REPO: ${{ gitea.repository }} TAG: ${{ gitea.ref_name }} run: | $ErrorActionPreference = "Stop" if ([string]::IsNullOrWhiteSpace($env:TOKEN)) { throw "Missing secret: GITEA_TOKEN" } if ([string]::IsNullOrWhiteSpace($env:REPO)) { throw "Missing context: gitea.repository" } if ([string]::IsNullOrWhiteSpace($env:TAG)) { throw "Missing tag name" } $remote = (git config --get remote.origin.url).Trim() if ([string]::IsNullOrWhiteSpace($remote)) { throw "Cannot determine remote.origin.url" } $baseUrl = $null if ($remote -match '^(https?)://') { $u = [uri]$remote $path = $u.AbsolutePath -replace '\.git$','' $basePath = ($path -replace '/[^/]+/[^/]+$','') if ([string]::IsNullOrWhiteSpace($basePath) -or $basePath -eq '/') { $baseUrl = "{0}://{1}" -f $u.Scheme, $u.Authority } else { $baseUrl = "{0}://{1}{2}" -f $u.Scheme, $u.Authority, $basePath.TrimEnd('/') } } elseif ($remote -match '^(?[^@]+)@(?[^:]+):(?.+)$') { $host = $Matches.host $path = ($Matches.path -replace '\.git$','') -replace '\\','/' $basePath = ($path -replace '/[^/]+/[^/]+$','') $basePath = $basePath.Trim('/') if ([string]::IsNullOrWhiteSpace($basePath)) { $baseUrl = "https://$host" } else { $baseUrl = "https://$host/$basePath" } } else { throw "Unsupported remote URL format: $remote" } Write-Host "Derived Gitea baseUrl: $baseUrl" $apiBase = "$($baseUrl.TrimEnd('/'))/api/v1" $headers = @{ Authorization = "token $env:TOKEN" } $rel = $null try { $rel = Invoke-RestMethod -Method Get -Headers $headers -Uri "$apiBase/repos/$env:REPO/releases/tags/$env:TAG" } catch { $rel = $null } if (-not $rel) { $body = @{ tag_name = $env:TAG name = $env:TAG body = "Automated build for $($env:TAG)" draft = $false prerelease = $false } | ConvertTo-Json -Depth 5 $rel = Invoke-RestMethod -Method Post -Headers $headers -ContentType "application/json" -Body $body -Uri "$apiBase/repos/$env:REPO/releases" } if (-not $rel.id) { throw "Failed to get/create release id" } ($rel.id.ToString().Trim()) | Out-File -FilePath release_id.txt -Encoding ascii Write-Host "Release id: $($rel.id)" - name: Upload release asset (zip) - replace if exists shell: powershell env: TOKEN: ${{ secrets.GITEA_TOKEN }} REPO: ${{ gitea.repository }} run: | $ErrorActionPreference = "Stop" if ([string]::IsNullOrWhiteSpace($env:TOKEN)) { throw "Missing secret: GITEA_TOKEN" } if ([string]::IsNullOrWhiteSpace($env:REPO)) { throw "Missing context: gitea.repository" } if (!(Test-Path "release_id.txt")) { throw "release_id.txt not found (Create release step failed?)" } if (!(Test-Path "zip_name.txt")) { throw "zip_name.txt not found (Zip step failed?)" } $releaseId = (Get-Content release_id.txt | Out-String).Trim() $zipName = (Get-Content zip_name.txt | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($releaseId)) { throw "releaseId is empty after reading release_id.txt" } if ([string]::IsNullOrWhiteSpace($zipName)) { throw "zipName is empty after reading zip_name.txt" } if (!(Test-Path "out\$zipName")) { throw "Zip file not found: out\$zipName" } $remote = (git config --get remote.origin.url).Trim() if ([string]::IsNullOrWhiteSpace($remote)) { throw "Cannot determine remote.origin.url" } $baseUrl = $null if ($remote -match '^(https?)://') { $u = [uri]$remote $path = $u.AbsolutePath -replace '\.git$','' $basePath = ($path -replace '/[^/]+/[^/]+$','') if ([string]::IsNullOrWhiteSpace($basePath) -or $basePath -eq '/') { $baseUrl = "{0}://{1}" -f $u.Scheme, $u.Authority } else { $baseUrl = "{0}://{1}{2}" -f $u.Scheme, $u.Authority, $basePath.TrimEnd('/') } } elseif ($remote -match '^(?[^@]+)@(?[^:]+):(?.+)$') { $host = $Matches.host $path = ($Matches.path -replace '\.git$','') -replace '\\','/' $basePath = ($path -replace '/[^/]+/[^/]+$','') $basePath = $basePath.Trim('/') if ([string]::IsNullOrWhiteSpace($basePath)) { $baseUrl = "https://$host" } else { $baseUrl = "https://$host/$basePath" } } else { throw "Unsupported remote URL format: $remote" } Write-Host "Derived Gitea baseUrl: $baseUrl" $apiBase = "$($baseUrl.TrimEnd('/'))/api/v1" $headers = @{ Authorization = "token $env:TOKEN" } $assets = Invoke-RestMethod -Method Get -Headers $headers -Uri "$apiBase/repos/$env:REPO/releases/$releaseId/assets" foreach ($a in $assets) { if ($a.name -eq $zipName) { Write-Host "Deleting existing asset: $zipName (id=$($a.id))" Invoke-RestMethod -Method Delete -Headers $headers -Uri "$apiBase/repos/$env:REPO/releases/$releaseId/assets/$($a.id)" } } $escaped = [uri]::EscapeDataString($zipName) $uploadUrl = "{0}/repos/{1}/releases/{2}/assets?name={3}" -f ` $apiBase.TrimEnd('/'), $env:REPO, $releaseId, $escaped Write-Host ("Uploading to: [{0}]" -f $uploadUrl) & curl.exe -L --fail -X POST ` -H "Authorization: token $env:TOKEN" ` -H "Content-Type: application/octet-stream" ` --data-binary "@out/$zipName" ` "$uploadUrl" if ($LASTEXITCODE -ne 0) { throw "curl upload failed: $zipName (exit=$LASTEXITCODE)" } # ------------------------- # Nextcloud(WebDAV) 업로드: 버전 폴더 + latest # ------------------------- - name: Upload zip to Nextcloud Releases (WebDAV) - version folder + latest shell: powershell env: NC_USER: ${{ secrets.NEXTCLOUD_USER }} NC_PASS: ${{ secrets.NEXTCLOUD_PASS }} NC_BASE: ${{ secrets.NEXTCLOUD_BASE }} # .../Releases APP: ${{ vars.APP_NAME }} # pop_dis TAG: ${{ gitea.ref_name }} # v2.5.10 run: | $ErrorActionPreference = "Stop" if ([string]::IsNullOrWhiteSpace($env:NC_USER)) { throw "Missing secret: NEXTCLOUD_USER" } if ([string]::IsNullOrWhiteSpace($env:NC_PASS)) { throw "Missing secret: NEXTCLOUD_PASS" } if ([string]::IsNullOrWhiteSpace($env:NC_BASE)) { throw "Missing secret: NEXTCLOUD_BASE" } if ([string]::IsNullOrWhiteSpace($env:APP)) { throw "Missing var: APP_NAME" } if ([string]::IsNullOrWhiteSpace($env:TAG)) { throw "Missing tag name" } if (!(Test-Path "zip_name.txt")) { throw "zip_name.txt not found (Zip step failed?)" } $zipName = (Get-Content zip_name.txt | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($zipName)) { throw "zipName is empty after reading zip_name.txt" } if (!(Test-Path "out\$zipName")) { throw "Zip file not found: out\$zipName" } $base = $env:NC_BASE.TrimEnd('/') $appPath = $env:APP.Trim('/') $tagPath = $env:TAG.Trim('/') $verDirUrl = "$base/$appPath/$tagPath" $latestDirUrl = "$base/$appPath/latest" $latestName = "$($env:APP).zip" $verFileUrl = "$verDirUrl/$zipName" $latestUrl = "$latestDirUrl/$latestName" Write-Host "Nextcloud base : $base" Write-Host "Version dir URL : $verDirUrl" Write-Host "Latest dir URL : $latestDirUrl" Write-Host "Version file URL : $verFileUrl" Write-Host "Latest file URL : $latestUrl" function MkcolIgnoreFail([string]$url) { & curl.exe -L -sS -o NUL -w "%{http_code}" -X MKCOL ` -u "$($env:NC_USER):$($env:NC_PASS)" ` "$url" | ForEach-Object { Write-Host "MKCOL $url -> HTTP $_" } } MkcolIgnoreFail "$base/$appPath" MkcolIgnoreFail "$base/$appPath/$tagPath" MkcolIgnoreFail "$base/$appPath/latest" Write-Host "Uploading versioned zip -> $verFileUrl" & curl.exe -L --fail -X PUT ` -u "$($env:NC_USER):$($env:NC_PASS)" ` --data-binary "@out/$zipName" ` "$verFileUrl" if ($LASTEXITCODE -ne 0) { throw "Nextcloud upload failed (versioned) (exit=$LASTEXITCODE)" } Write-Host "Uploading latest zip -> $latestUrl" & curl.exe -L --fail -X PUT ` -u "$($env:NC_USER):$($env:NC_PASS)" ` --data-binary "@out/$zipName" ` "$latestUrl" if ($LASTEXITCODE -ne 0) { throw "Nextcloud upload failed (latest) (exit=$LASTEXITCODE)" }