Release management · Android · DevOps
From git push to Google Play: The Full Android Release Lifecycle, Told by the Release Manager 🚀
Writing the code is the middle of the story. Before it there is a pull request, and after it there is a release branch, a build server, a handful of testers, three Google Play tracks, a rollout slider, and a message to everyone who cares. I have been the person holding the release button for a long time, and I love it: it is the moment where weeks of work meet real people. This is the whole journey, stage by stage, with who watches what, what breaks, and how to tell the team when it is done.
- A release is a conveyor belt of gates: PR checks → merge → release branch → CI builds one signed bundle → testers (Firebase App Distribution or DeployGate) → Play internal → closed/open → staged rollout → announce.
- Build once, promote the same artifact. The bundle testers touched is the bundle users get.
- Every stage has an owner, a signal to watch and a stop rule. Humans own decisions, robots own repetition.
- Companies differ in cadence (weekly train, monthly, continuous), but the shape is the same: freeze, verify, widen, watch, tell people.
- The release is not done when it is live. It is done when the numbers are healthy and everyone has been told.
Contents
- The cast
- The road map
- Stage 1–2: Pull request and merge
- Stage 3: Cut the release branch
- Stage 4: CI builds the release candidate
- Stage 5: Testers get the build
- Stage 6: Google Play, internal to closed
- Stage 7: Staged rollout to the market
- Stage 8: Announce, watch, close
- Who watches what
- The pipeline as code
- When it goes wrong: the hotfix lane
- Every company is different, and yet…
- Release-day runbook and classic oops moments
- Takeaways and glossary
1. The cast
A release is a team sport. Here is who is on the field. Notice how many of them are not developers.
2. The road map
Eight stations from an idea in a branch to a happy user. Different companies rename them, merge them or skip one, but I have never seen a healthy release that had none of them.
3. Stage 1–2: Pull request and merge
The PR is the first gate
Everything starts with a small pull request. The CI robot runs lint, unit tests, a debug build and static analysis on every push. A human reviews the change. Branch protection makes both of these mandatory, so “I forgot to run the tests” cannot happen.
Merge to main, and keep main green
Squash-merge so history is readable. After merge, a post-merge pipeline can build a nightly or per-merge internal build for employees to dogfood. The rule I defend loudly: main is always releasable. If main goes red, fixing it is everyone’s first job.
Incomplete features merge behind a feature flag, so unfinished work never blocks a release. This one habit saves more release days than any tool.
Release manager says
If I ever have to ask “is main OK?” in chat, our automation has already failed. The answer should be a green badge on the repo.
4. Stage 3: Cut the release branch
Freeze a slice of history
On release day minus a few, I cut release/2.4.0 from main. From that moment the branch accepts only bug fixes that are cherry-picked from main (or fixed on the branch and merged back). Main keeps moving with the next version’s work.
- Bump the version.
versionNameis for humans (2.4.0).versionCodeis a number that must go up with every upload to Play. Many teams derive it from CI’s build number so it can never be forgotten. - Write the changelog. Pull the merged PR titles since the last tag, then edit into plain language.
- Announce the freeze. A short message: version, branch name, date, who approves fixes.
5. Stage 4: CI builds the release candidate
The robot makes one signed bundle
A push to release/* (or a tag) triggers the release pipeline on GitHub Actions, CircleCI or Bitrise. Nobody builds the release on a laptop. The pipeline:
- Checks out the exact commit and restores Gradle caches.
- Runs unit tests, lint, and instrumented or managed-device tests as budget allows.
- Decodes the signing keystore from encrypted secrets (or uses Play App Signing with an upload key) and builds the AAB.
- Uploads the AAB and the R8 mapping file as artifacts. Without the mapping file, production stack traces are unreadable.
- Posts the build number and links to the team channel.
Why build once? If the build for testers and the build for the store are two different compilations, the QA sign-off applies to neither. Build one bundle, record its hash, and promote it.
6. Stage 5: Testers get the build
Firebase App Distribution or DeployGate
The pipeline pushes the same build to an install-anywhere service so QA, product owners and employees can test without waiting for Play:
| Firebase App Distribution | DeployGate | |
|---|---|---|
| Good at | Tight Firebase integration, groups, release notes, tester feedback, Crashlytics link | Instant install links and QR, version history, popular in Japan and for non-technical testers |
| Upload from CI | Gradle plugin, Firebase CLI or a CI step | REST API, CLI or a CI step |
| Typical use | Nightly dogfood, QA regression, UAT builds | Stakeholder demos, partner testing, device-lab installs |
This is where QA runs the regression checklist and the business runs UAT. Their feedback becomes either fixes on the release branch (back to stage 3) or a sign-off. See the validation strategy article for how I structure those gates.
7. Stage 6: Google Play, internal to closed
Same bundle, now in the store
Once QA says go, the pipeline (or a human with a deliberate click) uploads the same AAB to the Play Console internal testing track. Testers receive it through the normal Play Store in minutes, which proves the thing that App Distribution cannot: store install, Play signing, and update-from-previous-version all work.
- Internal track: up to 100 named testers, fastest feedback. Smoke checklist, install and upgrade checks.
- Closed / open testing: invited groups or anyone with the link. The Play pre-launch report runs automated robots on Google’s devices and reports crashes, accessibility and security warnings.
- Store paperwork: release notes per language, screenshots if changed, the Data safety form, permission declarations, target API requirements. A forgotten declaration can block a release for days.
- Review: Google reviews updates. Most pass quickly, but allow buffer for the occasional slow or rejected review. Submit before you need it.
8. Stage 7: Staged rollout to the market
Open the tap slowly
Promote to production at a small percentage: 1% or 5% for a canary, then 20%, 50%, 100%. At each step I wait for enough sessions to see a real difference, then compare the new version against the old on the same days.
My stop rules are written before I press the button: crash-free users below the agreed line, ANR above the Play threshold, or a key funnel (login, checkout) down by more than a set percentage. Breach means halt the rollout, flip the kill switch flag if the feature has one, then hotfix.
Halting a staged rollout stops new users from getting the version, but anyone who already updated keeps it. That is why risky features hide behind a remote flag: you can switch off the behavior without a new release.
9. Stage 8: Announce, watch, close
The release is not done until people know
When rollout reaches 100% and the numbers are healthy for an agreed window (I like 24 to 48 hours), I close the release:
- Tag and merge back. Tag the commit, merge the release branch into main, archive the mapping file and the final AAB hash.
- Announce to the team and stakeholders. Different audiences, different messages (below).
- Update the tracker. Mark tickets released, link the version, close the release ticket.
- Retrospective if anything hurt. One paragraph and one action item beats a long meeting.
What I send, and to whom
| Audience | Channel | What they need |
|---|---|---|
| Engineering | Team channel | Version, tag, what is in it, known issues, branch is unfrozen, who is on call |
| QA | Team channel | Version live, which areas to monitor, regression follow-ups |
| Product and business | Email or announcement channel | Plain-language highlights, rollout percentage and date, metrics to expect |
| Support and sales | Support channel | What changed for users, FAQ, known issues, how to escalate |
| Leadership | Short summary | Shipped on time or not, health status, any risk |
Announcement template
📣 Android 2.4.0 is live (100%). Highlights: faster checkout, dark mode for settings, bug fixes. Health: crash-free 99.7%, no ANR change. Known issue: coupon banner flickers on Android 8 (fix in 2.4.1). Release notes: <link>. On-call: <name>. Thank you, everyone! 🎉
10. Who watches what
The most common release failure I see is not a bug. It is nobody knowing they were supposed to be watching. I put this table on the wiki.
| Stage | Dev | QA | Release manager | DevOps | Product |
|---|---|---|---|---|---|
| PR + merge | Owns green checks | – | Glances at main health | Runner capacity, flaky infra | – |
| Release branch | Cherry-picks fixes | Plans regression | Owns freeze and scope | Branch rules | Approves scope |
| CI build | Fixes breakages | – | Verifies artifact and version | Owns pipeline, secrets, signing | – |
| Tester distribution | Triages bugs | Owns regression | Tracks blockers | Upload step works | Owns UAT sign-off |
| Play internal/closed | Standby | Smoke and upgrade | Owns Console, policy, review | Service account valid | Release notes |
| Staged rollout | On-call for crashes | Watches reports | Owns the slider and halt call | Backend and infra health | Watches funnels |
| Post-release | Fixes escaped bugs | Adds regression tests | Announces, tags, closes | Cleans artifacts | Reads metrics |
For the DevOps column, the tools and install guides are in The Android DevOps Toolbox.
11. The pipeline as code
A sketch of a GitHub Actions release workflow. CircleCI and Bitrise express the same steps in config.yml or bitrise.yml. Treat action names and versions as placeholders and copy current ones from each action’s documentation.
name: release
on:
push:
branches: ["release/**"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<version>
- uses: actions/setup-java@<version>
with: { distribution: temurin, java-version: "<jdk>" }
- uses: gradle/actions/setup-gradle@<version>
- name: Test
run: ./gradlew lintRelease testReleaseUnitTest
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/release.jks"
- name: Build signed bundle
run: ./gradlew bundleRelease -PversionCode=${{ github.run_number }}
env:
KEYSTORE_PATH: ${{ runner.temp }}/release.jks
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
- uses: actions/upload-artifact@<version>
with:
name: release-bundle
path: |
app/build/outputs/bundle/release/*.aab
app/build/outputs/mapping/release/mapping.txt
distribute-testers:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@<version>
# upload to Firebase App Distribution (Firebase CLI or a maintained action)
# or to DeployGate (its API or CLI), then post the link to the team channel
play-internal:
needs: distribute-testers
environment: play-internal # protected environment: needs a human approval
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@<version>
# upload the same .aab to the Play internal track with a service account
# (fastlane supply, Gradle Play Publisher or a maintained action)
- A protected environment turns “send to Play” into a deliberate, logged human approval.
- Secrets live in the CI vault, never in the repo. Rotate them when a person with access leaves.
- Mask logs. A keystore password printed in a build log is a security incident.
12. When it goes wrong: the hotfix lane
The lane is the same pipeline with fewer steps, never a different, unreviewed one. Keep a tested hotfix procedure written down, and practice it once a quarter in a quiet week.
13. Every company is different, and yet…
I have seen very different cadences. They are policies layered on the same skeleton.
| Style | Cadence | How it looks | Fits |
|---|---|---|---|
| Release train | Weekly or bi-weekly on a fixed day | Branch cut on a set day. Whatever is ready rides, the rest waits for the next train | Large apps, many teams |
| Monthly / scheduled | Every 2–6 weeks | Longer UAT, a release approval meeting or change-advisory board, marketing coordination | Enterprise, regulated or fintech apps |
| Continuous delivery | Every merge or daily | Trunk-based, heavy flags and automation. Staged rollout is the main safety net | Small teams with strong tests |
| Milestone / feature release | When the feature is done | Big launches with a coordinated campaign and a longer beta | Consumer launches, games |
| Region staged | Country by country | Start in a small market, expand as health is proven | Global consumer apps |
- A freeze point after which scope stops growing.
- A repeatable build that nobody makes by hand.
- Verification by someone who did not write the code.
- Gradual exposure with a way to stop.
- Communication to everyone affected, before and after.
14. Release-day runbook and classic oops moments
- ☐ Release branch is green; no open blocker bugs; scope matches the plan.
- ☐
versionCodeis higher than anything already uploaded;versionNameis right. - ☐ Backend changes the app depends on are already deployed and backward compatible.
- ☐ Feature flags have safe defaults; the kill switch is tested.
- ☐ Mapping file uploaded to Crashlytics (or stored); the AAB hash recorded.
- ☐ UAT sign-off is written down with the build number.
- ☐ Play listing, release notes, Data safety and policy items are complete.
- ☐ On-call person is named and reachable; stop rules are posted.
- ☐ Announcement draft is ready.
- Upload rejected: versionCode already used.
- Built with the debug keystore, or the wrong upload key.
- The app shipped but the backend change was not deployed, so login broke.
- A flag that defaults on exposed a half-built feature to 100%.
- No mapping file, so every crash was unreadable.
- Release scheduled for Friday evening. (Please don’t.)
- Review took longer than expected and the marketing campaign started before the app was live.
- Everyone assumed someone else was watching vitals.
15. Takeaways and glossary
- A release is a pipeline of small gates, not one heroic push.
- Build once, sign once, promote the same bundle through testers, Play tracks and production.
- Release branches give you a quiet room to stabilize; staged rollout gives you a brake in the market.
- Assign a named owner and a stop rule to every stage; write them down.
- Close the loop: tag, merge back, announce, and watch for a day or two.
| Term | In plain words |
|---|---|
| AAB | Android App Bundle, the file you upload to Play. Google builds device-specific APKs from it. |
| versionCode / versionName | A number that must increase on every upload, and the label users see. |
| Release branch | A branch frozen for stabilizing one version. |
| Cherry-pick | Copying one specific commit onto another branch. |
| Release candidate | The build that will ship if it passes every gate. |
| Track | A Play release channel: internal, closed, open or production. |
| Staged rollout | Releasing to a growing percentage of users. |
| Kill switch | A remote flag that disables a feature without a new release. |
| Mapping file | The R8 file that turns obfuscated stack traces back into readable ones. |
| Hotfix | An urgent fix released outside the normal schedule. |
Links and further reading
- Play Console: open, closed and internal tests · Staged rollouts · Android App Bundle
- Firebase App Distribution · DeployGate docs · GitHub Actions · CircleCI · Bitrise
- On this site: The Android DevOps Toolbox · Release validation strategy · Git branching guide · App Distribution overview · Crashlytics