Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/compute-next-version/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ bumping from the same final version.
| --- | --- | --- |
| `change-type` | no | The bump to apply: `Patch`, `Minor`, or `Major`. Empty resolves to `Patch`. |
| `mode` | no | `release` (bare version) or `prerelease` (suffix appended). Defaults to `release`. |
| `suffix` | no | Pre-release suffix body (e.g. `dev-myfeature.42`). Used only when `mode` is `prerelease`. |
| `suffix` | no | Pre-release suffix body (e.g. `a1b2c3d4.42`). Used only when `mode` is `prerelease`. |

## Outputs

| Output | Description |
| --- | --- |
| `version` | The computed version, e.g. `1.4.0` or `1.4.0-dev-myfeature.42`. |
| `version` | The computed version, e.g. `1.4.0` or `1.4.0-a1b2c3d4.42`. |
| `latest-tag` | The latest final tag the bump was computed from, or empty when none exists. |

## Rules
Expand All @@ -39,7 +39,7 @@ bumping from the same final version.
with:
change-type: ${{ steps.references.outputs.change-type }}
mode: prerelease
suffix: dev-myfeature.42
suffix: a1b2c3d4.42
```

The checkout that precedes this action must fetch tags (`fetch-depth: 0`) so the base tag can be
Expand Down
4 changes: 2 additions & 2 deletions .github/actions/compute-next-version/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ inputs:
required: false
default: release
suffix:
description: The pre-release suffix body (e.g. dev-myfeature.42). Used only when mode is prerelease.
description: The pre-release suffix body (e.g. a1b2c3d4.42). Used only when mode is prerelease.
required: false
default: ""

outputs:
version:
description: The computed version, e.g. 1.4.0 or 1.4.0-dev-myfeature.42.
description: The computed version, e.g. 1.4.0 or 1.4.0-a1b2c3d4.42.
value: ${{ steps.compute.outputs.version }}
latest-tag:
description: The latest final tag the bump was computed from, or empty when no final tag exists.
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/compute-next-version/test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ try {
Invoke-Case -Name 'Major bump on 1.3.5' -Tags @('1.3.5') -ChangeType 'Major' -Mode 'release' -Suffix '' -ExpectedVersion '2.0.0' -ExpectedLatestTag '1.3.5'
Invoke-Case -Name 'Patch bump, no tags' -Tags @() -ChangeType 'Patch' -Mode 'release' -Suffix '' -ExpectedVersion '0.0.1' -ExpectedLatestTag ''
Invoke-Case -Name 'Minor bump ignores pre-release' -Tags @('1.3.5', '1.4.0-dev.1') -ChangeType 'Minor' -Mode 'release' -Suffix '' -ExpectedVersion '1.4.0' -ExpectedLatestTag '1.3.5'
Invoke-Case -Name 'Minor pre-release with suffix' -Tags @('1.3.5') -ChangeType 'Minor' -Mode 'prerelease' -Suffix 'dev-x.42' -ExpectedVersion '1.4.0-dev-x.42' -ExpectedLatestTag '1.3.5'
Invoke-Case -Name 'Minor pre-release with suffix' -Tags @('1.3.5') -ChangeType 'Minor' -Mode 'prerelease' -Suffix 'a1b2c3d4.42' -ExpectedVersion '1.4.0-a1b2c3d4.42' -ExpectedLatestTag '1.3.5'
Invoke-Case -Name 'Empty change-type defaults Patch' -Tags @('1.3.5') -ChangeType '' -Mode 'release' -Suffix '' -ExpectedVersion '1.3.6' -ExpectedLatestTag '1.3.5'

# Extra guards beyond the plan corpus.
Expand Down
25 changes: 18 additions & 7 deletions .github/actions/determine-version/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# determine-version

Determines the **single canonical build version** shared by every job in the Master Workflow, and a
strict 4-field numeric companion for consumers that reject SemVer suffixes.
Determines the **single canonical build version** shared by every job in the Master Workflow, a
strict 4-field numeric companion for assemblies, and an MSI-specific ProductVersion.

- **Tag builds** use the tag name verbatim (e.g. `2.3.1`, `1.4.0-dev-myfeature.42`).
- **Tag builds** use the tag name verbatim (e.g. `2.3.1`, `1.4.0-a1b2c3d4.42`).
- **Branch builds** keep the legacy `0.0.<run-number>` in all modes.

## Inputs
Expand All @@ -19,7 +19,9 @@ strict 4-field numeric companion for consumers that reject SemVer suffixes.
| Output | Suffix allowed? | Description |
| --- | :--: | --- |
| `version` | ✔ | Full SemVer — for MSBuild `Version` / `PackageVersion`, NuGet, `.dmapp` / Catalog, `.deb` (after its own `~` normalisation), DxM release. |
| `numeric-version` | ✘ | Strict 4-field `major.minor.patch.<build>` (every field wrapped `% 65535`) — for `AssemblyVersion` / `FileVersion` and the WiX MSI `ProductVersion`. `<build>` = the run number, or the version's own 4th field when it already has one. |
| `numeric-version` | ✘ | Strict 4-field `major.minor.patch.<build>` (every field wrapped `% 65535`) — for `AssemblyVersion` / `FileVersion`. `<build>` = the run number, or the version's own 4th field when it already has one. |
| `product-version` | ✘ | Stable three-part tags use `major.minor.build`; prereleases, branches, and explicit four-part tags retain `numeric-version`'s fourth field for Skyline release classification. |
| `product-version-valid` | — | `true` when MSI limits are met: major/minor ≤ 255 and build ≤ 65,535. |

## Rules

Expand All @@ -35,9 +37,18 @@ strict 4-field numeric companion for consumers that reject SemVer suffixes.
whose patch field is the (unbounded) run number.
- A tag whose core is not `major.minor.patch` or `major.minor.patch.build` (e.g. `release-1`, `1.2`)
fails the action with a clear error — such a version could not build anyway.
- **MSI caveat:** Windows Installer compares only the first **three** fields of `ProductVersion` for
upgrade detection; the 4th (run number) is parsed but ignored. A real upgrade must still move
`major.minor.patch` (the tag / auto-tag bump already does).
- Stable three-part tags use the normalized three-field numeric core (`1.2.3`). Pre-release tags
cannot carry their SemVer suffix in ProductVersion, so they use the suffix-free four-field
`numeric-version` (`1.2.3-sha.run` → `1.2.3.<workflow-run>`). Branch builds and explicit legacy
four-part tags also retain that fourth field. Skyline tooling uses this shape to distinguish a
prerelease/non-final package from a stable release.
- MSI limits are independent from assembly limits: **major ≤ 255, minor ≤ 255, build ≤ 65,535**.
`product-version-valid` exposes the result. The Master Workflow fails clearly on an invalid value
only when a WiX project exists, so a non-MSI repository may still use a date-style tag such as
`2026.07.08.230`.
- Windows Installer itself ignores the fourth field for upgrade comparison, so a real MSI upgrade
must still change at least one of the first three fields. Retaining field four is a Skyline package
convention; it does not change native MSI upgrade ordering.

## Usage

Expand Down
13 changes: 10 additions & 3 deletions .github/actions/determine-version/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ description: >
4-field numeric version (pre-release/build suffix stripped; the run
number is appended as 4th field unless the version already has one, e.g.
date-based tags; every field wrapped into the assembly-metadata range
with % 65535) for MSBuild AssemblyVersion/FileVersion and the WiX MSI
ProductVersion.
with % 65535) for MSBuild AssemblyVersion/FileVersion. Also derives an
MSI ProductVersion (three fields for stable releases, four for prereleases)
plus an MSI-specific bounds result.

inputs:
ref-type:
Expand All @@ -21,11 +22,17 @@ inputs:

outputs:
version:
description: The full SemVer version — the tag name on tag builds (e.g. `2.3.1`, `1.4.0-dev-myfeature.42`), `0.0.<run-number>` on branch builds.
description: The full SemVer version — the tag name on tag builds (e.g. `2.3.1`, `1.4.0-a1b2c3d4.42`), `0.0.<run-number>` on branch builds.
value: ${{ steps.determine.outputs.version }}
numeric-version:
description: Strict 4-field `major.minor.patch.<build>` version with any pre-release/build suffix stripped; `<build>` is the run number, or the version's own 4th field when it already has one; every field is wrapped into the valid assembly-metadata range (`% 65535`, max 65534).
value: ${{ steps.determine.outputs.numeric-version }}
product-version:
description: MSI ProductVersion; stable three-part tags use `major.minor.build`, while prereleases, branches, and explicit four-part tags retain numeric-version's fourth field.
value: ${{ steps.determine.outputs.product-version }}
product-version-valid:
description: Whether ProductVersion satisfies MSI limits (major/minor <= 255 and build <= 65535).
value: ${{ steps.determine.outputs.product-version-valid }}

runs:
using: composite
Expand Down
26 changes: 25 additions & 1 deletion .github/actions/determine-version/determine-version.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,33 @@ $numericVersion = '{0}.{1}.{2}.{3}' -f `
([long]$match.Groups[3].Value % 65535), `
($fourthField % 65535)

Write-Host "Determined version '$version' and numeric-version '$numericVersion' (ref-type: $refType, run-number: $runNumber)."
# Windows Installer evaluates major.minor.build and ignores a fourth field. We retain
# that fourth field for prerelease/branch identification in our package conventions,
# while stable three-part tags use the natural three-field ProductVersion.
# MSI limits differ from assembly metadata: major/minor <= 255 and build <= 65535.
$productMajor = [long]$match.Groups[1].Value
$productMinor = [long]$match.Groups[2].Value
$productBuild = [long]$match.Groups[3].Value
$isStableThreePartTag = $refType -eq 'tag' -and -not $version.Contains('-') -and -not $match.Groups[4].Success
$productVersion = if ($isStableThreePartTag) {
'{0}.{1}.{2}' -f $productMajor, $productMinor, $productBuild
} elseif ($refType -eq 'tag') {
$numericFourthField = $numericVersion.Split('.')[3]
'{0}.{1}.{2}.{3}' -f $productMajor, $productMinor, $productBuild, $numericFourthField
} else {
$numericVersion
}
$productVersionValid = if ($refType -eq 'tag') {
$productMajor -le 255 -and $productMinor -le 255 -and $productBuild -le 65535
} else {
$true
}

Write-Host "Determined version '$version', numeric-version '$numericVersion', and product-version '$productVersion' (valid: $($productVersionValid.ToString().ToLowerInvariant()), ref-type: $refType, run-number: $runNumber)."

@(
"version=$version"
"numeric-version=$numericVersion"
"product-version=$productVersion"
"product-version-valid=$($productVersionValid.ToString().ToLowerInvariant())"
) | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
43 changes: 28 additions & 15 deletions .github/actions/determine-version/test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ function Invoke-Case {
[AllowEmptyString()][string]$RefName,
[Parameter(Mandatory)][string]$RunNumber,
[Parameter(Mandatory)][string]$ExpectedVersion,
[Parameter(Mandatory)][string]$ExpectedNumericVersion
[Parameter(Mandatory)][string]$ExpectedNumericVersion,
[Parameter(Mandatory)][string]$ExpectedProductVersion,
[bool]$ExpectedProductVersionValid = $true
)

$outputFile = Join-Path -Path $script:temporaryDirectory -ChildPath "$([System.Guid]::NewGuid()).txt"
Expand All @@ -57,9 +59,13 @@ function Invoke-Case {

$version = Get-OutputValue -Name 'version' -Path $outputFile
$numericVersion = Get-OutputValue -Name 'numeric-version' -Path $outputFile
$productVersion = Get-OutputValue -Name 'product-version' -Path $outputFile
$productVersionValid = Get-OutputValue -Name 'product-version-valid' -Path $outputFile

Assert-Equal -Actual $version -Expected $ExpectedVersion -Label "${Name} (version)"
Assert-Equal -Actual $numericVersion -Expected $ExpectedNumericVersion -Label "${Name} (numeric-version)"
Assert-Equal -Actual $productVersion -Expected $ExpectedProductVersion -Label "${Name} (product-version)"
Assert-Equal -Actual $productVersionValid -Expected $ExpectedProductVersionValid.ToString().ToLowerInvariant() -Label "${Name} (product-version-valid)"

Write-Host "PASS: ${Name}"
} catch {
Expand Down Expand Up @@ -103,26 +109,33 @@ function Invoke-FailureCase {

try {
# Branch builds keep the legacy 0.0.<run-number> in all modes.
Invoke-Case -Name 'Branch build' -RefType 'branch' -RefName 'dev/my-feature' -RunNumber '42' -ExpectedVersion '0.0.42' -ExpectedNumericVersion '0.0.42.42'
Invoke-Case -Name 'Branch build, large run number' -RefType 'branch' -RefName 'main' -RunNumber '70000' -ExpectedVersion '0.0.70000' -ExpectedNumericVersion '0.0.4465.4465'
Invoke-Case -Name 'Branch build' -RefType 'branch' -RefName 'dev/my-feature' -RunNumber '42' -ExpectedVersion '0.0.42' -ExpectedNumericVersion '0.0.42.42' -ExpectedProductVersion '0.0.42.42'
Invoke-Case -Name 'Branch build, large run number' -RefType 'branch' -RefName 'main' -RunNumber '70000' -ExpectedVersion '0.0.70000' -ExpectedNumericVersion '0.0.4465.4465' -ExpectedProductVersion '0.0.4465.4465'

# Tag builds use the tag name; numeric-version strips the suffix + appends the run number.
Invoke-Case -Name 'Final release tag' -RefType 'tag' -RefName '1.2.3' -RunNumber '7' -ExpectedVersion '1.2.3' -ExpectedNumericVersion '1.2.3.7'
Invoke-Case -Name 'Pre-release tag' -RefType 'tag' -RefName '1.4.0-dev-myfeature.42' -RunNumber '99' -ExpectedVersion '1.4.0-dev-myfeature.42' -ExpectedNumericVersion '1.4.0.99'
Invoke-Case -Name 'Build-metadata tag' -RefType 'tag' -RefName '2.3.1+build.5' -RunNumber '3' -ExpectedVersion '2.3.1+build.5' -ExpectedNumericVersion '2.3.1.3'
Invoke-Case -Name 'Pre-release + metadata tag' -RefType 'tag' -RefName '1.2.3-rc.1+meta' -RunNumber '8' -ExpectedVersion '1.2.3-rc.1+meta' -ExpectedNumericVersion '1.2.3.8'
Invoke-Case -Name 'Leading v prefix tolerated' -RefType 'tag' -RefName 'v1.2.3' -RunNumber '5' -ExpectedVersion 'v1.2.3' -ExpectedNumericVersion '1.2.3.5'
Invoke-Case -Name 'Final release tag' -RefType 'tag' -RefName '1.2.3' -RunNumber '7' -ExpectedVersion '1.2.3' -ExpectedNumericVersion '1.2.3.7' -ExpectedProductVersion '1.2.3'
Invoke-Case -Name 'Pre-release tag' -RefType 'tag' -RefName '1.4.0-a1b2c3d4.42' -RunNumber '99' -ExpectedVersion '1.4.0-a1b2c3d4.42' -ExpectedNumericVersion '1.4.0.99' -ExpectedProductVersion '1.4.0.99'
Invoke-Case -Name 'Build-metadata tag' -RefType 'tag' -RefName '2.3.1+build.5' -RunNumber '3' -ExpectedVersion '2.3.1+build.5' -ExpectedNumericVersion '2.3.1.3' -ExpectedProductVersion '2.3.1'
Invoke-Case -Name 'Pre-release + metadata tag' -RefType 'tag' -RefName '1.2.3-rc.1+meta' -RunNumber '8' -ExpectedVersion '1.2.3-rc.1+meta' -ExpectedNumericVersion '1.2.3.8' -ExpectedProductVersion '1.2.3.8'
Invoke-Case -Name 'Leading v prefix tolerated' -RefType 'tag' -RefName 'v1.2.3' -RunNumber '5' -ExpectedVersion 'v1.2.3' -ExpectedNumericVersion '1.2.3.5' -ExpectedProductVersion '1.2.3'

# 4-part cores (e.g. date-based tags) keep their own 4th field — the run number is not appended.
Invoke-Case -Name 'Four-part date tag' -RefType 'tag' -RefName '2026.07.08.230' -RunNumber '44' -ExpectedVersion '2026.07.08.230' -ExpectedNumericVersion '2026.7.8.230'
Invoke-Case -Name 'Four-part tag keeps 4th field' -RefType 'tag' -RefName '1.2.3.4' -RunNumber '99' -ExpectedVersion '1.2.3.4' -ExpectedNumericVersion '1.2.3.4'
Invoke-Case -Name 'Four-part tag wraps 4th field' -RefType 'tag' -RefName '1.2.3.70000' -RunNumber '7' -ExpectedVersion '1.2.3.70000' -ExpectedNumericVersion '1.2.3.4465'
Invoke-Case -Name 'Four-part date tag' -RefType 'tag' -RefName '2026.07.08.230' -RunNumber '44' -ExpectedVersion '2026.07.08.230' -ExpectedNumericVersion '2026.7.8.230' -ExpectedProductVersion '2026.7.8.230' -ExpectedProductVersionValid $false
Invoke-Case -Name 'Four-part tag keeps 4th field' -RefType 'tag' -RefName '1.2.3.4' -RunNumber '99' -ExpectedVersion '1.2.3.4' -ExpectedNumericVersion '1.2.3.4' -ExpectedProductVersion '1.2.3.4'
Invoke-Case -Name 'Four-part tag wraps 4th field' -RefType 'tag' -RefName '1.2.3.70000' -RunNumber '7' -ExpectedVersion '1.2.3.70000' -ExpectedNumericVersion '1.2.3.4465' -ExpectedProductVersion '1.2.3.4465'

# Fields must be < 65535 (assembly-metadata max 65534): wrap-around % 65535, never a clamp.
Invoke-Case -Name 'Run number 65534 fits' -RefType 'tag' -RefName '1.0.0' -RunNumber '65534' -ExpectedVersion '1.0.0' -ExpectedNumericVersion '1.0.0.65534'
Invoke-Case -Name 'Run number wraps at 65535' -RefType 'tag' -RefName '1.0.0' -RunNumber '65535' -ExpectedVersion '1.0.0' -ExpectedNumericVersion '1.0.0.0'
Invoke-Case -Name 'Run number 65536 wraps to 1' -RefType 'tag' -RefName '1.0.0' -RunNumber '65536' -ExpectedVersion '1.0.0' -ExpectedNumericVersion '1.0.0.1'
Invoke-Case -Name 'Run number above 65536' -RefType 'tag' -RefName '1.0.0' -RunNumber '70000' -ExpectedVersion '1.0.0' -ExpectedNumericVersion '1.0.0.4465'
Invoke-Case -Name 'Run number 65534 fits' -RefType 'tag' -RefName '1.0.0' -RunNumber '65534' -ExpectedVersion '1.0.0' -ExpectedNumericVersion '1.0.0.65534' -ExpectedProductVersion '1.0.0'
Invoke-Case -Name 'Run number wraps at 65535' -RefType 'tag' -RefName '1.0.0' -RunNumber '65535' -ExpectedVersion '1.0.0' -ExpectedNumericVersion '1.0.0.0' -ExpectedProductVersion '1.0.0'
Invoke-Case -Name 'Run number 65536 wraps to 1' -RefType 'tag' -RefName '1.0.0' -RunNumber '65536' -ExpectedVersion '1.0.0' -ExpectedNumericVersion '1.0.0.1' -ExpectedProductVersion '1.0.0'
Invoke-Case -Name 'Run number above 65536' -RefType 'tag' -RefName '1.0.0' -RunNumber '70000' -ExpectedVersion '1.0.0' -ExpectedNumericVersion '1.0.0.4465' -ExpectedProductVersion '1.0.0'

# Windows Installer ProductVersion bounds: major/minor <= 255; build <= 65535.
Invoke-Case -Name 'ProductVersion maximum fields' -RefType 'tag' -RefName '255.255.65535' -RunNumber '1' -ExpectedVersion '255.255.65535' -ExpectedNumericVersion '255.255.0.1' -ExpectedProductVersion '255.255.65535'
Invoke-Case -Name 'Prerelease ProductVersion maximum' -RefType 'tag' -RefName '255.255.65535-rc.1' -RunNumber '2' -ExpectedVersion '255.255.65535-rc.1' -ExpectedNumericVersion '255.255.0.2' -ExpectedProductVersion '255.255.65535.2'
Invoke-Case -Name 'ProductVersion major out of range' -RefType 'tag' -RefName '256.1.1' -RunNumber '1' -ExpectedVersion '256.1.1' -ExpectedNumericVersion '256.1.1.1' -ExpectedProductVersion '256.1.1' -ExpectedProductVersionValid $false
Invoke-Case -Name 'ProductVersion minor out of range' -RefType 'tag' -RefName '1.256.1' -RunNumber '1' -ExpectedVersion '1.256.1' -ExpectedNumericVersion '1.256.1.1' -ExpectedProductVersion '1.256.1' -ExpectedProductVersionValid $false
Invoke-Case -Name 'ProductVersion build out of range' -RefType 'tag' -RefName '1.1.65536' -RunNumber '1' -ExpectedVersion '1.1.65536' -ExpectedNumericVersion '1.1.1.1' -ExpectedProductVersion '1.1.65536' -ExpectedProductVersionValid $false

# Failure cases: tags that have no major.minor.patch[.build] core cannot yield a numeric-version.
Invoke-FailureCase -Name 'Non-SemVer tag rejected' -RefType 'tag' -RefName 'release-1' -RunNumber '1'
Expand Down
Loading
Loading