Repository navigation
chore: automate example app publishing to app stores #5118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
4f76fd0
8047374
977d006
1342c6e
8e2440e
8fb6c25
959a4ba
57b9cf8
121126e
a9fa195
69f201a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,225 @@ | ||||||||||||||||||||||||
| name: Publish example app | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||
| release: | ||||||||||||||||||||||||
| types: [published] | ||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||
| tag: | ||||||||||||||||||||||||
| description: Release tag to publish the example app for, e.g. v6.0.0 | ||||||||||||||||||||||||
| required: true | ||||||||||||||||||||||||
| platform: | ||||||||||||||||||||||||
| description: Platform to build. Defaults to all. | ||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||
| default: all | ||||||||||||||||||||||||
| type: choice | ||||||||||||||||||||||||
| options: | ||||||||||||||||||||||||
| - all | ||||||||||||||||||||||||
| - ios | ||||||||||||||||||||||||
| - android | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| defaults: | ||||||||||||||||||||||||
| run: | ||||||||||||||||||||||||
| shell: bash | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||
| group: publish-example-app | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||
| check: | ||||||||||||||||||||||||
| name: Check release is on main | ||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||
| on-main: ${{ steps.check.outputs.on-main }} | ||||||||||||||||||||||||
| sha: ${{ steps.check.outputs.sha }} | ||||||||||||||||||||||||
|
Comment on lines
+31
to
+36
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have
Suggested change
Then we can drop TAG: ${{ needs.check.outputs.tag }} |
||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||
| - id: check | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||||||||
| GH_REPO: ${{ github.repository }} | ||||||||||||||||||||||||
| TAG: ${{ github.event.release.tag_name || inputs.tag }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| status=$(gh api "repos/$GH_REPO/compare/main...$TAG" --jq .status) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if [ "$status" = "identical" ] || [ "$status" = "behind" ]; then | ||||||||||||||||||||||||
| echo "on-main=true" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "sha=$(gh api "repos/$GH_REPO/commits/$TAG" --jq .sha)" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| else | ||||||||||||||||||||||||
| echo "on-main=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "$TAG is not on main ($status), nothing to deploy." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| publish: | ||||||||||||||||||||||||
| name: Build and submit example app | ||||||||||||||||||||||||
| needs: check | ||||||||||||||||||||||||
| if: needs.check.outputs.on-main == 'true' | ||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||
| timeout-minutes: 120 | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, 6 hours sounds fine. The repo is public, so the waiting runner doesn't cost us anything, and 6 hours is also the limit for GitHub-hosted runners anyway. That's enough room for the free-tier queue plus both builds and submissions. If a run ever goes past that, the build keeps going on EAS, so the worst case is a failed run while the build still lands. Let's remove the line. |
||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||
| - name: Checkout | ||||||||||||||||||||||||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||
| ref: ${{ needs.check.outputs.sha }} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Setup | ||||||||||||||||||||||||
| uses: ./.github/actions/setup | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Resolve release tag and submit profile | ||||||||||||||||||||||||
| id: release | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| TAG: ${{ github.event.release.tag_name || inputs.tag }} | ||||||||||||||||||||||||
| PLATFORM: ${{ inputs.platform }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| if [ -z "$TAG" ]; then | ||||||||||||||||||||||||
| echo "::error::No release tag to publish the example app for." | ||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check never runs.
Suggested change
|
||||||||||||||||||||||||
| if [ "${TAG#*-}" != "$TAG" ]; then | ||||||||||||||||||||||||
| profile=preview | ||||||||||||||||||||||||
| else | ||||||||||||||||||||||||
| profile=production | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| platform=${PLATFORM:-all} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "profile=$profile" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "platform=$platform" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "Publishing $platform for $TAG using the $profile submit profile." | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Setup Expo | ||||||||||||||||||||||||
| uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1 | ||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||
| eas-version: latest | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we pin |
||||||||||||||||||||||||
| token: ${{ secrets.EXPO_TOKEN }} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Check Expo authentication | ||||||||||||||||||||||||
| working-directory: ./example | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| if ! eas whoami; then | ||||||||||||||||||||||||
| echo "::error::Could not authenticate with Expo, check EXPO_TOKEN." | ||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Set example app version from the release tag | ||||||||||||||||||||||||
| working-directory: ./example | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| TAG: ${{ steps.release.outputs.tag }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| version="${TAG#v}" | ||||||||||||||||||||||||
| version="${version%%-*}" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if ! echo "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then | ||||||||||||||||||||||||
| echo "::error::Could not read a major.minor.patch version from $TAG." | ||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| jq --arg version "$version" '.expo.version = $version' app.json > app.json.tmp | ||||||||||||||||||||||||
| mv app.json.tmp app.json | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Decide between an update and a build | ||||||||||||||||||||||||
| id: decide | ||||||||||||||||||||||||
| working-directory: ./example | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| PROFILE: ${{ steps.release.outputs.profile }} | ||||||||||||||||||||||||
| PLATFORM: ${{ steps.release.outputs.platform }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| platforms="$PLATFORM" | ||||||||||||||||||||||||
| if [ "$PLATFORM" = "all" ]; then | ||||||||||||||||||||||||
| platforms="ios android" | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if [ -z "$platforms" ]; then | ||||||||||||||||||||||||
| echo "::error::No platforms to check." | ||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here,
Suggested change
|
||||||||||||||||||||||||
| build=false | ||||||||||||||||||||||||
| for platform in $platforms; do | ||||||||||||||||||||||||
| hash=$(npx expo-updates fingerprint:generate --platform "$platform" | jq -r '.hash') | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ensure fingerprint doesn't change because of version bumps. otherwise it won't match next run and it'll trigger rebuild again. may need a fingerprint config ( /** @type {import('expo/fingerprint').Config} */
module.exports = {
sourceSkips: [
'ExpoConfigVersions',
'PackageJsonAndroidAndIosScriptsIfNotContainRun',
],
};
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great catch, thank you |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| existing=$(eas build:list \ | ||||||||||||||||||||||||
| --platform "$platform" \ | ||||||||||||||||||||||||
| --channel "$PROFILE" \ | ||||||||||||||||||||||||
| --fingerprint-hash "$hash" \ | ||||||||||||||||||||||||
| --status finished \ | ||||||||||||||||||||||||
| --limit 1 \ | ||||||||||||||||||||||||
| --json \ | ||||||||||||||||||||||||
| --non-interactive | jq 'if type == "array" then length else "notanarray" end') | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| case "$existing" in | ||||||||||||||||||||||||
| ''|*[!0-9]*) | ||||||||||||||||||||||||
| echo "::error::Could not read builds for $platform." | ||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||
| ;; | ||||||||||||||||||||||||
| esac | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| echo "$platform: fingerprint $hash, $existing matching build(s)" | ||||||||||||||||||||||||
| [ "$existing" -eq 0 ] && build=true | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When We could keep a list of the platforms that need a build, build only those, and publish an update for the rest. Not a blocker though, fine as a follow-up.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did it in this PR since it was small. Decide now splits platforms into a build list and an update list, so only the one with native changes gets built and the other gets an OTA update. The update step also runs if the other platform's build fails. |
||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Build and submit to the app stores | ||||||||||||||||||||||||
| id: submit | ||||||||||||||||||||||||
| if: steps.decide.outputs.build == 'true' | ||||||||||||||||||||||||
| working-directory: ./example | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| PROFILE: ${{ steps.release.outputs.profile }} | ||||||||||||||||||||||||
| PLATFORM: ${{ steps.release.outputs.platform }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| eas build \ | ||||||||||||||||||||||||
| --platform "$PLATFORM" \ | ||||||||||||||||||||||||
| --profile "$PROFILE" \ | ||||||||||||||||||||||||
| --auto-submit-with-profile "$PROFILE" \ | ||||||||||||||||||||||||
| --non-interactive | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Publish an update | ||||||||||||||||||||||||
| id: update | ||||||||||||||||||||||||
| if: steps.decide.outputs.build == 'false' | ||||||||||||||||||||||||
| working-directory: ./example | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| PROFILE: ${{ steps.release.outputs.profile }} | ||||||||||||||||||||||||
| PLATFORM: ${{ steps.release.outputs.platform }} | ||||||||||||||||||||||||
| TAG: ${{ steps.release.outputs.tag }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| eas update \ | ||||||||||||||||||||||||
| --channel "$PROFILE" \ | ||||||||||||||||||||||||
| --platform "$PLATFORM" \ | ||||||||||||||||||||||||
| --message "$TAG" \ | ||||||||||||||||||||||||
| --non-interactive | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Summarise | ||||||||||||||||||||||||
| if: ${{ !cancelled() && (steps.submit.outcome == 'success' || steps.update.outcome == 'success') }} | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can drop this Or we could write the summary at the end of the build and update steps and remove this step entirely.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Went with the second option, the build and update steps write their own summary and the Summarise step is gone. |
||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| TAG: ${{ steps.release.outputs.tag }} | ||||||||||||||||||||||||
| PROFILE: ${{ steps.release.outputs.profile }} | ||||||||||||||||||||||||
| BUILT: ${{ steps.decide.outputs.build }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| if [ "$BUILT" = "true" ]; then | ||||||||||||||||||||||||
| echo "Built and submitted $TAG using the $PROFILE profile." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
| else | ||||||||||||||||||||||||
| echo "Published an OTA update for $TAG on the $PROFILE channel." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| report-failure: | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We start a separate runner here just to print one annotation. The run already shows up as failed, and GitHub already sends an email about it to whoever published the release. If we want to keep the message, a last step in
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved it into a last step in |
||||||||||||||||||||||||
| name: Report failure | ||||||||||||||||||||||||
| needs: [check, publish] | ||||||||||||||||||||||||
| if: ${{ always() && (needs.check.result == 'failure' || (needs.publish.result != 'success' && needs.publish.result != 'skipped')) }} | ||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||
| - name: Report failure | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| TAG: ${{ github.event.release.tag_name || inputs.tag }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| echo "::error::Example app deployment failed for $TAG. The release itself is fine, only the demo app did not update." | ||||||||||||||||||||||||
| echo "Check EAS and the stores before re-running, a build may already have been submitted." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # built by pod install, would make the fingerprint differ from the one EAS computes | ||
| **/expo-modules-jsi/apple/Products/** |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,11 +16,12 @@ | |
| "owner": "react-native-paper", | ||
| "ios": { | ||
| "bundleIdentifier": "com.callstack.reactnativepaperexample", | ||
| "buildNumber": "26.0.5" | ||
| "infoPlist": { | ||
| "ITSAppUsesNonExemptEncryption": false | ||
| } | ||
| }, | ||
| "android": { | ||
| "package": "com.callstack.reactnativepaperexample", | ||
| "versionCode": 38, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| "permissions": [ | ||
| "READ_EXTERNAL_STORAGE", | ||
| "WRITE_EXTERNAL_STORAGE" | ||
|
|
@@ -37,7 +38,7 @@ | |
| } | ||
| }, | ||
| "runtimeVersion": { | ||
| "policy": "sdkVersion" | ||
| "policy": "fingerprint" | ||
| }, | ||
| "updates": { | ||
| "url": "https://u.expo.dev/ba8f5139-58fe-48f0-b2f2-2d675b6eb2d4" | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||
| { | ||||
| "cli": { | ||||
| "appVersionSource": "remote" | ||||
| }, | ||||
| "build": { | ||||
| "production": { | ||||
| "node": "24.13.0", | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the profile doesn't set
Suggested change
One catch is that ln -s ../.nvmrc example/.nvmrc |
||||
| "autoIncrement": true, | ||||
| "android": { | ||||
| "buildType": "app-bundle" | ||||
| }, | ||||
| "channel": "production" | ||||
| }, | ||||
| "preview": { | ||||
| "extends": "production", | ||||
| "channel": "preview" | ||||
| } | ||||
| }, | ||||
| "submit": { | ||||
| "production": { | ||||
| "android": { | ||||
| "track": "production" | ||||
| }, | ||||
| "ios": { | ||||
| "ascAppId": "1548934513" | ||||
| } | ||||
| }, | ||||
| "preview": { | ||||
| "extends": "production", | ||||
| "android": { | ||||
| "track": "internal" | ||||
| } | ||||
| } | ||||
| } | ||||
| } | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** @type {import('expo/fingerprint').Config} */ | ||
| module.exports = { | ||
| sourceSkips: [ | ||
| 'ExpoConfigVersions', | ||
| 'PackageJsonAndroidAndIosScriptsIfNotContainRun', | ||
| ], | ||
| }; |
There was a problem hiding this comment.
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