diff --git a/.github/workflows/publish-example-app.yml b/.github/workflows/publish-example-app.yml new file mode 100644 index 0000000000..7c9a86531f --- /dev/null +++ b/.github/workflows/publish-example-app.yml @@ -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 }} + 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 \`. 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" 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/.nvmrc b/example/.nvmrc new file mode 120000 index 0000000000..3d48ffa461 --- /dev/null +++ b/example/.nvmrc @@ -0,0 +1 @@ +../.nvmrc \ No newline at end of file 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..482e583d8e --- /dev/null +++ b/example/eas.json @@ -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" + } + } + } +} 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', + ], +};