Skip to content

chore: automate example app publishing to app stores - #5118

Open
KisaneNeko wants to merge 9 commits into
callstack:mainfrom
KisaneNeko:feat/automate-example-app-release
Open

KisaneNeko wants to merge 9 commits into
callstack:mainfrom
KisaneNeko:feat/automate-example-app-release

Conversation

@KisaneNeko

@KisaneNeko KisaneNeko commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

The example app in the stores is 20 months out of date. It's only ever been updated by hand (18 times in nine years, against 83 library releases), so it drifts. This ships it with each library release instead.

Related issue

Resolves #4993

Screenshots / Videos

No UI changes.

Test plan

Needs EXPO_TOKEN and the store credentials in EAS first, setup steps are in CONTRIBUTING.md.

On the next release: if no native code changed it should publish an OTA update and stop there. If it did, it should build, land on the Play internal track and TestFlight, and open a version bump PR. Stable releases go to Play production instead.

This also replaces updates.yml, which did the OTA half on labelled PR merges. It never ran once (wrong trigger, so the label check never matched), and both halves belong on the same event.

To check the failure path, cancel a run from the Actions tab and confirm the run page says the library release is unaffected.

@KisaneNeko
KisaneNeko marked this pull request as draft September 14, 2026 06:46
@KisaneNeko
KisaneNeko force-pushed the feat/automate-example-app-release branch from 8da1cee to d1f8f7a Compare September 14, 2026 19:36
@KisaneNeko
KisaneNeko force-pushed the feat/automate-example-app-release branch from d1f8f7a to 4bd1cee Compare September 22, 2026 10:26
@github-actions

Copy link
Copy Markdown

Found potential problems with the pull request:

  • Screenshot or video evidence is missing. Make sure to include one if it affects the UI.

@KisaneNeko
KisaneNeko marked this pull request as ready for review September 22, 2026 11:24
@KisaneNeko
KisaneNeko force-pushed the feat/automate-example-app-release branch from 4bd1cee to fb463b2 Compare September 23, 2026 05:58
@KisaneNeko
KisaneNeko marked this pull request as draft September 24, 2026 10:09
@KisaneNeko
KisaneNeko marked this pull request as ready for review September 25, 2026 13:12
Radoslaw Nowacki and others added 5 commits September 29, 2026 09:11
The example app in the stores drifts away from the library: it was released
eighteen times in nine years against eighty-three library releases, all by
hand, and the last one was twenty months ago. Ship it from the release that
prompts it instead.

What it ships depends on whether native code changed. `runtimeVersion` uses
the fingerprint policy, so the workflow can compare the project against the
builds already out there: unchanged means the new JavaScript runs on what
people already have, and goes out as an update; changed means a build and a
trip through the stores. Only a build carries a version to a store, so only a
build bumps one, and that bump comes back as a pull request.

This replaces the updates workflow, which published updates when a labelled
pull request merged. It never ran once: it triggers on push, where the
pull_request it tests does not exist, so all sixty-nine runs skipped. Tying
both outcomes to the release removes the label nobody remembered to add.

Prereleases and stable releases build against separate channels so an update
published for an alpha cannot reach the stable app. Where a build lands is
decided by the tag rather than by the release's prerelease flag, because
.release-it.json pins every release to `preRelease: true` for the 6.0 alpha
cycle, and reading that flag would route a stable 6.0.0 to the internal track.

Publishing runs after a release already exists, so it cannot fail, revoke or
roll back one. A run that does not succeed says so on its own run page, where
the mail GitHub already sends the publisher leads, and says the release is
unaffected.

Store credentials live in EAS rather than here, so EXPO_TOKEN is the only
secret this needs. Setup and the known limitations are in CONTRIBUTING.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Whether EXPO_TOKEN still works could otherwise only be found out by a real
run, which builds and submits. A dry run authenticates, works out whether the
release would build or update, reports that, and stops.

Also check authentication in its own step, so a missing or expired token fails
in seconds with a message that says so rather than several minutes later
inside a build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ExpoModulesJSI.podspec builds an xcframework into its own source directory
from a prepare_command, and that directory is fingerprinted. EAS fingerprints
after installing pods and the CLI fingerprints before, so the two disagreed
and the build stopped with a runtime version mismatch.

Ignore the generated products. The sources they are built from are
fingerprinted already, so nothing that changes the native runtime stops being
noticed. Without this the workflow would also never publish an update: the
fingerprint it computes on a runner could never match the one EAS recorded.

Drop the build number and version code from the app config as well, now that
EAS assigns both remotely and warns on every command that the values in the
config are ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dry run existed to answer whether EXPO_TOKEN still worked and whether the
store credentials would resolve, without spending a real run. Both have since
been answered by building and submitting for real, so what is left is a second
path through the workflow kept alive to derisk a first run whose worst outcome
is that a demo app does not update.

Trim what publishing adds to CONTRIBUTING.md down to what would otherwise go
wrong quietly: that nothing reaches the App Store on its own, that the version
bump pull request has to be merged before the next release, where the
credentials live, and which generated files have to stay out of the
fingerprint. The run page and the issue a failed run opens already say the
rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@KisaneNeko
KisaneNeko force-pushed the feat/automate-example-app-release branch from eb1f63e to 8e2440e Compare September 29, 2026 07:12
publish:
name: Build and submit example app
runs-on: ubuntu-latest
timeout-minutes: 120

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure about this timeout. since we're on free plan, it could take longer than 2 hours.

probably simpler to just use --no-wait and exit the workflow early, and check status on EAS.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entirely your call so just LMK which option you prefer, but I'd suggest to just remove the line and use default 6 hours timeout. Should be more than enough for the builds to pass, we can see errors without going into EAS, and since waiting is free for opensource repos the only downside is the 6-hour release lock if the build gets stuck which doesn't seem to be a problem with the current release cadence.

Comment on lines +149 to +159
- name: Open version bump PR
if: ${{ always() && (steps.submit.outcome == 'success' || steps.submit.outcome == 'failure' || steps.submit.outcome == 'cancelled') }}
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: main
add-paths: example/app.json
branch: chore/example-app-version-${{ steps.release.outputs.tag }}
commit-message: 'chore: bump example app version for ${{ steps.release.outputs.tag }}'
title: 'chore: bump example app version for ${{ steps.release.outputs.tag }}'
labels: example app

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not have separate PR/commit for version bumps. Does it need to be stored in the repo? Can't it be derived from release tag automatically?

- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why check main? shouldn't it check the tag that was released? it could result in later commits being published instead of the released tag

Comment on lines +88 to +116
run: |
platforms="$PLATFORM"
if [ "$PLATFORM" = "all" ]; then
platforms="ios android"
fi

build=false
for platform in $platforms; do
hash=$(npx expo-updates fingerprint:generate --platform "$platform" | jq -r '.hash')

existing=$(eas build:list \
--platform "$platform" \
--channel "$PROFILE" \
--fingerprint-hash "$hash" \
--status finished \
--limit 1 \
--json \
--non-interactive | jq 'length')

echo "$platform: fingerprint $hash, $existing matching build(s)"
[ "$existing" -eq 0 ] && build=true
done

echo "build=$build" >> "$GITHUB_OUTPUT"
if [ "$build" = true ]; then
echo "Native code changed, building and submitting."
else
echo "No native change, publishing an update instead."
fi

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you check if this fails the workflow on an error instead of continuing? from claude (please verify):

a failed command sends the run down the OTA path.
- The default run shell is bash -e without pipefail. If eas build:list or the fingerprint command fails, jq 'length' gets empty input and prints nothing, so existing is empty.
- [ "" -eq 0 ] then errors inside a && list, which doesn't trigger -e. build stays false, and the run publishes an OTA update and reports success.
- Fix: add shell: bash (it runs with -eo pipefail) and check that existing is a number.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed & fixed

@@ -0,0 +1,50 @@
import { afterEach, expect, it } from '@jest/globals';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't really need tests for basic scripts

Comment thread example/app.json
},
"android": {
"package": "com.callstack.reactnativepaperexample",
"versionCode": 38,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with appVersionSource: "remote" and no local versionCode, ensure we have run eas build:version:set so EAS has a correct version to start from

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is done, I already seeded the values (iOS 26.0.5, Android 38) and they auto-incremented as expected to 26.0.7 and 39 from my test builds

Comment thread example/.fingerprintignore Outdated
@@ -0,0 +1,2 @@
# prevents build failures due to version mismatch

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please clarify this comment. what kind of version mismatch and build failure?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this good enough explaination?
# built by pod install, would make the fingerprint differ from the one EAS computes

previously it had full context but I rewrote it as minimalistic as possible following suggestion on removing AI prose:

# Generated by `pod install`, not source.
#
# ExpoModulesJSI.podspec has a prepare_command that builds an xcframework into
# this directory. EAS fingerprints after installing pods and the CLI fingerprints
# before, so without this the two disagree and a build fails with a runtime
# version mismatch. The sources it is built from are fingerprinted already.

Comment thread CONTRIBUTING.md Outdated

NOTE: You must have a `GITHUB_TOKEN` environment variable available. You can create a GitHub access token with the "repo" access [here](https://github.com/settings/tokens).

We use EAS for auto-deployments of the example app. Releases with no native changes ship OTA. Releases with native code changes will be pushed to Stores automatically and job will create a PR with version bump, merge it to avoid two releases shipping under the same version. Android will also be auto-submitted for review, iOS still requires manual submission. Pre-releases are shipped to internal track and TestFlight only.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make this more user focused. it should say what to do. something like:

Suggested change
We use EAS for auto-deployments of the example app. Releases with no native changes ship OTA. Releases with native code changes will be pushed to Stores automatically and job will create a PR with version bump, merge it to avoid two releases shipping under the same version. Android will also be auto-submitted for review, iOS still requires manual submission. Pre-releases are shipped to internal track and TestFlight only.
Releases from main also deploy the example app via EAS. If there were no native code changes, it'll ship an OTA update.
Otherwise, for stable releases:
- The Android build is automatically submitted to Google Play for review
- The iOS build is automatically uploaded to App Store Connect and needs to be manually submitted for review
Pre-release builds are shipped to the internal track on Google Play for Android and TestFlight for iOS. They shouldn't be submitted to the App Store for review, or the stable build with the same version would be rejected.

Comment on lines +4 to +5
release:
types: [published]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure we only deploy on releases on main. we may need to fetch tags from main and compare to skip releases from other branches such as 5.x

Comment on lines +144 to +147
eas update \
--branch "$PROFILE" \
--message "$TAG" \
--non-interactive

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
eas update \
--branch "$PROFILE" \
--message "$TAG" \
--non-interactive
eas update \
--branch "$PROFILE" \
--platform "$PLATFORM" \
--message "$TAG" \
--non-interactive

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: automate publishing of the Example App

2 participants