-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
base: main
Are you sure you want to change the base?
Changes from all commits
4f76fd0
8047374
977d006
1342c6e
8e2440e
8fb6c25
959a4ba
57b9cf8
121126e
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 }} | ||||||||||||||||||||
| 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. |
||||||||||||||||||||
| 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 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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 | ||||||||||||||||||||
| 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 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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 | ||||||||||||||||||||
| 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 | ||||||||||||||||||||
|
Comment on lines
+130
to
+170
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 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.
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. confirmed & fixed |
||||||||||||||||||||
|
|
||||||||||||||||||||
| - 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 | ||||||||||||||||||||
|
Comment on lines
+195
to
+199
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.
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| - name: Summarise | ||||||||||||||||||||
| if: ${{ !cancelled() && (steps.submit.outcome == 'success' || steps.update.outcome == 'success') }} | ||||||||||||||||||||
| 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: | ||||||||||||||||||||
| 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", | ||
| "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