diff --git a/.gitea/workflows/android-promote.yml b/.gitea/workflows/android-promote.yml index efedb712..693b4b01 100644 --- a/.gitea/workflows/android-promote.yml +++ b/.gitea/workflows/android-promote.yml @@ -6,7 +6,7 @@ # android.yml would mean an `if:` on all ten of its build steps. # # What it is for: -# * promote a tested build up a track (alpha -> production) +# * promote a tested build up a track (beta -> production) # * roll production back by re-pointing it at an older versionCode (to_track=production, # version_code=, from_track blank) # * halt a rollout (status=halted) @@ -36,7 +36,7 @@ on: from_track: description: 'track to verify it is on, then clear (blank = touch nothing else)' required: false - default: 'alpha' + default: 'beta' notes_tag: description: "tag whose docs/releases/whatsnew/.txt to attach, e.g. v0.23.0 (blank = none)" required: false diff --git a/.gitea/workflows/android.yml b/.gitea/workflows/android.yml index 3b4b5b76..ea1f7726 100644 --- a/.gitea/workflows/android.yml +++ b/.gitea/workflows/android.yml @@ -36,8 +36,13 @@ on: - '.gitea/workflows/android.yml' # Single project version: a `vX.Y.Z` tag is THE release (publishes to Play `production` at # 100% + attaches the .aab/.apk to the unified Gitea Release). A main push is canary - # (Play `internal`). Production access was granted 2026-08-01; before that a tag could only - # reach `alpha` and someone had to promote it by hand in the Console. + # (Play `beta` = open testing: public opt-in, no tester list — but unlike the previous + # `internal` target, every canary now passes Google review before testers see it, so a + # canary lands in hours/days, not minutes). The same canary versionCode is also assigned + # to `alpha` (closed testing) in the same Play edit, so the pre-production-access closed + # testers keep receiving builds without re-opting-in. Production access was granted + # 2026-08-01; before that a tag could only reach `alpha` and someone had to promote it + # by hand in the Console. tags: ['v*'] pull_request: paths: @@ -94,8 +99,9 @@ jobs: # store listing. Failing here also means a missing file cannot leave a half-published # release: nothing is built, nothing is attached to the Gitea release, nothing reaches Play. # - # Canary is exempt on purpose: it has no curated notes, and Play reusing text for internal - # testers costs nothing. + # Canary is exempt on purpose: it has no curated notes. Open-testing users therefore see + # the previous release's text on a canary — cosmetic, and cheaper than gating every main + # push on a notes file. - name: Play release notes gate (tags only) if: startsWith(github.ref, 'refs/tags/v') run: | @@ -226,11 +232,12 @@ jobs: run: | eval "$(bash scripts/ci/pf-version.sh)" # -> PF_BASE (one minor ahead of the latest stable tag) case "$GITHUB_REF" in - refs/tags/v*) VN="${GITHUB_REF_NAME#v}"; TRACK="production" ;; - *) VN="${PF_BASE}-ci${GITHUB_RUN_NUMBER}"; TRACK="internal" ;; + refs/tags/v*) VN="${GITHUB_REF_NAME#v}"; TRACK="production"; ALSO="" ;; + *) VN="${PF_BASE}-ci${GITHUB_RUN_NUMBER}"; TRACK="beta"; ALSO="alpha" ;; esac echo "VERSION_NAME=$VN" >> "$GITHUB_ENV" echo "PLAY_TRACK=$TRACK" >> "$GITHUB_ENV" + echo "PLAY_ALSO_TRACK=$ALSO" >> "$GITHUB_ENV" # Play's own "What's new" (500-char cap, its own file — the vX.Y.Z.md body is ~34 KB). # On a tag the gate step above already proved this exists, so the else branch is only # ever the canary path. See docs/releases/README.md. @@ -240,7 +247,7 @@ jobs: else echo "no Play release notes at $NOTES (canary — Play keeps the previous text)" fi - echo "android version $VN -> Play track '$TRACK'" + echo "android version $VN -> Play track '$TRACK'${ALSO:+ (+ '$ALSO')}" - name: Build Release (signed AAB + universal APK) if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) @@ -312,7 +319,8 @@ jobs: # Direct Publishing-API upload instead of r0adkll/upload-google-play — that action hides the # real API error behind "Unknown error occurred."; this prints it. stdlib + openssl only (no # pip), reuses SERVICE_ACCOUNT_JSON (raw JSON or base64), auto-handles changesNotSentForReview. - # Track: canary main -> `internal`; a vX.Y.Z release -> `production` at 100% (`completed`). + # Track: canary main -> `beta` (open testing) + the same versionCode on `alpha` (closed + # testing) in the same Play edit; a vX.Y.Z release -> `production` at 100% (`completed`). # # A tag therefore ships to real users with no further click. Two things keep that honest: # the tag is only pushed once every platform is green, and Play reviews each production @@ -324,9 +332,10 @@ jobs: env: SERVICE_ACCOUNT_JSON: ${{ secrets.SERVICE_ACCOUNT_JSON }} run: | - echo "uploading to Play track '$PLAY_TRACK'" + echo "uploading to Play track '$PLAY_TRACK'${PLAY_ALSO_TRACK:+ (+ '$PLAY_ALSO_TRACK')}" set -- --package io.unom.punktfunk \ --aab clients/android/app/build/outputs/bundle/release/app-release.aab \ --track "$PLAY_TRACK" --status completed + if [ -n "${PLAY_ALSO_TRACK:-}" ]; then set -- "$@" --also-track "$PLAY_ALSO_TRACK"; fi if [ -n "${PLAY_NOTES:-}" ]; then set -- "$@" --release-notes-file "$PLAY_NOTES"; fi python3 clients/android/ci/play-upload.py "$@" diff --git a/clients/android/README.md b/clients/android/README.md index 1d06356e..97dfcba1 100644 --- a/clients/android/README.md +++ b/clients/android/README.md @@ -22,7 +22,8 @@ Google TV, budget Amlogic boxes) that otherwise reject a 64-bit-only build as "n ## Get it -Published to **Google Play (Internal Testing)** — join the beta via the +Published to **Google Play (Open Testing)** — join via the +[public opt-in link](https://play.google.com/apps/testing/io.unom.punktfunk) or the [Discord](https://discord.gg/kaPNvzMuGU). Per-device setup and pairing: **[docs.punktfunk.unom.io/docs/install-client](https://docs.punktfunk.unom.io/docs/install-client)**. diff --git a/clients/android/ci/play-upload.py b/clients/android/ci/play-upload.py index b557707d..777f3e20 100644 --- a/clients/android/ci/play-upload.py +++ b/clients/android/ci/play-upload.py @@ -9,7 +9,8 @@ tolerates it being raw JSON *or* base64-encoded JSON. Usage (upload a new build): SERVICE_ACCOUNT_JSON='' \ python3 play-upload.py --package io.unom.punktfunk \ - --aab path/to/app-release.aab --track internal --status completed [--no-commit] + --aab path/to/app-release.aab --track beta --also-track alpha \ + --status completed [--no-commit] Usage (promote a build that is already on Play, no rebuild): python3 play-upload.py --package io.unom.punktfunk \ @@ -164,6 +165,9 @@ def main(): ap.add_argument("--promote-from", metavar="TRACK", help="with --promote: assert the code is on TRACK, then clear TRACK") ap.add_argument("--track", default="internal") + ap.add_argument("--also-track", action="append", default=[], metavar="TRACK", + help="assign the same versionCode to this track too, in the same edit " + "(repeatable). Canary uses it to feed open + closed testing at once.") ap.add_argument("--status", default="completed") ap.add_argument("--user-fraction", type=float, help="staged rollout fraction, 0` would assign and clear the same track in one edit; + # whichever PUT lands second silently wins. Refuse the ambiguity instead. + if t in (a.track, a.promote_from): + sys.exit(f"ERROR: --also-track {t} duplicates --track/--promote-from") notes = load_release_notes(a.release_notes_file, a.release_notes_language) \ if a.release_notes_file else None @@ -209,6 +218,11 @@ def main(): put_track(app, edit, tok, a.track, [vc], a.status, a.user_fraction, notes) print(f"assigned versionCode={vc} -> track={a.track} status={a.status}" + (f" userFraction={a.user_fraction}" if a.user_fraction is not None else "")) + # Same edit, so one commit (and one Play review) covers every track the code lands on — + # the tracks can never disagree about which canary is current. + for t in a.also_track: + put_track(app, edit, tok, t, [vc], a.status, a.user_fraction, notes) + print(f"assigned versionCode={vc} -> track={t} status={a.status}") # Same edit as the assignment above, so the code is never active on both tracks at once. if a.promote_from: put_track(app, edit, tok, a.promote_from, [], a.status) diff --git a/docs/releases/README.md b/docs/releases/README.md index 4004d86f..4f7878cc 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -54,8 +54,8 @@ no build, no assets on the Gitea release, nothing on Play. Two more checks sit d `play-upload.py` refuses text over the 500-char cap (printing the real count) before it uploads, because the API only rejects oversized notes at commit, by which point the AAB is already on Play. -Canary is exempt: it has no curated notes, and Play reusing text for internal testers costs -nothing. +Canary is exempt: it has no curated notes; open-testing users see the previous release's text on +a canary, which is cosmetic and cheaper than gating every main push on a notes file. Same freeze rule as the notes: once the tag exists, this file is the record of what that versionCode shipped.