Files

160 lines
7.9 KiB
Markdown
Raw Permalink Normal View History

# app-stores
Two Gitea Actions that publish an app from any project's release workflow: `play` for Google Play and `apple` for
the App Store. Each is one Python file talking to the store's own web API: Google's Play Developer API and Apple's
App Store Connect API. Nothing else is needed on the runner beyond Python 3.9 or newer and `openssl`, which every
Linux and Mac runner has. No fastlane, no Ruby, no service in between.
```yaml
- uses: https://git.thepra.dev/Arasaka/app-stores/play@v1
- uses: https://git.thepra.dev/Arasaka/app-stores/apple@v1
```
> Status: checked against fakes of both stores built from their published API definitions (Google's discovery
> document of 22 October 2026, Apple's OpenAPI 4.5.1), with every request body, query and signature verified, and
> called by URL from jobs on both of this Gitea's runners, where both stores refused only the made-up keys. Not yet
> run against a real store account; the first project to use one is the first real run.
## Google Play: `play`
Two things it does, one per run:
- **Upload**: given `aab`, it uploads the bundle and puts it on `track`.
- **Move**: given `from-track`, it takes the newest release already on that track and puts it on `track`, with
its notes unless `notes` is given. This promotes internal testing to production, raises a staged rollout
(`from-track: production`, `track: production`, a bigger `fraction`), completes it (`status: completed`) or halts
it (`status: halted`).
| Input | | |
|---|---|---|
| `service-account` | required | the JSON key of the service account, from a secret |
| `package` | required | the application id, `software.arasaka.meweb` |
| `aab` | upload | the signed `.aab` |
| `from-track` | move | the track to take the release from |
| `track` | `internal` | `internal`, `alpha` (closed testing), `beta` (open testing), `production` or a custom closed track |
| `status` | `completed` | `completed`, `draft`, `inProgress` (staged, needs `fraction`) or `halted` |
| `fraction` | | the share of users of a staged rollout, between 0 and 1 |
| `notes` | | a folder of `<language>.txt` files, `en-US.txt`, `it-IT.txt`, 500 characters each |
| `name` | | the release name in Play Console; Google uses the version name when empty |
Every tag on the internal testing track:
```yaml
- name: Google Play, internal testing
if: ${{ github.ref_type == 'tag' }}
uses: https://git.thepra.dev/Arasaka/app-stores/play@v1
with:
service-account: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
package: software.arasaka.meweb
aab: dist/android/software.arasaka.meweb-Signed.aab
notes: store/notes
```
Production by hand, from the Actions tab:
```yaml
name: Google Play production
on:
workflow_dispatch:
inputs:
fraction:
description: "Share of users, 0.1 for 10%; empty for everyone"
default: "0.1"
jobs:
promote:
runs-on: nuvola
steps:
- uses: https://git.thepra.dev/Arasaka/app-stores/play@v1
with:
service-account: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
package: software.arasaka.meweb
from-track: internal
track: production
status: ${{ inputs.fraction && 'inProgress' || 'completed' }}
fraction: ${{ inputs.fraction }}
```
## App Store: `apple`
- **Upload**: given `ipa`, it reads the version and build number from the app's `Info.plist`, uploads it, waits
until Apple has processed it (up to an hour and a half), writes what to test from `notes`, and gives it to the
TestFlight `groups`. A group outside the team (external testers) also sends the build to TestFlight review.
- **Submit**: with `submit: true`, it sends the build for App Store review: the uploaded one, or the newest
processed build of `version` (or exactly `build`) when no `ipa` is given. It reuses the version waiting to be
submitted, or creates it, links the build, writes what is new from `notes`, and submits. With `release:
after-approval` the version goes on sale when Apple approves it; with `manual` it waits for you in App Store
Connect. `phased: true` spreads the update over seven days.
| Input | | |
|---|---|---|
| `issuer-id`, `key-id`, `private-key` | required | the team API key: two ids and the text of the `.p8` file, from secrets |
| `bundle-id` | required | the app's bundle identifier |
| `ipa` | upload | the signed `.ipa` |
| `notes` | | a folder of `<locale>.txt` files, `en-US.txt`, `it.txt`, 4,000 characters each |
| `groups` | | TestFlight groups by name, separated by commas or lines |
| `submit` | `false` | `true` sends the build for App Store review |
| `version`, `build` | submit | which build to submit when no `ipa` is given |
| `release` | `after-approval` | or `manual` |
| `phased` | `false` | `true` for a seven-day phased release |
The build itself needs a Mac with Xcode; the upload does not. A Mac job can build, sign and run this step itself,
or hand the `.ipa` to a Linux job that runs it:
```yaml
- name: TestFlight
uses: https://git.thepra.dev/Arasaka/app-stores/apple@v1
with:
issuer-id: ${{ secrets.ASC_ISSUER_ID }}
key-id: ${{ secrets.ASC_KEY_ID }}
private-key: ${{ secrets.ASC_PRIVATE_KEY }}
bundle-id: software.arasaka.meweb
ipa: dist/ios/MeWeb.ipa
notes: store/notes
groups: Team
```
## Once per app, by hand
Neither store lets an API create an app, so the first steps happen in their consoles.
**Google Play**
1. A Play Console developer account ($25 once). A personal account opened after 13 November 2023 must run a closed
test with at least 12 testers for 14 days before it can publish to production; an organisation account needs a
D-U-N-S number.
2. Create the app and fill in what Play Console asks before a first release: the store listing, the content rating
questionnaire, the target audience, data safety and a privacy policy address.
3. Upload the first `.aab` by hand, on internal testing. Play asks then which key signs the app for users (Play
App Signing). To keep a copy installed from a website and the Play copy updating each other, give Play the key
the website APK is already signed with, rather than letting Google generate a new one. The choice is permanent.
4. In Google Cloud, create a project, enable the **Google Play Android Developer API**, create a service account,
and make a JSON key for it. Store the whole file as a secret (`PLAY_SERVICE_ACCOUNT`).
5. In Play Console, Users and permissions, invite the service account's email address with release rights for
this app (testing tracks, and production if a workflow promotes to it).
6. Until the app's first release is published by hand in Play Console, Google accepts only `status: draft`.
**App Store**
1. The Apple Developer Program ($99 a year).
2. Register the bundle identifier (Certificates, Identifiers and Profiles) and create the app in App Store Connect.
3. Fill in the App Privacy answers, the age rating, the screenshots, the description and the support address for
the first version.
4. Users and Access, Integrations, App Store Connect API: generate a **team key** with the App Manager role. Store
the issuer id, the key id and the text of the `.p8` file (downloadable once) as three secrets.
5. Put `ITSAppUsesNonExemptEncryption` in the app's `Info.plist`. Without it every build waits for an export
compliance answer in App Store Connect, so the action refuses the upload.
6. Create the TestFlight groups. An external group needs the Beta App Review information (contact and notes)
filled in once.
## Notes
One file per language, the file name being the code: Google uses language and region (`en-US`, `it-IT`), Apple
its own list (`en-US`, `it`, `de-DE`). Empty files are skipped. Apple takes no release notes for an app's first
version; the action leaves them out and says so.
## Versions
`v1` moves forward with every change that keeps the inputs working; a change that breaks them becomes `v2`. Pin a
commit instead of `v1` to never move.