Release management · Android · DevOps

From git push to Google Play: The Full Android Release Lifecycle, Told by the Release Manager 🚀

Khairul Alam·October 2026·~20 min read
Release managementAndroidCI/CDFirebase App DistributionDeployGateGoogle Play

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.

The whole post in 60 seconds
  • 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
  1. The cast
  2. The road map
  3. Stage 1–2: Pull request and merge
  4. Stage 3: Cut the release branch
  5. Stage 4: CI builds the release candidate
  6. Stage 5: Testers get the build
  7. Stage 6: Google Play, internal to closed
  8. Stage 7: Staged rollout to the market
  9. Stage 8: Announce, watch, close
  10. Who watches what
  11. The pipeline as code
  12. When it goes wrong: the hotfix lane
  13. Every company is different, and yet…
  14. Release-day runbook and classic oops moments
  15. 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.

💻Devwrites the code🔍Reviewerguards mainCI Robotbuilds + ships🧪QA / testersbreak it nicely🧭Release Managerthat is me📣Productsays go
Six players, one goal. The robot does the boring parts so humans can make the judgment calls.

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.

🛠️1Code + PR✅2Merge to main🌿3Cut release branch🤖4CI builds the AAB📲5Testers get it🧪6Play internal / closed🚀7Staged rollout📣8Announce + watchSTARTFINISH: users have it, the team knows, we are watching
Follow the dotted road. At every station there is a check, and the build only moves forward when the check is green.
GitHubPR + branches CI serviceActions · CircleCI · Bitrise Signed AABbuilt once FirebaseApp Distribution DeployGate QA + UAT + dogfood installs Play internal≤100 testers, minutes Closed / openbeta, pre-launch report Productionstaged rollout 1→100% same AAB, promoted Watchvitals Crashlytics + Android vitals feed back: halt, hotfix, or carry on
The same bundle flows left to right. The dashed line is the feedback loop that makes you brave.

3. Stage 1–2: Pull request and merge

1

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.

Dev + ReviewerGitHub Actions / CircleCI / BitrisePR check time, flaky tests
2

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.

DevBranch protection, squash merge, Remote Config flagsmain build status

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

3

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. versionName is for humans (2.4.0). versionCode is 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.
Release managergit, GitHub release notes, Gradle propertiesfix requests creeping in
main release/2.4.0 hotfix/2.4.1 cut here fix #1fix #2 cherry-pick tag v2.4.0 🚀 fixv2.4.1 merge back
The release branch is a small, quiet room. Only fixes get in. After the release it is merged back so main never loses a fix.

5. Stage 4: CI builds the release candidate

4

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:

  1. Checks out the exact commit and restores Gradle caches.
  2. Runs unit tests, lint, and instrumented or managed-device tests as budget allows.
  3. Decodes the signing keystore from encrypted secrets (or uses Play App Signing with an upload key) and builds the AAB.
  4. Uploads the AAB and the R8 mapping file as artifacts. Without the mapping file, production stack traces are unreadable.
  5. Posts the build number and links to the team channel.
DevOps + Release managerGitHub Actions · CircleCI · Bitrise · Gradle · fastlanebuild time, signing, secrets, artifact size

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

5

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 DistributionDeployGate
Good atTight Firebase integration, groups, release notes, tester feedback, Crashlytics linkInstant install links and QR, version history, popular in Japan and for non-technical testers
Upload from CIGradle plugin, Firebase CLI or a CI stepREST API, CLI or a CI step
Typical useNightly dogfood, QA regression, UAT buildsStakeholder 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.

QA + ProductFirebase App Distribution · DeployGatetester coverage, open blockers

7. Stage 6: Google Play, internal to closed

6

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.
Release managerPlay Console · Play Developer API · fastlane supply / Gradle Play Publisherpre-launch report, policy warnings, review status

8. Stage 7: Staged rollout to the market

7

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.

Release manager + on-call engineerPlay staged rollout · Crashlytics · Android vitals · analyticscrash-free, ANR, funnel, ratings, support tickets
🙈 Halting does not un-install

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

8

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.
Release managerSlack / Teams, Jira, GitHub releases, emailpost-release health for 24–48 h

What I send, and to whom

AudienceChannelWhat they need
EngineeringTeam channelVersion, tag, what is in it, known issues, branch is unfrozen, who is on call
QATeam channelVersion live, which areas to monitor, regression follow-ups
Product and businessEmail or announcement channelPlain-language highlights, rollout percentage and date, metrics to expect
Support and salesSupport channelWhat changed for users, FAQ, known issues, how to escalate
LeadershipShort summaryShipped 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.

🛡️Crash-free99.7%line: ≥ 99.5 🧊ANR rate0.2%under Play limit ⭐Rating4.6new reviews 🛒Checkout−3%limit: −5% 🚨Stop rule, agreed in advance:past the line → halt → kill switch → page on-call → hotfix
The numbers and thresholds are examples. Pick yours with product before release day, not during an incident.
StageDevQARelease managerDevOpsProduct
PR + mergeOwns green checks–Glances at main healthRunner capacity, flaky infra–
Release branchCherry-picks fixesPlans regressionOwns freeze and scopeBranch rulesApproves scope
CI buildFixes breakages–Verifies artifact and versionOwns pipeline, secrets, signing–
Tester distributionTriages bugsOwns regressionTracks blockersUpload step worksOwns UAT sign-off
Play internal/closedStandbySmoke and upgradeOwns Console, policy, reviewService account validRelease notes
Staged rolloutOn-call for crashesWatches reportsOwns the slider and halt callBackend and infra healthWatches funnels
Post-releaseFixes escaped bugsAdds regression testsAnnounces, tags, closesCleans artifactsReads 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)

12. When it goes wrong: the hotfix lane

🚨Alert ✋Halt rollout Kill switch?flip a flag Hotfix branchfrom release tag Fast pipelinebuild + smoke test Release x.y.1faster rollout 📝 incident note + prevention
A rehearsed hotfix lane is boring. An improvised one is a long night.

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.

StyleCadenceHow it looksFits
Release trainWeekly or bi-weekly on a fixed dayBranch cut on a set day. Whatever is ready rides, the rest waits for the next trainLarge apps, many teams
Monthly / scheduledEvery 2–6 weeksLonger UAT, a release approval meeting or change-advisory board, marketing coordinationEnterprise, regulated or fintech apps
Continuous deliveryEvery merge or dailyTrunk-based, heavy flags and automation. Staged rollout is the main safety netSmall teams with strong tests
Milestone / feature releaseWhen the feature is doneBig launches with a coordinated campaign and a longer betaConsumer launches, games
Region stagedCountry by countryStart in a small market, expand as health is provenGlobal consumer apps
What is the same everywhere
  • 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

My release-day checklist
  • ☐ Release branch is green; no open blocker bugs; scope matches the plan.
  • ☐ versionCode is higher than anything already uploaded; versionName is 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.
🙈 Oops moments I have seen (or caused)
  • 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

TermIn plain words
AABAndroid App Bundle, the file you upload to Play. Google builds device-specific APKs from it.
versionCode / versionNameA number that must increase on every upload, and the label users see.
Release branchA branch frozen for stabilizing one version.
Cherry-pickCopying one specific commit onto another branch.
Release candidateThe build that will ship if it passes every gate.
TrackA Play release channel: internal, closed, open or production.
Staged rolloutReleasing to a growing percentage of users.
Kill switchA remote flag that disables a feature without a new release.
Mapping fileThe R8 file that turns obfuscated stack traces back into readable ones.
HotfixAn urgent fix released outside the normal schedule.
← Back to all writing