chore: automate example app publishing to app stores - #5118
KisaneNeko wants to merge 9 commits into
Conversation
8da1cee to
d1f8f7a
Compare
d1f8f7a to
4bd1cee
Compare
|
Found potential problems with the pull request:
|
4bd1cee to
fb463b2
Compare
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>
eb1f63e to
8e2440e
Compare
| publish: | ||
| name: Build and submit example app | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 120 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| - 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
confirmed & fixed
| @@ -0,0 +1,50 @@ | |||
| import { afterEach, expect, it } from '@jest/globals'; | |||
There was a problem hiding this comment.
we don't really need tests for basic scripts
| }, | ||
| "android": { | ||
| "package": "com.callstack.reactnativepaperexample", | ||
| "versionCode": 38, |
There was a problem hiding this comment.
with appVersionSource: "remote" and no local versionCode, ensure we have run eas build:version:set so EAS has a correct version to start from
There was a problem hiding this comment.
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
| @@ -0,0 +1,2 @@ | |||
| # prevents build failures due to version mismatch | |||
There was a problem hiding this comment.
please clarify this comment. what kind of version mismatch and build failure?
There was a problem hiding this comment.
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.
|
|
||
| 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. |
There was a problem hiding this comment.
let's make this more user focused. it should say what to do. something like:
| 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. |
| release: | ||
| types: [published] |
There was a problem hiding this comment.
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
| eas update \ | ||
| --branch "$PROFILE" \ | ||
| --message "$TAG" \ | ||
| --non-interactive |
There was a problem hiding this comment.
| eas update \ | |
| --branch "$PROFILE" \ | |
| --message "$TAG" \ | |
| --non-interactive | |
| eas update \ | |
| --branch "$PROFILE" \ | |
| --platform "$PLATFORM" \ | |
| --message "$TAG" \ | |
| --non-interactive |
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_TOKENand 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.