Skip to content
Draft
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
146 changes: 146 additions & 0 deletions .github/PUBLISHING_EXAMPLE_APP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Publishing the example app

The example app is the demo of the library that we ship to the App Store and the
Play Store. It is published automatically by the
[`Publish example app`](workflows/publish-example-app.yml) workflow whenever a
library release is published.

## This does not affect library releases

The workflow runs **after** a release already exists. By the time it starts,
`release-it` has already published to npm and created the GitHub release, so
nothing the workflow does can fail, revoke or roll back a release.

A run that does not succeed says so on its own run page, above the logs: the
library release published normally and only the demo app is missing an update.
Whoever published the release is already emailed by GitHub that the run failed,
so that page is where they land.

Before re-running, check the EAS dashboard and the stores. A build may already
have been submitted, and submitting the same version twice is the one thing
worth avoiding — cancelling the workflow does not cancel a build already running
on EAS, so even a cancelled run can ship on its own. The run page spells this
out.

## What happens on a release

The submit profile is chosen from the release tag: a tag containing a hyphen is
a semver prerelease, and anything else is stable.

| Tag | Android | iOS |
| ---------------- | --------------------------- | ----------------------------- |
| `v6.0.0-alpha.1` | Play Store internal track | App Store Connect, TestFlight |
| `v6.0.0` | Play Store production track | App Store Connect, TestFlight |

The tag is used rather than the release's prerelease flag because
[`.release-it.json`](../.release-it.json) currently pins every release to
`preRelease: true` for the 6.0 alpha cycle. Reading that flag would send a
stable 6.0.0 release to the internal track if somebody forgot to revert the pin.

Note that `eas submit` uploads an iOS build to App Store Connect, where it
becomes available in TestFlight. Releasing that build to the App Store is a
separate manual step in App Store Connect, so the iOS half behaves the same for
prereleases and stable releases.

## Versions

The example app keeps its own version line, independent of the library. Each
release bumps its minor version, so a library release of `v6.0.0` moves the
example app from `3.16.0` to `3.17.0`. After submitting, the workflow opens a
pull request with that bump.

That pull request needs merging before the next release. The bump is computed
from `main`, so two releases while it is still open both start from the same
version and ship under it. The stores accept that, because EAS assigns build
numbers separately, which is exactly why it is worth watching for: the two
builds are then hard to tell apart.

iOS build numbers and Android version codes are **not** kept in the repository.
EAS assigns them remotely via `appVersionSource: remote` and `autoIncrement`, so
retries and reruns cannot collide with an already submitted build. The
`buildNumber` and `versionCode` values still in `example/app.json` are ignored by
EAS, which warns about them on every build. They are kept only because local
`expo run:` builds still read them, and they are no longer authoritative.

The workflow always builds the example app as it is on `main`, not as it was at
the release tag. `release-it` tags the tip of `main`, so these are the same at
release time, and for a demo app the newer one is the one worth shipping anyway.

The bump pull request is opened with `GITHUB_TOKEN`, so CI does not run on it.
That is a known GitHub limitation rather than a choice, and it is acceptable
here because the pull request only changes a version string.

## Retrying a failed deployment

Check the stores first, as above. Then run the workflow by itself, without
cutting a new library release:

**Actions → Publish example app → Run workflow → the release tag**

If the previous run shipped one platform and failed the other, pick that one
platform in the `platform` input. Retrying with `all` would submit a duplicate
to the platform that already shipped.

Retry **before** merging the version bump pull request that the run opened.
Merging it first moves `main` on, so the retry bumps again and ships the second
platform under a different version from the first.

## One-time setup

### Repository secret

`EXPO_TOKEN` is the only secret this workflow needs. It should be a token for an
Expo bot account belonging to the `react-native-paper` organisation rather than
a personal account, so that it survives people joining and leaving.

Store credentials deliberately live in EAS, not in GitHub. Nothing about the
Apple or Google accounts is configured in this repository.

### Repository setting

Enable **Settings → Actions → General → Allow GitHub Actions to create and
approve pull requests**. Without it the version bump pull request fails with a
403 _after_ the app has already been submitted.

### Store credentials

Run once, from `example/`:

```sh
eas credentials
```

- **iOS**: use an App Store Connect API key. Apple ties app-specific passwords to
an individual person's account, so they break when that person rotates their
password or leaves.
- **Android**: upload a Google Play service account key with the
_Release manager_ role.

### Seeding remote build numbers

Remote versioning starts from scratch, and both stores reject a build that does
not increase. Seed the remote values from the ones currently in
`example/app.json` before the first automated run, from `example/`:

```sh
eas build:version:set --platform android # set to at least 38
eas build:version:set --platform ios # set to at least 26.0.5
```

## Rollback

A build that has already been submitted cannot be withdrawn by re-running or
deleting anything in GitHub. Roll it back where it was published:

- **Play Store**: halt the rollout in the Play Console, and promote the previous
release if it was already live.
- **App Store**: expire the TestFlight build, or reject it in App Store Connect
if it had been submitted for review.

Then land the fix and cut a new release as usual.

## Reference

- [EAS Build](https://docs.expo.dev/build/introduction/)
- [EAS Submit](https://docs.expo.dev/submit/introduction/)
- [App version management](https://docs.expo.dev/build-reference/app-versions/)
194 changes: 194 additions & 0 deletions .github/workflows/publish-example-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
name: Publish example app

# Publishes the example app that demonstrates the library to the App Store and
# Play Store. This runs *after* a library release already exists, and cannot
# affect it: see the failure report in the `report-failure` job below.

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: Platforms to build and submit. Pick one to retry after a partial failure, so the platform that already shipped is not submitted twice.
required: false
default: all
type: choice
options:
- all
- ios
- android

permissions:
contents: read

concurrency:
group: publish-example-app

jobs:
publish:
name: Build and submit example app
runs-on: ubuntu-latest
# An EAS build for two platforms is slow, but not this slow. Without a
# timeout a stuck run holds the concurrency lock for six hours.
timeout-minutes: 120
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main

- 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

# A semver version with a prerelease identifier (v6.0.0-alpha.1) goes
# to the test tracks, a stable one (v6.0.0) goes to production. This
# is derived from the tag rather than from the release's prerelease
# flag on purpose: .release-it.json currently pins every release to
# `preRelease: true` for the 6.0 alpha cycle, so that flag stays true
# for a stable release until somebody remembers to revert it.
if [ "${TAG#*-}" != "$TAG" ]; then
profile=preview
else
profile=production
fi

# A release always publishes both platforms. Only a manual retry can
# narrow it, to avoid resubmitting a platform that already shipped.
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: Bump example app version
run: node scripts/bump-example-version.ts example/app.json

- name: Setup Expo
uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}

# Build numbers are assigned remotely by EAS (`appVersionSource: remote`),
# so a retry can never collide with an already submitted build.
- name: Build and submit to the app stores
id: submit
working-directory: ./example
env:
SUBMIT_PROFILE: ${{ steps.release.outputs.profile }}
PLATFORM: ${{ steps.release.outputs.platform }}
run: |
eas build \
--platform "$PLATFORM" \
--profile production \
--auto-submit-with-profile "$SUBMIT_PROFILE" \
--non-interactive

# Runs whenever the submit step ran at all, however it ended. `eas build`
# can ship one platform and fail the other, and cancelling the workflow
# does not cancel the build on EAS, so in every one of those cases a
# version may have reached a store and has to be recorded.
- name: Open example app version bump pull request
if: ${{ always() && (steps.submit.outcome == 'success' || steps.submit.outcome == 'failure' || steps.submit.outcome == 'cancelled') }}
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: main
add-paths: example/app.json
branch: chore/example-app-version-${{ steps.release.outputs.tag }}
commit-message: 'chore: bump example app version for ${{ steps.release.outputs.tag }}'
title: 'chore: bump example app version for ${{ steps.release.outputs.tag }}'
labels: example app
body: |
Records the example app version this run used for
${{ steps.release.outputs.tag }}.

A build for that version may already have been submitted, so this only
keeps `example/app.json` in sync. iOS build numbers and Android version codes are not in here:
EAS assigns those remotely.

- name: Summarise
if: ${{ !cancelled() && steps.submit.outcome == 'success' }}
env:
TAG: ${{ steps.release.outputs.tag }}
PROFILE: ${{ steps.release.outputs.profile }}
run: |
{
echo "### Example app submitted for \`$TAG\`"
echo
echo "Submit profile: \`$PROFILE\`."
if [ "$PROFILE" = "production" ]; then
echo "- Android: Play Store **production** track."
else
echo "- Android: Play Store **internal** track."
fi
echo "- iOS: uploaded to App Store Connect, available in TestFlight."
echo
echo "Releasing an iOS build from TestFlight to the App Store is a"
echo "separate manual step in App Store Connect."
} >> "$GITHUB_STEP_SUMMARY"

report-failure:
name: Report example app deployment problem
needs: publish
# Not `failure()`: that misses a cancelled run, and cancellation is the case
# most likely to leave a build running on EAS with nobody told about it.
if: ${{ always() && needs.publish.result != 'success' }}
runs-on: ubuntu-latest
steps:
# Whoever published the release is already emailed that this run failed.
# What that email cannot tell them is that their release is fine, so say
# it here, where they land when they follow it.
- name: Say that the library release is unaffected
env:
TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: |
echo "::error::The example app deployment for $TAG did not finish. The $TAG library release itself published successfully and is not affected."

cat >> "$GITHUB_STEP_SUMMARY" <<EOF
## The \`$TAG\` library release is not affected

It is published on npm and on GitHub Releases exactly as expected, and
**nothing here changes that**. This workflow only ever runs *after* a
release already exists, so it cannot fail, revoke or roll back one.

- Users of the library need to do nothing. Installing \`react-native-paper\` is unaffected.
- Nothing has to be re-released to fix this.
- Only the demo app we ship to the App Store and Play Store did not update.

## Before re-running

**Check the EAS dashboard and the stores first.** A build may already
have been submitted, and submitting the same version twice is the one
thing worth avoiding. Cancelling this workflow does not cancel a build
already running on EAS, so a cancelled run can still ship on its own.

Once you have confirmed what did and did not reach the stores, re-run
this workflow on its own, without cutting a new library release:

**Actions → Publish example app → Run workflow → tag \`$TAG\`**

If only one platform failed, pick it in the \`platform\` input so the
other one is not submitted a second time. Retry before merging the
version bump pull request this run opened, since merging it first
moves \`main\` on and the retry would ship under a different version.
EOF
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ 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).

Publishing a release also publishes the example app to the app stores. That happens after the release, in a separate workflow that cannot affect it. See [publishing the example app](.github/PUBLISHING_EXAMPLE_APP.md).

## 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
30 changes: 30 additions & 0 deletions example/eas.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"cli": {
"appVersionSource": "remote"
},
"build": {
"production": {
"node": "24.13.0",
"autoIncrement": true,
"android": {
"buildType": "app-bundle"
}
}
},
"submit": {
"production": {
"android": {
"track": "production"
},
"ios": {
"ascAppId": "1548934513"
}
},
"preview": {
"extends": "production",
"android": {
"track": "internal"
}
}
}
}
Loading