Реализована проверка версионности и требование soft\force обновлений приложения

This commit is contained in:
mi
2026-09-03 19:12:38 +03:00
parent 465a70d488
commit 44db38f6fe
37 changed files with 3057 additions and 53 deletions
@@ -0,0 +1,479 @@
# Runbook внедрения soft/force update мобильного приложения
## 1. Назначение и границы
Документ описывает выпуск нативных версий HAN Chat через Google Play, RuStore
и App Store и последующее включение `soft`/`force update` через публичную
backend-политику.
Механика не использует EAS OTA Update. Пользователь всегда направляется в
магазин, соответствующий каналу установленной сборки:
- `google_play`;
- `rustore`;
- `app_store`.
Канал встраивается в бинарный файл через
`EXPO_PUBLIC_DISTRIBUTION_STORE`. Поэтому Android-сборки Google Play и RuStore
нужно собирать отдельно.
Решение принимает мобильный клиент:
- `current_build < minimum_build` — обязательное обновление (`force`);
- `minimum_build <= current_build < latest_build` — мягкое обновление (`soft`);
- `current_build >= latest_build` — карточка не показывается.
`latest_version` используется только в интерфейсе. Сравнение выполняется по
целому Android `versionCode` или iOS `buildNumber`.
## 2. Ответственные и данные окна выпуска
Перед началом назначьте:
- ответственного за EAS Build;
- ответственного за Google Play Console;
- ответственного за RuStore Console;
- ответственного за App Store Connect, если выпускается iOS;
- оператора ВМ1, применяющего backend settings;
- владельца решения о переводе soft update в force update.
Создайте запись окна выпуска:
```text
Маркетинговая версия:
Git commit/tag мобильного приложения:
Git commit/tag backend:
Google Play EAS build ID:
Google Play versionCode:
RuStore EAS build ID:
RuStore versionCode:
App Store EAS build ID:
App Store buildNumber:
Дата полной доступности каждого релиза:
Минимальная поддерживаемая сборка каждого канала:
```
Не вычисляйте пороги по порядку запуска команд. Записывайте фактические значения
из завершённой EAS-сборки и подтверждайте их в консоли соответствующего магазина.
## 3. Важная особенность EAS remote version
В проекте используется:
```json
{
"cli": {
"appVersionSource": "remote"
}
}
```
Android remote `versionCode` привязан к application ID `ru.han.chat`. Профили
`google-play` и `rustore` используют один application ID и общий счётчик.
Если текущее remote-значение равно `6`, последовательная сборка обычно даст:
1. первая Android-сборка — `versionCode=7`;
2. вторая Android-сборка — `versionCode=8`.
Это допустимо. Политики Google Play и RuStore независимы, поэтому в backend
следует указать `latest_build=7` для одного канала и `latest_build=8` для
другого, если именно такие артефакты опубликованы.
Локальный `android.versionCode` в `app.config.ts` не является источником истины
при remote version source. Источник истины для rollout — опубликованный
артефакт магазина.
## 4. Предварительные проверки
### 4.1. Мобильное приложение
На локальной машине:
```powershell
cd C:\Users\MI\Documents\Assistent\HAN_chat_specification\VM4_Expo-mobile
npm run typecheck
npm test
npx eas-cli whoami
```
Проверьте:
- `version` в `app.config.ts` соответствует выпускаемой маркетинговой версии;
- профиль `google-play` содержит `EXPO_PUBLIC_DISTRIBUTION_STORE=google_play`;
- профиль `rustore` содержит `EXPO_PUBLIC_DISTRIBUTION_STORE=rustore`;
- профиль `app-store` содержит `EXPO_PUBLIC_DISTRIBUTION_STORE=app_store`;
- `preview` и `development` не включают store policy;
- production API указывает на `https://chat.han0107.ru`.
### 4.2. Backend
На локальной машине:
```powershell
cd C:\Users\MI\Documents\Assistent\HAN_chat_specification\VM1_app\codebase\backend\api-backend
python -m pytest
cd ..
python -m pytest tests
```
Проверьте, что backend-релиз содержит:
- строгий объект `mobile_update` в `/api/v1/public/app-config`;
- поддержку `ETag` и `If-None-Match`;
- TTL app-config 60 секунд;
- валидацию build numbers и store URL;
- актуальный `openapi.yaml`;
- nginx `proxy_cache_valid 200 60s` для app-config.
## 5. Безопасное внедрение backend до выпуска приложения
Сначала разверните backend-код и nginx, затем мобильные сборки. Старые клиенты
игнорируют новую секцию `mobile_update`.
До публикации новой версии политика должна быть безопасной:
- либо канал отключён;
- либо `latest_build` не превышает уже опубликованный build;
- `minimum_build` не должен внезапно исключать поддерживаемые версии.
Начальные `latest_build=2` при фактической установленной сборке `6` не показывают
карточку и поэтому безопасны как временное no-op состояние.
Развёртывание backend выполняйте по основному production runbook:
`deployment/RUNBOOK.production.ru.md`.
После обновления образов и конфигурации оператор ВМ1 выполняет штатный job:
```sh
/usr/local/sbin/han-vm1-compose --profile ops run --rm seed-settings
```
Команда идемпотентна и включает `validate_settings`. Невалидная комбинация
порогов или URL должна завершить job ошибкой.
Проверка публичного контракта с внешней машины:
```sh
curl -fsS https://chat.han0107.ru/api/v1/public/app-config
ETAG="$(curl -fsSI https://chat.han0107.ru/api/v1/public/app-config \
| awk -F': ' 'tolower($1)=="etag" {gsub("\r","",$2); print $2}')"
curl -sS -o /dev/null -w '%{http_code}\n' \
-H "If-None-Match: $ETAG" \
https://chat.han0107.ru/api/v1/public/app-config
```
Ожидается:
- первый запрос — `200`;
- в ответе присутствуют три store policy;
- повторный запрос с актуальным ETag — `304`;
- `Cache-Control` содержит `max-age=60`.
## 6. Получение текущего remote build number
На локальной машине:
```powershell
cd C:\Users\MI\Documents\Assistent\HAN_chat_specification\VM4_Expo-mobile
npx eas-cli build:version:get --platform android --profile google-play
npx eas-cli build:version:get --platform android --profile rustore
npx eas-cli build:version:get --platform ios --profile app-store
```
Для автоматической обработки:
```powershell
npx eas-cli build:version:get --platform android --profile google-play --json
```
Одинаковое значение для двух Android-профилей до сборки ожидаемо: они используют
общий application ID. Каждая последующая Android-сборка с `autoIncrement`
увеличивает общий счётчик.
### 6.1. Тестовый APK с каналом RuStore
Профиль `preview-rustore` создаёт внутренний APK, обращается к
`https://dev-chat.han0107.ru` и встраивает канал `rustore`:
```powershell
npx eas-cli build:version:get --platform android --profile preview-rustore
npx eas-cli build --profile preview-rustore --platform android
```
У профиля задано `autoIncrement: false`, поэтому тестовая сборка не расходует
следующий production `versionCode`. Пусть фактический build APK равен `B`. Для
проверки на dev-backend задайте:
```text
soft: latest_build = B + 1, minimum_build <= B
force: latest_build = B + 1, minimum_build = B + 1
none: latest_build <= B
```
Меняйте только RuStore policy dev-окружения и применяйте её через штатный
`seed-settings` этого окружения. Не используйте тестовые пороги на production.
После отказа от soft update запись сохраняется в SecureStore без TTL. Для
повторной проверки той же политики очистите данные приложения либо увеличьте
`latest_build`.
APK использует package `ru.han.chat` и может заменить установленную
store-сборку. Для теста предпочтительно отдельное устройство.
## 7. Создание store-сборок
Рекомендуется собирать и публиковать магазины по одному, сразу записывая
фактический build number:
```powershell
npx eas-cli build --profile google-play --platform android
npx eas-cli build --profile rustore --platform android
npx eas-cli build --profile app-store --platform ios
```
App Store-команду не выполняйте, пока не настроены Apple credentials, Apple App
ID и рабочий `store_url`.
После каждой сборки:
```powershell
npx eas-cli build:list --platform android --limit 5
npx eas-cli build:view <BUILD_ID>
```
Зафиксируйте:
- EAS build ID;
- commit;
- channel/profile;
- `version`;
- фактический `versionCode`/`buildNumber`;
- checksum скачанного артефакта, если он используется в процедуре публикации.
Не запускайте вторую Android-сборку, пока не записан номер первой.
## 8. Публикация и проверка магазинов
### 8.1. Google Play
1. Загрузите AAB в требуемый track.
2. Убедитесь, что Console показывает ожидаемый `versionCode`.
3. Проведите internal/closed testing.
4. Проверьте установку и переход по ссылке:
`https://play.google.com/store/apps/details?id=ru.han.chat`.
5. Зафиксируйте процент rollout и время полной доступности.
### 8.2. RuStore
1. Загрузите предназначенный для RuStore артефакт.
2. Убедитесь, что Console показывает фактический `versionCode`.
3. Проведите тестирование канала.
4. Проверьте страницу:
`https://www.rustore.ru/catalog/app/ru.han.chat`.
5. Зафиксируйте статус модерации и время доступности.
### 8.3. App Store
До включения политики:
1. получите Apple App ID;
2. опубликуйте и проверьте сборку в App Store Connect/TestFlight;
3. укажите канонический URL `https://apps.apple.com/.../id<APPLE_ID>`;
4. подтвердите фактический `buildNumber`;
5. только после этого установите `enabled: true`.
## 9. Включение soft update
Изменяйте
`deployment/app-settings.production-like.yaml` отдельно для каждого магазина.
Пример, если Google Play опубликовал build `7`, а RuStore — build `8`:
```yaml
mobile_update.google_play.enabled: {type: boolean, value: true, public: true}
mobile_update.google_play.latest_build: {type: integer, value: 7, public: true}
mobile_update.google_play.minimum_build: {type: integer, value: 1, public: true}
mobile_update.google_play.latest_version: {type: string, value: "1.0.1", public: true}
mobile_update.rustore.enabled: {type: boolean, value: true, public: true}
mobile_update.rustore.latest_build: {type: integer, value: 8, public: true}
mobile_update.rustore.minimum_build: {type: integer, value: 1, public: true}
mobile_update.rustore.latest_version: {type: string, value: "1.0.1", public: true}
```
Выбор `minimum_build` требует отдельного решения:
- оставить `1` — все более старые builds получают soft update;
- установить `6` — builds `15` немедленно получают force update, а build `6`
получает soft update;
- установить новый build (`7` или `8`) — все предыдущие builds получают force.
Для первого rollout рекомендуется сохранить прежний минимальный поддерживаемый
build и включить только soft update.
После review и merge настроек оператор ВМ1 выполняет:
```sh
/usr/local/sbin/han-vm1-compose --profile ops run --rm seed-settings
```
Подождите до 60 секунд и повторно запросите app-config. Если внешний nginx уже
имел закешированный ответ, допускайте до двух минут на проверку с разных
клиентов, но не продолжайте rollout при значении старше ожидаемого.
## 10. Приёмка soft update
Используйте реальное устройство со старой store-сборкой каждого канала.
Проверьте:
1. При cold start появляется «Доступно обновление».
2. Указаны правильные текущая и новая версии.
3. Кнопка открывает правильный магазин, а не другой Android-магазин.
4. «Позже», крестик и Android Back закрывают карточку.
5. После отказа карточка той же `latest_build` не появляется при следующем
запуске: отказ хранится в SecureStore без TTL.
6. После увеличения `latest_build` появляется новая карточка.
7. После установки нового build карточка исчезает.
8. При недоступном backend приложение не блокируется.
Если продукту требуется повторное напоминание через интервал, текущую механику
следует изменить отдельно: сейчас soft-dismiss действует до появления нового
`latest_build`, очистки данных или переустановки.
## 11. Перевод в force update
Force разрешено включать только когда обязательный build:
- прошёл модерацию;
- доступен в нужном production track;
- доступен всем пользователям, которых затронет `minimum_build`;
- устанавливается и запускается;
- корректно открывается по `store_url`;
- backend и store не находятся в инциденте.
Для Google Play build `7`:
```yaml
mobile_update.google_play.latest_build: {type: integer, value: 7, public: true}
mobile_update.google_play.minimum_build: {type: integer, value: 7, public: true}
```
Для RuStore build `8`:
```yaml
mobile_update.rustore.latest_build: {type: integer, value: 8, public: true}
mobile_update.rustore.minimum_build: {type: integer, value: 8, public: true}
```
Не повышайте minimum одного магазина только потому, что релиз доступен в другом.
После изменения снова примените `seed-settings`, проверьте app-config и
протестируйте старую сборку:
- force-карточка не имеет крестика и кнопки «Позже»;
- Android Back не закрывает её;
- кнопка открывает правильный магазин;
- после возврата без установки карточка остаётся;
- после установки поддерживаемого build блокировка исчезает.
## 12. App Store policy
Пока Apple App ID неизвестен, политика должна оставаться полностью выключенной:
```yaml
mobile_update.app_store.enabled: {type: boolean, value: false, public: true}
mobile_update.app_store.latest_build: {type: integer, value: null, public: true}
mobile_update.app_store.minimum_build: {type: integer, value: null, public: true}
mobile_update.app_store.latest_version: {type: string, value: "", public: true}
mobile_update.app_store.store_url: {type: string, value: "", public: true}
mobile_update.app_store.release_notes: {type: string, value: "", public: true}
```
Отключённая политика не должна содержать частично заполненные release-поля:
backend отклонит такую конфигурацию.
## 13. Откат
### 13.1. Немедленно снять force
Понизьте `minimum_build` до последнего подтверждённого поддерживаемого значения,
не меняя `latest_build`, затем примените seed.
Пример:
```yaml
mobile_update.google_play.latest_build: {type: integer, value: 7, public: true}
mobile_update.google_play.minimum_build: {type: integer, value: 1, public: true}
```
После успешного получения новой политики клиент снимет force-блокировку.
Кратковременная сетевая ошибка сохраняет уже показанный force до следующей
успешной проверки, поэтому дополнительно подтвердите доступность app-config.
### 13.2. Полностью отключить канал
Установите `enabled=false`, integer-поля в `null`, строковые release-поля в
пустую строку:
```yaml
mobile_update.google_play.enabled: {type: boolean, value: false, public: true}
mobile_update.google_play.latest_build: {type: integer, value: null, public: true}
mobile_update.google_play.minimum_build: {type: integer, value: null, public: true}
mobile_update.google_play.latest_version: {type: string, value: "", public: true}
mobile_update.google_play.store_url: {type: string, value: "", public: true}
mobile_update.google_play.release_notes: {type: string, value: "", public: true}
```
Не откатывайте уже использованный store build number и не публикуйте другой
артефакт с тем же `versionCode`/`buildNumber`.
### 13.3. Дефект новой версии
Если новая версия дефектна:
1. не направляйте на неё новых пользователей — отключите policy или верните
`latest_build` к безопасному опубликованному build;
2. остановите rollout в соответствующем магазине;
3. выпустите исправленную сборку с новым build number;
4. после публикации укажите новый `latest_build`;
5. только после приёмки принимайте решение о новом `minimum_build`.
## 14. Наблюдение после включения
В течение окна наблюдения контролируйте:
- `5xx` и latency `/api/v1/public/app-config`;
- долю `200/304`;
- ошибки rate limit публичного endpoint;
- доступность страниц магазинов;
- crash/error rate новой мобильной версии;
- обращения о циклической force-карточке;
- соответствие фактического store build политике каждого канала.
Stop conditions:
- URL ведёт не в тот магазин или не на HAN Chat;
- опубликованный build ниже `latest_build`;
- часть rollout-групп не может скачать minimum build;
- app-config отдаёт старую или частичную политику дольше двух минут;
- новая версия не запускается или не проходит авторизацию;
- force нельзя снять успешным изменением backend policy.
## 15. Контрольный чек-лист
- [ ] Backend с `mobile_update` развёрнут до мобильного rollout.
- [ ] ETag/304 и TTL 60 секунд проверены извне.
- [ ] Фактические build numbers записаны после каждой EAS-сборки.
- [ ] Build numbers подтверждены в консолях магазинов.
- [ ] Google Play и RuStore thresholds заполнены независимо.
- [ ] Soft update проверен на старой сборке каждого канала.
- [ ] Soft-dismiss и повторный показ для нового latest build проверены.
- [ ] Force включается только после полной доступности minimum build.
- [ ] App Store остаётся disabled до получения Apple App ID.
- [ ] Процедура отката проверена до включения force.
- [ ] Итоговые значения политики и время применения записаны в журнал выпуска.