Files
app-stores/README.md
T
thepraandClaude Opus 5.5 5ced47c2b9 changelog: a path input for a checkout that is not the workspace itself
decePubClient fetches itself into a folder of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:15:22 +01:00

11 KiB

app-stores

Three Gitea Actions for any project's release workflow: play publishes to Google Play, apple to the App Store, and changelog writes what changed since the previous release from the commit messages, in every format a release needs. Each is one Python file; the store ones talk to the store's own web API (Google's Play Developer API, 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.

- uses: https://git.thepra.dev/Arasaka/app-stores/play@v1
- uses: https://git.thepra.dev/Arasaka/app-stores/apple@v1
- uses: https://git.thepra.dev/Arasaka/app-stores/changelog@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:

      - 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:

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:

      - 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.

Release notes: changelog

The commit messages are the changelog (owner, 1 November 2026: every commit between two releases, the whole message, in every place a release is shown). The action takes the commits since the previous tag of the same kind (prefix: v, app-v, server-v; the previous one is the highest lower version among those tags), leaves out merges and the Co-Authored-By, Signed-off-by and Claude attribution lines, and writes, into output:

File For
notes.md "What's new in ": each commit's subject as a heading, its body below; the Releases tab, a page
notes.txt the same as plain text: Velopack's --releaseNotes, an update dialog, F-Droid
CHANGELOG.md the last history releases, newest first
changelog.yml the same releases for nfpm's changelog: (chglog), so the deb carries changelog.Debian.gz and the rpm its %changelog; bodies become one line per paragraph
notes.json everything above as data
store/en-US.txt the subjects, cut to 500 characters, for the notes folder of play and apple

With release: 'true' it also writes the notes onto this tag's release on the Releases tab: it creates the release when there is none, and otherwise replaces only what sits between <!-- changelog --> and <!-- /changelog --> (adding that block at the end the first time), so a release text written by the project itself stays. Give the job permissions: contents: write.

      - uses: actions/checkout@v4
      - name: Release notes
        id: notes
        uses: https://git.thepra.dev/Arasaka/app-stores/changelog@v1
        with:
          prefix: app-v
          name: SimpleSchool
          packager: Scuola di lingua Cinese di Varese <info@scuolacinese.org>
          output: release-notes
          release: 'true'
      - run: echo "the notes are in ${{ steps.notes.outputs.dir }}, the previous release was ${{ steps.notes.outputs.previous }}"
Input
path . the git checkout to read, when it is not the workspace itself
prefix v the tags of this kind of release
tag the tag that started the run the release being made; empty on a branch, which writes everything since the newest tag as unreleased
name the repository's name the product's name in headings
strip a regular expression removed from the start of each subject (me-web? removes its “me-web?” 0.15.4: )
packager Arasaka di Hvozdikov Yevheniy <https://arasaka.software/contact> Name <address> for the deb and rpm changelogs: the package's own maintainer
history 20 how many releases CHANGELOG.md and the package changelog carry
output release-notes the folder
release false true writes the notes onto the Releases tab
token the job token only with release

A shallow checkout is fetched in full, with its tags, before anything is read. A first release lists its newest 100 commits. The same file reads the history from Gitea's API instead of a checkout (--gitea URL --repository owner/name, token in READ_TOKEN); that is how thepra/packages.arasaka.software writes the notes for every app it publishes, so the two can never tell a release differently.

Store 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.