Switch from Gitea Actions to local release.ps1
- Remove .gitea/workflows + .github/workflows (Runner setup unnecessary for personal use) - Add release.ps1: one-shot local build + push + Release publish + asset upload - Update README/CHANGELOG to document new release flow Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bedbb1e9ec
commit
5a44ca0492
@ -1,38 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov
|
||||
|
||||
- name: pytest unit tests
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
run: pytest tests -v
|
||||
|
||||
- name: integration tests
|
||||
run: python _integration_test.py
|
||||
|
||||
- name: i18n GUI tests
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
run: python _i18n_gui_test.py
|
||||
@ -1,84 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # v1.0.0, v2.1.0 등 태그 push 시 트리거
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install dependencies + PyInstaller
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: Run unit tests
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
run: |
|
||||
pip install pytest
|
||||
pytest tests -q
|
||||
|
||||
- name: Run integration tests
|
||||
run: python _integration_test.py
|
||||
|
||||
- name: Build main.exe
|
||||
run: python -m PyInstaller --clean main.spec
|
||||
|
||||
- name: Build updater.exe
|
||||
run: python -m PyInstaller --clean updater.spec
|
||||
|
||||
- name: Verify both exe exist
|
||||
run: |
|
||||
if (-not (Test-Path dist/main.exe)) { throw "main.exe missing" }
|
||||
if (-not (Test-Path dist/updater.exe)) { throw "updater.exe missing" }
|
||||
|
||||
- name: Create release archive
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path dist/release -Force
|
||||
Copy-Item dist/main.exe dist/release/
|
||||
Copy-Item dist/updater.exe dist/release/
|
||||
Compress-Archive -Path dist/release/* -DestinationPath dist/ClockOutCalculator.zip -Force
|
||||
|
||||
- name: Publish Release (Gitea)
|
||||
uses: actions/release-action@main
|
||||
with:
|
||||
api_key: ${{ secrets.RELEASE_TOKEN }}
|
||||
files: |
|
||||
dist/main.exe
|
||||
dist/updater.exe
|
||||
dist/ClockOutCalculator.zip
|
||||
|
||||
# GitHub Actions 호환 fallback (Gitea Actions에서 동작 안 할 경우)
|
||||
# 위 release-action이 실패하면 아래를 활용:
|
||||
#
|
||||
# - name: Publish Release (manual via API)
|
||||
# shell: pwsh
|
||||
# env:
|
||||
# GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
# GITEA_HOST: https://kindnick-git.duckdns.org
|
||||
# OWNER: kindnick
|
||||
# REPO: Clock_out_Time_Calculator
|
||||
# run: |
|
||||
# $tag = $env:GITHUB_REF -replace 'refs/tags/', ''
|
||||
# $body = @{ tag_name = $tag; name = $tag; draft = $false } | ConvertTo-Json
|
||||
# $headers = @{ Authorization = "token $env:GITEA_TOKEN" }
|
||||
# $release = Invoke-RestMethod -Uri "$env:GITEA_HOST/api/v1/repos/$env:OWNER/$env:REPO/releases" `
|
||||
# -Method Post -Headers $headers -ContentType 'application/json' -Body $body
|
||||
# $uploadUrl = "$env:GITEA_HOST/api/v1/repos/$env:OWNER/$env:REPO/releases/$($release.id)/assets"
|
||||
# foreach ($f in @('dist/main.exe', 'dist/updater.exe')) {
|
||||
# $name = [System.IO.Path]::GetFileName($f)
|
||||
# Invoke-RestMethod -Uri "$uploadUrl?name=$name" -Method Post `
|
||||
# -Headers $headers -InFile $f -ContentType 'application/octet-stream'
|
||||
# }
|
||||
76
.github/workflows/ci.yml
vendored
76
.github/workflows/ci.yml
vendored
@ -1,76 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov
|
||||
|
||||
- name: Run integration tests
|
||||
run: python _integration_test.py
|
||||
|
||||
- name: Run i18n GUI tests (offscreen)
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
run: python _i18n_gui_test.py
|
||||
|
||||
- name: Run pytest (if tests/ exists)
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
run: |
|
||||
if (Test-Path tests) { pytest tests --cov=core --cov=utils }
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: windows-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install dependencies + PyInstaller
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: Build exe
|
||||
run: python -m PyInstaller --clean main.spec
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ClockOutCalculator-exe
|
||||
path: dist/main.exe
|
||||
retention-days: 14
|
||||
@ -15,8 +15,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- **`core/version.py`** — `__version__` 상수
|
||||
- **`updater.py` + `updater.spec`** — 독립 자가 업데이터 (Python 표준 라이브러리만, 6MB)
|
||||
- **`utils/updater_client.py`** — Gitea/GitHub 호환 Releases API 클라이언트
|
||||
- **Gitea Actions 워크플로** — `.gitea/workflows/{ci,release}.yml`
|
||||
- `v*` 태그 push 시 두 .exe 자동 빌드 + Releases 첨부 + ZIP 패키징
|
||||
- **`release.ps1`** — 로컬 원클릭 릴리스 스크립트 (Runner 불필요)
|
||||
- 빌드 + 태그 push + Gitea Release 생성 + 자산 업로드 자동화
|
||||
- CHANGELOG에서 릴리스 노트 자동 추출
|
||||
- `--DryRun` / `--SkipTests` 옵션
|
||||
|
||||
### Changed
|
||||
- `Settings → 데이터 관리`에 "버전 표시 + 업데이트 확인" 추가
|
||||
|
||||
34
README.md
34
README.md
@ -141,22 +141,32 @@ python -m PyInstaller --clean updater.spec # → dist/updater.exe (~6MB)
|
||||
배포 시 두 .exe를 같은 폴더에 둬야 자동 업데이트가 동작합니다. 빌드 시
|
||||
`dist/main.exe`가 실행 중이면 PermissionError가 발생 — 종료 후 재실행하세요.
|
||||
|
||||
## 릴리스 (Gitea Actions)
|
||||
## 릴리스 ([release.ps1](release.ps1))
|
||||
|
||||
태그 push로 자동 릴리스:
|
||||
```bash
|
||||
# version.py 업데이트 후
|
||||
git tag v2.2.0
|
||||
git push origin v2.2.0
|
||||
로컬에서 한 줄로 빌드 → 태그 push → Gitea Release 생성 → 자산 업로드까지:
|
||||
|
||||
```powershell
|
||||
# 최초 1회: PAT 환경변수 등록
|
||||
[Environment]::SetEnvironmentVariable('GITEA_TOKEN', 'your_pat', 'User')
|
||||
|
||||
# 릴리스 실행
|
||||
.\release.ps1 v2.2.0
|
||||
```
|
||||
|
||||
[.gitea/workflows/release.yml](.gitea/workflows/release.yml)이 자동으로:
|
||||
1. 단위/통합 테스트 실행
|
||||
2. main.exe + updater.exe 빌드
|
||||
3. ZIP 패키징
|
||||
4. Gitea Releases에 첨부
|
||||
스크립트 동작:
|
||||
1. `core/version.py` 업데이트
|
||||
2. pytest + 통합 테스트 실행
|
||||
3. main.exe + updater.exe 빌드
|
||||
4. ZIP 패키징
|
||||
5. git commit + tag + push
|
||||
6. Gitea API로 Release 생성 (CHANGELOG.md에서 노트 자동 추출)
|
||||
7. 두 .exe + ZIP을 Release 자산으로 업로드
|
||||
|
||||
⚠️ Gitea Actions 활성화 + `RELEASE_TOKEN` secret(저장소 쓰기 권한) 등록 필요.
|
||||
옵션:
|
||||
- `--SkipTests`: 테스트 건너뛰기 (긴급 핫픽스)
|
||||
- `--DryRun`: 실제 push/release 없이 미리보기
|
||||
|
||||
PAT 발급: Gitea → Settings → Applications → Generate New Token (권한: `repository: Read and Write`).
|
||||
|
||||
## 주의사항
|
||||
|
||||
|
||||
226
release.ps1
Normal file
226
release.ps1
Normal file
@ -0,0 +1,226 @@
|
||||
<#
|
||||
.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
|
||||
$notes = "Release $Version"
|
||||
if (Test-Path CHANGELOG.md) {
|
||||
$changelog = Get-Content CHANGELOG.md -Raw
|
||||
$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
|
||||
Loading…
x
Reference in New Issue
Block a user