Android canary ships to Play open + closed testing instead of internal #234
@@ -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=<the good one>, 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/<tag>.txt to attach, e.g. v0.23.0 (blank = none)"
|
||||
required: false
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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)**.
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ tolerates it being raw JSON *or* base64-encoded JSON.
|
||||
Usage (upload a new build):
|
||||
SERVICE_ACCOUNT_JSON='<raw-or-base64 SA key>' \
|
||||
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<f<1; required by --status inProgress")
|
||||
@@ -183,6 +187,11 @@ def main():
|
||||
sys.exit(f"ERROR: --user-fraction must be strictly between 0 and 1 (got {a.user_fraction})")
|
||||
if a.aab and not os.path.isfile(a.aab):
|
||||
sys.exit(f"ERROR: AAB not found: {a.aab}")
|
||||
for t in a.also_track:
|
||||
# `--also-track <promote-from>` 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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user