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

43
src/shared/types.test.ts Normal file
View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest'
import {
DEFAULT_SETTINGS,
GAME_STATS,
SAMPLE_EXERCISES,
STAT_LABELS,
type GameStat
} from './types'
describe('DEFAULT_SETTINGS', () => {
it('uses safe defaults that do not surprise the user', () => {
expect(DEFAULT_SETTINGS.globalEnabled).toBe(true)
expect(DEFAULT_SETTINGS.notificationMode).toBe('modal')
expect(DEFAULT_SETTINGS.minimizeToTray).toBe(true)
expect(DEFAULT_SETTINGS.startWithWindows).toBe(false) // never auto-enroll
expect(DEFAULT_SETTINGS.snoozeMinutes).toBeGreaterThan(0)
})
})
describe('SAMPLE_EXERCISES', () => {
it('ships at least one enabled sample so the app is not empty on first launch', () => {
expect(SAMPLE_EXERCISES.length).toBeGreaterThan(0)
expect(SAMPLE_EXERCISES.some((e) => e.enabled)).toBe(true)
})
it('all samples have positive reps and intervals', () => {
for (const ex of SAMPLE_EXERCISES) {
expect(ex.reps, `reps for ${ex.name}`).toBeGreaterThan(0)
expect(ex.intervalMinutes, `interval for ${ex.name}`).toBeGreaterThan(0)
expect(ex.icon.length, `icon set for ${ex.name}`).toBeGreaterThan(0)
}
})
})
describe('STAT_LABELS', () => {
it('has a Russian label for every GameStat in every GAME_STATS bundle', () => {
for (const stats of Object.values(GAME_STATS)) {
for (const stat of stats as readonly GameStat[]) {
expect(STAT_LABELS[stat], `label for ${stat}`).toBeTruthy()
}
}
})
})