Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions .github/workflows/publish-example-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
name: Publish example app

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

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

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

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.

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')

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 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 (example/fingerprint.config.js) with something like:

/** @type {import('expo/fingerprint').Config} */
module.exports = {
  sourceSkips: [
    'ExpoConfigVersions',
    'PackageJsonAndroidAndIosScriptsIfNotContainRun',
  ],
};

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.

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

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


- 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

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


- 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"
27 changes: 0 additions & 27 deletions .github/workflows/updates.yml

This file was deleted.

9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ yarn release

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).

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.

## Reporting issues

You can report issues on our [bug tracker](https://github.com/callstack/react-native-paper/issues). Please follow the issue template when opening an issue.
Expand Down
2 changes: 2 additions & 0 deletions example/.fingerprintignore
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/**
7 changes: 4 additions & 3 deletions example/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,

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

"permissions": [
"READ_EXTERNAL_STORAGE",
"WRITE_EXTERNAL_STORAGE"
Expand All @@ -37,7 +38,7 @@
}
},
"runtimeVersion": {
"policy": "sdkVersion"
"policy": "fingerprint"
},
"updates": {
"url": "https://u.expo.dev/ba8f5139-58fe-48f0-b2f2-2d675b6eb2d4"
Expand Down
35 changes: 35 additions & 0 deletions example/eas.json
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"
}
}
}
}
7 changes: 7 additions & 0 deletions example/fingerprint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('expo/fingerprint').Config} */
module.exports = {
sourceSkips: [
'ExpoConfigVersions',
'PackageJsonAndroidAndIosScriptsIfNotContainRun',
],
};
Loading