Free weekly email: the week's new posts, videos and one tip from the books, every Sunday. Get it free →
Homelab the Right Way

Home › Posts

The trap of the version code reset

You set a version code in CI to prevent duplicates, but the workflow recreation resets the counter, forcing you to use a code Play has already rejected.

· 4 min read · androidgradlegoogle playversioningci

The trap of the version code reset

Most developers set a version code in CI to guarantee that every upload is unique. That is the trap. A workflow recreation resets the counter to 1, so every new run tries to upload a version code Play has already seen, and the upload is refused. You might see the error message in the console or the Play Console, but you might not realize the root cause is the counter resetting. The book's example shows a workflow that computes the code using the CI run number, which starts at 1. This number would create a conflict with the manual release, and Play rejects any code at or below the live one.

The book "Pass Google Play Review" explains that versionCode must be a positive integer, each release must use a greater value than the one before, and Play refuses an upload that reuses a code. The greatest value Google Play allows is 2100000000. If you compute the code from the CI run number, you discover the app's first release was uploaded by hand with a higher code, so every automated upload is refused. The trap is that you think you are being safe by enforcing uniqueness, but you are actually creating a conflict with the manual release. You must ensure your code always increases and stays far below the limit.

The rule: pass the code in from the build

The book recommends the safest pattern for automated releases: pass the version code in from the build, with a fallback for local builds. You do not hardcode the number in the source. This approach ensures the build system pulls the number from the environment. If the environment variable is missing, it falls back to 1 for local development. This separation of concerns keeps your build configuration clean and allows you to control the version number in your CI pipeline.

android {
    defaultConfig {
        versionCode = System.getenv("VERSION_CODE")?.toIntOrNull() ?: 1
        versionName = System.getenv("VERSION_NAME") ?: "0.1.0"
    }
}

This configuration reads the version code from the environment variable VERSION_CODE. If the variable is not present, it defaults to 1. This is useful for local builds where you might not have the CI environment variables set. However, for CI builds, you must provide the variable. This pattern prevents you from hardcoding a specific number that might conflict with existing releases.

The trap: the CI run number resets

The book warns that a CI run number restarts when the workflow is recreated. If you compute the code from the clock in CI, you rely on a counter that can start over. The book shows an example of this pitfall in the "CaptionBurn" workflow. The workflow in chartmann1590/captionburn computes the code exactly this way. The workflow's own run counter starts at 1 and would undercut the version code already live on Play (116, published manually). Play rejects any code at or below the live one.

CODE=$(date -u +'%y%j%H%M')
echo "VERSION_CODE=$CODE" >> "$GITHUB_ENV"

The comment in the workflow explains why: the workflow's own run counter starts at 1 and would undercut the version code already live on Play. Its first successful automated release used version code 262850104 and versionName 0.1.20261012.0104. The trap is that the workflow counter is not persistent. When you recreate the workflow, the counter goes back to 1. If you are already past 116, the new code will be rejected. You need a number that is guaranteed to be unique and larger than the previous one.

The fix: compute from the clock, not the counter

The book suggests computing the code from the clock in CI so it only ever goes up. You should use a date-based code that always increases and stays far below the limit. The example in the book uses the year, day of year, and time in UTC. This method guarantees that every new build gets a unique, increasing number. It does not rely on a workflow counter that can restart. The Dosely workflow takes the opposite approach and asks for the code as a manual input, with a description that tells you to check App bundle explorer for the highest code already used.

# yy + day of year + HHMM in UTC, e.g. 262850104 (2026, day 285, 01:04)
CODE=$(date -u +'%y%j%H%M')
echo "VERSION_CODE=$CODE" >> "$GITHUB_ENV"

This code calculates the version code based on the current UTC date and time. The format is yy (year), j (day of year), H (hour), M (minute). This ensures that every build gets a unique number based on when it was built. Since the date and time always increase, the version code will never decrease or repeat. This is the safest way to generate version codes in CI, as described in the book.

Checklist: Chapter 2

☐ compileSdk on the newest stable level; targetSdk meets this year's requirement (API 36 at the time of writing) ☐ Target level raised in spring on a branch, tested against the behavior-changes page ☐ Extension requested in Play Console only if you truly cannot make August 31 ☐ Version code passed in from CI, always increasing, below 2100000000 ☐ Highest version code per app known, across phone and Wear bundles

More articles