mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(release): unify cross-platform versioning
This commit is contained in:
44
packaging/version/README.md
Normal file
44
packaging/version/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Application versioning
|
||||
|
||||
`version.properties` at the repository root is the single source of truth for
|
||||
the VniDrop application version. Platform projects and release workflows read
|
||||
that file rather than accepting independent version overrides.
|
||||
|
||||
Keep it as plain `KEY=VALUE` assignments: the same file is parsed by shell,
|
||||
PowerShell, Gradle, Rust, and Xcode.
|
||||
|
||||
The product uses numeric semantic versions. While the app is in beta, feature
|
||||
releases increment the minor component (`0.2.0`, `0.3.0`) and fixes increment
|
||||
the patch component (`0.2.1`). Release channels belong in
|
||||
`RELEASE_CHANNEL`; they are not appended to store version fields.
|
||||
|
||||
| Platform | Product version | Platform build/package version |
|
||||
| --- | --- | --- |
|
||||
| Android | `PRODUCT_VERSION` | `ANDROID_VERSION_CODE` |
|
||||
| Apple | `PRODUCT_VERSION` | `APPLE_BUILD_NUMBER` |
|
||||
| Linux and direct macOS | `PRODUCT_VERSION` | Native package revision |
|
||||
| Rust handshake | `PRODUCT_VERSION` | Rust crate version remains independent |
|
||||
| Microsoft Store | `PRODUCT_VERSION` in the app | Derived MSIX dot-quad |
|
||||
|
||||
MSIX requires a non-zero first component and reserves the fourth component for
|
||||
the Store. Its version is:
|
||||
|
||||
```text
|
||||
(product major + WINDOWS_VERSION_EPOCH).product minor.product patch.0
|
||||
```
|
||||
|
||||
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 or Apple upload must increment its platform build number. Every
|
||||
changed Windows Store package must increment the product version because the
|
||||
Store-reserved fourth component cannot carry a rebuild number.
|
||||
|
||||
Before releasing:
|
||||
|
||||
```bash
|
||||
make check-version
|
||||
```
|
||||
|
||||
Release tags must exactly match `vPRODUCT_VERSION`. Manual workflow dispatches
|
||||
also build the committed version and do not accept free-form version inputs.
|
||||
97
packaging/version/resolve-version.ps1
Normal file
97
packaging/version/resolve-version.ps1
Normal file
@@ -0,0 +1,97 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet("Product", "Channel", "AndroidCode", "AppleBuild", "WindowsPackage", "Json", "Verify")]
|
||||
[string] $Field = "Verify",
|
||||
|
||||
[switch] $VerifyTag,
|
||||
|
||||
[string] $VersionFile
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($VersionFile)) {
|
||||
$VersionFile = Join-Path $PSScriptRoot "..\..\version.properties"
|
||||
}
|
||||
$VersionFile = (Resolve-Path -LiteralPath $VersionFile).Path
|
||||
|
||||
function Read-VersionProperty {
|
||||
param([string] $Name)
|
||||
|
||||
$prefix = "$Name="
|
||||
$matches = @(Get-Content -LiteralPath $VersionFile | Where-Object { $_.StartsWith($prefix) })
|
||||
if ($matches.Count -ne 1) {
|
||||
throw "Expected exactly one $Name entry in $VersionFile"
|
||||
}
|
||||
return $matches[0].Substring($prefix.Length)
|
||||
}
|
||||
|
||||
function Convert-CanonicalInteger {
|
||||
param(
|
||||
[string] $Name,
|
||||
[string] $Value,
|
||||
[long] $Minimum,
|
||||
[long] $Maximum
|
||||
)
|
||||
|
||||
if ($Value -notmatch "^(0|[1-9][0-9]*)$") {
|
||||
throw "$Name must be a canonical non-negative integer"
|
||||
}
|
||||
$number = 0L
|
||||
if (-not [long]::TryParse($Value, [ref] $number) -or $number -lt $Minimum -or $number -gt $Maximum) {
|
||||
throw "$Name must be between $Minimum and $Maximum"
|
||||
}
|
||||
return $number
|
||||
}
|
||||
|
||||
$productVersion = Read-VersionProperty "PRODUCT_VERSION"
|
||||
$releaseChannel = Read-VersionProperty "RELEASE_CHANNEL"
|
||||
$androidVersionCodeText = Read-VersionProperty "ANDROID_VERSION_CODE"
|
||||
$appleBuildNumber = Read-VersionProperty "APPLE_BUILD_NUMBER"
|
||||
$windowsVersionEpochText = Read-VersionProperty "WINDOWS_VERSION_EPOCH"
|
||||
|
||||
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
|
||||
if ($releaseChannel -notmatch "^[a-z][a-z0-9-]*$") {
|
||||
throw "RELEASE_CHANNEL contains unsupported characters"
|
||||
}
|
||||
$androidVersionCode = Convert-CanonicalInteger "ANDROID_VERSION_CODE" $androidVersionCodeText 1 2100000000
|
||||
if ($appleBuildNumber -notmatch "^[1-9][0-9]*(\.[0-9]+){0,2}$") {
|
||||
throw "APPLE_BUILD_NUMBER must contain one to three period-separated non-negative integers and start above zero"
|
||||
}
|
||||
$windowsVersionEpoch = Convert-CanonicalInteger "WINDOWS_VERSION_EPOCH" $windowsVersionEpochText 1 65535
|
||||
$windowsMajor = $productMajor + $windowsVersionEpoch
|
||||
if ($windowsMajor -gt 65535) {
|
||||
throw "Derived Windows package major exceeds 65535"
|
||||
}
|
||||
$windowsPackageVersion = "$windowsMajor.$($productParts[1]).$($productParts[2]).0"
|
||||
|
||||
if ($VerifyTag -and $env:GITHUB_REF_TYPE -eq "tag" -and $env:GITHUB_REF_NAME -ne "v$productVersion") {
|
||||
throw "Release tag must be v$productVersion, got $($env:GITHUB_REF_NAME)"
|
||||
}
|
||||
|
||||
$versionInfo = [ordered] @{
|
||||
productVersion = $productVersion
|
||||
releaseChannel = $releaseChannel
|
||||
androidVersionCode = $androidVersionCode
|
||||
appleBuildNumber = $appleBuildNumber
|
||||
windowsPackageVersion = $windowsPackageVersion
|
||||
}
|
||||
|
||||
switch ($Field) {
|
||||
"Product" { $productVersion }
|
||||
"Channel" { $releaseChannel }
|
||||
"AndroidCode" { $androidVersionCode }
|
||||
"AppleBuild" { $appleBuildNumber }
|
||||
"WindowsPackage" { $windowsPackageVersion }
|
||||
"Json" { $versionInfo | ConvertTo-Json -Compress }
|
||||
"Verify" {
|
||||
"VniDrop $productVersion ($releaseChannel), Android $androidVersionCode, Apple $appleBuildNumber, MSIX $windowsPackageVersion"
|
||||
}
|
||||
}
|
||||
95
packaging/version/resolve-version.sh
Executable file
95
packaging/version/resolve-version.sh
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$script_dir/../.." && pwd)"
|
||||
version_file="${VNIDROP_VERSION_FILE:-$repo_root/version.properties}"
|
||||
|
||||
fail() {
|
||||
printf '%s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
read_property() {
|
||||
local key=$1
|
||||
local matches
|
||||
matches="$(sed -n "s/^${key}=//p" "$version_file")"
|
||||
[[ -n "$matches" ]] || fail "Missing $key in $version_file"
|
||||
[[ $(printf '%s\n' "$matches" | wc -l | tr -d ' ') == 1 ]] ||
|
||||
fail "Duplicate $key in $version_file"
|
||||
printf '%s' "$matches"
|
||||
}
|
||||
|
||||
validate_canonical_integer() {
|
||||
local name=$1
|
||||
local value=$2
|
||||
local minimum=$3
|
||||
local maximum=$4
|
||||
[[ $value =~ ^(0|[1-9][0-9]*)$ ]] ||
|
||||
fail "$name must be a canonical non-negative integer"
|
||||
(( 10#$value >= minimum && 10#$value <= maximum )) ||
|
||||
fail "$name must be between $minimum and $maximum"
|
||||
}
|
||||
|
||||
product_version="$(read_property PRODUCT_VERSION)"
|
||||
release_channel="$(read_property RELEASE_CHANNEL)"
|
||||
android_version_code="$(read_property ANDROID_VERSION_CODE)"
|
||||
apple_build_number="$(read_property APPLE_BUILD_NUMBER)"
|
||||
windows_version_epoch="$(read_property WINDOWS_VERSION_EPOCH)"
|
||||
|
||||
[[ $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
|
||||
[[ $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
|
||||
[[ $apple_build_number =~ ^[1-9][0-9]*(\.[0-9]+){0,2}$ ]] ||
|
||||
fail "APPLE_BUILD_NUMBER must contain one to three period-separated non-negative integers and start above zero"
|
||||
validate_canonical_integer "WINDOWS_VERSION_EPOCH" "$windows_version_epoch" 1 65535
|
||||
|
||||
windows_major=$((10#$product_major + 10#$windows_version_epoch))
|
||||
(( windows_major <= 65535 )) ||
|
||||
fail "Derived Windows package major exceeds 65535"
|
||||
windows_package_version="$windows_major.$product_minor.$product_patch.0"
|
||||
|
||||
verify_tag() {
|
||||
local tag=${1:-${GITHUB_REF_NAME:-}}
|
||||
if [[ ${GITHUB_REF_TYPE:-} == tag || -n ${1:-} ]]; then
|
||||
[[ $tag == "v$product_version" ]] ||
|
||||
fail "Release tag must be v$product_version, got ${tag:-<empty>}"
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-verify}" in
|
||||
product)
|
||||
printf '%s\n' "$product_version"
|
||||
;;
|
||||
channel)
|
||||
printf '%s\n' "$release_channel"
|
||||
;;
|
||||
android-code)
|
||||
printf '%s\n' "$android_version_code"
|
||||
;;
|
||||
apple-build)
|
||||
printf '%s\n' "$apple_build_number"
|
||||
;;
|
||||
windows-package)
|
||||
printf '%s\n' "$windows_package_version"
|
||||
;;
|
||||
verify)
|
||||
verify_tag
|
||||
printf 'VniDrop %s (%s), Android %s, Apple %s, MSIX %s\n' \
|
||||
"$product_version" "$release_channel" "$android_version_code" \
|
||||
"$apple_build_number" "$windows_package_version"
|
||||
;;
|
||||
verify-tag)
|
||||
verify_tag "${2:-}"
|
||||
;;
|
||||
*)
|
||||
fail "Usage: $0 {product|channel|android-code|apple-build|windows-package|verify|verify-tag [tag]}"
|
||||
;;
|
||||
esac
|
||||
51
packaging/version/test-version.sh
Executable file
51
packaging/version/test-version.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
resolver="$script_dir/resolve-version.sh"
|
||||
scratch="$(mktemp -d)"
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
|
||||
write_version() {
|
||||
printf '%s\n' \
|
||||
"PRODUCT_VERSION=$1" \
|
||||
"RELEASE_CHANNEL=$2" \
|
||||
"ANDROID_VERSION_CODE=$3" \
|
||||
"APPLE_BUILD_NUMBER=$4" \
|
||||
"WINDOWS_VERSION_EPOCH=$5" \
|
||||
> "$scratch/version.properties"
|
||||
}
|
||||
|
||||
resolve() {
|
||||
VNIDROP_VERSION_FILE="$scratch/version.properties" "$resolver" "$@"
|
||||
}
|
||||
|
||||
expect_failure() {
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
printf 'Expected command to fail: %s\n' "$*" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
write_version 0.2.0 beta 2 2 1
|
||||
[[ $(resolve product) == 0.2.0 ]]
|
||||
[[ $(resolve android-code) == 2 ]]
|
||||
[[ $(resolve apple-build) == 2 ]]
|
||||
[[ $(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 42 1
|
||||
[[ $(resolve windows-package) == 2.0.0.0 ]]
|
||||
|
||||
write_version 01.0.0 beta 2 2 1
|
||||
expect_failure resolve verify
|
||||
|
||||
write_version 0.2.0 beta 0 2 1
|
||||
expect_failure resolve verify
|
||||
|
||||
write_version 65535.0.0 stable 2 2 1
|
||||
expect_failure resolve verify
|
||||
|
||||
printf 'Version resolver tests passed.\n'
|
||||
Reference in New Issue
Block a user