Skip to content
Plazoleta
· Raúl López

From `main` to Google Play Without Touching Anything by Hand

At Jornia, every push to main that touches the app ends up on a tester's phone without anyone opening the Play console. What got automated, what was left manual on purpose, and why that second list matters just as much as the first.

The question we get asked most often about this isn't "do you have CI/CD?". It's "who actually hits the publish button?". The answer is a little uncomfortable the first time you say it out loud: nobody. Every push to main that touches the app builds, signs, and uploads the AAB to Play. Publishing isn't an event some person decides on a Tuesday afternoon. It's part of the build.

And yet there are things that are still manual, on purpose, with the decision written right into the pipeline itself. That second list is the interesting part of this post.

What gets automated

android-release.yml fires on every push to main that touches app/. It runs the unit tests first — nothing ships with red tests, that's the bare minimum gate for any CD that takes itself seriously — builds the signed AAB, and uploads it to Play. No special artifact, no manual step in the console: the same workflow that runs on every push is the one you trigger by hand when you need to publish somewhere else, with a parameter for which track.

The versionCode — the internal number Play uses to know which build is newer — is the GitHub Actions run number. It grows on its own, never repeats, and if an upload fails and the workflow has to be re-run, nobody cares about the gap it leaves in the numbering. It's a small detail, but it avoids the usual alternative: someone keeping count by hand in a spreadsheet or, worse, computing it from the last number they remember.

There's a less obvious check worth mentioning, because it isn't decided by your own code: before uploading the artifact, a script checks that the AAB's 64-bit native libraries are aligned to 16 KB. Play warns about — and at some point will start rejecting — artifacts that don't comply, and what determines whether you comply isn't anything you wrote: it's which NDK version each dependency you're pulling in was built with. That can change just from bumping a library, with nobody asking for it. That's why the check isn't run against the source code, it's run against the already-compiled artifact, right before uploading it: that's the only place where the question has a real answer.

And uploads queue up, they don't stomp on each other: the workflow has a concurrency group that makes the second release wait if the first one is still running, instead of cancelling it. It makes sense as soon as you think about what a release actually is: it's not a disposable job you can throw away and repeat, it's a version that may already have reached someone.

Why the default track is "alpha" and not "internal"

This decision changed on July 17, 2026, and the reason isn't in any CI/CD best-practices manual: it's in Play's rules for graduating out of closed testing. During the twelve-active-testers-for-fourteen-days window, Google doesn't just look at whether you have enough people signed up. It also looks at whether the closed track has active releases. A frozen track — even one with more than twelve testers — delays access to production.

So the push to main publishes to alpha, which is closed testing, not the internal track that would be the "safer" textbook option. The team's dogfooding loses nothing from the switch: the same AAB reaches them just the same, only through the track that also counts toward Google's gate. It's the kind of decision that only makes sense if you know the fine print of the program you're applying to, and that without that fine print would look like the opposite of cautious.

What stays manual, on purpose

Production is locked. Not out of an oversight or lack of time to set it up: the service account that uploads artifacts doesn't have permission to release to production, and that absence is a dated decision, not a placeholder. Promoting a release to production — the one that reaches anyone who installs the app from the public listing — is a human act. Someone goes into the Play console and does it while watching, not a job that runs on its own on a Friday night.

The rest of the machinery already knows how to run the gradual rollout when the time comes: the same manual dispatch supports releasing to production with a fraction of users instead of all at once. It's built and tested on the other tracks. The only thing missing is the permission, and it's missing because nobody has granted it yet, on purpose.

There's a second manual step with less ceremony but just as real: the first upload of a brand-new app to Play has to be done by hand in the console, no way around it. The automated publishing API can't create the app listing the first time — it needs the app to already exist on Play's side to be able to talk to it. So the first AAB, the one that creates Jornia as an entity in the console, gets uploaded by someone from their machine or from an artifact already built by CI. From that point on, and only from that point on, the pipeline can take over. It's a chicken-and-egg problem with no possible automation: you can't automate the creation of the thing that automation needs to already exist.

What we take away from this

"CI/CD all the way to Play" isn't a binary goal. There's a long stretch that gets automated without a second thought — tests, signing, upload, release notes — and a short stretch, with a name and a decision date attached, that's left manual because automating it would take away from a person a decision that needs to stay theirs.

The third party's rules can flip what looks prudent. Publishing to the "closed" track instead of the "internal" one sounds more exposed. Here it's the opposite, because Google's gate doesn't reward caution: it rewards activity. Automating without reading the program's fine print would have automated the wrong decision.

Bootstrap by hand whatever automation can't reach yet. The first upload of a new app is, by the platform's own design, the one step you can't skip. Accepting that costs less than chasing an automation that doesn't exist.


If you're setting up your app's release pipeline and you've run into one of these chicken-and-egg problems, tell me about it: there's bound to be one more we haven't seen yet.

Got a similar problem?

Tell us about it. You'll be talking straight to whoever writes the code.