Справочник CLI
otapush — консольный клиент для сервера Otapush: он связывает проект с приложением, публикует обновления и двигает каналы вперёд и назад. Установка и встроенная справка:
npm install -g otapush # или: bun add -g otapush
otapush --help
Набор команд намеренно мал: login, whoami, init, publish, list, rollback, promote, keys. Всё, для чего CLI нет — создание приложений и каналов, графики статистики, кнопки отката по платформам, — живёт в веб-портале; работа через портал описана в Быстром старте.
Два хранилища учётных данных
~/.otapushrc.json— глобальный, создаётся командойotapush login: URL сервера и JWT-токен портала. Файл создаётся с правами0600. Читается всеми командами../otapush.config.json— проектный, создаётся командамиotapush initиotapush keys create:server,appId,slug,runtimeVersion,projectType(expoилиbare) и, после создания ключа,apiKey. Нужен всем проектным командам (publish,list,rollback,promote,keys) — без него они завершаются ошибкой.- Переменная окружения
OTAPUSH_API_KEYпереопределяетapiKeyиз проектного конфига. Других переменных окружения нет, режима вывода--jsonтоже нет.
--server <url> есть только у login и init; остальные команды берут сервер из otapush.config.json.
Кто как аутентифицируется
login,whoami,keys— Bearer-токен портала из~/.otapushrc.json. Без логина они останавливаются с «Runotapush login --server <url>first».publish— только API-ключ (apiKeyиз проектного конфига илиOTAPUSH_API_KEY). Сессии логина недостаточно: эндпоинт публикации принимает толькоx-api-key.list,rollback,promote— и то и другое: API-ключ, если он есть, иначе токен логина.
API-ключи привязаны к одному приложению — сервер проверяет, что ключ принадлежит приложению из пути запроса, и отвечает 403 в противном случае.
otapush login — сохранить учётные данные портала
otapush login --server https://ota.example.com
Интерактивная команда: спросит URL сервера (по умолчанию http://localhost:3000, если не передан --server), затем email и пароль, и сохранит JWT в ~/.otapushrc.json. Запускается один раз на машину, до init. Команды logout нет — чтобы забыть учётные данные, удалите ~/.otapushrc.json.
otapush whoami — проверить, под кем вы вошли
otapush whoami
Выводит URL сервера и аутентифицированного пользователя (имя, email, id). Требует сессии логина. Полезно после смены сервера или аккаунта — чтобы понять, от чьего имени сработает следующая команда.
otapush init — привязать проект к приложению
otapush init [--server <url>]
Требует сессии логина. Флагов --app и --force нет: init показывает список приложений аккаунта и спрашивает, какое привязать (или создать новое по имени и slug); повторный запуск и повторный ответ на вопрос — способ перепривязать проект, и весь патчинг идемпотентен — существующие значения заменяются, а не дублируются.
Что он пишет для managed-проекта Expo (есть app.json):
expo.updates—url,enabled,codeSigningCertificate,codeSigningMetadata. Существующий блокupdatesдополняется, а не заменяется.expo.runtimeVersion— только если вapp.jsonего ещё нет; заданное вручную значение остаётся нетронутым.certs/certificate.pem— публичный сертификат приложения, при каждом запуске запрашивается с сервера заново.otapush.config.json— привязка проекта (причёмruntimeVersionздесь всегда берётся из записи приложения на сервере — именно еёpublishиспользует по умолчанию).
Если ваш конфиг — app.config.js/app.config.ts вместо app.json, init его не патчит, а печатает точный JSON для вставки в объект expo.
Что он пишет для bare-проекта React Native (нет app.json, есть зависимость react-native): meta-data-записи в android/app/src/main/AndroidManifest.xml, ключи EXUpdates* в ios/<ProjectName>/Expo.plist, тот же файл сертификата и тот же конфиг с projectType: "bare" — всё это описано в Bare React Native.
init ничего не меняет на сервере: приложение, его каналы и пара ключей уже существуют. Сценарии отказа — неверный каталог, отсутствующие файлы, неопознанный тип проекта — перечислены в разделах «что пошло не так» в Настройке Expo и Bare React Native.
otapush publish — экспортировать и загрузить обновление
otapush publish [--channel dev] [--platform ios|android|all] [--message <msg>] [--runtime-version <v>]
Рабочая лошадка. Значения по умолчанию: --channel dev, --platform all (экспортирует и публикует обе платформы по очереди). --platform принимает одно значение — чтобы опубликовать одну платформу, назовите её; коротких флагов и повторяемого -p нет.
Требует API-ключ и отказывается работать только с сессией логина. Затем, для каждой платформы:
- Экспорт. Managed-проекты:
npx expo export --platform <p> --output-dir dist-ota, бандл и ассеты берутся из сгенерированногоmetadata.json. Bare-проекты:npx react-native bundle --dev false --entry-file index.js --bundle-output … --assets-dest …(entry-файлindex.js/index.tsопределяется автоматически). - Загрузка multipart-формой: бандл, каждый ассет плюс
channel,platform,runtimeVersion,messageи текущий git-коммит — автоматически изgit rev-parse HEAD, если проект является git-репозиторием (флага для переопределения нет; вне репозитория поле просто опускается).
Что меняется на сервере: создаётся новая запись обновления; бандл сохраняется в bundles/<appId>/<updateId>/bundle.js; ассеты сохраняются по содержимому как assets/<sha256><ext>, и ассет, который на сервере уже есть — из любого более раннего обновления этого приложения, — повторно не записывается; наконец указатель канала для этой платформы переводится на новое обновление, а флаг отката канала сбрасывается.
Ошибки, в которые можно упереться: «No API key» (создайте ключ командой keys create или задайте OTAPUSH_API_KEY), channel '<name>' not found (каналы создаются в портале — три существуют по умолчанию) и ненулевой код выхода инструмента экспорта, который передаётся как есть.
otapush list — посмотреть обновления и что сейчас в эфире
otapush list [--channel <name>] [--platform ios|android]
Печатает таблицу обновлений приложения — id, канал, платформа, версия рантайма, сообщение, время создания — где * отмечает обновление, на которое канал указывает сейчас. Фильтры складываются. Аутентификация — API-ключ или сессия логина.
otapush rollback --channel <name> — откатить канал назад
otapush rollback --channel prod
Возвращает канал к предыдущему обновлению для платформы этого обновления; устройства восстанавливаются при следующей проверке. Откат первого обновления канала на платформе — особый случай, обработанный на сервере: канал переходит в состояние rollBackToEmbedded, и клиенты возвращаются к бандлу, вшитому в бинарник, — Протокол разбирает эту директиву.
Две вещи, которые стоит знать до того, как тянуться к команде в спешке:
- Какая платформа пострадает, решает «одиночный указатель» канала, как его видит CLI: сначала iOS. Если у канала есть текущие обновления обеих платформ, CLI откатит iOS; до Android-указателя дело дойдёт, только когда iOS-ого нет. Чтобы откатить ровно одну платформу, используйте вкладку Updates портала.
- Сервер ответит
400, если обновление не является текущим для канала, — откат не текущего обновления молча перепрыгнул бы через версии, которые никто не смотрел.
otapush promote --from <ch> --to <ch> — скопировать текущее обновление в другой канал
otapush promote --from staging --to prod
Продвигает текущее обновление канала --from в канал --to, для платформы этого обновления, и сбрасывает флаг отката целевого канала. Это стандартный ход staging → prod: устройства получают ровно те байты, которые вы проверяли, а не пересборку. --from и --to должны различаться, оба канала должны существовать. CLI напоминает, что каналы поплатформенные, — повторите команду для второй платформы или воспользуйтесь порталом.
otapush keys create|list|revoke — управление API-ключами
otapush keys create ci # печатает ключ один раз, сохраняет в otapush.config.json
otapush keys list
otapush keys revoke <keyId>
create <name>требует и сессии логина, и проектного конфига — ключ принадлежит привязанному приложению. Полный ключ показывается ровно один раз, а на сервере хранится только его хеш. Внутри проекта ключ дополнительно записывается вotapush.config.jsonкакapiKey.listпоказывает id, имя и дату создания; сам ключ больше не показывается никогда.revoke <keyId>удаляет ключ на сервере. CLI предупредит, если вotapush.config.jsonвсё ещё лежит отозванный ключ, — удалите его или создайте новый.
Использование CLI в CI
login интерактивен, поэтому CI работает только с API-ключом. Настройка занимает три строки:
- Локально, один раз:
otapush init(создастotapush.config.json), затемotapush keys create ci. - Закоммитьте
otapush.config.json— там URL сервера, id приложения и slug, без секретов, — а ключ положите в хранилище секретов CI какOTAPUSH_API_KEY. (Если ключ попал в конфиг изkeys create, удалите полеapiKeyперед коммитом — или держите секрет только в переменной окружения.) - В CI команды
publish,list,rollbackиpromoteработают с одной лишь переменной окружения.
Пример для GitHub Actions:
- run: otapush publish --channel prod --platform all --message "${{ github.event.head_commit.message }}"
env:
OTAPUSH_API_KEY: ${{ secrets.OTAPUSH_API_KEY }}
Помните: publish запускает инструмент экспорта вашего проекта (npx expo export или npx react-native bundle), поэтому CI-задаче нужна обычная JavaScript-обвязка проекта — установленные зависимости и всё прочее, а не только бинарник otapush.
Коды выхода
0 — успех. 1 — любая ошибка: неверные аргументы, HTTP-ошибки от сервера (включая 401/403), неудачный экспорт, недостижимый сервер. Других кодов нет; считайте любой ненулевой код признаком сбоя и читайте stderr — в сообщении названы команда, статус и, где возможно, текст ошибки самого сервера.
Вопросы и ответы
Почему publish говорит «No API key»?
Публикация авторизуется API-ключом, привязанным к приложению, а не сессией otapush login. CLI ищет apiKey в otapush.config.json, затем OTAPUSH_API_KEY в окружении. Решение: выполните otapush keys create <name> внутри проекта (ключ сохранится в конфиг автоматически) или экспортируйте переменную.
Можно ли работать в CI, ни разу не вызывая otapush login?
Почти. publish, list, rollback и promote работают только с API-ключом, а keys create достаточно выполнить один раз на машине разработчика. Интерактивный логин действительно нужен лишь init (привязка проекта) и управлению ключами — и то и другое делается один раз при настройке.
Какой канал реально проверяет устройство?
Тот, что указан в URL манифеста. otapush init записывает «голый» URL без параметра канала, а сервер при отсутствии канала обслуживает prod, — значит, сборка по умолчанию проверяет prod, и публикация в dev (а это значение publish по умолчанию!) для неё невидима. Непродовым сборкам нужен URL с ?channel=<name>. См. вопрос про каналы в Быстром старте.
Как откатить только Android-обновление?
Через CLI — никак, пока у канала есть и текущее iOS-обновление: представление CLI о «текущем обновлении канала» ставит iOS вперёд, поэтому otapush rollback --channel <name> целяется в iOS. Используйте вкладку Updates портала — там откат выполняется по платформам.
Что publish делает с уже загруженными ассетами?
Не записывает их повторно. Ассеты хранятся по SHA-256, поэтому повторная публикация неизменившейся картинки не создаёт второй копии — новое обновление просто ссылается на уже сохранённый объект. Бандл, в отличие от ассетов, сохраняется для каждого обновления заново.
Где оказываются --message и git-коммит?
В списке обновлений портала и внутри поля extra отдаваемого манифеста — в приложении оно читается как Updates.manifest.extra. См. «A real manifest response» в Протоколе.