OpenAIExpoJul 23, 2026, 1:15 PM

How Posh went from manual weekly mobile releases to continuous delivery with Expo

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

openaienmodel: gpt-5-mini-2025-08-07

How Posh moved from weekly manual releases to continuous delivery with Expo

Key Points

  • Automated PR previews with QR-code installs and fingerprint checks
  • OTA branches batch updates to preview channels; version branches gate native releases
  • Builds/submissions run only on labeled version branches; store release remains manual

Summary

Posh rebuilt its mobile CI/CD using Expo Updates, Expo Fingerprints, and EAS Workflows to remove manual weekly release tasks. The new system uses automated PR previews, OTA update branches, and version branches to only trigger builds when necessary, batch OTA publishes, and keep store submissions gated by human approval. The result: multiple OTA updates per week, monthly store submissions, fewer release bottlenecks, and faster review cycles.

Key Points

  • Preconditions: split app environments (dev/preview/production) and enable Expo Updates with fingerprints.
  • Automation 1 — PR previews: EAS workflow comments a QR code on PRs (skip builds if no fingerprint), adds a "needs new build" label for native changes, and provides install/troubleshooting instructions for fast review.
  • Automation 2 — OTA branches: master opens or rebases ota_update_* branches to batch OTA publishes; pushes to those branches publish to preview environments only and changelog diff contains batched PR entries.
  • Automation 3 — Version branches: when native changes land, master closes OTA branches and creates a version branch; builds & submissions are only run after manually adding a "build & submit" label, which triggers EAS prepackaged builds and staged submissions, auto-invites testers, and posts build links.
  • Automation 4 — Master workflow: central orchestration that fingerprints commits, creates/rebases OTA or version branches, and creates GitHub releases when branches merge; store release remains manual to preserve human review.
  • Practical rules: avoid over-triggering EAS builds, gate store submissions with labels, auto-update changelog & package.json version, and notify QA/testers via Slack or PR comments.

Impact

  • Reduced release friction and cognitive load: no weekly rotating release manager.
  • Faster iteration: multiple OTA updates per week and fewer, deliberate store releases.
  • Safer releases: targeted builds, staged preview publishes, and automated tester notifications.

Next steps (engineer checklist)

  • Configure environments per Beto's multi-env tutorial.
  • Enable Expo Fingerprints and install EAS Workflows templates.
  • Implement PR preview workflow that comments QR codes and sets labels.
  • Add master workflows to manage ota_update_* and version branches and gate builds with labels.

Full Translation

Translations

A translation section that keeps the flow of the original article.

openaijamodel: gpt-5-mini-2025-08-07

Poshが週次の手動モバイルリリースからExpoを使った継続的デリバリへ移行した方法

ゲスト投稿 — David Davidov(Posh創業エンジニア)

これは、Posh(イベントを通じて主催者がコミュニティを作り、スケールし、マネタイズできるリアル体験のソーシャルマーケットプレイス)で創業エンジニアを務めるDavid Davidovによるゲスト投稿です。

数週間にわたる長いアプリ審査サイクルやタイムセンシティブな手動リリースに悩まされた結果、我々のチームはモバイルのCI/CDパイプラインを再構築し、可能な限り手動作業を排除する投資を行いました。参考までに、当社のアプリはiOSとAndroidで何百万ものユーザーにサービスを提供しているため、ストアへのリリースを安全かつ確実に、かつ予定通りに行うことは常に最優先事項でした。その投資は実を結び、リリース速度の改善はApp StoreのEntertainmentカテゴリでNo.1アプリを獲得する成長を支えるのに役立ちました。

Poshが大手と競う中で判明した課題

モバイルアプリの開発を始めてから2年以上、ストア経由のリリースという手動プロセスがずっと嫌いでした。我々はExpo Updatesを知っていましたが、fingerprintingやEAS Workflowsのpre-packaged jobs以前は、OTAアップデートの自動化はどこか不安定でエラーの余地が大きいと感じていました。

直近2年間で直面した主な痛点は次のとおりです。

  • Linear上のタスクが特定のアプリバージョンにいつリリースされたかの可視化がない。
  • 週替わりでRelease Trainマネージャーをまわす必要があり、その人がその週の変更セットのリリース責任を負っていた ← これが最大のボトルネック
  • リリースプロセスへのE2Eテストの自動統合
  • 維持が大変な不安定なGitHub Actionsでビルドや提出をキックしていた
  • エンジニアのPRが新しいネイティブ変更を導入したかどうかを理解する手段がない

これらすべての痛点が、モバイル上での開発をクリエイティブで高速な体験というよりは雑務のように感じさせていました。Expoのfingerprinting、workflows、OTAアップデートに関する進化を改めて見直した後、私はリリースシステムを根本から再設計し、重要な手動承認ポイントは残しつつ可能な限り自動化することにしました。この自動化により、我々は週次の手動リリースからほぼ毎月のストアリリースと週に複数回のOTAアップデートへと移行できました。

解決したかったこと

  • PRで導入された変更をインストール・テストする簡単な方法、かつそのPRがネイティブの変更を導入したかどうかを理解できること
  • EASビルドを過剰にトリガーしないシステム。テストを開始する準備ができたときだけ起動する
  • OTAアップデートを段階的に導入し、ステージングでテスト可能にすること(masterにマージされた変更が自動的に本番にデプロイされないようにする)
  • ネイティブ変更がmasterに入ったときだけ新しいバージョンブランチを作る(常にブランチを増やす必要を減らす)
  • 本番に新しいバージョンがリリースされたときに、チームに通知してそのバージョンに含まれる変更を自動で伝えることを排除する

以下では、本番リリースにかける時間を減らすために我々が行ったことを紹介します。

前提条件

  • Betoの「アプリ環境を分割する」ビデオを見ることを推奨します。これは特定のupdate channels / app environmentsを設定する際に必要になります。チュートリアルの最後には1台のデバイスにdevelopment、preview/staging、productionビルドをインストールできるようになります。これによりビルド間の切り替えが簡単になり、アプリのテストが容易になります。
  • プロジェクトに対してExpo UpdatesをExpo Fingerprintsで設定してください。

Automation 1: PRプレビューの設定

これはCI/CDワークフローに必須ではありませんが、特にagenticなワークフローを好むチームでは変更のレビューを速めるのに役立ちます。この記事を読んで刺激を受け、私はQRコードでPRにコメントしてdevelopmentアプリに新しい変更をインストールできるEAS workflowを作成しました。コードの詳細には深入りしませんが、このワークフローでいくつか変更した点は次の通りです。

  • fingerprintが見つからなければアプリをビルドしない(developmentにOTAできないため)。クレジット消費が膨らむと思ったのでこの判断にしました。将来的にエージェントを使ったパッケージ更新を自動化したくなれば変更するかもしれません。
  • 新しいネイティブコードを導入するPRには needs new build ラベルを付与する。これはGitHub PRのフィルタリング用です(ラベルは私のお気に入りです)。
  • Expoの例にあるコメントをカスタムに編集。いくつかの手順、当該fingerprintの最近のビルドへのリンク、トラブルシューティング手順、大きめのQRコードを追加しました(画面で目立つように)。

PR Review [object Object]

PR Review workflow [object Object]

これにより、DevinやClaudeに特定箇所の修正を依頼したときにすぐにアプリをフラッシュして何が変わったかを把握できます。外出先でもレビューして出荷できるようになりました🏃‍♂️✨

Automation 2: OTAブランチ

いくつかのPRをmasterにマージしたとします。それらのPRが自動的に本番へデプロイされるのは避けたいです。OTAブランチを作ることで、各PRがOTAをトリガーする代わりに複数の変更をまとめてOTAをバッチ化する機会を得られます。

OTAブランチでは2つのワークフローが必要です。

  • master/main用ワークフロー。これで以下を行います。

    • カレントのアプリバージョン下にOTAブランチを立ち上げる(任意でOTAブランチラベル、例: 8.0.0 OTA #1
    • ラベルによりPRのフィルタリングが容易になる✨
    • 既にOTAブランチが開いている場合はそれをmasterの上にリベースする
    • そのPR内のchangelogに新しく導入されたPRタイトルとIssue番号を自動で追加する
    • OTAブランチがmasterにマージされたときに本番向けのEAS Updateをトリガーするステップ
    • master/mainワークフローは継続して拡張します。最後のセクションでmasterワークフローの概観を示します。
  • OTAブランチに対するpushで動くワークフロー。これには次を含みます。

    • preview環境へのOTA publish

例は次のようになります(YAML):

name: OTA Preview
on:
  push:
    branches: ['ota_update_*']
jobs:
  fingerprint:
    name: Fingerprint
    type: fingerprint
    environment: preview
  publish_preview_android:
    name: Publish Android preview OTA
    needs: [fingerprint]
    type: update
    environment: preview
    params:
      platform: android
      branch: preview
  publish_preview_ios:
    name: Publish iOS preview OTA
    needs: [fingerprint]
    type: update
    environment: preview
    params:
      platform: ios
      branch: preview

OTA Update branch [object Object]

PR preview workflow [object Object]

OTAブランチ内の差分は基本的にchangelogの更新だけにするべきです。例:

## v8.15.1 OTA 1 (2026-04-13)
- Sample JS Changes white again #166
- Sample JS Changes green again #165
- Sample JS Changes #163
- Sample JS Changes #161
- Sample JS Changes #160
- Sample JS Changes #158

## v8.15.1 (older version)

最後に、changelogの使い方に関しては後の自動化セクションで再度触れます。

Automation 3: Versionブランチ

PRで新しいネイティブ変更が入ったとします。その場合、進行中のOTAブランチをクローズ(無効化)して、新しいversionブランチを立ち上げることを期待します。デプロイされるまでそのブランチを使います。これがエンジニアチームにとって、ストア向けの新しいリリースを作る必要があるかどうかの真実のソースになります。

VersionブランチでもOTAと同様に2つのワークフローを用意します。

  • masterワークフローを拡張してネイティブ変更を扱う。

    • もしOTAブランチが存在するならそれを閉じ、新しいVersionブランチを作成してVersion Branchラベルを付ける
    • mobile appのpackage.jsonのversionを自動で上げる
    • 既にversionブランチが存在する場合はそれをmasterにリベースし、OTAブランチと同様にchangelogを更新する
  • 手動でbuild & submit bundlesラベルを付けたときに実行されるワークフロー。

    • これにより、チームがビルドを切る準備ができたときだけストアに提出することを保証する
    • Expoのprepackaged workflowコマンドを使ってstagingトラック向けにビルド&提出する
    • 両ストアで新しいバンドルが検出されると、我々のE2Eテスターを自動招待する
    • 提出後、Slack経由でE2Eテスターにテスト開始を通知する

例(YAML):

name: Version Branch Builds
on:
  pull_request:
    types: [labeled]
concurrency:
  cancel_in_progress: true
  group: ${{ workflow.filename }}-${{ github.ref }}
jobs:
  build_production_android:
    name: Build Production Android
    if: ${{ github.event.label.name == 'build and submit' }}
    type: build
    environment: production
    params:
      platform: android
      profile: production
  build_production_ios:
    name: Build Production iOS
    if: ${{ github.event.label.name == 'build and submit' }}
    type: build
    environment: production
    params:
      platform: ios
      profile: production
  build_preview_android:
    name: Build Preview Android
    if: ${{ github.event.label.name == 'build and submit' }}
    type: build
    environment: preview
    params:
      platform: android
      profile: preview
  build_preview_ios:
    name: Build Preview iOS
    if: ${{ github.event.label.name == 'build and submit' }}
    type: build
    environment: preview
    params:
      platform: ios
      profile: preview
  build_development_android:
    name: Build Development Android
    if: ${{ github.event.label.name == 'build and submit' }}
    type: build
    environment: development
    params:
      platform: android
      profile: development
  build_development_ios:
    name: Build Development iOS
    if: ${{ github.event.label.name == 'build and submit' }}
    type: build
    environment: development
    params:
      platform: ios
      profile: development
  submit_production_android:
    name: Submit Production Android
    needs: [build_production_android]
    type: submit
    environment: production
    params:
      build_id: ${{ needs.build_production_android.outputs.build_id }}
      profile: production
  submit_production_ios:
    name: Submit Production iOS
    needs: [build_production_ios]
    type: submit
    environment: production
    params:
      build_id: ${{ needs.build_production_ios.outputs.build_id }}
      profile: production
  submit_preview_android:
    name: Submit Preview Android
    needs: [build_preview_android]
    type: submit
    environment: preview
    params:
      build_id: ${{ needs.build_preview_android.outputs.build_id }}
      profile: preview
  submit_preview_ios:
    name: Submit Preview iOS
    needs: [build_preview_ios]
    type: submit
    environment: preview
    params:
      build_id: ${{ needs.build_preview_ios.outputs.build_id }}
      profile: preview
  notify_testers:
    name: Notify QA Team
    needs: [build_preview_android, build_preview_ios, submit_preview_android, submit_preview_ios]
    environment: preview
    steps:
      - name: Send Slack message
        run: |
          # Notify your QA team that new preview builds are available.
          # Include whatever your team needs, such as build numbers,
          # install links, release notes, or testing instructions.
  comment_builds:
    name: Post Build Links
    needs: [ build_production_android, build_production_ios, build_preview_android, build_preview_ios, build_development_android, build_development_ios ]
    environment: production
    steps:
      - name: Post comment on PR
        run: |
          # Post a PR comment with links to the generated EAS builds.
          # The implementation depends on how your CI environment exposes
          # repository, pull request, and authentication context.

workflow graph [object Object]

PRには新しいバージョン番号を反映したchangelogとmobile appのpackage.jsonの変更が入るはずです。手動の部分はこれで提出して審査に出し、リリースボタンを押すことだけになります。バンドル以外の変更が含まれる可能性があるため、我々はその部分を自動化しない方針です。

Automation 4: masterワークフロー

この段階で、masterワークフローは次を担うべきです。

  • PRがマージされるたびにOTAブランチを作成・リベースする
  • PRがマージされるたびにversionブランチを作成・リベースし、重複するOTAブランチを閉じる
  • OTAまたはversionブランチがマージされたときにGitHub上で新しいリリースを作成する

例(YAML):

name: Production Deploy
on:
  push:
    branches: ['main']
concurrency:
  cancel_in_progress: true
  group: ${{ workflow.filename }}-${{ github.ref }}
jobs:
  fingerprint:
    name: Fingerprint
    type: fingerprint
    environment: production
  get_android_build:
    name: Check Android build
    needs: [fingerprint]
    type: get-build
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
      profile: produc

(注: 上記は抜粋です。実際のファイルでは更に多くのステップとエラーハンドリングがあります。)


これらの自動化を組み合わせることで、我々はリリースにかかる人的コストを大幅に削減し、頻繁なOTAと計画されたストア提出の両方を安全に実行できるようになりました。興味があれば、具体的なワークフローの全文やスクリプト例も共有します。