◉ANTIGRAVITY LABJP
Articles/App Development
▣ App Development/2026-06-24Advanced

Three Paths Were Each Picking Their Own Number — Deriving versionCode From a Single Source So Releases Stop Stalling

Now that AI Studio can generate an app from one prompt and push it straight to Play's internal testing track, three paths — you, CI, and the agent — each allocate versionCode on their own and collide. Here's how to derive the number from a single source and add a pre-upload guard, with working code.

antigravity461android30google-play5ci-cd16versioning

✦ Premium Article

One morning the overnight release pipeline had stopped on "Version code 1037 has already been used. Try another version code." When I traced it, there wasn't a single culprit. The night before I had pushed one build from my machine to internal testing, CI had built an AAB from the same commit and assigned the same number, and AI Studio had streamed a generated verification build to the internal track as well. Three paths, each allocating versionCode in its own way.

Since I/O 2026, AI Studio can generate a Kotlin/Jetpack Compose app from a text prompt, run it in an embedded emulator, push it to a physical device over USB, and deliver it all the way to Google Play's internal testing track from one screen. I'm glad the distance from "build it" to "ship it" collapsed, but behind that convenience, one more path to Play has appeared. On top of manual and CI uploads, a path where "the machine ships on its own" is now a permanent resident.

To Play, versionCode is the integer that orders an app's builds uniquely. It has to increase monotonically across tracks, and a number used once can never be reused. If a single actor allocates the number, collisions don't happen. The problem is that the number of allocators grew to three, and none of them knows how high the others have gone.

Why three paths collide

The heart of the collision is that "who decides the next number" is distributed. The common allocation schemes each look at different state.

Allocation schemeState it readsVisible to other paths?
Bump versionCode +1 by hand in the editorLocal build.gradleNo (local only)
CI adds +1 to the previous build's valueCI cache / last artifactNo (inside CI)
Agent / AI Studio allocates from current time, etc.The runtime environmentNo (that run only)

Each scheme works fine on its own. But when all three push to the same app in parallel, they don't share "how high have we gone," so they assign the same number twice or assign a lower number afterward. Play demands monotonic increase and uniqueness, so a later low number or a duplicate gets rejected and the release stalls.

Having shipped apps solo for a long time, my sense is that this class of failure can't be fixed with "smarter." Making each path bump +1 more cleverly leaves the collision in place if the state they read is fragmented. What works is consolidating how the number is decided.

Deriving versionCode from a single source

In this situation I choose to take versionCode out of "a value a human holds by hand" and turn it into a deterministic function that yields the same integer no matter who computes it. The most workable single source is the repository history itself. The number of commits on main is the same value computed from any machine, by any path.

// app/build.gradle.kts
// Derive versionCode deterministically from the commit count of main.
// The point: the same integer whether computed locally, in CI, or by an agent.
import java.io.ByteArrayOutputStream
 
fun gitVersionCode(): Int {
    // A shallow clone in CI loses commits, so leave an escape hatch
    // that lets an environment variable override the value.
    System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull()?.let { return it }
 
    val out = ByteArrayOutputStream()
    val result = exec {
        commandLine("git", "rev-list", "--count", "HEAD")
        standardOutput = out
        isIgnoreExitValue = true
    }
    val count = out.toString().trim().toIntOrNull()
    require(result.exitValue == 0 && count != null && count > 0) {
        "Could not derive versionCode from commit count. " +
            "For a shallow clone, set fetch-depth: 0 or pass VERSION_CODE_OVERRIDE."
    }
    // Add a base offset so the new numbers never collide with the
    // hand-allocated band you used in the past.
    return 100000 + count
}
 
android {
    defaultConfig {
        versionCode = gitVersionCode()
        versionName = "2.${gitVersionCode() - 100000}"
    }
}

Two things matter here. One is that a shallow clone in CI reports a commit count that diverges from reality, so set fetch-depth: 0 (fetch full history) or allow an explicit override. The other is to shift the new numbering band away from the band you used under hand allocation (say the 1000s) with a base offset. When I migrated, I added an offset comfortably larger than the maximum of the old band so old and new never cross.

You can also use a CI build number as the source of truth. On GitHub Actions, github.run_number increases monotonically, so passing it into VERSION_CODE_OVERRIDE consolidates allocation around CI. Whether you make repository history or the CI build number your truth, the essential thing is to pick exactly one and align every path to it. If there are two sources of truth, there is no truth.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦If your automated releases have been stalling on 'Version code already used', you can build a setup today where no path — manual, CI, or agent — ever collides on the number
✦You'll be able to derive versionCode deterministically from git commit count or a CI build number, and add a guard that checks Play's current highest value before uploading
✦You'll avoid undocumented traps like the shared numbering space across internal/closed/production and the monotonic-increase constraint, so staged rollout keeps flowing
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

▣ App Dev2026-08-31
Why I took the show-or-not decision away from every dialog
A paywall prompt, a review request and a rewarded-ad confirmation landed on the same frame. Here is how I moved the show-or-not decision out of each dialog into a single gate, how I rewrote the agent instructions as invariants, and how a CI check keeps it that way.
▣ App Dev2026-08-18
Ordering Six App Updates for the API Level 36 Deadline, and Sizing Each Staged Rollout
Google Play's API level 36 requirement lands on every app you own with the same date. When the deadline is fixed, the only thing left to decide is the order. Here is how I ranked six apps by fragility and worked each rollout step backwards from the observations I actually needed.
▣ App Dev2026-04-03
Antigravity × Capacitor Hybrid App Implementation Guide: An AI-Driven Workflow for Taking a Web App to iOS/Android Native
Implementation notes for taking an existing web app to iOS/Android with Capacitor and Antigravity, drawn from running indie apps. Covers native API integration, AdMob initialization order, launch-time white flash, and pre-submission checks, separating what is verified from what is not.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links