---
name: release
description: Amply's release process — versioning via version.properties/bump.sh, signing material, the release-prepare/release-tag workflows, store-listing metadata, Play Store screenshot generation, and the Google Play publication gate. Use when cutting a release, bumping a version, regenerating store screenshots, or touching signing/release CI.
---

# Release

## Current state (honest)

Amply is **pre-launch** (`0.1.0-beta1`). It uses CAPod's versioning system: a `version.properties` source of truth,
a `VERSION` mirror, a `tools/release/bump.sh` bump/validate tool, and `release-prepare` / `release-tag` workflows. The
tooling exists but no release has been cut. Fastlane provides **store-listing metadata + screenshots**
(`fastlane/metadata/android/en-US/`) plus manual `supply` lanes in `fastlane/Fastfile`. Those lanes are **not** wired
into CI and are gated (see below), so `release-tag.yml` still publishes a **FOSS GitHub release only** (see CI below).

## Store listing & screenshots

- **Metadata**: `fastlane/metadata/android/en-US/{title,short_description,full_description}.txt` + `changelogs/default.txt`,
  validated in CI by `check_metadata_length.sh` (title 30 / short 80 / full 3800 / changelog 500 chars).
- **Screenshots**: generated from `@Preview` composables on the JVM (no device) via the Compose Preview Screenshot
  Testing plugin (`com.android.compose.screenshot`, enabled by `android.experimental.enableScreenshotTest=true` in
  `gradle.properties`). The store composables live in `app/src/debug/.../screenshots/ScreenshotContent.kt`; the
  capture entry points (`@PreviewTest`) and locale annotations live in `app/src/screenshotTest/.../screenshots/`.

  ```bash
  # 1. Render (writes to app/src/screenshotTestGplayDebug/reference/, which is gitignored)
  ./fastlane/generate_screenshots.sh
  # 2. Normalize (flatten alpha → opaque 1080x1920) + sort into the committed metadata tree
  ./fastlane/copy_screenshots.sh
  ```

  Committed output lands in `fastlane/metadata/android/en-US/images/phoneScreenshots/` as `1_dashboard_light.png …
  6_reconnect_gesture.png` (names come from `copy_screenshots.sh`'s `screen_file` map). Both scripts fail loudly on
  any count/dimension/format mismatch and `copy_screenshots.sh` requires ImageMagick. Needs the JDK 21 build
  toolchain like everything else. CI compiles these sources (`compileGplayDebugScreenshotTestKotlin`) but does
  **not** render — layoutlib output differs across machines — so **regenerating screenshots is a manual pre-release
  step**.
- **`supply` lanes** (`beta` / `production` / `listing_only` / `screenshots_only`): every lane that mutates Play state
  refuses to run unless `AMPLY_PLAY_PUBLISH_APPROVED=1`, keeping publication behind the `specialUse` gate below.
  Prerequisites (all manual, none automated): an existing Play app for `eu.darken.amply`, a service-account JSON at the
  `Appfile` path (or `AMPLY_SUPPLY_JSON_KEY`), gplay signing material, and the `specialUse` review passed.

## Versioning

- Single source: `version.properties` at the repo root, parsed at configuration time by the buildSrc
  `ProjectConfigPlugin` (CAPod's scheme): `versionName = "major.minor.patch-type{build}"`,
  `versionCode = major*10000000 + minor*100000 + patch*1000 + build*10`. `type` must be `rc` or `beta` (bump.sh
  rejects anything else). A root `VERSION` file (`<name> <code>`) mirrors it as a drift check.
- Constraints (enforced by both `ProjectConfig`'s `require`s and `bump.sh`): `minor`, `patch`, `build` are `0..99` —
  overflow collides with the next-higher field (e.g. `patch=0,build=100` equals `patch=1,build=0`). Changing only
  `type` does **not** change the versionCode, so a store update needs another field bumped. versionCode stays
  monotonic (`bump.sh` refuses a non-increasing code).
- **Bumping**: don't hand-edit — run `./tools/release/bump.sh --mode=plan --bump-kind=<build|patch|minor|major>`
  to preview, `--mode=write` to apply (rewrites both files and re-verifies). `--mode=check` validates consistency
  (also run in CI). Prefer the `release-prepare` workflow, which wraps bump.sh with tag-collision guards.
- No build-type or flavor suffixes: every variant installs as `eu.darken.amply` with the same versionName. Because
  signing certificates differ (debug key vs foss key vs gplay upload key), installed variants are mutually exclusive
  on a device — switching requires an uninstall.

## Signing

Release/beta signing loads per flavor (`releaseFoss`, `releaseGplay`) from either:

- Environment variables: `STORE_PATH`, `STORE_PASSWORD`, `KEY_ALIAS`, `KEY_PASSWORD` (checked first), or
- Properties files under `~/.config/projects/eu.darken.amply/`:
  `signing-foss.properties` and `signing-gplay-upload.properties`, using keys `release.storePath`,
  `release.storePassword`, `release.keyAlias`, `release.keyPassword`.

If signing material is absent, the flavor simply builds unsigned — the config degrades gracefully rather than failing.
Never commit signing material or point these at repo paths.

## CI

`.github/workflows/code-checks.yml` runs on push to `main` and on PRs as matrix jobs (CAPod parity): wrapper
validation (in the shared `common-setup` action), `lintVital{Foss,Gplay}{Beta,Release}`, `assemble{Foss,Gplay}Debug`,
`test{Foss,Gplay}DebugUnitTest`, a fastlane metadata length check, and a **release-tooling check** (shellcheck +
bats on `bump.sh`, plus `bump.sh --mode=check`). It skips version-bump-only pushes (`paths-ignore: VERSION,
version.properties`). CI does **not** run R8/minified packaging (lintVital only compiles the beta/release sources) —
run `./gradlew assembleFossRelease assembleGplayRelease` locally before tagging a release.

## Release workflows

- **`release-prepare.yml`** (`workflow_dispatch`): computes the next version with `bump.sh`, guards tag collisions,
  and — when `dry_run=false` — commits `version.properties`+`VERSION`, tags `v<name>`, and pushes. The push job needs
  the `RELEASE_APP_CLIENT_ID` / `RELEASE_APP_PRIVATE_KEY` GitHub App secrets; the dry-run plan needs no secrets.
  A manual `workflow_dispatch` (dry-run/build-only) must be launched **from a `v*` tag ref**, not a branch — the
  tag-validation job intentionally fails on a branch ref so the publish job can never run against a non-tag.
- **`release-tag.yml`** (on a `v*` tag): validates the tag against `version.properties`, builds `assembleFossBeta`
  (for `-beta` tags) or `assembleFossRelease`, and attaches the versioned APK to a GitHub (pre-)release. Signed only
  when `SIGNING_KEYSTORE_BASE64` + `STORE_PASSWORD`/`KEY_ALIAS`/`KEY_PASSWORD` secrets are present; otherwise the APK
  is unsigned (its filename carries an `-UNSIGNED` marker).
- **Deliberately omitted vs. CAPod**: no Google Play upload job (the manual `supply` lanes exist but are gated on
  `AMPLY_PLAY_PUBLISH_APPROVED` and never wired into CI, and Play
  publication must stay gated — see below) and no Pages deploy (the project website is deferred).

## Google Play gate

Google Play publication must remain gated on approval of the declared `specialUse` foreground-service use case (see
`privileged-access.md`). Do not treat a green CI build as clearance to publish. `release-tag.yml` intentionally does
**not** upload to Play — that step is added only after the use-case approval lands.
