Справочник 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. Без логина они останавливаются с «Run otapush 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.updatesurl, 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-ключ и отказывается работать только с сессией логина. Затем, для каждой платформы:

  1. Экспорт. 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 определяется автоматически).
  2. Загрузка 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-ключом. Настройка занимает три строки:

  1. Локально, один раз: otapush init (создаст otapush.config.json), затем otapush keys create ci.
  2. Закоммитьте otapush.config.json — там URL сервера, id приложения и slug, без секретов, — а ключ положите в хранилище секретов CI как OTAPUSH_API_KEY. (Если ключ попал в конфиг из keys create, удалите поле apiKey перед коммитом — или держите секрет только в переменной окружения.)
  3. В 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» в Протоколе.