PowerShell 5.1 Get-Content defaults to system ANSI (cp949 in KR locale), which corrupted UTF-8 Korean text in release notes. Use .NET API [System.IO.File]::ReadAllText with explicit UTF-8 encoding instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
229 lines
7.5 KiB
PowerShell
229 lines
7.5 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Local build + Gitea Releases auto-publish.
|
|
|
|
.DESCRIPTION
|
|
One-shot release: version bump -> tests -> build -> tag push -> Release -> upload.
|
|
No Runner needed. Run on your local PC.
|
|
|
|
.PARAMETER Version
|
|
Tag in 'v2.2.0' format.
|
|
|
|
.PARAMETER SkipTests
|
|
Skip pytest + integration (emergency hotfix only).
|
|
|
|
.PARAMETER DryRun
|
|
Preview without pushing or creating release.
|
|
|
|
.EXAMPLE
|
|
# First time only: register PAT as env var
|
|
[Environment]::SetEnvironmentVariable('GITEA_TOKEN', 'your_pat', 'User')
|
|
|
|
.\release.ps1 v2.2.0
|
|
|
|
.NOTES
|
|
- Kill any running main.exe before running.
|
|
- PAT scope: repository (read+write).
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory=$true, Position=0)]
|
|
[ValidatePattern('^v\d+\.\d+\.\d+$')]
|
|
[string]$Version,
|
|
|
|
[switch]$SkipTests,
|
|
[switch]$DryRun
|
|
)
|
|
|
|
# PowerShell 5.1: native command stderr triggers NativeCommandError under 'Stop'.
|
|
# Use 'Continue' and explicitly check $LASTEXITCODE after each native call.
|
|
$ErrorActionPreference = 'Continue'
|
|
|
|
function Invoke-Native {
|
|
param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Args)
|
|
& $Args[0] $Args[1..($Args.Count-1)] 2>&1 | Out-Host
|
|
return $LASTEXITCODE
|
|
}
|
|
|
|
# ====== Config ======
|
|
$GiteaHost = 'https://kindnick-git.duckdns.org'
|
|
$Owner = 'kindnick'
|
|
$Repo = 'Clock_out_Time_Calculator'
|
|
$ApiBase = "$GiteaHost/api/v1/repos/$Owner/$Repo"
|
|
$VersionRaw = $Version.TrimStart('v')
|
|
|
|
function Step($msg) { Write-Host "`n=== $msg ===" -ForegroundColor Cyan }
|
|
function Info($msg) { Write-Host " $msg" -ForegroundColor Gray }
|
|
function OkMsg($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
|
|
function Fail($msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red; exit 1 }
|
|
|
|
# ====== 0. Pre-checks ======
|
|
Step "Pre-checks"
|
|
|
|
if (-not $env:GITEA_TOKEN) {
|
|
Fail "GITEA_TOKEN env var missing. Set it first: `$env:GITEA_TOKEN = 'your_pat'"
|
|
}
|
|
|
|
$running = Get-Process -Name 'main' -ErrorAction SilentlyContinue
|
|
if ($running) {
|
|
Fail "main.exe is running (PID: $($running.Id -join ', ')). Kill it first."
|
|
}
|
|
|
|
$gitStatus = git status --porcelain
|
|
if ($gitStatus -and -not $DryRun) {
|
|
Write-Host " WARN: uncommitted changes:" -ForegroundColor Yellow
|
|
$gitStatus | ForEach-Object { Write-Host " $_" }
|
|
$confirm = Read-Host " Continue anyway? (y/N)"
|
|
if ($confirm -ne 'y') { Fail "User cancelled" }
|
|
}
|
|
|
|
$existingTag = git tag -l $Version
|
|
if ($existingTag) {
|
|
Fail "Tag $Version already exists. Bump version.py/CHANGELOG and use a new tag."
|
|
}
|
|
|
|
OkMsg "All checks passed (Version: $Version)"
|
|
|
|
# ====== 1. Bump version.py ======
|
|
Step "1/7 Bump core/version.py"
|
|
$verFile = 'core/version.py'
|
|
$verContent = Get-Content $verFile -Raw
|
|
$newContent = $verContent -replace "__version__ = '[^']+'", "__version__ = '$VersionRaw'"
|
|
if ($verContent -eq $newContent) {
|
|
Info "Already at $VersionRaw (no change)"
|
|
} else {
|
|
if (-not $DryRun) { Set-Content $verFile -Value $newContent -NoNewline }
|
|
OkMsg "$verFile -> $VersionRaw"
|
|
}
|
|
|
|
# ====== 2. Tests ======
|
|
if (-not $SkipTests) {
|
|
Step "2/7 Run tests"
|
|
Info "pytest unit tests..."
|
|
$rc = Invoke-Native python -m pytest tests -q
|
|
if ($rc -ne 0) { Fail "pytest failed (exit $rc)" }
|
|
|
|
Info "integration scenarios..."
|
|
$rc = Invoke-Native python _integration_test.py
|
|
if ($rc -ne 0) { Fail "integration tests failed (exit $rc)" }
|
|
|
|
OkMsg "All tests passed"
|
|
} else {
|
|
Step "2/7 Skipping tests (--SkipTests)"
|
|
}
|
|
|
|
# ====== 3. Build ======
|
|
Step "3/7 PyInstaller build"
|
|
|
|
Info "Building main.exe..."
|
|
$rc = Invoke-Native python -m PyInstaller --clean main.spec
|
|
if ($rc -ne 0) { Fail "main.exe build failed (exit $rc)" }
|
|
if (-not (Test-Path 'dist/main.exe')) { Fail "dist/main.exe missing" }
|
|
|
|
Info "Building updater.exe..."
|
|
$rc = Invoke-Native python -m PyInstaller --clean updater.spec
|
|
if ($rc -ne 0) { Fail "updater.exe build failed (exit $rc)" }
|
|
if (-not (Test-Path 'dist/updater.exe')) { Fail "dist/updater.exe missing" }
|
|
|
|
$mainSize = "{0:N1}MB" -f ((Get-Item dist/main.exe).Length / 1MB)
|
|
$updaterSize = "{0:N1}MB" -f ((Get-Item dist/updater.exe).Length / 1MB)
|
|
OkMsg "main.exe ($mainSize) + updater.exe ($updaterSize)"
|
|
|
|
# ====== 4. ZIP ======
|
|
Step "4/7 ZIP packaging"
|
|
$zipPath = "dist/ClockOutCalculator-$Version.zip"
|
|
if (Test-Path $zipPath) { Remove-Item $zipPath }
|
|
Compress-Archive -Path 'dist/main.exe', 'dist/updater.exe' -DestinationPath $zipPath
|
|
$zipSize = "{0:N1}MB" -f ((Get-Item $zipPath).Length / 1MB)
|
|
OkMsg "$zipPath ($zipSize)"
|
|
|
|
# ====== 5. Git commit + tag + push ======
|
|
Step "5/7 Git commit + tag + push"
|
|
|
|
if ($DryRun) {
|
|
Info "DryRun mode - skipping git ops"
|
|
} else {
|
|
git diff --quiet $verFile
|
|
$needsCommit = $LASTEXITCODE -ne 0
|
|
if ($needsCommit) {
|
|
$rc = Invoke-Native git add $verFile CHANGELOG.md
|
|
$rc = Invoke-Native git commit -m "Release $Version"
|
|
if ($rc -ne 0) { Fail "git commit failed (exit $rc)" }
|
|
OkMsg "Committed version bump"
|
|
}
|
|
|
|
$rc = Invoke-Native git tag $Version
|
|
if ($rc -ne 0) { Fail "git tag failed (exit $rc)" }
|
|
|
|
Info "Pushing main + tag..."
|
|
$rc = Invoke-Native git push origin main
|
|
if ($rc -ne 0) { Fail "git push main failed (exit $rc)" }
|
|
$rc = Invoke-Native git push origin $Version
|
|
if ($rc -ne 0) { Fail "git push tag failed (exit $rc)" }
|
|
OkMsg "Pushed main + $Version"
|
|
}
|
|
|
|
# ====== 6. Create Gitea Release ======
|
|
Step "6/7 Create Gitea Release"
|
|
|
|
if ($DryRun) {
|
|
Info "DryRun mode - skipping API call"
|
|
Info "Would POST $ApiBase/releases (tag=$Version)"
|
|
exit 0
|
|
}
|
|
|
|
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
|
|
|
|
# Extract notes from CHANGELOG.md
|
|
# IMPORTANT: PowerShell 5.1 Get-Content defaults to ANSI; use .NET API for UTF-8 (Korean).
|
|
$notes = "Release $Version"
|
|
if (Test-Path CHANGELOG.md) {
|
|
$changelogPath = (Resolve-Path CHANGELOG.md).Path
|
|
$changelog = [System.IO.File]::ReadAllText($changelogPath, [System.Text.Encoding]::UTF8)
|
|
$pattern = "## \[$([regex]::Escape($VersionRaw))\][\s\S]*?(?=`n## \[|\z)"
|
|
$regexMatch = [regex]::Match($changelog, $pattern)
|
|
if ($regexMatch.Success) { $notes = $regexMatch.Value.Trim() }
|
|
}
|
|
|
|
$bodyObj = @{
|
|
tag_name = $Version
|
|
target_commitish = 'main'
|
|
name = $Version
|
|
body = $notes
|
|
draft = $false
|
|
prerelease = $false
|
|
}
|
|
$bodyJson = $bodyObj | ConvertTo-Json -Compress
|
|
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($bodyJson)
|
|
|
|
try {
|
|
$release = Invoke-RestMethod -Uri "$ApiBase/releases" -Method Post -Headers $headers -ContentType 'application/json' -Body $bodyBytes
|
|
OkMsg "Release created (id=$($release.id))"
|
|
} catch {
|
|
Fail "Release creation failed: $($_.Exception.Message)"
|
|
}
|
|
|
|
# ====== 7. Upload assets ======
|
|
Step "7/7 Upload assets"
|
|
|
|
$assets = @('dist/main.exe', 'dist/updater.exe', $zipPath)
|
|
foreach ($f in $assets) {
|
|
if (-not (Test-Path $f)) { Info "Skip (missing): $f"; continue }
|
|
$name = Split-Path $f -Leaf
|
|
Info "Uploading $name..."
|
|
try {
|
|
$uploadUrl = "$ApiBase/releases/$($release.id)/assets?name=$name"
|
|
$null = Invoke-RestMethod -Uri $uploadUrl -Method Post -Headers $headers -ContentType 'application/octet-stream' -InFile $f
|
|
OkMsg $name
|
|
} catch {
|
|
Fail "Upload failed ($name): $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "========================================" -ForegroundColor Green
|
|
Write-Host " Release $Version published!" -ForegroundColor Green
|
|
Write-Host "========================================" -ForegroundColor Green
|
|
Write-Host " URL: $GiteaHost/$Owner/$Repo/releases/tag/$Version" -ForegroundColor White
|
|
Write-Host ""
|
|
Write-Host " Users will see the update on next app launch." -ForegroundColor Gray
|