From 4e7ee81d605145b8b287a14073381ec3b960990c Mon Sep 17 00:00:00 2001 From: test Date: Sat, 29 Aug 2026 22:30:01 +0000 Subject: [PATCH 1/2] ci: fail closed if a pre-release tag would publish to Play production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tag→track mapping already sends X.Y.Z to production and everything else to internal (#182). This makes that contract executable: resolve via scripts/assert-play-track.sh, write the track into the job summary, and refuse to run supply when a pre-release tag would hit production. Also drop the leftover test-job `SUPPLY_TRACK: production` env (it never reached fastlane) so it cannot be copied onto the publish job. Git-Session-Id: 5802 --- .github/workflows/build.yml | 29 +++++--- scripts/assert-play-track.sh | 139 +++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 10 deletions(-) create mode 100755 scripts/assert-play-track.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e52a909e..7be9f1f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -233,14 +233,18 @@ jobs: name: Test runs-on: ubuntu-24.04 needs: [build-rust] - env: - SUPPLY_TRACK: production # used by fastlane to determine track to publish to steps: - uses: actions/checkout@v4 with: submodules: 'recursive' + # Cheap, no SDK: keeps the Play-track contract from regressing on every PR. + # The leftover job-level `SUPPLY_TRACK: production` here never reached + # fastlane (release-fastlane sets its own) and was a copy-paste footgun. + - name: Assert Play-track resolver + run: bash scripts/assert-play-track.sh --self-test + - name: Set up JDK uses: actions/setup-java@v4 with: @@ -412,15 +416,18 @@ jobs: # Set SUPPLY_TRACK based on whether this is a stable (x.y.z) or pre-release tag. # NOTE: nowsprinting/check-version-format-action classifies 0.x.x as not stable # (semver convention), but aw-android uses 0.x.x for production releases. - # Check for pre-release suffixes directly instead. + # Check for pre-release suffixes directly instead (scripts/assert-play-track.sh). TAG="${{ github.ref_name }}" - VERSION="${TAG#v}" - if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - SUPPLY_TRACK="production" - else - SUPPLY_TRACK="internal" - fi - echo "SUPPLY_TRACK=${SUPPLY_TRACK}" >> $GITHUB_ENV + SUPPLY_TRACK="$(bash scripts/assert-play-track.sh resolve "$TAG")" + echo "SUPPLY_TRACK=${SUPPLY_TRACK}" >> "$GITHUB_ENV" + { + echo "## Play Store publish" + echo "- tag: \`${TAG}\`" + echo "- SUPPLY_TRACK: \`${SUPPLY_TRACK}\`" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Assert Play track matches tag + run: bash scripts/assert-play-track.sh assert "${{ github.ref_name }}" "${SUPPLY_TRACK}" - uses: adnsio/setup-age-action@v1.2.0 - name: Load Android secrets @@ -438,6 +445,8 @@ jobs: # Newer fastlane versions fail hard if rubygems.org isn't in the system gem sources list. # This is idempotent (exits 0 if already present). gem sources --add https://rubygems.org + # Re-check at the publish boundary: the env that actually reaches supply. + bash scripts/assert-play-track.sh assert "${{ github.ref_name }}" "${SUPPLY_TRACK}" # bundle exec fastlane supply run --apk dist/*.apk bundle exec fastlane supply run --aab dist/*.aab diff --git a/scripts/assert-play-track.sh b/scripts/assert-play-track.sh new file mode 100755 index 00000000..24d7de00 --- /dev/null +++ b/scripts/assert-play-track.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Resolve or assert the Play Store supply track for an aw-android release tag. +# +# Contract: +# X.Y.Z (no suffix) → production +# anything else → internal (0.14.0b2, 0.14.0devYYYYMMDD, 0.14.0-rc1, …) +# +# Fail closed: a pre-release tag must never publish to production, even if +# SUPPLY_TRACK is later hardcoded or the resolver regresses. Stable tags may +# still be sent to internal (staged rollout); that is not this guard. +# +# Usage: +# scripts/assert-play-track.sh resolve +# scripts/assert-play-track.sh assert +# scripts/assert-play-track.sh --self-test + +set -euo pipefail + +usage() { + cat <<'EOF' >&2 +Usage: + scripts/assert-play-track.sh resolve + scripts/assert-play-track.sh assert + scripts/assert-play-track.sh --self-test +EOF + exit 2 +} + +is_stable_version() { + [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +tag_to_version() { + local tag="$1" + if [[ -z "$tag" ]]; then + echo "error: empty tag" >&2 + return 1 + fi + echo "${tag#v}" +} + +resolve_track() { + local version + version="$(tag_to_version "$1")" + if is_stable_version "$version"; then + echo production + else + echo internal + fi +} + +assert_track() { + local tag="$1" + local track="$2" + local version + version="$(tag_to_version "$tag")" + + if [[ -z "$track" ]]; then + echo "error: SUPPLY_TRACK is empty; refusing to publish ${tag}" >&2 + return 1 + fi + if [[ "$track" != production && "$track" != internal ]]; then + echo "error: unknown SUPPLY_TRACK=${track} for ${tag}" >&2 + return 1 + fi + if [[ "$track" == production ]] && ! is_stable_version "$version"; then + echo "error: refusing to publish pre-release tag ${tag} to production (SUPPLY_TRACK=${track})" >&2 + return 1 + fi + echo "ok: tag=${tag} version=${version} SUPPLY_TRACK=${track}" +} + +self_test() { + local fail=0 + expect_resolve() { + local tag="$1" want="$2" got + got="$(resolve_track "$tag")" + if [[ "$got" != "$want" ]]; then + echo "FAIL resolve ${tag}: got ${got} want ${want}" >&2 + fail=1 + fi + } + expect_assert_ok() { + if ! assert_track "$1" "$2" >/dev/null; then + echo "FAIL assert should pass: tag=$1 track=$2" >&2 + fail=1 + fi + } + expect_assert_fail() { + if assert_track "$1" "$2" >/dev/null 2>&1; then + echo "FAIL assert should fail: tag=$1 track=$2" >&2 + fail=1 + fi + } + + expect_resolve v0.14.0 production + expect_resolve 0.14.0 production + expect_resolve v1.0.0 production + expect_resolve v0.14.0b2 internal + expect_resolve v0.14.0beta2 internal + expect_resolve v0.14.0dev20260723 internal + expect_resolve v0.14.0-rc1 internal + expect_resolve v0.14.0rc1 internal + expect_resolve v0.14 internal + + expect_assert_ok v0.14.0 production + expect_assert_ok v0.14.0 internal + expect_assert_ok v0.14.0b2 internal + expect_assert_fail v0.14.0b2 production + expect_assert_fail v0.14.0dev20260723 production + expect_assert_fail v0.14.0beta2 production + expect_assert_fail v0.14.0 "" + expect_assert_fail v0.14.0 alpha + expect_assert_fail "" production + + if [[ "$fail" -ne 0 ]]; then + echo "assert-play-track self-test FAILED" >&2 + return 1 + fi + echo "assert-play-track self-test passed" +} + +cmd="${1:-}" +case "$cmd" in + resolve) + [[ $# -eq 2 ]] || usage + resolve_track "$2" + ;; + assert) + [[ $# -eq 3 ]] || usage + assert_track "$2" "$3" + ;; + --self-test) + self_test + ;; + *) + usage + ;; +esac From 5e935bf1838e92cfe5c9047afa00074427d38891 Mon Sep 17 00:00:00 2001 From: test Date: Sat, 29 Aug 2026 22:34:20 +0000 Subject: [PATCH 2/2] ci: pass asserted Play track to fastlane supply on argv supply defaults to production. The historical path was SUPPLY_TRACK via GITHUB_ENV; pass --track explicitly so the asserted value is the one that actually uploads. Git-Session-Id: 5802 --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7be9f1f6..99c69765 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -445,10 +445,12 @@ jobs: # Newer fastlane versions fail hard if rubygems.org isn't in the system gem sources list. # This is idempotent (exits 0 if already present). gem sources --add https://rubygems.org - # Re-check at the publish boundary: the env that actually reaches supply. + # Re-check at the publish boundary, then pass the asserted track on + # argv. supply's default track is production; SUPPLY_TRACK via env is + # the historical path, but an explicit --track is the fail-closed one. bash scripts/assert-play-track.sh assert "${{ github.ref_name }}" "${SUPPLY_TRACK}" # bundle exec fastlane supply run --apk dist/*.apk - bundle exec fastlane supply run --aab dist/*.aab + bundle exec fastlane supply run --aab dist/*.aab --track "$SUPPLY_TRACK" release-gh: needs: [build-apk, test]