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 all 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,206 @@ | ||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||
| check: | ||||||||||||||||||||||||
| name: Check release is on main | ||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| TAG: ${{ github.event.release.tag_name || inputs.tag }} | ||||||||||||||||||||||||
| 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 }} |
||||||||||||||||||||||||
| tag: ${{ env.TAG }} | ||||||||||||||||||||||||
| profile: ${{ steps.check.outputs.profile }} | ||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||
| - id: check | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||||||||
| GH_REPO: ${{ github.repository }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| status=$(gh api "repos/$GH_REPO/compare/main...$TAG" --jq .status) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if [ "$status" = "identical" ] || [ "$status" = "behind" ]; then | ||||||||||||||||||||||||
| sha=$(gh api "repos/$GH_REPO/commits/$TAG" --jq .sha) | ||||||||||||||||||||||||
| echo "on-main=true" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "sha=$sha" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| else | ||||||||||||||||||||||||
| echo "on-main=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "::warning::$TAG is not on main ($status), nothing to deploy." | ||||||||||||||||||||||||
| echo "$TAG is not on main ($status), nothing to deploy." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if [ "${TAG#*-}" != "$TAG" ]; then | ||||||||||||||||||||||||
| echo "profile=preview" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| else | ||||||||||||||||||||||||
| echo "profile=production" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| publish: | ||||||||||||||||||||||||
| name: Build and submit example app | ||||||||||||||||||||||||
| needs: check | ||||||||||||||||||||||||
| if: needs.check.outputs.on-main == 'true' | ||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||
| # One queue per channel, so a pending preview run doesn't cancel a production one | ||||||||||||||||||||||||
| group: publish-example-app-${{ needs.check.outputs.profile }} | ||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||
| - name: Checkout | ||||||||||||||||||||||||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||
| ref: ${{ needs.check.outputs.sha }} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Setup | ||||||||||||||||||||||||
| uses: ./.github/actions/setup | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Setup Expo | ||||||||||||||||||||||||
| uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1 | ||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||
| eas-version: 24.12.1 | ||||||||||||||||||||||||
| 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: ${{ needs.check.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: ${{ needs.check.outputs.profile }} | ||||||||||||||||||||||||
| PLATFORM: ${{ inputs.platform || 'all' }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| platforms="$PLATFORM" | ||||||||||||||||||||||||
| if [ "$PLATFORM" = "all" ]; then | ||||||||||||||||||||||||
| platforms="ios android" | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| build=() | ||||||||||||||||||||||||
| update=() | ||||||||||||||||||||||||
| for platform in $platforms; do | ||||||||||||||||||||||||
| hash=$(npx expo-updates fingerprint:generate --platform "$platform" | jq -er .hash) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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)" | ||||||||||||||||||||||||
| if [ "$existing" -eq 0 ]; then | ||||||||||||||||||||||||
| build+=("$platform") | ||||||||||||||||||||||||
| else | ||||||||||||||||||||||||
| update+=("$platform") | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
| done | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # EAS takes a single platform or "all" | ||||||||||||||||||||||||
| to_platform() { | ||||||||||||||||||||||||
| case $# in | ||||||||||||||||||||||||
| 0) echo "" ;; | ||||||||||||||||||||||||
| 1) echo "$1" ;; | ||||||||||||||||||||||||
| *) echo all ;; | ||||||||||||||||||||||||
| esac | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| echo "build=$(to_platform "${build[@]}")" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "update=$(to_platform "${update[@]}")" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||
| echo "Building: ${build[*]:-none}. Updating: ${update[*]:-none}." | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Publish an update | ||||||||||||||||||||||||
| # Before the build, so a slow or timed-out build can't hold up the update | ||||||||||||||||||||||||
| if: steps.decide.outputs.update != '' | ||||||||||||||||||||||||
| working-directory: ./example | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| PROFILE: ${{ needs.check.outputs.profile }} | ||||||||||||||||||||||||
| PLATFORM: ${{ steps.decide.outputs.update }} | ||||||||||||||||||||||||
| TAG: ${{ needs.check.outputs.tag }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| eas update \ | ||||||||||||||||||||||||
| --channel "$PROFILE" \ | ||||||||||||||||||||||||
| --platform "$PLATFORM" \ | ||||||||||||||||||||||||
| --message "$TAG" \ | ||||||||||||||||||||||||
| --non-interactive | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| echo "Published an OTA update for $TAG for $PLATFORM on the $PROFILE channel." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Build and submit to the app stores | ||||||||||||||||||||||||
| # Don't let a failed update for one platform skip the other's build | ||||||||||||||||||||||||
| if: ${{ !cancelled() && steps.decide.outputs.build != '' }} | ||||||||||||||||||||||||
| working-directory: ./example | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| PROFILE: ${{ needs.check.outputs.profile }} | ||||||||||||||||||||||||
| PLATFORM: ${{ steps.decide.outputs.build }} | ||||||||||||||||||||||||
| TAG: ${{ needs.check.outputs.tag }} | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| eas build \ | ||||||||||||||||||||||||
| --platform "$PLATFORM" \ | ||||||||||||||||||||||||
| --profile "$PROFILE" \ | ||||||||||||||||||||||||
| --auto-submit-with-profile "$PROFILE" \ | ||||||||||||||||||||||||
| --non-interactive | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| echo "Built and submitted $TAG for $PLATFORM using the $PROFILE profile." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Report failure | ||||||||||||||||||||||||
| if: failure() || cancelled() | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| TAG: ${{ needs.check.outputs.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 still be running or already submitted. Re-running before it finishes starts a second build." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
| echo "If the build finished but the store upload failed, resubmit that build with \`eas submit --id <build id>\`. Re-running this workflow will publish an OTA update instead." >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||
| echo "If only the update failed, re-running is safe." >> "$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 |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../.nvmrc |
| 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,34 @@ | ||
| { | ||
| "cli": { | ||
| "appVersionSource": "remote" | ||
| }, | ||
| "build": { | ||
| "production": { | ||
| "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