# 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 ``` Зафиксируйте: - 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`; 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. - [ ] Итоговые значения политики и время применения записаны в журнал выпуска.