From 94a8b3481b6c0253ceecedc09ecbff9ef0cd8b8f Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Thu, 30 Jul 2026 17:40:00 +0200 Subject: [PATCH] feat(release): automate derived store versions --- Makefile | 6 ++- build.gradle.kts | 13 +++---- packaging/release/README.md | 16 +++++--- packaging/version/README.md | 40 ++++++++++++++------ packaging/version/prepare-release.sh | 53 +++++++++++++++++++++++++++ packaging/version/resolve-version.ps1 | 12 +++--- packaging/version/resolve-version.sh | 16 +++++--- packaging/version/test-version.sh | 38 ++++++++++++++----- version.properties | 1 - 9 files changed, 151 insertions(+), 44 deletions(-) create mode 100755 packaging/version/prepare-release.sh diff --git a/Makefile b/Makefile index 8928169..3227819 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ include $(ROOT)/make/release.mk .PHONY: test-rust-transfer test-rust-approval test-rust-lifecycle test-rust-output-sink .PHONY: check-shared test-shared test-android-host check-android verify-android-libs build-android run-desktop .PHONY: apple-core apple-version-config apple-project open-apple-project open-apple build-apple-macos build-apple-ios check-apple -.PHONY: check-version check-release check-localization localization localization-migrate +.PHONY: prepare-release check-version check-release check-localization localization localization-migrate .PHONY: check-docs run-docs check-diagnostics run-diagnostics diagnostics-db-local diagnostics-db-remote diagnostics-typegen deploy-diagnostics help: ## Show available commands and common configuration variables. @@ -61,6 +61,10 @@ test: test-rust test-shared ## Run the main Rust and shared JVM test suites. check: check-version check-rust check-shared check-localization check-docs check-diagnostics ## Run portable pre-PR verification. +prepare-release: ## Update PRODUCT_VERSION and show its derived store versions (RELEASE_VERSION=x.y.z). + @test -n "$(RELEASE_VERSION)" || { printf 'Usage: make prepare-release RELEASE_VERSION=x.y.z\n' >&2; exit 1; } + cd $(ROOT) && packaging/version/prepare-release.sh "$(RELEASE_VERSION)" + check-version: ## Validate the canonical version and its platform mappings. cd $(ROOT) && packaging/version/test-version.sh cd $(ROOT) && packaging/version/resolve-version.sh verify diff --git a/build.gradle.kts b/build.gradle.kts index ca0887e..f0e50f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,19 +41,18 @@ val productVersionMatch = Regex("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0- .matchEntire(productVersion) ?: error("PRODUCT_VERSION must use canonical MAJOR.MINOR.PATCH integers") val productVersionParts = productVersionMatch.groupValues.drop(1).map(String::toLong) -require(productVersionParts[0] <= 65534 && productVersionParts.drop(1).all { it <= 65535 }) { - "PRODUCT_VERSION components exceed the supported store ranges" +require(productVersionParts[0] <= 2099 && productVersionParts.drop(1).all { it <= 999 }) { + "PRODUCT_VERSION must use a major no greater than 2099 and minor/patch no greater than 999" } val releaseChannel = requiredVersionProperty("RELEASE_CHANNEL") require(releaseChannel.matches(Regex("[a-z][a-z0-9-]*"))) { "RELEASE_CHANNEL contains unsupported characters" } -val androidVersionCode = canonicalInteger( - "ANDROID_VERSION_CODE", - requiredVersionProperty("ANDROID_VERSION_CODE"), - 1L..2_100_000_000L, -).toInt() +val androidVersionCode = + (productVersionParts[0] * 1_000_000L + productVersionParts[1] * 1_000L + productVersionParts[2]) + .also { require(it in 1L..2_100_000_000L) { "Derived Android version code is out of range" } } + .toInt() val windowsVersionEpoch = canonicalInteger( "WINDOWS_VERSION_EPOCH", requiredVersionProperty("WINDOWS_VERSION_EPOCH"), diff --git a/packaging/release/README.md b/packaging/release/README.md index a999e03..915a477 100644 --- a/packaging/release/README.md +++ b/packaging/release/README.md @@ -27,13 +27,19 @@ private workflow artifacts. Partner Center submission stays manual until the first Microsoft Store release is certified. The Play release remains a draft on a closed-testing track; this pipeline cannot publish to production. -To release, first update and merge `version.properties`, including a monotonic -Android version code. Apple Store and Direct build numbers are derived -independently at build time. Then create and push the matching tag: +To release, prepare and merge the new product version. Android, Microsoft Store, +and Apple build/package versions are derived automatically: ```bash -git tag -s v0.2.0 -m "VniDrop 0.2.0" -git push origin v0.2.0 +make prepare-release RELEASE_VERSION=0.2.1 +make check-version +``` + +Then create and push the matching tag: + +```bash +git tag -s v0.2.1 -m "VniDrop 0.2.1" +git push origin v0.2.1 ``` The tag must point at the current `origin/master` commit. A failed run creates diff --git a/packaging/version/README.md b/packaging/version/README.md index 27ec40a..8ef28d8 100644 --- a/packaging/version/README.md +++ b/packaging/version/README.md @@ -1,9 +1,9 @@ # Application versioning `version.properties` at the repository root is the single source of truth for -the VniDrop product version and persistent store counters. Platform projects -and release workflows use the version resolver rather than accepting -independent version overrides. +the VniDrop product version and the permanent Windows version epoch. Platform +projects and release workflows derive their versions from it rather than +accepting independent release counters. Keep it as plain `KEY=VALUE` assignments: the same file is parsed by shell, PowerShell, Gradle, and Rust. Xcode receives resolver-generated xcconfig files. @@ -15,7 +15,7 @@ the patch component (`0.2.1`). Release channels belong in | Platform | Product version | Platform build/package version | | --- | --- | --- | -| Android | `PRODUCT_VERSION` | `ANDROID_VERSION_CODE` | +| Android | `PRODUCT_VERSION` | Derived monotonic integer | | Apple Store | `PRODUCT_VERSION` | Derived UTC `YYYYMMDD.HHMM.SS` | | Direct macOS | `PRODUCT_VERSION` | Independently derived UTC `YYYYMMDD.HHMM.SS` | | Linux | `PRODUCT_VERSION` | Native package revision | @@ -32,12 +32,23 @@ the Store. Its version is: With epoch `1`, product `0.2.0` maps to MSIX `1.2.0.0`, while product `1.0.0` maps to `2.0.0.0`. Do not change the epoch after publishing. -Every Android upload must increment `ANDROID_VERSION_CODE`. Apple build numbers -are derived at build time by `apple-store-build` and `apple-direct-build`; they -are kept as separate resolver outputs so App Store and Sparkle releases do not -consume each other's cadence. Every changed Windows Store package must -increment the product version because the Store-reserved fourth component -cannot carry a rebuild number. +Android derives its version code as: + +```text +product major * 1,000,000 + product minor * 1,000 + product patch +``` + +For example, `0.2.0` maps to Android code `2000`, `0.2.1` to `2001`, and +`1.0.0` to `1000000`. To keep that mapping unique and within store limits, +the product major may not exceed `2099`, and minor and patch may not exceed +`999`. A rejected store build must use a new patch version rather than +rebuilding a previously uploaded product version. + +Apple build numbers are derived at build time by `apple-store-build` and +`apple-direct-build`; they are kept as separate resolver outputs so App Store +and Sparkle releases do not consume each other's cadence. Every changed Windows +Store package must increment the product version because the Store-reserved +fourth component cannot carry a rebuild number. Apple projects read generated build settings rather than `version.properties` directly: @@ -50,7 +61,14 @@ The generated files under `apple/Generated/` are intentionally ignored. `VNIDROP_BUILD_TIME_UTC=YYYYMMDDHHMMSS` provides a deterministic clock for tests; distribution builds normally use the current UTC time. -Before releasing: +Prepare the next release by changing only the product version: + +```bash +make prepare-release RELEASE_VERSION=0.2.1 +``` + +The command refuses non-increasing versions, updates `PRODUCT_VERSION`, and +prints the derived Android and Microsoft Store versions. Then verify: ```bash make check-version diff --git a/packaging/version/prepare-release.sh b/packaging/version/prepare-release.sh new file mode 100755 index 0000000..7a7d5d3 --- /dev/null +++ b/packaging/version/prepare-release.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../.." && pwd)" +resolver="$script_dir/resolve-version.sh" +version_file="${VNIDROP_VERSION_FILE:-$repo_root/version.properties}" +next_version="${1:-}" + +fail() { + printf '%s\n' "$*" >&2 + exit 1 +} + +[[ -n $next_version ]] || + fail "Usage: $0 MAJOR.MINOR.PATCH" +[[ -f $version_file ]] || + fail "Version file not found: $version_file" +[[ $(grep -c '^PRODUCT_VERSION=' "$version_file") == 1 ]] || + fail "Expected exactly one PRODUCT_VERSION entry in $version_file" + +current_version="$( + VNIDROP_VERSION_FILE="$version_file" "$resolver" product +)" +current_android_code="$( + VNIDROP_VERSION_FILE="$version_file" "$resolver" android-code +)" +temporary="$(mktemp "$(dirname "$version_file")/.version.properties.XXXXXX")" +trap 'rm -f "$temporary"' EXIT + +sed "s/^PRODUCT_VERSION=.*/PRODUCT_VERSION=$next_version/" \ + "$version_file" > "$temporary" + +next_android_code="$( + VNIDROP_VERSION_FILE="$temporary" "$resolver" android-code +)" +next_windows_package="$( + VNIDROP_VERSION_FILE="$temporary" "$resolver" windows-package +)" +VNIDROP_VERSION_FILE="$temporary" "$resolver" verify >/dev/null + +(( next_android_code > current_android_code )) || + fail "New version must be greater than $current_version" + +chmod 644 "$temporary" +mv "$temporary" "$version_file" +trap - EXIT + +printf 'Prepared VniDrop %s\n' "$next_version" +printf ' Android version code: %s\n' "$next_android_code" +printf ' Microsoft Store package: %s\n' "$next_windows_package" +printf 'Next: make check-version\n' diff --git a/packaging/version/resolve-version.ps1 b/packaging/version/resolve-version.ps1 index 02d4467..641055b 100644 --- a/packaging/version/resolve-version.ps1 +++ b/packaging/version/resolve-version.ps1 @@ -47,7 +47,6 @@ function Convert-CanonicalInteger { $productVersion = Read-VersionProperty "PRODUCT_VERSION" $releaseChannel = Read-VersionProperty "RELEASE_CHANNEL" -$androidVersionCodeText = Read-VersionProperty "ANDROID_VERSION_CODE" $windowsVersionEpochText = Read-VersionProperty "WINDOWS_VERSION_EPOCH" $buildTimeUtc = $env:VNIDROP_BUILD_TIME_UTC if ([string]::IsNullOrWhiteSpace($buildTimeUtc)) { @@ -78,13 +77,16 @@ if ($productVersion -notmatch "^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]* throw "PRODUCT_VERSION must use canonical MAJOR.MINOR.PATCH integers" } $productParts = $productVersion.Split(".") -$productMajor = Convert-CanonicalInteger "PRODUCT_VERSION major" $productParts[0] 0 65534 -$null = Convert-CanonicalInteger "PRODUCT_VERSION minor" $productParts[1] 0 65535 -$null = Convert-CanonicalInteger "PRODUCT_VERSION patch" $productParts[2] 0 65535 +$productMajor = Convert-CanonicalInteger "PRODUCT_VERSION major" $productParts[0] 0 2099 +$productMinor = Convert-CanonicalInteger "PRODUCT_VERSION minor" $productParts[1] 0 999 +$productPatch = Convert-CanonicalInteger "PRODUCT_VERSION patch" $productParts[2] 0 999 if ($releaseChannel -notmatch "^[a-z][a-z0-9-]*$") { throw "RELEASE_CHANNEL contains unsupported characters" } -$androidVersionCode = Convert-CanonicalInteger "ANDROID_VERSION_CODE" $androidVersionCodeText 1 2100000000 +$androidVersionCode = $productMajor * 1000000L + $productMinor * 1000L + $productPatch +if ($androidVersionCode -lt 1 -or $androidVersionCode -gt 2100000000L) { + throw "Derived Android version code must be between 1 and 2100000000" +} $windowsVersionEpoch = Convert-CanonicalInteger "WINDOWS_VERSION_EPOCH" $windowsVersionEpochText 1 65535 $windowsMajor = $productMajor + $windowsVersionEpoch if ($windowsMajor -gt 65535) { diff --git a/packaging/version/resolve-version.sh b/packaging/version/resolve-version.sh index 589381f..1f5a9d9 100755 --- a/packaging/version/resolve-version.sh +++ b/packaging/version/resolve-version.sh @@ -34,23 +34,29 @@ validate_canonical_integer() { product_version="$(read_property PRODUCT_VERSION)" release_channel="$(read_property RELEASE_CHANNEL)" -android_version_code="$(read_property ANDROID_VERSION_CODE)" windows_version_epoch="$(read_property WINDOWS_VERSION_EPOCH)" build_time_utc="${VNIDROP_BUILD_TIME_UTC:-$(date -u +%Y%m%d%H%M%S)}" [[ $product_version =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || fail "PRODUCT_VERSION must use canonical MAJOR.MINOR.PATCH integers" IFS=. read -r product_major product_minor product_patch <<< "$product_version" -validate_canonical_integer "PRODUCT_VERSION major" "$product_major" 0 65534 -validate_canonical_integer "PRODUCT_VERSION minor" "$product_minor" 0 65535 -validate_canonical_integer "PRODUCT_VERSION patch" "$product_patch" 0 65535 +validate_canonical_integer "PRODUCT_VERSION major" "$product_major" 0 2099 +validate_canonical_integer "PRODUCT_VERSION minor" "$product_minor" 0 999 +validate_canonical_integer "PRODUCT_VERSION patch" "$product_patch" 0 999 [[ $release_channel =~ ^[a-z][a-z0-9-]*$ ]] || fail "RELEASE_CHANNEL must start with a lowercase letter and contain only lowercase letters, digits, and hyphens" -validate_canonical_integer "ANDROID_VERSION_CODE" "$android_version_code" 1 2100000000 validate_canonical_integer "WINDOWS_VERSION_EPOCH" "$windows_version_epoch" 1 65535 [[ $build_time_utc =~ ^[0-9]{14}$ ]] || fail "VNIDROP_BUILD_TIME_UTC must use YYYYMMDDHHMMSS" +android_version_code=$(( + (10#$product_major * 1000000) + + (10#$product_minor * 1000) + + 10#$product_patch +)) +(( android_version_code >= 1 && android_version_code <= 2100000000 )) || + fail "Derived Android version code must be between 1 and 2100000000" + build_month="${build_time_utc:4:2}" build_day="${build_time_utc:6:2}" build_hour="${build_time_utc:8:2}" diff --git a/packaging/version/test-version.sh b/packaging/version/test-version.sh index e883473..5fe1d44 100755 --- a/packaging/version/test-version.sh +++ b/packaging/version/test-version.sh @@ -4,6 +4,7 @@ set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" resolver="$script_dir/resolve-version.sh" +prepare_release="$script_dir/prepare-release.sh" scratch="$(mktemp -d)" trap 'rm -rf "$scratch"' EXIT @@ -11,8 +12,7 @@ write_version() { printf '%s\n' \ "PRODUCT_VERSION=$1" \ "RELEASE_CHANNEL=$2" \ - "ANDROID_VERSION_CODE=$3" \ - "WINDOWS_VERSION_EPOCH=$4" \ + "WINDOWS_VERSION_EPOCH=$3" \ > "$scratch/version.properties" } @@ -29,32 +29,42 @@ expect_failure() { export VNIDROP_BUILD_TIME_UTC=20260728143217 -write_version 0.2.0 beta 2 1 +write_version 0.2.0 beta 1 [[ $(resolve product) == 0.2.0 ]] -[[ $(resolve android-code) == 2 ]] +[[ $(resolve android-code) == 2000 ]] [[ $(resolve apple-store-build) == 20260728.1432.17 ]] [[ $(resolve apple-direct-build) == 20260728.1432.17 ]] [[ $(resolve windows-package) == 1.2.0.0 ]] resolve verify-tag v0.2.0 expect_failure resolve verify-tag v1.0.0 -write_version 1.0.0 stable 42 1 +write_version 1.0.0 stable 1 +[[ $(resolve android-code) == 1000000 ]] [[ $(resolve windows-package) == 2.0.0.0 ]] -write_version 01.0.0 beta 2 1 +write_version 2099.999.999 stable 1 +[[ $(resolve android-code) == 2099999999 ]] + +write_version 01.0.0 beta 1 expect_failure resolve verify -write_version 0.2.0 beta 0 1 +write_version 0.0.0 beta 1 expect_failure resolve verify -write_version 65535.0.0 stable 2 1 +write_version 2100.0.0 stable 1 +expect_failure resolve verify + +write_version 0.1000.0 stable 1 +expect_failure resolve verify + +write_version 0.0.1000 stable 1 expect_failure resolve verify VNIDROP_BUILD_TIME_UTC=20260728146000 expect_failure resolve verify VNIDROP_BUILD_TIME_UTC=2026-07-28 expect_failure resolve verify VNIDROP_BUILD_TIME_UTC=20260229080000 expect_failure resolve verify -write_version 0.2.0 beta 2 1 +write_version 0.2.0 beta 1 config_dir="$scratch/xcconfig" VNIDROP_APPLE_XCCONFIG_DIR="$config_dir" \ "$script_dir/generate-apple-xcconfig.sh" all @@ -64,4 +74,14 @@ grep -Fx "CURRENT_PROJECT_VERSION = 20260728.1432.17" \ grep -Fx "CURRENT_PROJECT_VERSION = 20260728.1432.17" \ "$config_dir/DirectVersion.xcconfig" >/dev/null +VNIDROP_VERSION_FILE="$scratch/version.properties" \ + "$prepare_release" 0.2.1 >/dev/null +[[ $(resolve product) == 0.2.1 ]] +[[ $(resolve android-code) == 2001 ]] +[[ $(resolve windows-package) == 1.2.1.0 ]] +expect_failure env VNIDROP_VERSION_FILE="$scratch/version.properties" \ + "$prepare_release" 0.2.1 +expect_failure env VNIDROP_VERSION_FILE="$scratch/version.properties" \ + "$prepare_release" 0.1.999 + printf 'Version resolver tests passed.\n' diff --git a/version.properties b/version.properties index 57070b6..69ffdb6 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,3 @@ PRODUCT_VERSION=0.2.0 RELEASE_CHANNEL=beta -ANDROID_VERSION_CODE=2 WINDOWS_VERSION_EPOCH=1