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:
@@ -27,9 +27,8 @@ artifacts. Manual runs retain build artifacts for 14 days. A pushed version tag
|
||||
whose commit is on `master` creates the matching GitHub Release with the `.deb`,
|
||||
`.rpm`, and a combined `SHA256SUMS`.
|
||||
|
||||
The existing `v1.0.0` tag predates this workflow and will not run it
|
||||
retroactively. Use the next version tag after this configuration reaches
|
||||
`master`.
|
||||
The legacy `v1.0.0` tag predates canonical versioning and does not define the
|
||||
current product version. New release tags must match `version.properties`.
|
||||
|
||||
## Install a downloaded package
|
||||
|
||||
@@ -61,11 +60,12 @@ Use JDK 21 and Rust 1.91. Build DEB packages on Debian/Ubuntu with `dpkg` and
|
||||
`fakeroot`; build RPM packages on Fedora with `rpm-build`. Building an RPM on
|
||||
Ubuntu prevents `jpackage` from discovering normal RPM dependencies.
|
||||
|
||||
From the repository root on the matching Linux family, run one of:
|
||||
Set the release in `version.properties`. From the repository root on the
|
||||
matching Linux family, run one of:
|
||||
|
||||
```bash
|
||||
make package-deb VERSION=1.0.0
|
||||
make package-rpm VERSION=1.0.0
|
||||
make package-deb
|
||||
make package-rpm
|
||||
```
|
||||
|
||||
The Make targets collect the Compose output under `build/release/linux/`, then
|
||||
|
||||
@@ -2,38 +2,14 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
version=${1:-1.0.0}
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
resolver="$script_dir/../version/resolve-version.sh"
|
||||
version="$("$resolver" product)"
|
||||
"$resolver" verify >/dev/null
|
||||
|
||||
if [[ ${GITHUB_REF_TYPE:-} == "tag" ]]; then
|
||||
if [[ ! ${GITHUB_REF_NAME:-} =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Linux release tags must use vMAJOR.MINOR.PATCH" >&2
|
||||
exit 1
|
||||
fi
|
||||
version=${GITHUB_REF_NAME#v}
|
||||
fi
|
||||
|
||||
if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Version must use MAJOR.MINOR.PATCH" >&2
|
||||
if [[ -n ${1:-} && $1 != "$version" ]]; then
|
||||
printf 'Version overrides are not supported; version.properties declares %s\n' "$version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=. read -r major minor patch <<< "$version"
|
||||
parts=("$major" "$minor" "$patch")
|
||||
|
||||
for index in "${!parts[@]}"; do
|
||||
part=${parts[$index]}
|
||||
if [[ $part != "0" && $part == 0* ]]; then
|
||||
echo "Version components must be canonical integers without leading zeroes" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( ${#part} > 5 )) || (( 10#$part > 65535 )); then
|
||||
echo "Version components must be between 0 and 65535" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( index == 0 && 10#$part == 0 )); then
|
||||
echo "The major version must be non-zero" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s\n' "$version"
|
||||
|
||||
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'
|
||||
@@ -24,8 +24,10 @@ after the first release. The manifest display name uses the exact reserved Store
|
||||
name; the product's in-app branding and launcher remain `VniDrop`.
|
||||
|
||||
The initial package targets Windows Desktop x64, Windows 10 version 2004
|
||||
(build 19041) or later. The fourth MSIX version component is reserved by the
|
||||
Store, so app version 1.2.3 becomes package version 1.2.3.0.
|
||||
(build 19041) or later. The product version comes from `version.properties`.
|
||||
Because MSIX requires a non-zero major and reserves the fourth component, the
|
||||
package version adds `WINDOWS_VERSION_EPOCH` to the product major. With epoch
|
||||
`1`, product version `0.2.0` becomes package version `1.2.0.0`.
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
@@ -87,9 +89,9 @@ The Store ID is a non-secret variable.
|
||||
From the repository root:
|
||||
|
||||
~~~powershell
|
||||
.\gradlew.bat :shared:jvmTest :desktopApp:createReleaseDistributable -Pvnidrop.version=1.0.0 -Pvnidrop.desktop.rustVariant=release -Pvnidrop.diagnostics.included=false --no-daemon --no-configuration-cache --stacktrace
|
||||
.\gradlew.bat :shared:jvmTest :desktopApp:createReleaseDistributable -Pvnidrop.desktop.rustVariant=release -Pvnidrop.diagnostics.included=false --no-daemon --no-configuration-cache --stacktrace
|
||||
|
||||
.\packaging\windows\build-msix.ps1 -Version 1.0.0 -AppImage .\desktopApp\build\compose\binaries\main-release\app\VniDrop -OutputDirectory .\build\release\windows
|
||||
.\packaging\windows\build-msix.ps1 -AppImage .\desktopApp\build\compose\binaries\main-release\app\VniDrop -OutputDirectory .\build\release\windows
|
||||
~~~
|
||||
|
||||
The packaging script requires Windows SDK 10.0.26100.0. It uses MakePri to
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $Version,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $AppImage,
|
||||
|
||||
@@ -87,16 +84,11 @@ if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
|
||||
throw "MSIX packaging must run on Windows"
|
||||
}
|
||||
|
||||
$versionParts = $Version.Split(".")
|
||||
Assert-Condition ($versionParts.Count -eq 3) "Version must use MAJOR.MINOR.PATCH"
|
||||
for ($index = 0; $index -lt $versionParts.Count; $index++) {
|
||||
$part = $versionParts[$index]
|
||||
$number = 0
|
||||
Assert-Condition ([int]::TryParse($part, [ref] $number)) "Version components must be integers"
|
||||
Assert-Condition ($number.ToString() -eq $part) "Version components must not contain leading zeroes"
|
||||
Assert-Condition ($number -ge $(if ($index -eq 0) { 1 } else { 0 }) -and $number -le 65535) "Version components must be between 0 and 65535, with a non-zero major"
|
||||
}
|
||||
$packageVersion = "$Version.0"
|
||||
$versionResolver = Join-Path $PSScriptRoot "..\version\resolve-version.ps1"
|
||||
$versionInfoJson = & $versionResolver -Field Json -VerifyTag
|
||||
$versionInfo = $versionInfoJson | ConvertFrom-Json
|
||||
$Version = [string] $versionInfo.productVersion
|
||||
$packageVersion = [string] $versionInfo.windowsPackageVersion
|
||||
|
||||
$appImagePath = (Resolve-Path -LiteralPath $AppImage).Path
|
||||
Assert-Condition (Test-Path -LiteralPath $appImagePath -PathType Container) "App image not found: $AppImage"
|
||||
|
||||
Reference in New Issue
Block a user