docs+chore: retry upload on TLS/504 + refresh README/RELEASING

Upload script:
- Retry curl on transient network failures (504, schannel TLS abrupt
  close): up to 4 retries with 15s/45s/2m/5m backoff. Before each retry,
  list the release assets server-side — Gitea sometimes commits the
  body but times out the response, so the file may already be there at
  the expected size (skip retry). If present at wrong size (partial),
  delete before re-uploading. ASCII-only (PS5.1 reads files in CP1251
  without BOM).

Docs:
- README: bump release/test badges to v0.5.1 / 51 tests; mention silent
  retry in the auto-update feature line.
- RELEASING: rewrite around the new update-channel architecture, bridge
  tags, and dropped Gitea Actions workflows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
AnRil
2026-05-18 22:37:33 +07:00
parent 6160ece8d4
commit d6f94ee1c9
3 changed files with 119 additions and 121 deletions

View File

@@ -2,8 +2,8 @@
Windows desktop приложение, которое напоминает делать упражнения во время работы за компьютером. Опционально подключается к Dota 2 и после каждого матча превращает статистику (смерти, убийства, ассисты) в количество повторений. Windows desktop приложение, которое напоминает делать упражнения во время работы за компьютером. Опционально подключается к Dota 2 и после каждого матча превращает статистику (смерти, убийства, ассисты) в количество повторений.
[![release](https://img.shields.io/badge/release-v0.4.0-orange)](https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/latest) [![release](https://img.shields.io/badge/release-v0.5.1-orange)](https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/latest)
[![tests](https://img.shields.io/badge/tests-33%20passing-green)]() [![tests](https://img.shields.io/badge/tests-51%20passing-green)]()
[![platform](https://img.shields.io/badge/platform-Windows%2010%2F11-blue)]() [![platform](https://img.shields.io/badge/platform-Windows%2010%2F11-blue)]()
## Что внутри ## Что внутри
@@ -15,7 +15,7 @@ Windows desktop приложение, которое напоминает дел
- **Игровая интеграция (Dota 2)** — Game State Integration читает статистику матча, после Победа/Поражение показывает экран с «причитающимися» повторениями (например `10 смертей × 3 = 30 приседаний`). - **Игровая интеграция (Dota 2)** — Game State Integration читает статистику матча, после Победа/Поражение показывает экран с «причитающимися» повторениями (например `10 смертей × 3 = 30 приседаний`).
- **Apple-style интерфейс** — Plus Jakarta Sans + Bricolage Grotesque, iOS-палитра, vibrancy sidebar, spring-анимации, светлая/тёмная/системная тема. - **Apple-style интерфейс** — Plus Jakarta Sans + Bricolage Grotesque, iOS-палитра, vibrancy sidebar, spring-анимации, светлая/тёмная/системная тема.
- **Два языка** — русский и английский, переключение мгновенное. - **Два языка** — русский и английский, переключение мгновенное.
- **Auto-update** — приложение само скачивает новые версии из Gitea release (проверка каждый час). - **Auto-update** — приложение само скачивает новые версии из фиксированного `update-channel` (проверка каждый час, силент-ретрай при сетевых сбоях).
## Скриншоты ## Скриншоты
@@ -66,15 +66,17 @@ npm run release -- -Bump patch # bump версии + tag + push + upload в G
## Тесты ## Тесты
``` ```
src/shared/types.test.ts (4) src/shared/types.test.ts (4)
src/renderer/src/lib/format.test.ts (8) src/shared/quiet-hours.test.ts (5)
src/main/games/vdf.test.ts (11) src/renderer/src/lib/format.test.ts (8)
src/renderer/src/i18n/i18n.test.ts (10) src/renderer/src/lib/history.test.ts (13)
───────────────────────────────────── src/main/games/vdf.test.ts (11)
33 ✓ src/renderer/src/i18n/i18n.test.ts (10)
─────────────────────────────────────────
51 ✓
``` ```
Покрытие: чистые helpers (форматирование, парсер VDF для Steam-конфигов), i18n с плюрализацией для RU/EN, дефолты shared-типов. Покрытие: чистые helpers (форматирование, история/стрики, тихие часы, парсер VDF для Steam-конфигов), i18n с плюрализацией для RU/EN, дефолты shared-типов.
## Лицензия ## Лицензия

View File

@@ -1,146 +1,142 @@
# Релиз и автообновления # Релиз и автообновления
Документ описывает три способа выпустить новую версию. Все опираются на Документ описывает, как выпускать новые версии и как устроена система
один и тот же артефакт — NSIS-инсталлятор `Exercise-Reminder-Setup-X.Y.Z.exe`, авто-обновлений.
который сам решает: устанавливать заново или обновлять существующую копию.
## TL;DR ## TL;DR
```pwsh ```pwsh
$env:GITEA_TOKEN = '<token из Gitea Settings → Applications>' $env:GITEA_TOKEN = '<token из Gitea Settings → Applications>'
npm run release -- -Bump patch # 0.2.0 → 0.2.1 npm run release -- -Bump patch # 0.5.1 → 0.5.2
# или npm run release -- -Bump minor -BridgeTags v0.5.0 # 0.5.x → 0.6.0 + bridge
npm run release -- -Version 0.3.0 npm run release -- -Version 1.0.0
``` ```
Скрипт сделает всё сам: бамп версии, коммит, тег, push, тесты, сборка Скрипт делает всё сам: бамп версии, коммит, тег, push, тесты, сборка
инсталлятора, создание Gitea release с заметками из коммитов, загрузка инсталлятора, загрузка в Gitea releases.
артефактов.
После публикации релиза установленные у пользователей копии в течение ## Архитектура auto-update
~6 часов проверят `latest.yml` на Gitea и предложат обновление через UI.
--- ### Где лежат артефакты
## Как работает auto-update Каждый выпуск публикует три файла:
1. На каждом релизе вместе с `.exe` публикуется `latest.yml` ```
манифест с версией, размером, sha512 хешем. Exercise-Reminder-Setup-X.Y.Z.exe # NSIS-инсталлятор (~80 MB)
2. Приложение (через `electron-updater`) каждые 6 часов делает HTTP Exercise-Reminder-Setup-X.Y.Z.exe.blockmap # для differential update (~90 KB)
GET на `<gitea>/AnRil/laude/releases/download/v<current>/latest.yml`. latest.yml # манифест: версия + хеш + размер
3. Если версия в манифесте выше текущей — статус становится ```
`available`, в Settings → Обновления появляется кнопка «Скачать».
4. После скачивания — статус `downloaded`, кнопка «Перезапустить».
5. При перезапуске NSIS установщик из дельты или полный накатывается
поверх существующей инсталляции. Данные в `%APPDATA%\Exercise Reminder\`
сохраняются.
**Важно:** репозиторий `laude` приватный. Чтобы auto-update работал на И они одновременно публикуются в **три-четыре места** на Gitea:
машинах конечных пользователей, либо:
- сделать репозиторий публичным, либо
- сделать публичными только релизы (Gitea: Release Settings),
- либо подписывать запросы токеном (нужен код в `updater.ts`,
использующий `autoUpdater.requestHeaders`).
## Способ 1 — скрипт релиза (рекомендованный сейчас) | Release tag | Назначение |
|------------------|-------------------------------------------------------------|
| `vX.Y.Z` | Архив + changelog для людей |
| `update-channel` | **Фиксированный URL для auto-updater** (никогда не меняется) |
| `vN.M.K` (bridge)| Мост: чтобы клиенты на старых версиях нашли обновление |
Самый прямой путь, не зависит от Gitea Actions runners. ### Что приложение запекает в бинарник
В `package.json``build.publish.url`:
```
https://xn--90adajar8af4h.xn--p1ai/git/AnRil/laude/releases/download/update-channel
```
Этот URL **никогда не меняется**. Все версии (и сегодняшние, и будущие)
проверяют один и тот же `update-channel/latest.yml`.
### Цикл проверки
1. При запуске и каждый час `electron-updater` делает GET на
`…/update-channel/latest.yml`.
2. Если в манифесте версия выше текущей — Settings → Обновления показывает
«Доступно vX.Y.Z». По клику качается `.exe` (или differential по
`.blockmap`).
3. После скачивания — кнопка «Перезапустить». NSIS обновляет инсталляцию
поверх с сохранением `%APPDATA%\Exercise Reminder\app-state.json`.
### Bridge-теги (миграционный период)
До v0.5.1 publish.url был `…/releases/download/v${version}`у каждой
версии свой адрес. Установленные ранее копии запекли старый URL.
Чтобы они нашли обновление, новые артефакты также заливаются в их
старые releases (флаг `-BridgeTags`).
После того как все клиенты получили v0.5.1 или выше, аргумент
`-BridgeTags` можно перестать использовать — все будущие версии берут
обновления через `update-channel`.
### Поведение при ошибках
- Hourly auto-check работает в **silent**-режиме: сетевые ошибки
логируются в консоль, но **не** показываются как красный баннер.
Следующая попытка через час.
- Boot-check ретраит 3 раза с backoff 30s/2m/5m перед тем как сдаться.
- Только ручной клик «Проверить обновления» показывает ошибку, если
она есть.
## Команды
```pwsh ```pwsh
# Один раз — получить токен в Gitea (Settings Applications) # Один раз — токен из Gitea Settings -> Applications (write:repository).
# и сохранить в переменную окружения. Право — write:repository.
[Environment]::SetEnvironmentVariable('GITEA_TOKEN', '<token>', 'User') [Environment]::SetEnvironmentVariable('GITEA_TOKEN', '<token>', 'User')
# Релиз # Релиз
npm run release -- -Bump patch # patch (0.2.0 → 0.2.1) npm run release -- -Bump patch # patch (0.5.1 -> 0.5.2)
npm run release -- -Bump minor # minor (0.2.0 → 0.3.0) npm run release -- -Bump minor # minor (0.5.x -> 0.6.0)
npm run release -- -Bump major # major (0.2.0 → 1.0.0) npm run release -- -Bump major # major
npm run release -- -Version 1.2.3 # точная версия npm run release -- -Version 1.2.3 # точная версия
npm run release -- -DryRun # посмотреть план без действий npm run release -- -BridgeTags v0.4.0,v0.5.0 # дополнительные мосты
npm run release -- -DryRun # план без действий
``` ```
Что делает скрипт: Что делает `release.ps1`:
1. Проверяет что нет незакоммиченных изменений
2. Бампит версию в `package.json`, коммитит
3. Прогоняет `npm run typecheck` и `npm run test:run`
4. Собирает `npm run dist` (NSIS + блокмап + latest.yml)
5. Создаёт тег `vX.Y.Z`, пушит main и тег в origin
6. Через Gitea API создаёт release с заметками из git log
7. Загружает три файла как assets: `.exe`, `.exe.blockmap`, `latest.yml`
## Способ 2 — Gitea Actions (если есть runners) 1. Проверяет чистоту дерева.
2. Бампит `package.json`, коммитит как `chore(release): vX.Y.Z`.
Workflows лежат в `.gitea/workflows/`: 3. `npm run typecheck` + `npm run test:run`.
4. `npm run dist` → NSIS-инсталлятор + blockmap + latest.yml в `release/`.
- **`ci.yml`** — на push в main и на PR. Запускает typecheck + 5. `git tag vX.Y.Z` и push main + tag в origin.
unit-тесты + smoke-сборку (без NSIS). Кладёт распакованную сборку 6. Через `upload-release-assets.ps1` заливает артефакты в каждый тег
как artifact на 7 дней. из списка: `vX.Y.Z`, `update-channel`, и все `-BridgeTags`.
- **`release.yml`** — на push тега `v*.*.*`. Сверяет тег с версией 7. Каждая заливка ретраит до 4 раз с backoff 15s/45s/2m/5m на 504.
в `package.json`, прогоняет тесты, собирает NSIS-инсталлятор,
создаёт Gitea release с заметками, загружает артефакты.
Чтобы release workflow работал — в репозитории нужен secret
`GITEA_TOKEN` (Gitea Repo Settings → Secrets). Этот же токен может быть
переиспользован из `Способа 1`.
Для запуска release workflow:
```bash
git tag v0.3.0
git push origin v0.3.0
```
## Способ 3 — руками
Если что-то сломалось в автоматизации:
```pwsh
npm run typecheck
npm run test:run
npm run dist
# В release/ появятся:
# Exercise-Reminder-Setup-X.Y.Z.exe
# Exercise-Reminder-Setup-X.Y.Z.exe.blockmap
# latest.yml
```
Затем в Gitea UI: Releases → Draft new release → загрузить три файла.
## Тестирование auto-update ## Тестирование auto-update
Удобный способ проверить, что цикл работает: 1. Установить какую-нибудь старую версию через `.exe` из её release.
2. Релизнуть свежую версию.
3. В установленной копии: Settings → Обновления → Проверить.
4. Должно показать «Доступна vX.Y.Z» с кнопкой «Скачать».
5. Скачать → Перезапустить → проверить версию.
1. Релизнуть `0.x.0` через `npm run release`. Для `npm run dev` auto-updater отключён — статус сразу `unsupported`.
2. Установить полученный `.exe` на машину.
3. Релизнуть `0.x.1` (любой бамп).
4. На установленной копии открыть Settings → Обновления → Проверить.
Должно показать «Доступно обновление v0.x.1».
5. Скачать → Перезапустить → проверить версию в окне «О программе»
(или в Settings).
Для dev-режима (`npm run dev`) auto-updater отключён — статус сразу
становится `unsupported` с пояснением.
## Откат релиза ## Откат релиза
Если опубликовали плохой релиз:
1. Удалить release в Gitea UI (или через API). 1. Удалить release в Gitea UI (или через API).
2. Удалить тег: `git push origin :refs/tags/vX.Y.Z` и локально 2. `git push origin :refs/tags/vX.Y.Z` и `git tag -d vX.Y.Z`.
`git tag -d vX.Y.Z`. 3. `git revert <bump-hash>` (бамп уже запушен).
3. Откатить bump-коммит: `git revert <hash>` или `git reset --hard HEAD~1` 4. Если артефакты успели уехать в `update-channel` — перезалить туда
(если ещё не пушили дальше). предыдущую версию: `pwsh scripts/upload-release-assets.ps1 -Tag update-channel -AssetVersion <previous>`.
4. Релизнуть тот же номер заново — auto-updater на клиентах увидит
тот же манифест и не предложит обновление (если sha512 совпадёт). На практике лучше выпустить hotfix-патч `X.Y.Z+1`, чем откатывать.
Если содержание поменялось — увидит и предложит обновиться. На
практике лучше выпустить hotfix-патч `X.Y.Z+1`, чем переписывать ## Gitea Actions
существующий релиз.
Раньше в `.gitea/workflows/` лежали `ci.yml` и `release.yml`. Они
требуют Gitea Actions runners (отдельная служба, у нас не настроена),
поэтому каждая push-операция оставляла зависший workflow run в Actions
tab. Workflows удалены, has_actions на репозитории выключен,
Actions tab возвращает 404. Если когда-нибудь захочется CI — добавить
обратно `.gitea/workflows/*.yml` + поднять runners.
## Что попадает в установщик ## Что попадает в установщик
См. `build` секцию `package.json`: См. `build.files` в `package.json`:
- `out/**/*` — собранный код (main + preload + renderer) - `out/**/*` — собранный код (main + preload + renderer)
- `resources/**/*` — иконки - `resources/**/*` — иконки
Никаких node_modules, исходников, тестов, README`electron-builder` Без `node_modules`, без исходников, без тестов — `electron-builder`
сам распаковывает и упаковывает только необходимое. сам выбирает только необходимое.

View File

@@ -48,7 +48,7 @@ if (-not $Tag) {
$Tag = "v$pkgVersion" $Tag = "v$pkgVersion"
} }
if (-not $AssetVersion) { if (-not $AssetVersion) {
# Derive from tag when possible (vX.Y.Z X.Y.Z); otherwise read package.json. # Derive from tag when possible (vX.Y.Z -> X.Y.Z); otherwise read package.json.
if ($Tag -match '^v\d+\.\d+\.\d+') { if ($Tag -match '^v\d+\.\d+\.\d+') {
$AssetVersion = $Tag.TrimStart('v') $AssetVersion = $Tag.TrimStart('v')
} else { } else {
@@ -101,7 +101,7 @@ try {
if ($prev) { if ($prev) {
$log = (& git log --pretty=format:"- %s" "$prev..$Tag") -join "`n" $log = (& git log --pretty=format:"- %s" "$prev..$Tag") -join "`n"
} else { } else {
# No prior tag list last 10 commits up to this tag. # No prior tag - list last 10 commits up to this tag.
$log = (& git log --pretty=format:"- %s" -n 10 "$Tag") -join "`n" $log = (& git log --pretty=format:"- %s" -n 10 "$Tag") -join "`n"
} }
$body = "### Changes`n`n$log`n`n---`n`nInstaller below: run it; if app is already installed, it updates in place and keeps your settings." $body = "### Changes`n`n$log`n`n---`n`nInstaller below: run it; if app is already installed, it updates in place and keeps your settings."
@@ -181,7 +181,7 @@ foreach ($asset in @($installer, $blockmap, $manifest)) {
-Method Get -Headers $headers -Method Get -Headers $headers
$existing = $check | Where-Object { $_.name -eq $name } $existing = $check | Where-Object { $_.name -eq $name }
if ($existing -and $existing.size -eq $size) { if ($existing -and $existing.size -eq $size) {
Write-Host " Asset already present server-side ($($existing.size) bytes) skipping retry." -ForegroundColor DarkGray Write-Host " Asset already present server-side ($($existing.size) bytes) - skipping retry." -ForegroundColor DarkGray
$uploaded = $true $uploaded = $true
break break
} }
@@ -210,7 +210,7 @@ foreach ($asset in @($installer, $blockmap, $manifest)) {
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
$uploaded = $true $uploaded = $true
} else { } else {
Write-Host " curl exit $LASTEXITCODE will retry." -ForegroundColor Yellow Write-Host " curl exit $LASTEXITCODE - will retry." -ForegroundColor Yellow
$attempt++ $attempt++
} }
} }