feat: auto-update, тесты и CI/CD
Some checks failed
CI / Typecheck + Tests (push) Has been cancelled
CI / Build (Windows) (push) Has been cancelled

Полная автоматизация релизного цикла.

== Auto-update (electron-updater) ==
- src/main/updater.ts — обёртка над autoUpdater с дискриминированным
  UpdaterStatus union и broadcast через IPC. autoDownload=false,
  пользователь сам жмёт «Скачать». allowDowngrade=false. Проверка
  каждые 6 часов, первая через 5с после старта.
- В dev-режиме (app.isPackaged=false) статус сразу становится
  'unsupported' с пояснением — никаких exceptions из updater'а.
- build.publish в package.json: provider=generic, url указывает на
  Gitea release assets конкретной версии.
- src/main/ipc.ts: 4 новых канала — status/check/download/install.
- src/preload: API window.api.updater* + onUpdaterStatus.
- src/renderer/src/components/UpdaterCard.tsx: HUD-карточка в Settings
  с состояниями idle/checking/available/downloading/downloaded/error,
  прогресс-бар с скоростью в МБ/с.

== Тесты (vitest) ==
- vitest.config.ts с алиасами @shared / @renderer
- 23 теста, все зелёные:
  * format.test.ts — formatCountdown, formatInterval (8 cases)
  * vdf.test.ts — parseVdf / stringifyVdf / round-trip (11 cases)
  * types.test.ts — DEFAULT_SETTINGS, SAMPLE_EXERCISES sanity (4)
- npm scripts: test (watch), test:run (CI)

== CI/CD (Gitea Actions) ==
- .gitea/workflows/ci.yml — на push/PR: typecheck + тесты + smoke-сборка
- .gitea/workflows/release.yml — на тег v*.*.*: сборка NSIS + Gitea release

== Локальный релизный скрипт ==
- scripts/release.ps1 — один скрипт от бампа версии до публикации
  через Gitea API (params: -Bump patch/minor/major, -Version, -DryRun)
- npm run release — обёртка
- RELEASING.md — полная инструкция

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
AnRil
2026-05-16 20:32:59 +07:00
parent 757352e447
commit 92e15e69a3
16 changed files with 1149 additions and 3 deletions

196
scripts/release.ps1 Normal file
View File

@@ -0,0 +1,196 @@
<#
.SYNOPSIS
Локальный релиз: бамп версии → коммит → тег → push → сборка → upload в Gitea release.
.DESCRIPTION
Один скрипт от и до. Если Gitea Actions не настроено, это рабочая альтернатива.
.PARAMETER Bump
Какую часть semver инкрементировать: patch (по умолчанию), minor, major.
Альтернатива — указать -Version явно.
.PARAMETER Version
Точная версия (напр. "0.3.0"). Если задана, -Bump игнорируется.
.PARAMETER SkipBuild
Пропустить сборку (если уже собрано вручную, .exe лежит в release/).
.PARAMETER DryRun
Показать что произойдёт, но ничего не делать.
.EXAMPLE
pwsh scripts/release.ps1 -Bump minor
pwsh scripts/release.ps1 -Version 0.3.0
pwsh scripts/release.ps1 -Bump patch -DryRun
.NOTES
Требует переменную окружения GITEA_TOKEN с правом write:repository
(создаётся в Gitea: Settings → Applications → Generate New Token).
#>
param(
[ValidateSet('patch', 'minor', 'major')]
[string]$Bump = 'patch',
[string]$Version,
[switch]$SkipBuild,
[switch]$DryRun
)
$ErrorActionPreference = 'Stop'
# --- Config ---------------------------------------------------------------
$repoOwner = 'AnRil'
$repoName = 'laude'
$giteaHost = 'xn--90adajar8af4h.xn--p1ai/git'
$apiBase = "https://$giteaHost/api/v1"
# --- Pre-flight checks ---------------------------------------------------
$root = Resolve-Path (Join-Path $PSScriptRoot '..')
Set-Location $root
if (-not $env:GITEA_TOKEN -and -not $DryRun) {
Write-Error 'GITEA_TOKEN не задан. Создай в Gitea Settings → Applications и export GITEA_TOKEN=...'
exit 1
}
$status = git status --porcelain
if ($status) {
Write-Error "Есть незакоммиченные изменения. Сначала закоммить или stash."
exit 1
}
$branch = git rev-parse --abbrev-ref HEAD
if ($branch -ne 'main') {
Write-Warning "Текущая ветка не main, а $branch. Продолжить? (Ctrl+C для отмены)"
Read-Host 'Press Enter'
}
# --- Compute next version ------------------------------------------------
$pkg = Get-Content package.json | ConvertFrom-Json
$current = $pkg.version
if ($Version) {
$next = $Version
} else {
$parts = $current.Split('.')
$major = [int]$parts[0]; $minor = [int]$parts[1]; $patch = [int]$parts[2]
switch ($Bump) {
'major' { $major++; $minor = 0; $patch = 0 }
'minor' { $minor++; $patch = 0 }
'patch' { $patch++ }
}
$next = "$major.$minor.$patch"
}
$tag = "v$next"
Write-Host ""
Write-Host "→ Release plan" -ForegroundColor Cyan
Write-Host " current : v$current"
Write-Host " next : $tag"
Write-Host " bump : $Bump"
Write-Host ""
if ($DryRun) {
Write-Host '(dry run — exiting)' -ForegroundColor Yellow
exit 0
}
# --- Bump version in package.json ---------------------------------------
Write-Host "→ Bumping package.json to $next" -ForegroundColor Cyan
$pkgJson = (Get-Content package.json -Raw) -replace "`"version`":\s*`"$current`"", "`"version`": `"$next`""
Set-Content -Path package.json -Value $pkgJson -NoNewline -Encoding utf8
git add package.json
git commit -m "chore(release): $tag"
# --- Build (typecheck + tests + dist) ------------------------------------
if (-not $SkipBuild) {
Write-Host "→ Running typecheck…" -ForegroundColor Cyan
npm run typecheck
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "→ Running tests…" -ForegroundColor Cyan
npm run test:run
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "→ Building installer (npm run dist)…" -ForegroundColor Cyan
npm run dist
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
# --- Verify artifacts exist ---------------------------------------------
$installer = Join-Path 'release' "Exercise-Reminder-Setup-$next.exe"
$blockmap = "$installer.blockmap"
$manifest = Join-Path 'release' 'latest.yml'
foreach ($f in @($installer, $blockmap, $manifest)) {
if (-not (Test-Path $f)) {
Write-Error "Не найден артефакт: $f"
exit 1
}
}
# --- Tag + push ----------------------------------------------------------
Write-Host "→ Tagging $tag and pushing…" -ForegroundColor Cyan
git tag -a $tag -m "Release $tag"
git push origin main
git push origin $tag
# --- Create release via Gitea API ----------------------------------------
Write-Host "→ Creating Gitea release $tag" -ForegroundColor Cyan
$headers = @{
Authorization = "token $env:GITEA_TOKEN"
Accept = 'application/json'
}
# Release notes from commits since previous tag
$prev = git describe --tags --abbrev=0 "$tag^" 2>$null
if ($prev) {
$log = git log --pretty=format:"- %s" "$prev..$tag" | Out-String
} else {
$log = git log --pretty=format:"- %s" "$tag" | Out-String
}
$body = @"
### Изменения
$log
---
**Установщик ниже** запустить и следовать мастеру. Если приложение уже стояло обновится поверх, настройки сохранятся.
"@
$releaseBody = @{
tag_name = $tag
name = "Exercise Reminder $tag"
body = $body
draft = $false
prerelease = $false
} | ConvertTo-Json -Depth 5
$release = Invoke-RestMethod `
-Uri "$apiBase/repos/$repoOwner/$repoName/releases" `
-Method Post `
-Headers $headers `
-Body $releaseBody `
-ContentType 'application/json'
Write-Host " Release id: $($release.id)" -ForegroundColor DarkGray
# --- Upload assets -------------------------------------------------------
foreach ($asset in @($installer, $blockmap, $manifest)) {
$name = Split-Path $asset -Leaf
Write-Host "→ Uploading $name" -ForegroundColor Cyan
$uri = "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($name))"
Invoke-RestMethod `
-Uri $uri `
-Method Post `
-Headers $headers `
-InFile $asset `
-ContentType 'application/octet-stream' | Out-Null
}
$releaseUrl = "https://$giteaHost/$repoOwner/$repoName/releases/tag/$tag"
Write-Host ""
Write-Host "Release published" -ForegroundColor Green
Write-Host " $releaseUrl"
Write-Host ""
Write-Host "Auto-updater подхватит обновление на установленных копиях в течение ~6 часов."