Google Play and App Store publishing as two Gitea actions
play uploads an Android App Bundle to a track or moves a release between tracks; apple uploads an iOS build for TestFlight and submits a version for review. Each is one Python file over the store's own API, with openssl for the signed tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KizzWs2WQPWN2qgE7G6r8B
This commit is contained in:
commit
b3c2714862
7 files changed
+848
No files matched your search
@@ -0,0 +1,159 @@
|
||||
# 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. 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.
|
||||
Reference in new issue
Block a user