Skip to content

Implement 30-bit version code schema with automatic derivation from git tags - #4

Merged
MinecraftFuns merged 4 commits into
masterfrom
copilot/update-versioning-schema
Dec 17, 2025
Merged

MinecraftFuns merged 4 commits into
masterfrom
copilot/update-versioning-schema

Conversation

Copilot AI commented Dec 17, 2025 •

Copy link
Copy Markdown

Implements automatic version code derivation using a 30-bit schema (7-7-7-9 bits for Major-Minor-Patch-Qualifier) that ensures proper ordering (1.2.0-beta < 1.2.0 < 1.2.1) while staying under Google Play's 2.1B limit.

Version Code Schema

Component Bits Range Description
Major 7 0-127 << 23
Minor 7 0-127 << 16
Patch 7 0-127 << 9
Qualifier 9 0-511 511=stable, 0-510=beta

Changes

  • app/build.gradle: Version code/name auto-derived from latest v*.*.* git tag

    • Stable releases: qualifier=511, name=v1.2.3
    • Beta builds: patch incremented by 1, qualifier=commits-since-tag, name=v1.2.4-beta.X
    • This ensures beta builds are always HIGHER than the previous stable release
    • Includes overflow handling (patch > 127 rolls to next minor, minor > 127 rolls to next major)
    • Environment variables (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_BUILD_COUNT, VERSION_STABLE) override git values for CI
  • CI Workflows: All updated with fetch-depth: 0 and version calculation steps

    • ci-build.yml: Beta versioning for PR/push builds with patch increment
    • release.yml: Stable versioning for v*.*.* tag releases
    • build-release.yaml: Legacy workflow aligned with schema
    • ci-tests.yml: Fetch depth for version consistency
  • Documentation: Created comprehensive docs/VERSIONING.md and updated README.md with versioning section

Example

After releasing v5.4.3:

// v5.4.3 stable   → (5 << 23) | (4 << 16) | (3 << 9) | 511 = 42,207,231
// v5.4.4-beta.1   → (5 << 23) | (4 << 16) | (4 << 9) | 1   = 42,207,233  ← Higher!
// v5.4.4-beta.2   → (5 << 23) | (4 << 16) | (4 << 9) | 2   = 42,207,234  ← Higher!
// v5.4.4 stable   → (5 << 23) | (4 << 16) | (4 << 9) | 511 = 42,207,743  ← Highest!
Original prompt

I want the versionCode for the project to be automatically derived following the schema attached. I want you to carefully think through the schema and how to apply that in the most elegant way to the current build pipeline. (The Python script is for your reference; it's probably not the best way to integrate this versioning schema into the project). Please follow best practices and write idiomatic code for this significant change to the build pipeline, and ensure you look through all CI files that produce binaries carefully to upgrade them to the same schema so there must be no difference. Also you must ensure all release versions follows the v*.. pattern by updating the gradle build file and all files you deem necessary to achieve a consistent state (which may involve changes to the gradle configuration and CI files as well) and all commit/pr CI generate binaries with sensible version name and version code following the same schema as well.

=== versioning documentation ===

To ensure ordering (1.2.0-beta < 1.2.0 < 1.2.1), safety (under 2.1B), and automation, I recommend a 30-bit schema that partitions the integer into four fixed-width fields.

Using 30 bits ensures the maximum value ($\approx 1.07$ billion) is safely under Google Play’s 2.1 billion limit ($2^{31}-1$ is the hard technical max, but 2.1B is the safe zone).

The Recommended "Safe 30-Bit" Schema

This split balances standard SemVer growth with a healthy buffer for CI/test builds between releases.

Component Bits Range Shift Position Description
Major 7 0 – 127 << 23 Up to v127 (plenty for most apps).
Minor 7 0 – 127 << 16 Up to v127.
Patch 7 0 – 127 << 9 Up to v127.
Qualifier 9 0 – 511 << 0 Reserved for stability/test status.

Total Capacity: $2^{30}-1 = 1,073,741,823$ (Safe).


The Logic: How to Interleave Test vs. Stable

To satisfy the rule Test < Stable, we reserve the maximum possible value of the "Qualifier" field for the Stable release. All test/beta builds use numbers lower than that maximum.

1. For a Stable Release (1.2.3):
Set the Qualifier to its max value (511 or binary 111111111).
[
\text{Code} = (\text{Major} \times 2^{23}) + (\text{Minor} \times 2^{16}) + (\text{Patch} \times 2^{9}) + 511
]

2. For a Test/Beta Release (1.2.3-beta.X):
Set the Qualifier to your build counter (derived from git). It must be $&lt; 511$.
[
\text{Code} = (\text{Major} \times 2^{23}) + (\text{Minor} \times 2^{16}) + (\text{Patch} \times 2^{9}) + \text{BuildCount}
]

Why this works (Example)

Notice how the integer naturally scales. Even the "highest" beta for 1.2.0 is lower than the stable 1.2.0, and stable 1.2.0 is lower than the very first beta of 1.2.1.

  1. 1.2.0-beta.55 $\rightarrow$ ...010 0000000 00110111 (Ends in 55)
  2. 1.2.0 (Stable) $\rightarrow$ ...010 0000000 11111111 (Ends in 511) $\leftarrow$ Upgrade Safe
  3. 1.2.1-beta.1 $\rightarrow$ ...010 0000001 00000001 (Patch bit flips, value jumps) $\leftarrow$ Upgrade Safe

How to Derive the "BuildCount" Automatically

Since you only have 9 bits (511 slots) for the test field, you cannot use the global commit count (which reaches thousands). You must use the commits since the last tag.

Git Command for CI:

# Count commits since the last tag reachable from HEAD
git rev-list --count HEAD ^$(git describe --tags --abbrev=0)

If you are on a fresh version tag (e.g., just tagged 1.2.3), this returns 0. For the beta releases following it, it counts up 1, 2, 3...

Python Implementation

Here is a script you can use in your build pipeline to generate this deterministically.

def generate_version_code(major, minor, patch, is_stable=False, build_count=0):
    # Constants for the 30-bit Schema (7-7-7-9)
    MAX_MAJOR = 127
    MAX_MINOR = 127
    MAX_PATCH = 127
    MAX_QUALIFIER = 511  # 9 bits (2^9 - 1)

    if major > MAX_MAJOR or minor > MAX_MINOR or patch > MAX_PATCH:
        raise ValueError("Version components exceed allocated bit width")

    # If stable, use the max value (511) to ensure it overrides any beta
    # If beta, use the build_count (must be < 511)
    qualifier = MAX_QUALIFIER if is_stable else build_count
    
    if not is_stable and build_count >= MAX_QUALIFIER:
        raise ValueError(f"Build count {build_count} exceeds limit of {MAX_QUALIFIER} for this patch version.")

    # Bit shifting
    # Major (7 bits) << 23
    # Minor (7 bits) << 16
    # Patch (7 bits) << 9
    # Qualifier (9 bits) << 0
    version_code = (major << 23) | (minor << 16) | (patch << 9) | qualifier
    
    return version_code

# --- Examples ---
# 1. Beta build (Build #45 working towards 1.5.2)
print(f"Beta:   {generate_version_code(1, 5, 2, is_stable=False, build_count=45)}")
# Output: 11604461

# 2. Stable build (1.5.2 Fin...

</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

Copilot AI and others added 2 commits December 17, 2025 17:47
- Add version code calculation using 7-7-7-9 bit schema (Major-Minor-Patch-Qualifier)
- Version code: Stable releases use qualifier=511, beta builds use commits-since-tag
- Version name follows v*.*.* pattern (stable) or v*.*.*-beta.X (development)
- Update all CI workflows to pass version environment variables
- Add fetch-depth: 0 to checkout for proper git history access

Co-authored-by: MinecraftFuns <25814618+MinecraftFuns@users.noreply.github.com>
- Add clearer comments explaining intentional exception handling
- Clarify that environment variables take precedence over git-derived values
- Document that VERSION_STABLE must be explicitly "true" for stable releases

Co-authored-by: MinecraftFuns <25814618+MinecraftFuns@users.noreply.github.com>
Copilot AI changed the title [WIP] Update build pipeline for versionCode schema Implement 30-bit version code schema with automatic derivation from git tags Dec 17, 2025
Copilot AI requested a review from MinecraftFuns December 17, 2025 17:51
@MinecraftFuns

Copy link
Copy Markdown

@copilot assuming the latest release version is v5.4.3(.511), your current method for computing the major.minor.patch.qualifier would cause the debug/test versions for the next semver to stay v5.4.3(.0-.510) depending the commit since last release, which isn't ideal since these versions should be higher than v5.4.3; therefore, the correct approach is to increase the .patch by 1 so that the "test releases" work towards v5.4.4(.0-.510) and then the next release whether it be v5.4.4 or v5.5.0 are both fine. can you verify my logic and if this fix makes sense, implement it? ensure to follow best practices and write idiomatic code; document our versionCode generation logic carefully in the docs/ and improve all documentations as well.

- For beta builds, increment patch by 1 so betas are higher than previous stable
- Add bounds checking for version overflow (patch > 127 rolls to next minor)
- Update CI workflows with patch increment logic
- Create comprehensive docs/VERSIONING.md documentation
- Add versioning documentation link to README.md

Co-authored-by: MinecraftFuns <25814618+MinecraftFuns@users.noreply.github.com>

Copilot AI commented Dec 17, 2025

Copy link
Copy Markdown
Author

@copilot assuming the latest release version is v5.4.3(.511), your current method for computing the major.minor.patch.qualifier would cause the debug/test versions for the next semver to stay v5.4.3(....

Verified and implemented in a13df66. The logic is correct - after v5.4.3, betas now become v5.4.4-beta.X with version codes higher than v5.4.3.

Changes made:

  • Increment patch by 1 for beta builds when buildCount > 0
  • Added overflow handling (patch 128 → minor 0.1, etc.)
  • Updated CI workflows with same logic
  • Created comprehensive docs/VERSIONING.md documentation

@MinecraftFuns
MinecraftFuns marked this pull request as ready for review December 17, 2025 19:25
@MinecraftFuns
MinecraftFuns merged commit a0dc582 into master Dec 17, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants