Files
laude/RELEASING.md
AnRil f3367e09de chore+fix: repo hygiene, code-review fixes, audit cleanup
Three independent code reviews + a security audit produced ~200 findings.
This commit lands the high-impact subset. Tests pass (53), typecheck
clean, eslint clean (3 minor exhaustive-deps warnings left).

REPO HYGIENE
- Add .editorconfig, .prettierrc.json, .prettierignore.
- Add ESLint flat config (.eslintrc.cjs) — correctness-focused, no style
  rules (Prettier owns formatting).
- Add `format` / `format:check` / `lint` npm scripts.
- Add CHANGELOG.md (Keep a Changelog format, back-filled to 0.1.x).
- Reformat all source via Prettier so future diffs stay small.

DATA SAFETY (src/main/store.ts)
- Atomic write (tmp + rename) with retry on transient EBUSY/EPERM —
  was non-atomic writeFileSync, vulnerable to truncation on power loss.
- On corrupt JSON, rename to `app-state.json.corrupt-<ts>` instead of
  silently overwriting the user's exercises/history with defaults.
- Validate parsed shape before merging — reject arrays/scalars where
  objects expected; per-field array checks.
- Strip `id` from incoming patches in updateExercise/updateChallenge —
  a runtime caller (IPC) could otherwise smuggle id changes through.
- clearHistory now refuses an unbounded wipe (no beforeTs => no-op);
  callers must pass an explicit boundary.
- unref() the debounce timer so it doesn't keep the event loop alive.

SECURITY (src/main/*)
- gsi-server: hard 256 KB body cap (was unbounded — local OOM vector),
  reject any Origin/Sec-Fetch-Site header (blocks browser CSRF from
  visited pages), require application/json Content-Type, generic 400
  on parse error (no error string echo to client), closeAllConnections
  + async close on stop.
- dota2: validate auth.token from payload with timingSafeEqual against
  the per-install token — was unauthenticated, any local process could
  forge match-end events. Narrow object shape before spread-merge to
  avoid throws on hostile payloads like {player:"x"}. Reset latest /
  prevState after match_end so the next match starts clean.
- ipc: gate `dev:simulateMatchEnd` registration behind `!app.isPackaged`
  so it does not exist in shipped builds.
- preload: gate the matching `simulateMatchEnd` export behind
  `import.meta.env.MODE !== 'production'` so the bundler dead-code-
  eliminates it from the production preload bundle.
- windows: shell.openExternal allowlist (http/https/mailto only) — was
  forwarding any URL, including file:/javascript:/custom URI handlers
  (some Windows handlers have been RCE vectors). will-navigate blocks
  navigation to anywhere except file:// or the dev URL.

CORRECTNESS (src/main/* + src/shared/*)
- shared/types.ts isQuietAt: fix wrap-around + day-of-week filter.
  With from=22:00 to=07:00 days=[Mon..Fri], the window started THE
  PREVIOUS DAY when we're in the AM half — old code checked today's
  day-of-week and got the wrong answer Sat 02:00 and Mon 01:00. Now
  the filter is evaluated against the window's START day. Also reject
  malformed HH:MM strings instead of producing NaN.
- scheduler: call broadcastState() after firing exercises so the
  renderer's Dashboard/Exercises pages don't show stale nextFireAt
  until the next state-changing IPC. Guard powerMonitor listeners
  against double-registration on dev hot-reload.
- dota2: fix `launchOptionStatus = steamRunning ? 'queued' : 'queued'`
  tautology — both branches now correctly read 'queued'.
- steam-launch-options: replace `require('node:fs')` inside atomicWrite
  with the top-level import; retry on transient EBUSY/EPERM.

CORRECTNESS (src/renderer/*)
- lib/history.ts: replace `today.getTime() - i * MS_DAY` arithmetic
  with `setDate(date - i)` calendar arithmetic in dailyRepsRange and
  currentStreak — DST transitions shift epoch math by ±1h and cause
  dayKey() to emit duplicate or missing days at the boundary.
- lib/icon.tsx: restrict name lookup to ICON_CHOICES set — an arbitrary
  string from a corrupted state file could otherwise resolve to
  unrelated Lucide exports and crash the renderer.
- lib/format.ts: guard formatCountdown against NaN/Infinity.
- i18n/index.ts: replace regex-based interpolation with split/join so
  variable values containing regex metacharacters interpolate
  literally; warn in dev on missing keys; clamp pluralRu(-N) via abs.
- ReminderApp: keyboard shortcuts moved INTO ExerciseReminder so Enter
  respects the stepper's `adjusted` flag (was always passing planned
  reps). Stepper capped at 5× planned. Don't hijack Space when a
  button is focused. `key={exercise.id+nextFireAt}` forces a fresh
  component for back-to-back reminders so stepper state resets. Match
  summary view gets Esc-to-close. Functional setMode in onMarkDone
  avoids races against stale `mode.done`.
- UpdaterCard: guard against NaN/Infinity in download-progress events
  (electron-updater fires early events with undefined fields).
- Games: gate DevPanel behind `import.meta.env.DEV` in addition to the
  main-side IPC gate, and narrow the `simulateMatchEnd` access.
- Add aria-labels for the +/- stepper buttons (i18n keys added).

TESTS
- +2 quiet-hours tests covering wrap-around + day-filter combo and
  malformed HH:MM fallback. Total 53 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 23:04:49 +07:00

7.2 KiB
Raw Blame History

Релиз и автообновления

Документ описывает, как выпускать новые версии и как устроена система авто-обновлений.

TL;DR

$env:GITEA_TOKEN = '<token из Gitea Settings → Applications>'
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 1.0.0

Скрипт делает всё сам: бамп версии, коммит, тег, push, тесты, сборка инсталлятора, загрузка в Gitea releases.

Архитектура auto-update

Где лежат артефакты

Каждый выпуск публикует три файла:

Exercise-Reminder-Setup-X.Y.Z.exe          # NSIS-инсталлятор (~80 MB)
Exercise-Reminder-Setup-X.Y.Z.exe.blockmap # для differential update (~90 KB)
latest.yml                                  # манифест: версия + хеш + размер

И они одновременно публикуются в три-четыре места на Gitea:

Release tag Назначение
vX.Y.Z Архив + changelog для людей
update-channel Фиксированный URL для auto-updater (никогда не меняется)
vN.M.K (bridge) Мост: чтобы клиенты на старых версиях нашли обновление

Что приложение запекает в бинарник

В package.jsonbuild.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 перед тем как сдаться.
  • Только ручной клик «Проверить обновления» показывает ошибку, если она есть.

Команды

# Один раз — токен из Gitea Settings -> Applications (write:repository).
[Environment]::SetEnvironmentVariable('GITEA_TOKEN', '<token>', 'User')

# Релиз
npm run release -- -Bump patch                  # patch (0.5.1 -> 0.5.2)
npm run release -- -Bump minor                  # minor (0.5.x -> 0.6.0)
npm run release -- -Bump major                  # major
npm run release -- -Version 1.2.3               # точная версия
npm run release -- -BridgeTags v0.4.0,v0.5.0    # дополнительные мосты
npm run release -- -DryRun                      # план без действий

Что делает release.ps1:

  1. Проверяет чистоту дерева.
  2. Бампит package.json, коммитит как chore(release): vX.Y.Z.
  3. npm run typecheck + npm run test:run.
  4. npm run dist → NSIS-инсталлятор + blockmap + latest.yml в release/.
  5. git tag vX.Y.Z и push main + tag в origin.
  6. Через upload-release-assets.ps1 заливает артефакты в каждый тег из списка: vX.Y.Z, update-channel, и все -BridgeTags.
  7. Каждая заливка ретраит до 4 раз с backoff 15s/45s/2m/5m на 504.

Тестирование auto-update

  1. Установить какую-нибудь старую версию через .exe из её release.
  2. Релизнуть свежую версию.
  3. В установленной копии: Settings → Обновления → Проверить.
  4. Должно показать «Доступна vX.Y.Z» с кнопкой «Скачать».
  5. Скачать → Перезапустить → проверить версию.

Для npm run dev auto-updater отключён — статус сразу unsupported.

Откат релиза

  1. Удалить release в Gitea UI (или через API).
  2. git push origin :refs/tags/vX.Y.Z и git tag -d vX.Y.Z.
  3. git revert <bump-hash> (бамп уже запушен).
  4. Если артефакты успели уехать в update-channel — перезалить туда предыдущую версию: pwsh scripts/upload-release-assets.ps1 -Tag update-channel -AssetVersion <previous>.

На практике лучше выпустить 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.files в package.json:

  • out/**/* — собранный код (main + preload + renderer)
  • resources/**/* — иконки

Без node_modules, без исходников, без тестов — electron-builder сам выбирает только необходимое.