diff --git a/.github/workflows/publish-example-app.yml b/.github/workflows/publish-example-app.yml new file mode 100644 index 0000000000..d0ca481096 --- /dev/null +++ b/.github/workflows/publish-example-app.yml @@ -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 + 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') + + 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 + + - 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') }} + 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" diff --git a/.github/workflows/updates.yml b/.github/workflows/updates.yml deleted file mode 100644 index 336a01184e..0000000000 --- a/.github/workflows/updates.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Expo EAS Updates -on: - push: - branches: - - main -jobs: - publish: - if: ${{ (contains(github.event.pull_request.labels.*.name, 'example app')) }} - name: Install and publish - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup - uses: ./.github/actions/setup - - - name: Setup Expo - uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1 - with: - expo-version: latest - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: Publish Expo app update - working-directory: ./example - run: eas update --auto diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 62c3b9c59b..3e3fa7278f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/example/.fingerprintignore b/example/.fingerprintignore new file mode 100644 index 0000000000..b3c5af777d --- /dev/null +++ b/example/.fingerprintignore @@ -0,0 +1,2 @@ +# built by pod install, would make the fingerprint differ from the one EAS computes +**/expo-modules-jsi/apple/Products/** diff --git a/example/app.json b/example/app.json index d8b2b6debb..4dd835b98f 100644 --- a/example/app.json +++ b/example/app.json @@ -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, "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" diff --git a/example/eas.json b/example/eas.json new file mode 100644 index 0000000000..b4f19d3a15 --- /dev/null +++ b/example/eas.json @@ -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" + } + } + } +} diff --git a/example/fingerprint.config.js b/example/fingerprint.config.js new file mode 100644 index 0000000000..501ccf5ba3 --- /dev/null +++ b/example/fingerprint.config.js @@ -0,0 +1,7 @@ +/** @type {import('expo/fingerprint').Config} */ +module.exports = { + sourceSkips: [ + 'ExpoConfigVersions', + 'PackageJsonAndroidAndIosScriptsIfNotContainRun', + ], +};