diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 260032d..588c0c0 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -43,6 +43,14 @@ on: description: "Existing tag to build, for example android-v0.1.0" required: true type: string + track: + description: >- + Play track to publish to. Overrides the PLAY_TRACK repository + variable for this run only. + required: false + default: "" + type: choice + options: ["", "internal", "alpha", "beta", "production"] skip_play_upload: description: >- Build and publish the GitHub release but do NOT upload to Play. @@ -215,7 +223,38 @@ jobs: workload_identity_provider: ${{ vars.PLAY_WIF_PROVIDER }} service_account: ${{ vars.PLAY_PUBLISHER_SERVICE_ACCOUNT }} - - name: Upload to the Play internal track + # Which track a tag ships to is configuration, not code: `PLAY_TRACK` moves + # the whole pipeline from testing to production without touching this file + # or cutting a release to change it. It stays `internal` until the app has + # actually launched — Play refuses a production release while the store + # listing, content rating, data safety and target-audience forms are + # outstanding, so defaulting to production would fail every release until + # someone finished them. + # + # `PLAY_USER_FRACTION` (e.g. 0.1) turns a production release into a staged + # rollout, which is the only undo Play offers once a build is live. + - name: Decide the track + id: play + if: ${{ !inputs.skip_play_upload }} + run: | + TRACK="${{ inputs.track }}" + [ -n "$TRACK" ] || TRACK="${{ vars.PLAY_TRACK }}" + [ -n "$TRACK" ] || TRACK="internal" + FRACTION="${{ vars.PLAY_USER_FRACTION }}" + if [ "$TRACK" = "production" ] && [ -n "$FRACTION" ]; then + STATUS="inProgress" + else + STATUS="completed" + FRACTION="" + fi + echo "track=$TRACK" >> "$GITHUB_OUTPUT" + echo "status=$STATUS" >> "$GITHUB_OUTPUT" + echo "user_fraction=$FRACTION" >> "$GITHUB_OUTPUT" + echo "### Play release" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Track \`$TRACK\`, status \`$STATUS\`${FRACTION:+, rolling out to $FRACTION of users}." >> "$GITHUB_STEP_SUMMARY" + + - name: Upload to Play if: ${{ !inputs.skip_play_upload }} uses: r0adkll/upload-google-play@935ef9c68bb393a8e6116b1575626a7f5be3a7fb # v1.1.3 with: @@ -226,8 +265,9 @@ jobs: packageName: com.pathors.parley releaseFiles: android/app/build/outputs/bundle/release/app-release.aab mappingFile: android/app/build/outputs/mapping/release/mapping.txt - track: internal - status: completed + track: ${{ steps.play.outputs.track }} + status: ${{ steps.play.outputs.status }} + userFraction: ${{ steps.play.outputs.user_fraction }} # Deliberately loud. The run is green, so the summary is the only place # that says this build is not on Play yet. diff --git a/android/RELEASING.md b/android/RELEASING.md index f089165..df8ff31 100644 --- a/android/RELEASING.md +++ b/android/RELEASING.md @@ -226,26 +226,31 @@ release exists, and nobody finds out until someone asks where the build went. The check runs immediately after checkout, so a misconfigured release costs seconds and leaves no half-made release behind. -#### The two steps with no API +#### Granting the service account access (done 2026-08-20) -Linking a Cloud project to a Play account is what *enables* the Play Developer -API, so it cannot go through the API it enables. +**There is no "API access" page and no "Release manager" role any more.** Older +guides — including earlier versions of this one — send you to +`Setup → API access`, which now redirects to the app list. Google folded service +accounts into ordinary user management, and roles into individual permissions. -1. Play Console → **Setup → API access** → link the Cloud project `pathors-play`. -2. Same page, under *Service accounts*, - `parley-play-publisher@pathors-play.iam.gserviceaccount.com` appears → - **Grant access** → **Release manager** (upload and release to testing tracks - and production, but no access to payments or account settings) → restrict to - Parley under *App permissions* → **Invite user**. +The current path is **Users and permissions → Invite new user**, with the +service account's email as the user: -Permissions take minutes to hours to propagate on a fresh account. A -`403 The caller does not have permission` on the first run is usually just that, -not a misconfiguration. +1. Play Console → **使用者和權限 / Users and permissions** → **邀請新的使用者 / + Invite new user** +2. Email: `parley-play-publisher@pathors-play.iam.gserviceaccount.com` +3. **App permissions** tab (not Account permissions — account permissions apply + to every app in the developer account) → add **Parley** → grant: + - 查看應用程式資訊 (唯讀) / View app information — the read access every + other permission is layered on + - 將應用程式發布至測試群組 / Release to testing tracks + - 發布正式版… / Release to production +4. **Invite user** -**The first bundle for a package cannot go through the API at all** — Play -refuses until the app has a release created in the console. Run the workflow -with `skip_play_upload=true`, upload that `.aab` by hand once, and every release -after it goes through the API. +No Cloud-project link is involved anywhere in this flow. + +Permissions can take minutes to propagate. A `403 The caller does not have +permission` on the first run is usually just that, not a misconfiguration. ### 8. Dependency verification @@ -313,11 +318,29 @@ are easy to get wrong: ``` Verify: sign-in deep link, live meeting, file import, upload queue drain. -5. **The first bundle only.** Play refuses a package's first bundle over the - API, so run the workflow manually with `skip_play_upload=true`, then - Play Console → Release → (track) → Create new release → upload the `.aab` - from the GitHub release, release notes (en + zh-TW), review, roll out. Every - release after that goes through the API and needs no console step. +5. **Which track a tag ships to is configuration, not code.** The repository + variable `PLAY_TRACK` (default `internal`) decides; a `workflow_dispatch` + run can override it with the `track` input for one run. Moving the whole + pipeline to production is therefore one variable, with no commit and no + release cut to change it. + + `PLAY_USER_FRACTION` (e.g. `0.1`) turns a production release into a staged + rollout — worth having, because it is the only undo Play offers once a build + is live. + + **`PLAY_TRACK` stays `internal` until the app has actually launched.** Play + refuses a production release while the store listing, content rating, data + safety and target-audience forms are outstanding, so pointing at production + early would fail every release until someone finished them. As of 2026-08-20 + the app dashboard reads *已完成 6 項,共 11 項*; the five outstanding ones are + 登入詳細資料, 內容分級, 目標對象, 資料安全性, and the app category/contact + details. They are declarations about the app, not build config — nobody can + automate them, and the prepared answers are in `AppStore/`. + + The first bundle for the package was uploaded by hand on 2026-08-18 + (`0.1.0 (1)`, internal track), so the API can take over from `versionCode` + 2 onward. Play rejects a `versionCode` it has already seen, which is why + `android-v0.1.0` could never have been published over the API. 6. Keep the R8 `mapping.txt`. Every workflow run attaches it as an artifact and the Play upload step sends it along, so crash reports deobfuscate; a stack trace from a build whose mapping was lost is unreadable. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c3f6888..60a2a9b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -84,8 +84,8 @@ android { // minSdk 29: MediaMuxer OGG output + MediaCodec Opus encoder both require API 29. minSdk = 29 targetSdk = 35 - versionCode = 1 - versionName = "0.1.0" + versionCode = 2 + versionName = "0.1.1" } signingConfigs {