Реализована проверка версионности и требование soft\force обновлений приложения
This commit is contained in:
@@ -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 `1–5` немедленно получают 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.
|
||||
- [ ] Итоговые значения политики и время применения записаны в журнал выпуска.
|
||||
Reference in New Issue
Block a user