Add preview release versioning and update channel support (#49414)

## Summary
- Publish scheduled `main` builds as GitHub prereleases while keeping
manual `main` runs as preview validation builds.
- Add an opt-in Settings switch for prerelease update checks; stable
updates remain the default.
- Use one MSI-safe version across bundles, MSI packages, binaries,
symbols, and package manifests.
- Prevent preview releases from triggering Microsoft Store, WinGet, or
public-symbol publication.
- Label preview builds explicitly in Settings, update notifications, and
What's New.

## Build intent

| Source | Trigger | Intent |
| --- | --- | --- |
| `main` | Scheduled | Publish a preview release |
| `main` | Manual | Validate a preview build without publishing |
| `stable` | Manual | Produce a stable release |
| Other branches | Any supported trigger | Produce a private validation
build |

## MSI-safe release versioning

Windows Installer compares only `major.minor.build` and ignores the
fourth version component. Preview and stable release builds therefore
use:

```text
major.minor.YDDDB.0
```

- `Y`: zero-based number of calendar years since `ReleaseTrainEpoch`.
- `DDD`: three-position calendar day of year.
- `B`: daily release sequence `1-9`.
- The fourth component is always `0`.

With `ReleaseTrainVersion=0.100` and `ReleaseTrainEpoch=2026-01-01`:

```text
0.100.2111.0   = July 30, 2026, release build 1
0.100.3659.0   = December 31, 2026, release build 9
0.100.10011.0  = January 1, 2027, release build 1
```

The allocator formats `DDD` as exactly three digits before converting
the MSI component to its numeric representation. Leading zeros may not
be displayed because Windows version components are numeric; decoding
remains positional:

```text
B   = component % 10
DDD = (component / 10) % 1000
Y   = component / 10000
```

`ReleaseTrainVersion` and `ReleaseTrainEpoch` are checked in under
`src/Version.props`. The epoch remains January 1 of the active epoch
year and advances on the first release-train minor change in a new year.

## Daily release counter

Azure DevOps persists the daily sequence server-side using a counter
keyed as `release-YYYYMMDD`.

- `main` and `stable` share the same daily counter.
- Other branches do not evaluate or consume the release counter.
- Failed or canceled `main`/`stable` runs may leave gaps.
- The build fails when the daily sequence exceeds `9`.
- The counter date and encoded `YDDD` date both use
`pipeline.startTime`.

Private branches retain independent `0.0.<extended-day><NN>.0`
validation versions.

## Update behavior
- Stable users continue to query GitHub's stable latest-release path.
- Users who explicitly enable preview updates can select newer GitHub
prereleases.
- Preview releases and notifications are labeled as PowerToys Preview.
- What's New separates preview entries from stable release history and
hides previews by default.

## Validation
- 17 Pester tests cover `main`, `stable`, private branches, year
rollover, epoch reset, monotonicity, override validation, sequence
limits, and date alignment.
- Version propagation verified `0.100.2111.0` in `Version.props` and all
affected AppX/MSIX manifests.
- Azure DevOps pipeline dry-runs succeeded for both `refs/heads/main`
and `refs/heads/stable`.
- The affected native version project builds successfully.
- PR CI is green for x64, ARM64, Command Palette SDK, dependency review,
telemetry detection, and CLA.

## Remaining end-to-end checks
- Install two locally or officially produced installers with consecutive
MSI-visible `YDDDB` versions and verify the upgrade preserves binaries,
package registrations, hardlinks, and shell integrations.
- On the first natural post-merge `main` or `stable` run, verify the
production counter value and resolved version in the release logs.
## Local GPO verification

Validated locally with the signed `v0.100.2171` build from Azure DevOps
build
[153961073](https://microsoft.visualstudio.com/Dart/_build/results?buildId=153961073).
These checks cover the administrative-template integration and Settings
behavior.

### Policy enabled: preview updates are disabled

With `PreviewUpdatesDisabled=1`, **Include prerelease updates** is
forced off and locked, and Settings displays the
managed-by-your-organization notice.

![PowerToys Settings with preview updates disabled by
policy](https://raw.githubusercontent.com/LegendaryBlair/PowerToys/df808630b04e65ba437081aff9401c4efd58e67f/.github/pr-assets/49414/gpo-policy-enforced.png)

### Policy removed: the user preference is preserved

After removing `PreviewUpdatesDisabled` and restarting PowerToys, the
previously selected preview-update preference is restored and editable.
The policy suppresses the preference without overwriting it.

![PowerToys Settings with the preview-update preference
restored](https://raw.githubusercontent.com/LegendaryBlair/PowerToys/df808630b04e65ba437081aff9401c4efd58e67f/.github/pr-assets/49414/gpo-preference-restored.png)

### Group Policy Editor

After importing the updated ADMX/ADML templates, **Disable preview build
updates** appears under **Microsoft PowerToys > Installer and Updates**.
The policy dialog documents that **Enabled** blocks preview updates,
while **Disabled** or **Not Configured** leaves the choice available to
the user.

![Disable preview build updates in Local Group Policy
Editor](https://raw.githubusercontent.com/LegendaryBlair/PowerToys/e9e5c12f4480ef895d263460a59982246b7654dc/.github/pr-assets/49414/group-policy-editor-policy-dialog.png)

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ad8b7909-0472-4464-bdee-deaeca726f94
Copilot-Session: 8e04a72e-3b0f-4ac4-8156-d04ea9b8bb85
This commit is contained in:
Boliang Zhang
2026-08-06 23:45:11 +08:00
committed by GitHub
parent 0126a1aff0
commit 558e633c59
55 changed files with 1672 additions and 118 deletions

View File

@@ -401,6 +401,17 @@ HHH
riday
YYY
# Release versioning identifiers and format fragments
DAILYVERSIONSEQUENCE
DDNNN
Mdd
SOURCEBRANCH
SOURCEVERSION
VERSIONDATE
YDDD
YDDDB
YYMM
# Unicode
precomposed

View File

@@ -0,0 +1,213 @@
<#
.SYNOPSIS
Creates or updates a GitHub draft release from a prepared PowerToys release folder.
.DESCRIPTION
Validates a prepared release folder, finds the four installers, two symbol
zips, GPO zip, and release notes, then uses GitHub CLI (`gh`) to create or
update a draft release and upload the assets. Asset filenames use the
numeric product version; -TagName may include channel suffixes such as
v0.100.2607.08001-preview.
#>
param(
[Parameter(Mandatory = $true)]
[string]$ReleaseFolder,
[string]$GitHubRepo = "microsoft/PowerToys",
[string]$TagName,
[string]$Target,
[string]$Title,
[string]$NotesFile,
[switch]$Prerelease,
[switch]$LatestFalse,
[switch]$Clobber,
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
function Resolve-ExistingPath {
param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$Description)
$resolved = Resolve-Path -LiteralPath $Path -ErrorAction SilentlyContinue
if (-not $resolved) {
throw "$Description not found: $Path"
}
return $resolved.ProviderPath
}
function Invoke-Gh {
param([Parameter(Mandatory)][string[]]$Arguments, [switch]$AllowFailure)
if ($DryRun) {
Write-Host "gh $($Arguments -join ' ')" -ForegroundColor DarkGray
return $null
}
$output = & gh @Arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0 -and -not $AllowFailure) {
throw "gh $($Arguments -join ' ') failed with exit code $exitCode.`n$output"
}
if ($exitCode -ne 0) {
return $null
}
return $output
}
function Get-ReleaseNotesFile {
param([Parameter(Mandatory)][string]$Folder, [Parameter(Mandatory)][string]$Version, [string]$ExplicitNotesFile)
if ($ExplicitNotesFile) {
return Resolve-ExistingPath -Path $ExplicitNotesFile -Description "Release notes file"
}
$markdownFiles = @(Get-ChildItem -LiteralPath $Folder -File -Filter "*.md" | Where-Object {
$_.Name -notmatch '^(hashes|sha256|checksums)\.md$'
})
foreach ($name in @("v$Version-release-notes.md", "$Version-release-notes.md", "release-notes.md", "ReleaseNotes.md", "notes.md")) {
$match = $markdownFiles | Where-Object { $_.Name -ieq $name } | Select-Object -First 1
if ($match) {
return $match.FullName
}
}
if ($markdownFiles.Count -eq 1) {
return $markdownFiles[0].FullName
}
throw "Could not infer release notes file. Pass -NotesFile explicitly."
}
function Test-ReleaseNotesInstallerLinks {
param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$Tag, [Parameter(Mandatory)][string]$Version)
$content = Get-Content -LiteralPath $Path -Raw
$expectedLinks = @{
"ptUserX64" = "https://github.com/$GitHubRepo/releases/download/$Tag/PowerToysUserSetup-$Version-x64.exe"
"ptUserArm64" = "https://github.com/$GitHubRepo/releases/download/$Tag/PowerToysUserSetup-$Version-arm64.exe"
"ptMachineX64" = "https://github.com/$GitHubRepo/releases/download/$Tag/PowerToysSetup-$Version-x64.exe"
"ptMachineArm64" = "https://github.com/$GitHubRepo/releases/download/$Tag/PowerToysSetup-$Version-arm64.exe"
}
foreach ($link in $expectedLinks.GetEnumerator()) {
if ($content -notmatch "(?im)^\[$([regex]::Escape($link.Key))\]:\s*$([regex]::Escape($link.Value))\s*$") {
throw "Release notes are missing installer link reference [$($link.Key)]: $($link.Value)"
}
}
}
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
throw "GitHub CLI (`gh`) was not found in PATH."
}
Invoke-Gh -Arguments @("auth", "status") | Out-Null
$releaseFolderPath = Resolve-ExistingPath -Path $ReleaseFolder -Description "Release folder"
if (-not (Test-Path -LiteralPath $releaseFolderPath -PathType Container)) {
throw "ReleaseFolder must be a directory: $releaseFolderPath"
}
$allFiles = @(Get-ChildItem -LiteralPath $releaseFolderPath -File)
$installerMatches = @()
foreach ($file in $allFiles) {
if ($file.Name -match '^PowerToys(?<User>User)?Setup-(?<Version>.+)-(?<Arch>x64|arm64)\.exe$') {
$installerMatches += [pscustomobject]@{ File = $file; Version = $Matches.Version; Arch = $Matches.Arch; IsUser = -not [string]::IsNullOrEmpty($Matches.User) }
}
}
if ($installerMatches.Count -ne 4) {
throw "Expected exactly four PowerToys installer EXEs, found $($installerMatches.Count)."
}
$versions = @($installerMatches | Select-Object -ExpandProperty Version -Unique)
if ($versions.Count -ne 1) {
throw "Installer versions do not match: $($versions -join ', ')"
}
$numericVersion = $versions[0]
if (-not $TagName) {
$TagName = "v$numericVersion"
}
if (-not $Title) {
$displayVersion = $TagName.TrimStart('v', 'V') -replace '-preview$', ''
$Title = if ($Prerelease) { "PowerToys v$displayVersion Preview" } else { "Release v$numericVersion" }
}
$filesByName = @{}
foreach ($file in $allFiles) {
$filesByName[$file.Name] = $file
}
$expectedAssets = @(
"PowerToysSetup-$numericVersion-x64.exe",
"PowerToysSetup-$numericVersion-arm64.exe",
"PowerToysUserSetup-$numericVersion-x64.exe",
"PowerToysUserSetup-$numericVersion-arm64.exe",
"symbols-x64.zip",
"symbols-arm64.zip",
"GroupPolicyObjectFiles-$numericVersion.zip"
)
$assets = New-Object System.Collections.Generic.List[string]
foreach ($name in $expectedAssets) {
if (-not $filesByName.ContainsKey($name)) {
throw "Required release asset is missing: $name"
}
$assets.Add($filesByName[$name].FullName)
}
$notesFilePath = Get-ReleaseNotesFile -Folder $releaseFolderPath -Version $numericVersion -ExplicitNotesFile $NotesFile
Test-ReleaseNotesInstallerLinks -Path $notesFilePath -Tag $TagName -Version $numericVersion
Write-Host "Release folder: $releaseFolderPath"
Write-Host "Repository: $GitHubRepo"
Write-Host "Numeric version:$numericVersion"
Write-Host "Tag: $TagName"
Write-Host "Title: $Title"
Write-Host "Notes: $notesFilePath"
if ($Target) {
Write-Host "Target: $Target"
} else {
Write-Warning "No -Target was provided. If the tag does not already exist, gh will create it from the repository default branch."
}
$releaseJson = Invoke-Gh -Arguments @("release", "view", $TagName, "--repo", $GitHubRepo, "--json", "isDraft,url") -AllowFailure
$releaseExists = -not [string]::IsNullOrWhiteSpace(($releaseJson | Out-String))
if ($releaseExists) {
$release = ($releaseJson | Out-String) | ConvertFrom-Json
if (-not $release.isDraft) {
throw "Release $TagName already exists and is not a draft: $($release.url)"
}
$editArgs = @("release", "edit", $TagName, "--repo", $GitHubRepo, "--draft", "--title", $Title, "--notes-file", $notesFilePath)
if ($Target) { $editArgs += @("--target", $Target) }
if ($Prerelease) { $editArgs += "--prerelease" }
Invoke-Gh -Arguments $editArgs | Out-Null
$uploadArgs = @("release", "upload", $TagName) + $assets + @("--repo", $GitHubRepo)
if ($Clobber) { $uploadArgs += "--clobber" }
Invoke-Gh -Arguments $uploadArgs | Out-Null
} else {
$createArgs = @("release", "create", $TagName) + $assets + @("--repo", $GitHubRepo, "--draft", "--title", $Title, "--notes-file", $notesFilePath)
if ($Target) { $createArgs += @("--target", $Target) }
if ($Prerelease) { $createArgs += "--prerelease" }
if ($Prerelease -or $LatestFalse) { $createArgs += "--latest=false" }
Invoke-Gh -Arguments $createArgs | Out-Null
}
if ($DryRun) {
Write-Host "Dry run complete. No GitHub release was created or modified." -ForegroundColor Yellow
} else {
$result = Invoke-Gh -Arguments @("release", "view", $TagName, "--repo", $GitHubRepo, "--json", "url", "--jq", ".url")
Write-Host "Draft release ready: $result" -ForegroundColor Green
}

View File

@@ -12,6 +12,7 @@ jobs:
microsoft_store:
name: Publish Microsoft Store
if: ${{ !github.event.release.prerelease }}
environment: store
runs-on: ubuntu-latest
steps:

View File

@@ -0,0 +1,339 @@
[CmdletBinding()]
param(
[AllowEmptyString()]
[string]$VersionOverride = "",
[string]$SourceBranch = $env:BUILD_SOURCEBRANCH,
[string]$BuildReason = $env:BUILD_REASON,
[string]$BuildNumber = $env:BUILD_BUILDNUMBER,
[AllowEmptyString()]
[string]$BuildDate = "",
[AllowEmptyString()]
[string]$DailyVersionSequence = "",
[string]$VersionPropsPath = (Join-Path $PSScriptRoot "..\src\Version.props")
)
$ErrorActionPreference = "Stop"
function Get-BuildStamp {
param([string]$PipelineBuildNumber)
if ([string]::IsNullOrWhiteSpace($PipelineBuildNumber)) {
$now = Get-Date
return [pscustomobject]@{
Date = $now.Date
Revision = 1
}
}
if ($PipelineBuildNumber -notmatch "_(?<yearMonth>\d{4})\.(?<day>\d{2})(?<revision>\d{3})(?:-.+)?$") {
throw "Build number '$PipelineBuildNumber' does not end with the expected _YYMM.DDNNN pattern"
}
try {
$date = [datetime]::ParseExact(
"20$($matches["yearMonth"])$($matches["day"])",
"yyyyMMdd",
[Globalization.CultureInfo]::InvariantCulture)
}
catch {
throw "Build number '$PipelineBuildNumber' contains an invalid date"
}
$revision = [int]::Parse($matches["revision"])
if ($revision -lt 1 -or $revision -gt 99) {
throw "Build number '$PipelineBuildNumber' has daily revision '$revision'; canonical versions support revisions 001 through 099"
}
return [pscustomobject]@{
Date = $date
Revision = $revision
}
}
function Test-VersionParts {
param([Parameter(Mandatory)][string[]]$Parts)
foreach ($part in $Parts) {
$value = [int]::Parse($part)
if ($value -lt 0 -or $value -gt [UInt16]::MaxValue) {
throw "Version component '$value' is outside the supported Windows version range 0-65535"
}
}
}
function Get-VersionDate {
param(
[AllowEmptyString()][string]$DateOverride,
[Parameter(Mandatory)]$BuildStamp
)
if ([string]::IsNullOrWhiteSpace($DateOverride)) {
return $BuildStamp.Date
}
try {
return [datetime]::ParseExact(
$DateOverride,
"yyyyMMdd",
[Globalization.CultureInfo]::InvariantCulture)
}
catch {
throw "Build date '$DateOverride' must use the yyyyMMdd format"
}
}
function Get-ReleaseTrainMetadata {
param([Parameter(Mandatory)][string]$Path)
[xml]$versionProps = Get-Content -LiteralPath $Path
$releaseTrain = [string]$versionProps.Project.PropertyGroup.ReleaseTrainVersion
if ($releaseTrain -notmatch "^(?<major>\d+)\.(?<minor>\d+)$") {
throw "ReleaseTrainVersion in '$Path' must use the major.minor format"
}
Test-VersionParts -Parts @($matches["major"], $matches["minor"])
if ([int]::Parse($matches["major"]) -gt 255 -or [int]::Parse($matches["minor"]) -gt 255) {
throw "ReleaseTrainVersion in '$Path' must keep major and minor within the MSI-supported range 0-255"
}
$epochText = [string]$versionProps.Project.PropertyGroup.ReleaseTrainEpoch
try {
$epoch = [datetime]::ParseExact(
$epochText,
"yyyy-MM-dd",
[Globalization.CultureInfo]::InvariantCulture)
}
catch {
throw "ReleaseTrainEpoch in '$Path' must use the yyyy-MM-dd format"
}
if ($epoch.Month -ne 1 -or $epoch.Day -ne 1) {
throw "ReleaseTrainEpoch in '$Path' must be January 1 of the active epoch year"
}
return [pscustomobject]@{
Version = $releaseTrain
Epoch = $epoch
}
}
function Get-ReleaseVersion {
param(
[Parameter(Mandatory)][string]$ReleaseTrain,
[Parameter(Mandatory)][datetime]$Epoch,
[Parameter(Mandatory)]$BuildStamp,
[Parameter(Mandatory)][int]$DailySequence
)
if ($BuildStamp.Date -lt $Epoch) {
throw "Build date '$($BuildStamp.Date.ToString("yyyy-MM-dd"))' is before ReleaseTrainEpoch '$($Epoch.ToString("yyyy-MM-dd"))'"
}
if ($DailySequence -lt 1 -or $DailySequence -gt 9) {
throw "Daily release sequence '$DailySequence' is outside the YDDDB-supported range 1-9"
}
$yearOffset = $BuildStamp.Date.Year - $Epoch.Year
if ($yearOffset -gt 6) {
throw "Release train year offset '$yearOffset' exceeds the MSI-safe YDDDB range 0-6; advance the release train and reset ReleaseTrainEpoch"
}
$thirdComponentText = "{0}{1:D3}{2}" -f $yearOffset, $BuildStamp.Date.DayOfYear, $DailySequence
$thirdComponent = [int]::Parse($thirdComponentText)
if ($thirdComponent -gt [UInt16]::MaxValue) {
throw "Generated version component '$thirdComponent' exceeds 65535; advance the release train and reset ReleaseTrainEpoch"
}
return "$ReleaseTrain.$thirdComponent.0"
}
function Get-PrivateVersion {
param(
[Parameter(Mandatory)][datetime]$Epoch,
[Parameter(Mandatory)]$BuildStamp
)
$extendedDay = ($BuildStamp.Date - $Epoch).Days + 1
if ($extendedDay -lt 1) {
throw "Build date '$($BuildStamp.Date.ToString("yyyy-MM-dd"))' is before ReleaseTrainEpoch '$($Epoch.ToString("yyyy-MM-dd"))'"
}
$thirdComponent = ($extendedDay * 100) + $BuildStamp.Revision
if ($thirdComponent -gt [UInt16]::MaxValue) {
throw "Generated private version component '$thirdComponent' exceeds 65535"
}
return "0.0.$thirdComponent.0"
}
function Get-ReleaseDailySequence {
param([AllowEmptyString()][string]$Sequence)
if ([string]::IsNullOrWhiteSpace($Sequence)) {
throw "DailyVersionSequence is required for main and stable builds"
}
if ($Sequence -notmatch "^\d+$") {
throw "Daily release sequence '$Sequence' must be numeric"
}
return [int]::Parse($Sequence)
}
function Get-PreviewVersion {
param(
[Parameter(Mandatory)][string]$ReleaseTrain,
[AllowEmptyString()][string]$Override,
[Parameter(Mandatory)][string]$GeneratedVersion
)
$inputVersion = $Override.Trim()
if ($inputVersion.EndsWith("-preview", [StringComparison]::OrdinalIgnoreCase)) {
$inputVersion = $inputVersion.Substring(0, $inputVersion.Length - "-preview".Length)
}
if ([string]::IsNullOrWhiteSpace($inputVersion)) {
return $GeneratedVersion
}
if ($inputVersion -match "^(?<major>\d+)\.(?<minor>\d+)$") {
if ($inputVersion -ne $ReleaseTrain) {
throw "Preview version base '$inputVersion' does not match ReleaseTrainVersion '$ReleaseTrain'"
}
return $GeneratedVersion
}
if ($inputVersion -notmatch "^(?<major>\d+)\.(?<minor>\d+)\.(?<revision>\d+)\.(?<build>\d+)$") {
throw "Preview version override must be major.minor or major.minor.YDDDB.0, optionally followed by -preview"
}
if ("$($matches["major"]).$($matches["minor"])" -ne $ReleaseTrain) {
throw "Preview version '$inputVersion' does not match ReleaseTrainVersion '$ReleaseTrain'"
}
if ([int]::Parse($matches["build"]) -ne 0) {
throw "Preview version '$inputVersion' must use 0 for the fourth component"
}
Test-VersionParts -Parts @($matches["major"], $matches["minor"], $matches["revision"], $matches["build"])
$parts = @($matches["major"], $matches["minor"], $matches["revision"], $matches["build"])
return ($parts | ForEach-Object { [int]::Parse($_) }) -join "."
}
function Get-MsiSafeVersionOverride {
param(
[Parameter(Mandatory)][string]$Override,
[Parameter(Mandatory)][string]$VersionKind
)
$inputVersion = $Override.Trim()
if ($inputVersion -notmatch "^(?<major>\d+)\.(?<minor>\d+)\.(?<revision>\d+)(?:\.(?<build>\d+))?$") {
throw "$VersionKind version override must be numeric major.minor.patch or major.minor.patch.build"
}
$parts = @($matches["major"], $matches["minor"], $matches["revision"])
if ($matches["build"]) {
$parts += $matches["build"]
}
else {
$parts += "0"
}
Test-VersionParts -Parts $parts
if ([int]::Parse($parts[0]) -gt 255 -or [int]::Parse($parts[1]) -gt 255) {
throw "$VersionKind version '$inputVersion' must keep major and minor within the MSI-supported range 0-255"
}
if ([int]::Parse($parts[3]) -ne 0) {
throw "$VersionKind version '$inputVersion' must use 0 for the fourth component"
}
return ($parts | ForEach-Object { [int]::Parse($_) }) -join "."
}
function Get-StableVersion {
param(
[Parameter(Mandatory)][AllowEmptyString()][string]$Override,
[Parameter(Mandatory)][string]$GeneratedVersion
)
if ([string]::IsNullOrWhiteSpace($Override)) {
return $GeneratedVersion
}
return Get-MsiSafeVersionOverride -Override $Override -VersionKind "Stable"
}
$isMain = $SourceBranch -eq "refs/heads/main"
$isStable = $SourceBranch -eq "refs/heads/stable"
$isScheduled = $BuildReason -eq "Schedule"
if ($isScheduled -and -not $isMain) {
throw "Scheduled release builds are only supported from refs/heads/main"
}
$releaseMetadata = Get-ReleaseTrainMetadata -Path $VersionPropsPath
$releaseTrain = $releaseMetadata.Version
$buildStamp = Get-BuildStamp -PipelineBuildNumber $BuildNumber
if ($isMain) {
if ($isScheduled -and -not [string]::IsNullOrWhiteSpace($VersionOverride)) {
throw "Scheduled main builds must use the checked-in ReleaseTrainVersion and cannot specify a version override"
}
$intent = if ($isScheduled) { "preview-release" } else { "preview-validation" }
$channel = "preview"
$buildStamp.Date = Get-VersionDate -DateOverride $BuildDate -BuildStamp $buildStamp
$releaseDailySequence = Get-ReleaseDailySequence -Sequence $DailyVersionSequence
$generatedVersion = Get-ReleaseVersion -ReleaseTrain $releaseTrain -Epoch $releaseMetadata.Epoch -BuildStamp $buildStamp -DailySequence $releaseDailySequence
$version = Get-PreviewVersion -ReleaseTrain $releaseTrain -Override $VersionOverride -GeneratedVersion $generatedVersion
$allowPublicSymbols = $false
$shouldPublishPreview = $isScheduled
}
elseif ($isStable) {
if ($isScheduled) {
throw "Stable release builds must be queued manually"
}
$intent = "stable-release"
$channel = "stable"
$buildStamp.Date = Get-VersionDate -DateOverride $BuildDate -BuildStamp $buildStamp
$releaseDailySequence = Get-ReleaseDailySequence -Sequence $DailyVersionSequence
$generatedVersion = Get-ReleaseVersion -ReleaseTrain $releaseTrain -Epoch $releaseMetadata.Epoch -BuildStamp $buildStamp -DailySequence $releaseDailySequence
$version = Get-StableVersion -Override $VersionOverride -GeneratedVersion $generatedVersion
$allowPublicSymbols = $true
$shouldPublishPreview = $false
}
else {
$intent = "private-validation"
$channel = "private"
$version = if ([string]::IsNullOrWhiteSpace($VersionOverride)) {
Get-PrivateVersion -Epoch $releaseMetadata.Epoch -BuildStamp $buildStamp
}
else {
Get-MsiSafeVersionOverride -Override $VersionOverride -VersionKind "Private"
}
$allowPublicSymbols = $false
$shouldPublishPreview = $false
}
Test-VersionParts -Parts ($version -split "\.")
Write-Host "Resolved build intent: $intent"
Write-Host "Resolved release channel: $channel"
Write-Host "Resolved version: $version"
[pscustomobject]@{
Intent = $intent
Channel = $channel
Version = $version
AllowPublicSymbols = $allowPublicSymbols
ShouldPublishPreview = $shouldPublishPreview
}

View File

@@ -0,0 +1,268 @@
# Copyright (c) Microsoft Corporation
# The Microsoft Corporation licenses this file to you under the MIT license.
$scriptPath = Join-Path $PSScriptRoot "..\resolveBuildMetadata.ps1"
function New-VersionProps {
param(
[string]$ReleaseTrain = "0.100",
[string]$Epoch = "2026-01-01"
)
$path = Join-Path $TestDrive "Version.props"
@"
<Project>
<PropertyGroup>
<ReleaseTrainVersion>$ReleaseTrain</ReleaseTrainVersion>
<ReleaseTrainEpoch>$Epoch</ReleaseTrainEpoch>
</PropertyGroup>
</Project>
"@ | Set-Content -LiteralPath $path
return $path
}
function Assert-Throws {
param([scriptblock]$Action)
$threw = $false
try {
& $Action | Out-Null
}
catch {
$threw = $true
}
$threw | Should Be $true
}
Describe "resolveBuildMetadata" {
It "generates the canonical preview version for main" {
$result = & $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Schedule" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30099-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
$result.Intent | Should Be "preview-release"
$result.Channel | Should Be "preview"
$result.Version | Should Be "0.100.2111.0"
}
It "uses the generated version by default for stable" {
$result = & $scriptPath `
-SourceBranch "refs/heads/stable" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30099-stable" `
-DailyVersionSequence "2" `
-VersionPropsPath (New-VersionProps)
$result.Intent | Should Be "stable-release"
$result.Channel | Should Be "stable"
$result.Version | Should Be "0.100.2112.0"
}
It "keeps private builds independent from the release counter" {
$result = & $scriptPath `
-SourceBranch "refs/heads/user/feature" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30003-feature" `
-BuildDate "20260731" `
-DailyVersionSequence "9" `
-VersionPropsPath (New-VersionProps)
$result.Version | Should Be "0.0.21103.0"
}
It "allows an explicit MSI-safe version for private validation" {
$result = & $scriptPath `
-VersionOverride "0.100.2151.0" `
-SourceBranch "refs/heads/LegendaryBlair/preview-version" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2608.03001-preview-version" `
-VersionPropsPath (New-VersionProps)
$result.Intent | Should Be "private-validation"
$result.Channel | Should Be "private"
$result.Version | Should Be "0.100.2151.0"
$result.AllowPublicSymbols | Should Be $false
$result.ShouldPublishPreview | Should Be $false
}
It "rejects a private override with a nonzero fourth component" {
Assert-Throws {
& $scriptPath `
-VersionOverride "0.100.2151.1" `
-SourceBranch "refs/heads/LegendaryBlair/preview-version" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2608.03001-preview-version" `
-VersionPropsPath (New-VersionProps)
}
}
It "increments the year digit across a calendar year boundary" {
$result = & $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2701.02001-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
$result.Version | Should Be "0.100.10021.0"
}
It "resets the year digit after the epoch advances with the release train" {
$result = & $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2701.02001-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps -ReleaseTrain "0.101" -Epoch "2027-01-01")
$result.Version | Should Be "0.101.21.0"
}
It "preserves monotonicity at the year boundary" {
$lastBuildOfYear = & $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2612.31099-main" `
-DailyVersionSequence "9" `
-VersionPropsPath (New-VersionProps)
$firstBuildOfNextYear = & $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2701.01001-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
[int]($firstBuildOfNextYear.Version -split "\.")[2] | Should BeGreaterThan ([int]($lastBuildOfYear.Version -split "\.")[2])
}
It "preserves an explicit stable override" {
$result = & $scriptPath `
-VersionOverride "0.100.2" `
-SourceBranch "refs/heads/stable" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-stable" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
$result.Version | Should Be "0.100.2.0"
}
It "rejects a stable override with a nonzero fourth component" {
Assert-Throws {
& $scriptPath `
-VersionOverride "0.100.2.1" `
-SourceBranch "refs/heads/stable" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-stable" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
}
}
It "rejects a stable override outside MSI major and minor limits" {
Assert-Throws {
& $scriptPath `
-VersionOverride "0.256.2.0" `
-SourceBranch "refs/heads/stable" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-stable" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
}
}
It "preserves a canonical full preview override" {
$result = & $scriptPath `
-VersionOverride "0.100.2111.0" `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
$result.Version | Should Be "0.100.2111.0"
}
It "rejects a preview override with a nonzero fourth component" {
Assert-Throws {
& $scriptPath `
-VersionOverride "0.100.2111.1" `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
}
}
It "rejects a preview override from a different release train" {
Assert-Throws {
& $scriptPath `
-VersionOverride "0.101.2111.0" `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
}
}
It "rejects daily release sequences above 9" {
Assert-Throws {
& $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-main" `
-DailyVersionSequence "10" `
-VersionPropsPath (New-VersionProps)
}
}
It "requires a release sequence for main and stable" {
Assert-Throws {
& $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-main" `
-VersionPropsPath (New-VersionProps)
}
}
It "uses the pipeline date for both YDDD and counter alignment" {
$result = & $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-main" `
-BuildDate "20260731" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
$result.Version | Should Be "0.100.2121.0"
}
It "requires the epoch to be January 1" {
Assert-Throws {
& $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_2607.30001-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps -Epoch "2026-02-01")
}
}
It "rejects release trains that exceed the YDDDB year range" {
Assert-Throws {
& $scriptPath `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber "PowerToys Signed YAML Release Build_3301.01001-main" `
-DailyVersionSequence "1" `
-VersionPropsPath (New-VersionProps)
}
}
}

View File

@@ -16,9 +16,9 @@ parameters:
default: false
- name: versionNumber
displayName: "Version Number"
displayName: "Version Override (optional; main and stable default to the release-train version)"
type: string
default: '0.0.1'
default: ''
- name: buildConfigurations
displayName: "Build Configurations"
@@ -42,6 +42,14 @@ name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
variables:
- template: templates/variables-nuget-package-version.yml
- name: versionDate
value: $[format('{0:yyyyMMdd}', pipeline.startTime)]
- ${{ if or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), eq(variables['Build.SourceBranch'], 'refs/heads/stable')) }}:
- name: dailyVersionSequence
value: $[counter(format('release-{0:yyyyMMdd}', pipeline.startTime), 1)]
- ${{ else }}:
- name: dailyVersionSequence
value: 0
extends:
template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates
@@ -110,7 +118,24 @@ extends:
# Sets versions for all PowerToy created DLLs
- pwsh: |-
.pipelines/versionSetting.ps1 -versionNumber '${{ parameters.versionNumber }}' -DevEnvironment ''
$metadata = .pipelines/resolveBuildMetadata.ps1 `
-VersionOverride '${{ parameters.versionNumber }}' `
-BuildDate '$(versionDate)' `
-DailyVersionSequence '$(dailyVersionSequence)'
$publishSymbolsToPublic = $${{ parameters.publishSymbolsToPublic }}
if ($publishSymbolsToPublic -and -not $metadata.AllowPublicSymbols) {
throw "Public symbols are only supported for manually queued stable-branch releases"
}
.pipelines/versionSetting.ps1 -versionNumber $metadata.Version -DevEnvironment '' -Channel $metadata.Channel
[xml]$versionProps = Get-Content 'src\Version.props'
$resolvedVersion = [string]$versionProps.Project.PropertyGroup.Version
Write-Host "Resolved PowerToys version: $resolvedVersion"
Write-Host "##vso[task.setvariable variable=ResolvedVersionNumber]$resolvedVersion"
Write-Host "##vso[task.setvariable variable=EffectiveVersionNumber]$resolvedVersion"
Write-Host "##vso[task.setvariable variable=ResolvedReleaseChannel]$($metadata.Channel)"
Write-Host "##vso[task.setvariable variable=ResolvedBuildIntent]$($metadata.Intent)"
Write-Host "##vso[task.setvariable variable=ShouldPublishPreview]$($metadata.ShouldPublishPreview)"
displayName: Prepare versioning
# Prepare the localizations and telemetry config before the release build

View File

@@ -65,6 +65,9 @@ parameters:
- name: versionNumber
type: string
default: '0.0.1'
- name: resolvedVersionNumber
type: string
default: ''
- name: useLatestWinAppSDK
type: boolean
default: false
@@ -140,6 +143,10 @@ jobs:
JobOutputDirectory: $(Build.ArtifactStagingDirectory)
LogOutputDirectory: $(Build.ArtifactStagingDirectory)\logs
JobOutputArtifactName: build-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }}
${{ if eq(parameters.resolvedVersionNumber, '') }}:
EffectiveVersionNumber: ${{ parameters.versionNumber }}
${{ if ne(parameters.resolvedVersionNumber, '') }}:
EffectiveVersionNumber: ${{ parameters.resolvedVersionNumber }}
NUGET_RESTORE_MSBUILD_ARGS: /p:Platform=$(BuildPlatform) # Required for nuget to work due to self contained
NODE_OPTIONS: --max_old_space_size=16384
${{ if or(eq(parameters.runTests, true), eq(parameters.buildTests, true)) }}:
@@ -599,7 +606,7 @@ jobs:
parameters:
codeSign: ${{ parameters.codeSign }}
signingIdentity: ${{ parameters.signingIdentity }}
versionNumber: ${{ parameters.versionNumber }}
versionNumber: $(EffectiveVersionNumber)
additionalBuildOptions: ${{ parameters.additionalBuildOptions }}
# This saves ~1GiB per architecture. We won't need these later.
@@ -663,7 +670,7 @@ jobs:
# Publishing the GPO files
- pwsh: |-
$GpoArchive = "$(JobOutputDirectory)\GroupPolicyObjectFiles-${{ parameters.versionNumber }}.zip"
$GpoArchive = "$(JobOutputDirectory)\GroupPolicyObjectFiles-$(EffectiveVersionNumber).zip"
tar -c -v --format=zip -C .\src\gpo\assets -f $GpoArchive *
displayName: Stage GPO files

View File

@@ -11,6 +11,9 @@ parameters:
- name: versionNumber
type: string
default: '0.0.1'
- name: resolvedVersionNumber
type: string
default: ''
- name: artifactStem
type: string
default: ''
@@ -39,7 +42,11 @@ jobs:
dependsOn: ${{ parameters.dependsOn }}
variables:
${{ insert }}: ${{ parameters.variables }}
SymbolsArtifactName: "PowerToys_${{parameters.versionNumber}}_$(Build.BuildNumber)"
${{ if eq(parameters.resolvedVersionNumber, '') }}:
EffectiveVersionNumber: ${{ parameters.versionNumber }}
${{ if ne(parameters.resolvedVersionNumber, '') }}:
EffectiveVersionNumber: ${{ parameters.resolvedVersionNumber }}
SymbolsArtifactName: "PowerToys_$(EffectiveVersionNumber)_$(Build.BuildNumber)"
steps:
- checkout: self
clean: true
@@ -48,6 +55,28 @@ jobs:
submodules: true
persistCredentials: True
- pwsh: |-
$effectiveVersionNumber = '${{ parameters.resolvedVersionNumber }}'
if ([string]::IsNullOrWhiteSpace($effectiveVersionNumber) -or $effectiveVersionNumber.StartsWith('$(')) {
$metadata = .pipelines/resolveBuildMetadata.ps1 `
-VersionOverride '${{ parameters.versionNumber }}' `
-BuildDate '$(versionDate)' `
-DailyVersionSequence '$(dailyVersionSequence)'
if ($${{ parameters.includePublicSymbolServer }} -and -not $metadata.AllowPublicSymbols) {
throw "Public symbols are only supported for manually queued stable-branch releases"
}
$effectiveVersionNumber = $metadata.Version
}
if ($effectiveVersionNumber -match '^(?<numeric>\d+\.\d+(?:\.\d+){0,2})-preview$') {
$effectiveVersionNumber = $matches['numeric']
}
Write-Host "##vso[task.setvariable variable=EffectiveVersionNumber]$effectiveVersionNumber"
Write-Host "##vso[task.setvariable variable=SymbolsArtifactName]PowerToys_${effectiveVersionNumber}_$(Build.BuildNumber)"
displayName: Resolve symbol version
- task: DownloadPipelineArtifact@2
displayName: Download all PDBs from all prior build phases
inputs:
@@ -83,7 +112,7 @@ jobs:
SymbolsMaximumWaitTime: 30
SymbolServerType: 'TeamServices'
SymbolsProduct: 'PowerToys Converged Symbols'
SymbolsVersion: '${{ parameters.versionNumber }}'
SymbolsVersion: '$(EffectiveVersionNumber)'
SymbolsArtifactName: $(SymbolsArtifactName)
SymbolExpirationInDays: ${{ parameters.symbolExpiryTime }}
env:

View File

@@ -5,37 +5,85 @@ Param(
[Parameter(Mandatory=$True,Position=2)]
[AllowEmptyString()]
[string]$DevEnvironment = "Local"
[string]$DevEnvironment = "Local",
[ValidateSet("stable", "preview", "private")]
[string]$Channel = "stable",
[string]$SourceCommit = $env:BUILD_SOURCEVERSION,
[string]$BuildNumber = $env:BUILD_BUILDNUMBER,
[string]$BuildDate = $env:VERSIONDATE,
[string]$DailyVersionSequence = $env:DAILYVERSIONSEQUENCE
)
Write-Host $PSScriptRoot
$versionRegex = "(\d+)\.(\d+)\.(\d+)"
if($versionNumber -match $versionRegEx)
{
#$buildDayOfYear = (Get-Date).DayofYear;
#$buildTime = Get-Date -Format HH;
#$buildTime = Get-Date -Format HHmmss;
#$buildYear = Get-Date -Format yy;
#$revision = [string]::Format("{0}{1}{2}", $buildYear, $buildDayOfYear, $buildTime )
function Get-NormalizedVersion {
param(
[Parameter(Mandatory = $true)]
[string]$InputVersion,
[Parameter(Mandatory = $true)]
[string]$ReleaseChannel,
[string]$PipelineBuildNumber,
[string]$PipelineBuildDate,
[string]$PipelineDailyVersionSequence
)
# max UInt16, 65535
#$revision = [string]::Format("{0}{1}", $buildDayOfYear, $buildTime )
#Write-Host "Revision" $revision
if ($InputVersion -match "^(?<numeric>\d+\.\d+(?:\.\d+){0,2})-(?<suffix>preview)$") {
if ($ReleaseChannel -ne "preview") {
throw "Version suffix '-preview' can only be used with the preview release channel"
}
$versionNumber = [int]::Parse($matches[1]).ToString() + "." + [int]::Parse($matches[2]).ToString() + "." + [int]::Parse($matches[3]).ToString() # + "." + $revision
Write-Host "Version Number" $versionNumber
$InputVersion = $matches["numeric"]
}
if ($ReleaseChannel -eq "preview" -and $InputVersion -match "^(\d+)\.(\d+)$") {
$metadata = & (Join-Path $PSScriptRoot "resolveBuildMetadata.ps1") `
-VersionOverride $InputVersion `
-SourceBranch "refs/heads/main" `
-BuildReason "Manual" `
-BuildNumber $PipelineBuildNumber `
-BuildDate $PipelineBuildDate `
-DailyVersionSequence $PipelineDailyVersionSequence
return $metadata.Version
}
if ($InputVersion -match "^(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?$") {
$versionParts = @([int]::Parse($matches[1]), [int]::Parse($matches[2]), [int]::Parse($matches[3]))
if ($matches[4]) {
$versionParts += [int]::Parse($matches[4])
}
return $versionParts -join "."
}
throw "Build format does not match the expected pattern (w.x, w.x.y, w.x.y.z, or w.x.y.z-preview for preview channel)"
}
else
{
throw "Build format does not match the expected pattern (buildName_w.x.y.z)"
$versionNumber = Get-NormalizedVersion `
-InputVersion $versionNumber `
-ReleaseChannel $Channel `
-PipelineBuildNumber $BuildNumber `
-PipelineBuildDate $BuildDate `
-PipelineDailyVersionSequence $DailyVersionSequence
foreach ($part in ($versionNumber -split '\.')) {
$value = [int]::Parse($part)
if ($value -lt 0 -or $value -gt [UInt16]::MaxValue) {
throw "Version component '$value' is outside the supported Windows version range 0-65535"
}
}
Write-Host "Version Number" $versionNumber
$verPropWriteFileLocation = $PSScriptRoot + '/../src/Version.props';
$verPropReadFileLocation = $verPropWriteFileLocation;
[XML]$verProps = Get-Content $verPropReadFileLocation
$verProps.Project.PropertyGroup.Version = $versionNumber;
$verProps.Project.PropertyGroup.VersionChannel = $Channel;
$verProps.Project.PropertyGroup.SourceCommit = if ([string]::IsNullOrWhiteSpace($SourceCommit)) { "" } else { $SourceCommit };
$verProps.Project.PropertyGroup.DevEnvironment = $DevEnvironment;
Write-Host "xml" $verProps.Project.PropertyGroup.Version
@@ -51,12 +99,14 @@ Write-Host "xml" $verProps.Project.PropertyGroup.Version
$verProps.Save($verPropWriteFileLocation);
#######
$manifestVersionNumber = if (($versionNumber.ToCharArray() | Where-Object { $_ -eq '.' }).Count -eq 2) { $versionNumber + '.0' } else { $versionNumber }
# Set PowerRenameContextMenu package version in AppManifest.xml
$powerRenameContextMenuAppManifestWriteFileLocation = $PSScriptRoot + '/../src/modules/powerrename/PowerRenameContextMenu/AppxManifest.xml';
$powerRenameContextMenuAppManifestReadFileLocation = $powerRenameContextMenuAppManifestWriteFileLocation;
[XML]$powerRenameContextMenuAppManifest = Get-Content $powerRenameContextMenuAppManifestReadFileLocation
$powerRenameContextMenuAppManifest.Package.Identity.Version = $versionNumber + '.0'
$powerRenameContextMenuAppManifest.Package.Identity.Version = $manifestVersionNumber
Write-Host "PowerRenameContextMenu version" $powerRenameContextMenuAppManifest.Package.Identity.Version
$powerRenameContextMenuAppManifest.Save($powerRenameContextMenuAppManifestWriteFileLocation);
@@ -65,7 +115,7 @@ $imageResizerContextMenuAppManifestWriteFileLocation = $PSScriptRoot + '/../src/
$imageResizerContextMenuAppManifestReadFileLocation = $imageResizerContextMenuAppManifestWriteFileLocation;
[XML]$imageResizerContextMenuAppManifest = Get-Content $imageResizerContextMenuAppManifestReadFileLocation
$imageResizerContextMenuAppManifest.Package.Identity.Version = $versionNumber + '.0'
$imageResizerContextMenuAppManifest.Package.Identity.Version = $manifestVersionNumber
Write-Host "ImageResizerContextMenu version" $imageResizerContextMenuAppManifest.Package.Identity.Version
$imageResizerContextMenuAppManifest.Save($imageResizerContextMenuAppManifestWriteFileLocation);
@@ -74,7 +124,7 @@ $fileLocksmithContextMenuAppManifestWriteFileLocation = $PSScriptRoot + '/../src
$fileLocksmithContextMenuAppManifestReadFileLocation = $fileLocksmithContextMenuAppManifestWriteFileLocation;
[XML]$fileLocksmithContextMenuAppManifest = Get-Content $fileLocksmithContextMenuAppManifestReadFileLocation
$fileLocksmithContextMenuAppManifest.Package.Identity.Version = $versionNumber + '.0'
$fileLocksmithContextMenuAppManifest.Package.Identity.Version = $manifestVersionNumber
Write-Host "FileLocksmithContextMenu version" $fileLocksmithContextMenuAppManifest.Package.Identity.Version
$fileLocksmithContextMenuAppManifest.Save($fileLocksmithContextMenuAppManifestWriteFileLocation);
@@ -83,6 +133,6 @@ $newPlusContextMenuAppManifestWriteFileLocation = $PSScriptRoot + '/../src/modul
$newPlusContextMenuAppManifestReadFileLocation = $newPlusContextMenuAppManifestWriteFileLocation;
[XML]$newPlusContextMenuAppManifest = Get-Content $newPlusContextMenuAppManifestReadFileLocation
$newPlusContextMenuAppManifest.Package.Identity.Version = $versionNumber + '.0'
$newPlusContextMenuAppManifest.Package.Identity.Version = $manifestVersionNumber
Write-Host "NewPlusContextMenu version" $newPlusContextMenuAppManifest.Package.Identity.Version
$newPlusContextMenuAppManifest.Save($newPlusContextMenuAppManifestWriteFileLocation);

View File

@@ -18,7 +18,7 @@
<AnalysisMode>Recommended</AnalysisMode>
<_SkipUpgradeNetAnalyzersNuGetWarning>true</_SkipUpgradeNetAnalyzersNuGetWarning>
<NuGetAuditMode>direct</NuGetAuditMode>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <!-- Don't add source revision hash to the product version of binaries. -->
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <!-- Source commit is exposed explicitly via Version.props/version_gen.h. -->
<PlatformTarget>$(Platform)</PlatformTarget>
<RestoreEnablePackagePruning Condition=" '$(VisualStudioVersion)' == '17.0'">false </RestoreEnablePackagePruning>
@@ -74,7 +74,9 @@
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<Version>$(Version).0</Version>
<VersionBuildSuffix Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(Version)', '^\d+\.\d+\.\d+$'))">.0</VersionBuildSuffix>
<VersionBuildSuffix Condition="'$(VersionBuildSuffix)' == ''"></VersionBuildSuffix>
<Version>$(Version)$(VersionBuildSuffix)</Version>
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
<RepositoryType>GitHub</RepositoryType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@@ -83,11 +83,11 @@
<Directory Id="WindowsPowerShellFolder" Name="WindowsPowerShell">
<Directory Id="PowerShellModulesFolder" Name="Modules">
<Directory Id="PowerToysDscFolder" Name="Microsoft.PowerToys.Configure">
<Directory Id="PowerToysDscVerFolder" Name="$(var.Version).0">
<Directory Id="PowerToysDscVerFolder" Name="$(var.DscModuleVersion)">
<Component Id="PowerToysDSC" Guid="C52AECA0-DA73-49B8-BB49-31EF6640FF1F" Bitness="always64">
<!-- Don't fail installation because of DSC. Files are marked as not vital. -->
<File Vital="no" Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psd1" Id="PTConf.psd1" />
<File Vital="no" Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psm1" Id="PTConf.psm1" />
<File Vital="no" Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.DscModuleVersion)\Microsoft.PowerToys.Configure.psd1" Id="PTConf.psd1" />
<File Vital="no" Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.DscModuleVersion)\Microsoft.PowerToys.Configure.psm1" Id="PTConf.psm1" />
</Component>
</Directory>
</Directory>

View File

@@ -12,8 +12,8 @@
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="DSCModulesReference" Value="" KeyPath="yes" />
</RegistryKey>
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psd1" Id="PTConfReference.psd1" />
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psm1" Id="PTConfReference.psm1" />
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.DscModuleVersion)\Microsoft.PowerToys.Configure.psd1" Id="PTConfReference.psd1" />
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.DscModuleVersion)\Microsoft.PowerToys.Configure.psm1" Id="PTConfReference.psm1" />
</Component>
<!-- DSC v3 JSON manifest files - Generated by generateAllFileComponents.ps1 -->

View File

@@ -3,8 +3,13 @@
<PropertyGroup>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<PropertyGroup>
<DscModuleVersionSuffix Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(Version)', '^\d+\.\d+\.\d+$'))">.0</DscModuleVersionSuffix>
<DscModuleVersionSuffix Condition="'$(DscModuleVersionSuffix)' == ''"></DscModuleVersionSuffix>
<DscModuleVersion>$(Version)$(DscModuleVersionSuffix)</DscModuleVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' == 'x64'">
<DefineConstants>Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)</DefineConstants> <!-- THIS IS AN INNER LOOP OPTIMIZATION
<DefineConstants>Version=$(Version);DscModuleVersion=$(DscModuleVersion);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)</DefineConstants> <!-- THIS IS AN INNER LOOP OPTIMIZATION
The build pipeline builds the Settings and Launcher projects for Publication
using a specific profile. If you're doing local installer builds, this will
simulate the build pipeline doing that for you. -->
@@ -17,7 +22,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' != 'x64'">
<DefineConstants>Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\ARM64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)</DefineConstants>
<DefineConstants>Version=$(Version);DscModuleVersion=$(DscModuleVersion);MonacoSRCHarvestPath=$(ProjectDir)..\..\ARM64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)</DefineConstants>
<PreBuildEvent>IF NOT DEFINED IsPipeline (
call "$([MSBuild]::GetVsInstallRoot())\Common7\Tools\VsDevCmd.bat" -arch=arm64 -host_arch=amd64 -winsdk=10.0.19041.0 -vcvars_ver=$(VCToolsVersion)
SET PTRoot=$(SolutionDir)\..

View File

@@ -146,7 +146,8 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
return std::nullopt;
}
const auto new_version_info = std::move(get_github_version_info_async()).get();
const bool include_prerelease_updates = PTSettingsHelper::load_general_settings().GetNamedBoolean(L"include_prerelease_updates", false);
const auto new_version_info = std::move(get_github_version_info_async(include_prerelease_updates)).get();
// Check for error BEFORE dereferencing — the old code crashed here
// when GitHub API was unreachable (new_version_info held an error string).

View File

@@ -2,6 +2,12 @@
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Version>0.0.1</Version>
<VersionChannel>private</VersionChannel>
<!-- Update once when main moves to the next stable release train. -->
<ReleaseTrainVersion>0.100</ReleaseTrainVersion>
<!-- Y in YDDDB counts calendar years from this January 1. Reset it on the first release-train minor change of a new year. -->
<ReleaseTrainEpoch>2026-01-01</ReleaseTrainEpoch>
<SourceCommit></SourceCommit>
<DevEnvironment>Local</DevEnvironment>
<!-- Forcing for every DLL on by default -->

View File

@@ -180,6 +180,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getDisableAutomaticUpdateDownloadValue());
}
GpoRuleConfigured GPOWrapper::GetDisablePreviewUpdatesValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getDisablePreviewUpdatesValue());
}
GpoRuleConfigured GPOWrapper::GetDisableShowWhatsNewAfterUpdatesValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getDisableShowWhatsNewAfterUpdatesValue());

View File

@@ -51,6 +51,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredPeekEnabledValue();
static GpoRuleConfigured GetDisableNewUpdateToastValue();
static GpoRuleConfigured GetDisableAutomaticUpdateDownloadValue();
static GpoRuleConfigured GetDisablePreviewUpdatesValue();
static GpoRuleConfigured GetDisableShowWhatsNewAfterUpdatesValue();
static GpoRuleConfigured GetAllowExperimentationValue();
static GpoRuleConfigured GetRunPluginEnabledValue(winrt::hstring const& pluginID);

View File

@@ -55,6 +55,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredPeekEnabledValue();
static GpoRuleConfigured GetDisableNewUpdateToastValue();
static GpoRuleConfigured GetDisableAutomaticUpdateDownloadValue();
static GpoRuleConfigured GetDisablePreviewUpdatesValue();
static GpoRuleConfigured GetDisableShowWhatsNewAfterUpdatesValue();
static GpoRuleConfigured GetAllowExperimentationValue();
static GpoRuleConfigured GetRunPluginEnabledValue(String pluginID);

View File

@@ -29,6 +29,7 @@ namespace UnitTestsVersionHelper
Assert::AreEqual(MAJOR_VERSION_0, sut.major);
Assert::AreEqual(MINOR_VERSION_12, sut.minor);
Assert::AreEqual(REVISION_VERSION_0, sut.revision);
Assert::AreEqual(0ull, sut.build);
}
TEST_METHOD (integerConstructorShouldProperlyInitializationWithDifferentVersionNumbers)
{
@@ -40,6 +41,16 @@ namespace UnitTestsVersionHelper
Assert::AreEqual(testcaseMajor, sut.major);
Assert::AreEqual(testcaseMinor, sut.minor);
Assert::AreEqual(testcaseRevision, sut.revision);
Assert::AreEqual(0ull, sut.build);
}
TEST_METHOD (integerConstructorShouldProperlyInitializeBuildVersionNumber)
{
VersionHelper sut(0, 100, 2607, 8001);
Assert::AreEqual(0ull, sut.major);
Assert::AreEqual(100ull, sut.minor);
Assert::AreEqual(2607ull, sut.revision);
Assert::AreEqual(8001ull, sut.build);
}
TEST_METHOD (stringConstructorShouldProperlyInitializationVersionNumbers)
{
@@ -48,6 +59,25 @@ namespace UnitTestsVersionHelper
Assert::AreEqual(0ull, sut->major);
Assert::AreEqual(12ull, sut->minor);
Assert::AreEqual(3ull, sut->revision);
Assert::AreEqual(0ull, sut->build);
}
TEST_METHOD (stringConstructorShouldProperlyInitializationFourPartVersionNumbers)
{
auto sut = VersionHelper::fromString("v0.100.2607.08001");
Assert::IsTrue(sut.has_value());
Assert::AreEqual(0ull, sut->major);
Assert::AreEqual(100ull, sut->minor);
Assert::AreEqual(2607ull, sut->revision);
Assert::AreEqual(8001ull, sut->build);
}
TEST_METHOD (stringConstructorShouldIgnorePreviewSuffix)
{
auto sut = VersionHelper::fromString("v0.100.2607.08001-preview");
Assert::IsTrue(sut.has_value());
Assert::AreEqual(0ull, sut->major);
Assert::AreEqual(100ull, sut->minor);
Assert::AreEqual(2607ull, sut->revision);
Assert::AreEqual(8001ull, sut->build);
}
TEST_METHOD (stringConstructorShouldProperlyInitializationWithDifferentVersionNumbers)
{
@@ -121,6 +151,12 @@ namespace UnitTestsVersionHelper
Assert::IsFalse(sut.has_value());
}
TEST_METHOD (tooManyVersionPartsNotAccepted)
{
auto sut = VersionHelper::fromString(L"v1.2.3.4.5");
Assert::IsFalse(sut.has_value());
}
TEST_METHOD (parsedWithoutLeadingV)
{
VersionHelper expected{ 12ull, 13ull, 111ull };
@@ -129,6 +165,14 @@ namespace UnitTestsVersionHelper
Assert::IsTrue(actual.has_value());
Assert::AreEqual(*actual, expected);
}
TEST_METHOD (parsedFourPartVersionWithoutLeadingV)
{
VersionHelper expected{ 12ull, 13ull, 111ull, 5ull };
auto actual = VersionHelper::fromString(L"12.13.111.5");
Assert::IsTrue(actual.has_value());
Assert::AreEqual(*actual, expected);
}
TEST_METHOD (whenMajorVersionIsGreaterComparisonOperatorShouldReturnProperValue)
{
VersionHelper lhs(MAJOR_VERSION_0 + 1, MINOR_VERSION_12, REVISION_VERSION_0);
@@ -179,5 +223,24 @@ namespace UnitTestsVersionHelper
Assert::IsFalse(lhs > rhs);
}
TEST_METHOD (whenMajorMinorAndRevisionAreEqualComparisonOperatorShouldCompareBuildValue)
{
VersionHelper lhs(MAJOR_VERSION_0, MINOR_VERSION_12, REVISION_VERSION_0, 1);
VersionHelper rhs(MAJOR_VERSION_0, MINOR_VERSION_12, REVISION_VERSION_0);
Assert::IsTrue(lhs > rhs);
}
TEST_METHOD (zeroBuildVersionShouldFormatAsThreePartVersion)
{
VersionHelper sut(MAJOR_VERSION_0, MINOR_VERSION_12, REVISION_VERSION_0);
Assert::AreEqual(std::wstring(L"v0.12.0"), sut.toWstring());
}
TEST_METHOD (nonZeroBuildVersionShouldFormatAsFourPartVersion)
{
VersionHelper sut(MAJOR_VERSION_0, MINOR_VERSION_12, REVISION_VERSION_0, 1);
Assert::AreEqual(std::wstring(L"v0.12.0.1"), sut.toWstring());
}
};
}

View File

@@ -9,4 +9,14 @@ namespace winrt::PowerToys::Interop::implementation
{
return hstring{ get_product_version() };
}
hstring CommonManaged::GetProductVersionChannel()
{
return hstring{ get_product_version_channel() };
}
hstring CommonManaged::GetProductVersionSourceCommit()
{
return hstring{ get_product_version_source_commit() };
}
}

View File

@@ -8,6 +8,8 @@ namespace winrt::PowerToys::Interop::implementation
CommonManaged() = default;
static hstring GetProductVersion();
static hstring GetProductVersionChannel();
static hstring GetProductVersionSourceCommit();
};
}
namespace winrt::PowerToys::Interop::factory_implementation

View File

@@ -4,6 +4,8 @@ namespace PowerToys
{
[default_interface] static runtimeclass CommonManaged {
static String GetProductVersion();
static String GetProductVersionChannel();
static String GetProductVersionSourceCommit();
}
}
}

View File

@@ -41,11 +41,11 @@ namespace updating
try
{
auto packages = package_manager.FindPackagesForUser({}, MSIX_PACKAGE_NAME, MSIX_PACKAGE_PUBLISHER);
VersionHelper current_version(VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION);
VersionHelper current_version(VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, VERSION_BUILD);
for (auto package : packages)
{
VersionHelper msix_version(package.Id().Version().Major, package.Id().Version().Minor, package.Id().Version().Revision);
VersionHelper msix_version(package.Id().Version().Major, package.Id().Version().Minor, package.Id().Version().Build, package.Id().Version().Revision);
if (msix_version < current_version)
{

View File

@@ -11,7 +11,7 @@ namespace
{
const wchar_t PERSISTENT_STATE_FILENAME[] = L"\\UpdateState.json";
const wchar_t UPDATE_STATE_MUTEX[] = L"Local\\PowerToysRunnerUpdateStateMutex";
const VersionHelper CURRENT_VERSION(VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION);
const VersionHelper CURRENT_VERSION(VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, VERSION_BUILD);
}
UpdateState deserialize(const json::JsonObject& json)
@@ -22,6 +22,7 @@ UpdateState deserialize(const json::JsonObject& json)
result.releasePageUrl = json.GetNamedString(L"releasePageUrl", L"");
result.githubUpdateLastCheckedDate = timeutil::from_string(json.GetNamedString(L"githubUpdateLastCheckedDate", L"invalid").c_str());
result.downloadedInstallerFilename = json.GetNamedString(L"downloadedInstallerFilename", L"");
result.isPrerelease = json.GetNamedBoolean(L"isPrerelease", false);
return result;
}
@@ -36,6 +37,7 @@ json::JsonObject serialize(const UpdateState& state)
json.SetNamedValue(L"releasePageUrl", json::value(state.releasePageUrl));
json.SetNamedValue(L"state", json::value(static_cast<double>(state.state)));
json.SetNamedValue(L"downloadedInstallerFilename", json::value(state.downloadedInstallerFilename));
json.SetNamedValue(L"isPrerelease", json::value(state.isPrerelease));
json.SetNamedValue(L"updateStateFileVersion", json::value(CURRENT_VERSION.toWstring()));

View File

@@ -18,6 +18,7 @@ struct UpdateState
std::wstring releasePageUrl;
std::optional<std::time_t> githubUpdateLastCheckedDate;
std::wstring downloadedInstallerFilename;
bool isPrerelease = false;
// To prevent concurrent modification of the file, we enforce this interface, which locks the file while
// the state_modifier is active.

View File

@@ -16,7 +16,7 @@ using namespace registry::install_scope;
namespace // Strings in this namespace should not be localized
{
const wchar_t LATEST_RELEASE_ENDPOINT[] = L"https://api.github.com/repos/microsoft/PowerToys/releases/latest";
const wchar_t ALL_RELEASES_ENDPOINT[] = L"https://api.github.com/repos/microsoft/PowerToys/releases";
const wchar_t ALL_RELEASES_ENDPOINT[] = L"https://api.github.com/repos/microsoft/PowerToys/releases?per_page=100";
const wchar_t LOCAL_BUILD_ERROR[] = L"Local build cannot be updated";
const wchar_t NETWORK_ERROR[] = L"Network error";
@@ -82,7 +82,7 @@ namespace updating
// prevent the warning that may show up depend on the value of the constants (#defines)
#pragma warning(push)
#pragma warning(disable : 4702)
wil::task<github_version_result> get_github_version_info_async(const bool prerelease)
wil::task<github_version_result> get_github_version_info_async(const bool include_prerelease)
{
// If the current version starts with 0.0.*, it means we're on a local build from a farm and shouldn't check for updates.
if constexpr (VERSION_MAJOR == 0 && VERSION_MINOR == 0)
@@ -94,18 +94,17 @@ namespace updating
{
http::HttpClient client;
json::JsonObject release_object;
const VersionHelper current_version(VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION);
const VersionHelper current_version(VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, VERSION_BUILD);
VersionHelper github_version = current_version;
if (prerelease)
if (include_prerelease)
{
const auto body = co_await client.request(Uri{ ALL_RELEASES_ENDPOINT });
for (const auto& json : json::JsonValue::Parse(body).GetArray())
{
auto potential_release_object = json.GetObjectW();
const bool is_prerelease = potential_release_object.GetNamedBoolean(L"prerelease", false);
auto extracted_version = extract_version_from_release_object(potential_release_object);
if (!is_prerelease || !extracted_version || *extracted_version <= github_version)
if (!extracted_version || *extracted_version <= github_version)
{
continue;
}
@@ -134,7 +133,8 @@ namespace updating
co_return new_version_download_info{ extract_release_page_url(release_object),
std::move(github_version),
std::move(installer_download_url),
std::move(installer_filename) };
std::move(installer_filename),
release_object.GetNamedBoolean(L"prerelease", false) };
}
catch (...)
{

View File

@@ -19,14 +19,15 @@ namespace updating
struct new_version_download_info
{
Uri release_page_uri = nullptr;
VersionHelper version{ 0, 0, 0 };
VersionHelper version{ 0, 0, 0, 0 };
Uri installer_download_url = nullptr;
std::wstring installer_filename;
bool is_prerelease = false;
};
using github_version_info = std::variant<new_version_download_info, version_up_to_date>;
using github_version_result = std::expected<github_version_info, std::wstring>;
wil::task<github_version_result> get_github_version_info_async(bool prerelease = false);
wil::task<github_version_result> get_github_version_info_async(bool include_prerelease = false);
wil::task<std::optional<std::filesystem::path>> download_new_version_async(new_version_download_info new_version);
std::filesystem::path get_pending_updates_path();
void cleanup_updates();

View File

@@ -80,6 +80,7 @@ namespace powertoys_gpo
const std::wstring POLICY_SUSPEND_NEW_UPDATE_TOAST = L"SuspendNewUpdateAvailableToast";
const std::wstring POLICY_DISABLE_NEW_UPDATE_TOAST = L"DisableNewUpdateAvailableToast";
const std::wstring POLICY_DISABLE_SHOW_WHATS_NEW_AFTER_UPDATES = L"DoNotShowWhatsNewAfterUpdates";
const std::wstring POLICY_DISABLE_PREVIEW_UPDATES = L"PreviewUpdatesDisabled";
// The registry value names for other PowerToys policies.
const std::wstring POLICY_ALLOW_EXPERIMENTATION = L"AllowExperimentation";
@@ -548,6 +549,11 @@ namespace powertoys_gpo
return getConfiguredValue(POLICY_DISABLE_SHOW_WHATS_NEW_AFTER_UPDATES);
}
inline gpo_rule_configured_t getDisablePreviewUpdatesValue()
{
return getConfiguredValue(POLICY_DISABLE_PREVIEW_UPDATES);
}
inline gpo_rule_configured_t getAllowExperimentationValue()
{
return getConfiguredValue(POLICY_ALLOW_EXPERIMENTATION);

View File

@@ -182,7 +182,7 @@ namespace package
if (packageFullName.contains(packageDisplayName))
{
// If checkVersion is true, verify if the package has the same version as PowerToys.
if ((!checkVersion) || (packageVersion.Major == VERSION_MAJOR && packageVersion.Minor == VERSION_MINOR && packageVersion.Revision == VERSION_REVISION))
if ((!checkVersion) || (packageVersion.Major == VERSION_MAJOR && packageVersion.Minor == VERSION_MINOR && packageVersion.Build == VERSION_REVISION && packageVersion.Revision == VERSION_BUILD))
{
return { package };
}

View File

@@ -3,12 +3,12 @@
#include "../utils/string_utils.h"
#include <algorithm>
#include <sstream>
VersionHelper::VersionHelper(const size_t major, const size_t minor, const size_t revision) :
VersionHelper::VersionHelper(const size_t major, const size_t minor, const size_t revision, const size_t build) :
major{ major },
minor{ minor },
revision{ revision }
revision{ revision },
build{ build }
{
}
@@ -40,17 +40,42 @@ std::optional<VersionHelper> fromString(std::basic_string_view<CharT> str)
{
str = left_trim<CharT>(trim<CharT>(str), Constants<CharT>::LOWER_V);
str = left_trim<CharT>(trim<CharT>(str), Constants<CharT>::UPPER_V);
std::basic_string<CharT> spacedStr{ str };
replace_chars<CharT>(spacedStr, Constants<CharT>::DOT, Constants<CharT>::SPACE);
std::basic_istringstream<CharT> ss{ spacedStr };
VersionHelper result{ 0, 0, 0 };
ss >> result.major;
ss >> result.minor;
ss >> result.revision;
if (!ss.fail() && ss.eof())
if (const auto suffixPos = str.find(static_cast<CharT>('-')); suffixPos != std::basic_string_view<CharT>::npos)
{
return result;
str = str.substr(0, suffixPos);
}
size_t parts[4]{};
size_t partCount = 0;
size_t start = 0;
while (start <= str.size() && partCount < std::size(parts))
{
const auto dot = str.find(Constants<CharT>::DOT[0], start);
const auto end = dot == std::basic_string_view<CharT>::npos ? str.size() : dot;
const auto part = str.substr(start, end - start);
if (part.empty() || !std::all_of(part.begin(), part.end(), [](const CharT c) { return c >= static_cast<CharT>('0') && c <= static_cast<CharT>('9'); }))
{
return std::nullopt;
}
parts[partCount++] = static_cast<size_t>(std::stoull(std::basic_string<CharT>{ part }));
if (dot == std::basic_string_view<CharT>::npos)
{
start = str.size() + 1;
break;
}
start = dot + 1;
}
if (partCount == 3 && start > str.size())
{
return VersionHelper{ parts[0], parts[1], parts[2] };
}
if (partCount == 4 && start > str.size())
{
return VersionHelper{ parts[0], parts[1], parts[2], parts[3] };
}
}
catch (...)
@@ -77,6 +102,11 @@ std::wstring VersionHelper::toWstring() const
result += std::to_wstring(minor);
result += L'.';
result += std::to_wstring(revision);
if (build != 0)
{
result += L'.';
result += std::to_wstring(build);
}
return result;
}
@@ -88,5 +118,10 @@ std::string VersionHelper::toString() const
result += std::to_string(minor);
result += '.';
result += std::to_string(revision);
if (build != 0)
{
result += '.';
result += std::to_string(build);
}
return result;
}

View File

@@ -6,7 +6,7 @@
struct VersionHelper
{
VersionHelper(const size_t major, const size_t minor, const size_t revision);
VersionHelper(const size_t major, const size_t minor, const size_t revision, const size_t build = 0);
auto operator<=>(const VersionHelper&) const = default;
@@ -16,6 +16,7 @@ struct VersionHelper
size_t major;
size_t minor;
size_t revision;
size_t build;
std::wstring toWstring() const;
std::string toString() const;

View File

@@ -5,10 +5,10 @@
#include "Generated Files\version_gen.h"
#define FILE_VERSION VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, 0
#define FILE_VERSION VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, VERSION_BUILD
#define FILE_VERSION_STRING \
STRINGIZE(VERSION_MAJOR) \
"." STRINGIZE(VERSION_MINOR) "." STRINGIZE(VERSION_REVISION) ".0"
"." STRINGIZE(VERSION_MINOR) "." STRINGIZE(VERSION_REVISION) "." STRINGIZE(VERSION_BUILD)
#define PRODUCT_VERSION FILE_VERSION
#define PRODUCT_VERSION_STRING FILE_VERSION_STRING
@@ -29,18 +29,41 @@ const wchar_t* get_architecture_string(const version_architecture);
inline std::wstring get_product_version(bool includeV = true)
{
static std::wstring version = (includeV ? L"v" : L"") + std::to_wstring(VERSION_MAJOR) +
L"." + std::to_wstring(VERSION_MINOR) +
L"." + std::to_wstring(VERSION_REVISION);
std::wstring version = includeV ? L"v" : L"";
version += std::to_wstring(VERSION_MAJOR);
version += L".";
version += std::to_wstring(VERSION_MINOR);
version += L".";
version += std::to_wstring(VERSION_REVISION);
if constexpr (VERSION_BUILD != 0)
{
version += L".";
version += std::to_wstring(VERSION_BUILD);
}
return version;
}
inline std::wstring get_std_product_version(bool includeV = true)
{
static std::wstring version = (includeV ? L"v" : L"") + std::to_wstring(VERSION_MAJOR) +
L"." + std::to_wstring(VERSION_MINOR) +
L"." + std::to_wstring(VERSION_REVISION) + L".0";
std::wstring version = includeV ? L"v" : L"";
version += std::to_wstring(VERSION_MAJOR);
version += L".";
version += std::to_wstring(VERSION_MINOR);
version += L".";
version += std::to_wstring(VERSION_REVISION);
version += L".";
version += std::to_wstring(VERSION_BUILD);
return version;
}
inline std::wstring get_product_version_channel()
{
return VERSION_CHANNEL;
}
inline std::wstring get_product_version_source_commit()
{
return VERSION_SOURCE_COMMIT;
}

View File

@@ -8,11 +8,23 @@
BeforeTargets="PrepareForBuild"
Inputs="$(RepoRoot)src\Version.props"
Outputs="$(MSBuildProjectDirectory)\Generated Files\version_gen.h">
<PropertyGroup>
<VersionBuild>0</VersionBuild>
<VersionBuild Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(Version)', '^\d+\.\d+\.\d+\.\d+$'))">$([System.String]::Copy('$(Version)').Split('.')[3])</VersionBuild>
<VersionSourceCommit Condition="'$(SourceCommit)' != ''">$(SourceCommit)</VersionSourceCommit>
<VersionSourceCommit Condition="'$(VersionSourceCommit)' == '' and '$(BUILD_SOURCEVERSION)' != ''">$(BUILD_SOURCEVERSION)</VersionSourceCommit>
<VersionSourceCommit Condition="'$(VersionSourceCommit)' == '' and '$(GITHUB_SHA)' != ''">$(GITHUB_SHA)</VersionSourceCommit>
<VersionSourceCommit Condition="'$(VersionSourceCommit)' == ''">unknown</VersionSourceCommit>
<VersionChannel Condition="'$(VersionChannel)' == ''">private</VersionChannel>
</PropertyGroup>
<ItemGroup>
<HeaderLines Include="#pragma once" />
<HeaderLines Include="#define VERSION_MAJOR $(Version.Split('.')[0])" />
<HeaderLines Include="#define VERSION_MINOR $(Version.Split('.')[1])" />
<HeaderLines Include="#define VERSION_REVISION $(Version.Split('.')[2])" />
<HeaderLines Include="#define VERSION_BUILD $(VersionBuild)" />
<HeaderLines Include="#define VERSION_CHANNEL L&quot;$(VersionChannel)&quot;" />
<HeaderLines Include="#define VERSION_SOURCE_COMMIT L&quot;$(VersionSourceCommit)&quot;" />
</ItemGroup>
<WriteLinesToFile File="Generated Files\version_gen.h" Lines="@(HeaderLines)" Overwrite="true" Encoding="Unicode" WriteOnlyWhenDifferent="true" />
</Target>

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.20" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.21" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyNamespaces>
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
</policyNamespaces>
<resources minRequiredRevision="1.20"/><!-- Last changed with PowerToys v0.98.0 -->
<resources minRequiredRevision="1.21"/><!-- Last changed with PowerToys v0.100.0 -->
<supportedOn>
<definitions>
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
@@ -30,6 +30,7 @@
<definition name="SUPPORTED_POWERTOYS_0_97_0" displayName="$(string.SUPPORTED_POWERTOYS_0_97_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_98_0" displayName="$(string.SUPPORTED_POWERTOYS_0_98_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_99_0" displayName="$(string.SUPPORTED_POWERTOYS_0_99_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_100_0" displayName="$(string.SUPPORTED_POWERTOYS_0_100_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
</definitions>
</supportedOn>
@@ -561,6 +562,16 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="DisablePreviewUpdates" class="Both" displayName="$(string.DisablePreviewUpdates)" explainText="$(string.DisablePreviewUpdatesDescription)" key="Software\Policies\PowerToys" valueName="PreviewUpdatesDisabled">
<parentCategory ref="InstallerUpdates" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_100_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="SuspendNewUpdateToast" class="Both" displayName="$(string.SuspendNewUpdateToast)" explainText="$(string.SuspendNewUpdateToastDescription)" key="Software\Policies\PowerToys" valueName="SuspendNewUpdateAvailableToast">
<parentCategory ref="InstallerUpdates" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_68_0" />

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.20" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.21" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<displayName>PowerToys</displayName>
<description>PowerToys</description>
<resources>
@@ -37,6 +37,7 @@
<string id="SUPPORTED_POWERTOYS_0_97_0">PowerToys version 0.97.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_98_0">PowerToys version 0.98.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_99_0">PowerToys version 0.99.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_100_0">PowerToys version 0.100.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
@@ -78,6 +79,11 @@ If enabled, per-user installation is not allowed.
If disabled or not configured, per-user installation is allowed.
</string>
<string id="DisablePreviewUpdatesDescription">This policy configures whether users can opt in to preview (prerelease) build updates.
If you enable this policy, preview build updates are turned off: the per-user setting to include preview/prerelease updates is ignored and cannot be changed. Only stable updates are offered.
If you disable or do not configure this policy, users can choose whether to receive preview build updates. Stable updates are unaffected either way.</string>
<string id="DisableAutomaticUpdateDownloadDescription">This policy configures whether or not the automatic download and installation of available updates is disabled. (On metered connections updates are never downloaded.)
If enabled, automatic download and installation is disabled.
@@ -289,6 +295,7 @@ If you don't configure this policy, the user will be able to control the setting
<string id="ConfigureEnabledUtilityZoomIt">Zoom It: Configure enabled state</string>
<string id="DisablePerUserInstallation">Disable per-user installation</string>
<string id="DisableAutomaticUpdateDownload">Disable automatic downloads</string>
<string id="DisablePreviewUpdates">Disable preview build updates</string>
<string id="DoNotShowWhatsNewAfterUpdates">Do not show the release notes after updates</string>
<string id="SuspendNewUpdateToast">Suspend Action Center notification for new updates</string>
<string id="DisableNewUpdateToast">Disable Action Center notification for new updates</string>

View File

@@ -153,6 +153,9 @@
<data name="GITHUB_NEW_VERSION_AVAILABLE" xml:space="preserve">
<value>An update to PowerToys is available.</value>
</data>
<data name="GITHUB_NEW_PREVIEW_VERSION_AVAILABLE" xml:space="preserve">
<value>A PowerToys Preview update is available.</value>
</data>
<data name="GITHUB_NEW_VERSION_UPDATE_NOW" xml:space="preserve">
<value>Update now</value>
</data>

View File

@@ -28,24 +28,54 @@ namespace
// How many minor versions to suspend the toast notification (example: installed=0.60.0, suspend=2, next notification=0.63.*)
// Attention: When changing this value please update the ADML file to.
const int UPDATE_NOTIFICATION_TOAST_SUSPEND_MINOR_VERSION_COUNT = 2;
// The per-user "include prerelease updates" opt-in, additionally gated by the DisablePreviewUpdates
// group policy: when that policy is Enabled, preview (prerelease) updates are forced off regardless
// of the user's setting. Stable updates are unaffected.
bool effective_include_prerelease_updates()
{
if (powertoys_gpo::getDisablePreviewUpdatesValue() == powertoys_gpo::gpo_rule_configured_enabled)
{
return false;
}
return get_general_settings().includePrereleaseUpdates;
}
}
using namespace notifications;
using namespace updating;
std::wstring AvailableVersionToWstring(const new_version_download_info& info)
{
auto result = info.version.toWstring();
if (info.is_prerelease)
{
result += L"-preview";
}
return result;
}
std::wstring CurrentVersionToNextVersion(const new_version_download_info& info)
{
auto result = VersionHelper{ VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION }.toWstring();
result += L" \u2192 "; // Right arrow
result += info.version.toWstring();
result += AvailableVersionToWstring(info);
return result;
}
std::wstring UpdateAvailableMessage(const new_version_download_info& info)
{
return info.is_prerelease ?
GET_RESOURCE_STRING(IDS_GITHUB_NEW_PREVIEW_VERSION_AVAILABLE) :
GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_AVAILABLE);
}
void ShowNewVersionAvailable(const new_version_download_info& info)
{
remove_toasts_by_tag(UPDATING_PROCESS_TOAST_TAG);
toast_params toast_params{ UPDATING_PROCESS_TOAST_TAG, false };
std::wstring contents = GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_AVAILABLE);
std::wstring contents = UpdateAvailableMessage(info);
contents += L'\n';
contents += CurrentVersionToNextVersion(info);
@@ -60,7 +90,7 @@ void ShowNewVersionAvailable(const new_version_download_info& info)
L"powertoys://open_overview/");
}
void ShowOpenSettingsForUpdate()
void ShowOpenSettingsForUpdate(const new_version_download_info& info)
{
remove_toasts_by_tag(UPDATING_PROCESS_TOAST_TAG);
@@ -70,7 +100,10 @@ void ShowOpenSettingsForUpdate()
link_button{ GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_MORE_INFO),
L"powertoys://open_overview/" },
};
show_toast_with_activations(GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_AVAILABLE),
auto contents = UpdateAvailableMessage(info);
contents += L'\n';
contents += AvailableVersionToWstring(info);
show_toast_with_activations(std::move(contents),
GET_RESOURCE_STRING(IDS_TOAST_TITLE),
{},
std::move(actions),
@@ -130,12 +163,14 @@ void ProcessNewVersionInfo(const github_version_info& version_info,
state.state = UpdateState::upToDate;
state.releasePageUrl = {};
state.downloadedInstallerFilename = {};
state.isPrerelease = false;
Logger::trace(L"Version is up to date");
dispatch_run_on_main_ui_thread([](PVOID) { set_tray_icon_update_available(false); }, nullptr);
return;
}
const auto new_version_info = std::get<new_version_download_info>(version_info);
state.releasePageUrl = new_version_info.release_page_uri.ToString().c_str();
state.isPrerelease = new_version_info.is_prerelease;
Logger::trace(L"Discovered new version {}", new_version_info.version.toWstring());
const bool already_downloaded = state.state == UpdateState::readyToInstall && state.downloadedInstallerFilename == new_version_info.installer_filename;
@@ -203,7 +238,7 @@ void ProcessNewVersionInfo(const github_version_info& version_info,
dispatch_run_on_main_ui_thread([](PVOID) { set_tray_icon_update_available(true); }, nullptr);
if (show_notifications)
{
ShowOpenSettingsForUpdate();
ShowOpenSettingsForUpdate(new_version_info);
}
}
}
@@ -237,7 +272,7 @@ void PeriodicUpdateWorker()
bool version_info_obtained = false;
try
{
const auto new_version_info = std::move(get_github_version_info_async()).get();
const auto new_version_info = std::move(get_github_version_info_async(effective_include_prerelease_updates())).get();
if (new_version_info.has_value())
{
version_info_obtained = true;
@@ -277,7 +312,7 @@ void CheckForUpdatesCallback()
auto state = UpdateState::read();
try
{
auto new_version_info = std::move(get_github_version_info_async()).get();
auto new_version_info = std::move(get_github_version_info_async(effective_include_prerelease_updates())).get();
if (!new_version_info)
{
// We couldn't get a new version from github for some reason, log error

View File

@@ -71,6 +71,7 @@ static bool show_theme_adaptive_tray_icon = false;
static bool run_as_elevated = false;
static bool show_new_updates_toast_notification = true;
static bool download_updates_automatically = true;
static bool include_prerelease_updates = false;
static bool show_whats_new_after_updates = true;
static bool enable_experimentation = true;
static bool enable_warnings_elevated_apps = true;
@@ -105,6 +106,7 @@ json::JsonObject GeneralSettings::to_json()
result.SetNamedValue(L"run_elevated", json::value(isRunElevated));
result.SetNamedValue(L"show_new_updates_toast_notification", json::value(showNewUpdatesToastNotification));
result.SetNamedValue(L"download_updates_automatically", json::value(downloadUpdatesAutomatically));
result.SetNamedValue(L"include_prerelease_updates", json::value(includePrereleaseUpdates));
result.SetNamedValue(L"show_whats_new_after_updates", json::value(showWhatsNewAfterUpdates));
result.SetNamedValue(L"enable_experimentation", json::value(enableExperimentation));
result.SetNamedValue(L"dashboard_sort_order", json::value(static_cast<int>(dashboardSortOrder)));
@@ -133,6 +135,7 @@ json::JsonObject load_general_settings()
run_as_elevated = loaded.GetNamedBoolean(L"run_elevated", false);
show_new_updates_toast_notification = loaded.GetNamedBoolean(L"show_new_updates_toast_notification", true);
download_updates_automatically = loaded.GetNamedBoolean(L"download_updates_automatically", true) && check_user_is_admin();
include_prerelease_updates = loaded.GetNamedBoolean(L"include_prerelease_updates", false);
show_whats_new_after_updates = loaded.GetNamedBoolean(L"show_whats_new_after_updates", true);
enable_experimentation = loaded.GetNamedBoolean(L"enable_experimentation", true);
enable_warnings_elevated_apps = loaded.GetNamedBoolean(L"enable_warnings_elevated_apps", true);
@@ -172,6 +175,7 @@ GeneralSettings get_general_settings()
.quickAccessShortcut = quick_access_shortcut,
.showNewUpdatesToastNotification = show_new_updates_toast_notification,
.downloadUpdatesAutomatically = download_updates_automatically && is_user_admin,
.includePrereleaseUpdates = include_prerelease_updates,
.showWhatsNewAfterUpdates = show_whats_new_after_updates,
.enableExperimentation = enable_experimentation,
.dashboardSortOrder = dashboard_sort_order,
@@ -335,6 +339,7 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
show_new_updates_toast_notification = general_configs.GetNamedBoolean(L"show_new_updates_toast_notification", true);
download_updates_automatically = general_configs.GetNamedBoolean(L"download_updates_automatically", true);
include_prerelease_updates = general_configs.GetNamedBoolean(L"include_prerelease_updates", false);
show_whats_new_after_updates = general_configs.GetNamedBoolean(L"show_whats_new_after_updates", true);
enable_experimentation = general_configs.GetNamedBoolean(L"enable_experimentation", true);
@@ -585,4 +590,3 @@ void start_enabled_powertoys()
}
}

View File

@@ -24,6 +24,7 @@ struct GeneralSettings
PowerToysSettings::HotkeyObject quickAccessShortcut;
bool showNewUpdatesToastNotification;
bool downloadUpdatesAutomatically;
bool includePrereleaseUpdates;
bool showWhatsNewAfterUpdates;
bool enableExperimentation;
DashboardSortOrder dashboardSortOrder;

View File

@@ -111,6 +111,7 @@ std::optional<std::wstring> dispatch_json_action_to_module(const json::JsonObjec
}
else if (action == L"check_for_updates")
{
apply_general_settings(value);
bool expected_isUpdateCheckThreadRunning = false;
if (isUpdateCheckThreadRunning.compare_exchange_strong(expected_isUpdateCheckThreadRunning, true))
{

View File

@@ -49,6 +49,7 @@ void Trace::SettingsChanged(const GeneralSettings& settings)
TraceLoggingWideString(enabledModules.c_str(), "ModulesEnabled"),
TraceLoggingBoolean(settings.isRunElevated, "AlwaysRunElevated"),
TraceLoggingBoolean(settings.downloadUpdatesAutomatically, "DownloadUpdatesAutomatically"),
TraceLoggingBoolean(settings.includePrereleaseUpdates, "IncludePrereleaseUpdates"),
TraceLoggingBoolean(settings.enableExperimentation, "EnableExperimentation"),
TraceLoggingWideString(settings.theme.c_str(), "Theme"),
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),

View File

@@ -89,6 +89,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("download_updates_automatically")]
public bool AutoDownloadUpdates { get; set; }
[JsonPropertyName("include_prerelease_updates")]
public bool IncludePrereleaseUpdates { get; set; }
[JsonPropertyName("show_whats_new_after_updates")]
public bool ShowWhatsNewAfterUpdates { get; set; }
@@ -112,6 +115,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
IsElevated = false;
ShowNewUpdatesToastNotification = true;
AutoDownloadUpdates = true;
IncludePrereleaseUpdates = false;
EnableExperimentation = true;
DashboardSortOrder = DashboardSortOrder.Alphabetical;
Theme = "system";

View File

@@ -38,6 +38,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("downloadedInstallerFilename")]
public string DownloadedInstallerFilename { get; set; }
[JsonPropertyName("isPrerelease")]
public bool IsPrerelease { get; set; }
// Non-localizable strings: Files
public const string SettingsFilePath = "\\Microsoft\\PowerToys\\";
public const string SettingsFile = "UpdateState.json";

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Settings.UI.UnitTests
{
[TestClass]
public class ScoobeReleaseTests
{
[TestMethod]
public void CreateReleaseGroupsHidesPrereleasesByDefault()
{
var groups = ScoobeWindow.CreateReleaseGroups(CreateReleases(), false);
Assert.AreEqual(2, groups.Count);
Assert.IsTrue(groups.SelectMany(group => group).All(release => !release.IsPrerelease));
}
[TestMethod]
public void CreateReleaseGroupsSeparatesPrereleasesWhenEnabled()
{
var groups = ScoobeWindow.CreateReleaseGroups(CreateReleases(), true);
Assert.AreEqual(3, groups.Count);
Assert.IsTrue(groups[0].All(release => release.IsPrerelease));
Assert.IsTrue(groups.Skip(1).SelectMany(group => group).All(release => !release.IsPrerelease));
}
[TestMethod]
public void UpdatingSettingsReadsPrereleaseState()
{
var settings = JsonSerializer.Deserialize<UpdatingSettings>("""{"state":2,"isPrerelease":true}""");
Assert.IsNotNull(settings);
Assert.IsTrue(settings.IsPrerelease);
}
private static IList<PowerToysReleaseInfo> CreateReleases()
{
return
[
new PowerToysReleaseInfo
{
TagName = "v0.100.2607.27001-preview",
IsPrerelease = true,
PublishedDate = new DateTimeOffset(2026, 7, 27, 0, 0, 0, TimeSpan.Zero),
},
new PowerToysReleaseInfo
{
TagName = "v0.100.2",
PublishedDate = new DateTimeOffset(2026, 6, 26, 0, 0, 0, TimeSpan.Zero),
},
new PowerToysReleaseInfo
{
TagName = "v0.99.1",
PublishedDate = new DateTimeOffset(2026, 4, 15, 0, 0, 0, TimeSpan.Zero),
},
];
}
}
}

View File

@@ -84,6 +84,7 @@ namespace ViewModelTests
// Verify that the old settings persisted
Assert.AreEqual(originalGeneralSettings.AutoDownloadUpdates, viewModel.AutoDownloadUpdates);
Assert.AreEqual(originalGeneralSettings.IncludePrereleaseUpdates, viewModel.IncludePrereleaseUpdates);
Assert.AreEqual(originalGeneralSettings.PowertoysVersion, viewModel.PowerToysVersion);
Assert.AreEqual(originalGeneralSettings.RunElevated, viewModel.RunElevated);
Assert.AreEqual(originalGeneralSettings.Startup, viewModel.Startup);
@@ -93,6 +94,67 @@ namespace ViewModelTests
BackCompatTestProperties.VerifyGeneralSettingsIOProviderWasRead(fileMock, expectedCallCount);
}
[TestMethod]
public void IncludePrereleaseUpdatesShouldSendUpdatedSettingWhenSuccessful()
{
bool sawExpectedIpcPayload = false;
bool sawExpectedUpdateCheckPayload = false;
Func<string, int> sendMockIPCConfigMSG = msg =>
{
if (string.IsNullOrWhiteSpace(msg))
{
return 0;
}
OutGoingGeneralSettings snd = JsonSerializer.Deserialize<OutGoingGeneralSettings>(msg);
if (snd?.GeneralSettings is null)
{
return 0;
}
Assert.IsTrue(snd.GeneralSettings.IncludePrereleaseUpdates);
sawExpectedIpcPayload = true;
return 0;
};
Func<string, int> sendRestartAdminIPCMessage = msg => { return 0; };
Func<string, int> sendCheckForUpdatesIPCMessage = msg =>
{
if (string.IsNullOrWhiteSpace(msg))
{
return 0;
}
GeneralSettingsCustomAction action = JsonSerializer.Deserialize<GeneralSettingsCustomAction>(msg);
if (action?.GeneralSettingsAction?.GeneralSettings is null)
{
return 0;
}
Assert.IsTrue(action.GeneralSettingsAction.GeneralSettings.IncludePrereleaseUpdates);
Assert.AreEqual("check_for_updates", action.GeneralSettingsAction.GeneralSettings.CustomActionName);
sawExpectedUpdateCheckPayload = true;
return 0;
};
GeneralViewModel viewModel = new TestGeneralViewModel(
settingsRepository: SettingsRepository<GeneralSettings>.GetInstance(mockGeneralSettingsUtils.Object),
"GeneralSettings_RunningAsAdminText",
"GeneralSettings_RunningAsUserText",
false,
false,
sendMockIPCConfigMSG,
sendRestartAdminIPCMessage,
sendCheckForUpdatesIPCMessage,
GeneralSettingsFileName);
Assert.IsFalse(viewModel.IncludePrereleaseUpdates);
viewModel.IncludePrereleaseUpdates = true;
Assert.IsTrue(sawExpectedIpcPayload);
Assert.IsTrue(sawExpectedUpdateCheckPayload);
}
[TestMethod]
public void IsElevatedShouldUpdateRunasAdminStatusAttrsWhenSuccessful()
{

View File

@@ -25,5 +25,8 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
[JsonPropertyName("body")]
public string ReleaseNotes { get; set; }
[JsonPropertyName("prerelease")]
public bool IsPrerelease { get; set; }
}
}

View File

@@ -108,6 +108,10 @@
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Settings.UI.UnitTests" />
</ItemGroup>
<ItemGroup>
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
<ProjectReference Include="..\..\common\Common.Search\Common.Search.csproj" />

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.PowerToys.Settings.UI.Helpers;
@@ -30,6 +31,8 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
/// </summary>
public string DateText { get; }
public bool IsPreview { get; }
public ScoobeReleaseGroupViewModel(IList<PowerToysReleaseInfo> releases)
{
Releases = releases ?? throw new ArgumentNullException(nameof(releases));
@@ -37,8 +40,11 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
if (releases.Count > 0)
{
var latestRelease = releases[0];
IsPreview = releases.All(release => release.IsPrerelease);
VersionText = GetVersionFromRelease(latestRelease);
DateText = latestRelease.PublishedDate.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
DateText = IsPreview ?
ResourceLoaderInstance.ResourceLoader.GetString("ScoobeReleaseGroup_Preview") :
latestRelease.PublishedDate.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
}
else
{

View File

@@ -118,6 +118,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
isFirst = false;
if (release.IsPrerelease)
{
releaseNotesHtmlBuilder.AppendLine(CultureInfo.InvariantCulture, $"**{ResourceLoaderInstance.ResourceLoader.GetString("ScoobeReleaseNotes_PreviewBadge")}**");
releaseNotesHtmlBuilder.AppendLine();
}
var releaseUrl = string.Format(CultureInfo.InvariantCulture, GitHubReleaseLinkTemplate, release.TagName);
releaseNotesHtmlBuilder.AppendLine(CultureInfo.InvariantCulture, $"# {release.Name}");
string formattedDate = release.PublishedDate.ToString($"{CultureInfo.CurrentCulture.DateTimeFormat.MonthDayPattern}, yyyy", CultureInfo.CurrentCulture);

View File

@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Views;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.UI.Xaml;
@@ -110,7 +111,7 @@ namespace Microsoft.PowerToys.Settings.UI
try
{
var releases = await FetchReleasesFromGitHubAsync();
ReleaseGroups = GroupReleasesByMajorMinor(releases);
ReleaseGroups = CreateReleaseGroups(releases, ShouldShowPrereleases());
PopulateNavigationItems();
}
catch (Exception ex)
@@ -137,7 +138,7 @@ namespace Microsoft.PowerToys.Settings.UI
using var httpClient = new HttpClient(proxyClientHandler);
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
string json = await httpClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases?per_page=20");
string json = await httpClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases?per_page=100");
var allReleases = JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(json, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo);
if (allReleases is null || allReleases.Count == 0)
@@ -150,12 +151,45 @@ namespace Microsoft.PowerToys.Settings.UI
.ToList();
}
private static IList<IList<PowerToysReleaseInfo>> GroupReleasesByMajorMinor(IList<PowerToysReleaseInfo> releases)
internal static IList<IList<PowerToysReleaseInfo>> CreateReleaseGroups(IList<PowerToysReleaseInfo> releases, bool showPrereleases)
{
return releases
var groups = new List<IList<PowerToysReleaseInfo>>();
if (showPrereleases)
{
var previewReleases = releases
.Where(r => r.IsPrerelease)
.OrderByDescending(r => r.PublishedDate)
.Take(10)
.ToList();
if (previewReleases.Count > 0)
{
groups.Add(previewReleases);
}
}
var stableGroups = releases
.Where(r => !r.IsPrerelease)
.OrderByDescending(r => r.PublishedDate)
.Take(20)
.GroupBy(GetMajorMinorVersion)
.Select(g => g.OrderByDescending(r => r.PublishedDate).ToList() as IList<PowerToysReleaseInfo>)
.ToList();
foreach (var stableGroup in stableGroups)
{
groups.Add(stableGroup);
}
return groups;
}
private static bool ShouldShowPrereleases()
{
var generalSettings = SettingsRepository<GeneralSettings>.GetInstance(SettingsUtils.Default).SettingsConfig;
bool isPreviewBuild = string.Equals(
global::PowerToys.Interop.CommonManaged.GetProductVersionChannel(),
"preview",
StringComparison.OrdinalIgnoreCase);
return generalSettings.IncludePrereleaseUpdates || isPreviewBuild;
}
private static string GetMajorMinorVersion(PowerToysReleaseInfo release)

View File

@@ -22,7 +22,22 @@
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<controls:SettingsGroup x:Uid="General_VersionAndUpdate" Margin="0,-32,0,0">
<tkcontrols:SettingsExpander Header="{x:Bind ViewModel.PowerToysVersion, Mode=OneWay}" HeaderIcon="{ui:FontIcon Glyph=&#xE895;}">
<tkcontrols:SettingsExpander HeaderIcon="{ui:FontIcon Glyph=&#xE895;}">
<tkcontrols:SettingsExpander.Header>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.PowerToysVersion, Mode=OneWay}" />
<Border
Padding="6,2"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="4"
Visibility="{x:Bind ViewModel.IsCurrentVersionPreview, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock
x:Uid="General_PreviewBadge"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}" />
</Border>
</StackPanel>
</tkcontrols:SettingsExpander.Header>
<tkcontrols:SettingsExpander.Description>
<StackPanel Orientation="Vertical">
<TextBlock Style="{StaticResource SecondaryTextStyle}">
@@ -77,6 +92,9 @@
Visibility="{x:Bind ViewModel.IsAdmin, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="GeneralPage_AutoDownloadAndInstallUpdates" IsChecked="{Binding Mode=TwoWay, Path=AutoDownloadUpdates}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsIncludePrereleaseUpdatesCardEnabled}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="GeneralPage_IncludePrereleaseUpdates" IsChecked="{Binding Mode=TwoWay, Path=IncludePrereleaseUpdates}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsShowNewUpdatesToastNotificationCardEnabled}">
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="GeneralPage_ShowNewUpdatesToast" IsChecked="{Binding Mode=TwoWay, Path=ShowNewUpdatesToastNotification}" />
</tkcontrols:SettingsCard>
@@ -104,7 +122,7 @@
<!-- New version available -->
<InfoBar
x:Uid="General_NewVersionAvailable"
Title="{x:Bind ViewModel.NewVersionAvailableTitle, Mode=OneWay}"
IsClosable="False"
IsOpen="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToDownload}"
IsTabStop="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToDownload}"
@@ -113,6 +131,17 @@
<InfoBar.Content>
<StackPanel Spacing="16">
<Border
Padding="6,2"
HorizontalAlignment="Left"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="4"
Visibility="{x:Bind ViewModel.IsPrereleaseUpdate, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock
x:Uid="General_PreviewBadge"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}" />
</Border>
<Button
x:Uid="General_DownloadAndInstall"
Margin="0,0,0,16"
@@ -145,19 +174,32 @@
<!-- Ready to install -->
<InfoBar
x:Uid="General_NewVersionReadyToInstall"
Title="{x:Bind ViewModel.NewVersionReadyToInstallTitle, Mode=OneWay}"
IsClosable="False"
IsOpen="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToInstall}"
IsTabStop="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToInstall}"
Message="{x:Bind ViewModel.PowerToysNewAvailableVersion, Mode=OneWay}"
Severity="Warning">
<InfoBar.Content>
<Button
x:Uid="General_InstallNow"
Margin="0,0,0,16"
Command="{Binding UpdateNowButtonEventHandler}"
IsEnabled="{Binding IsDownloadAllowed}"
Style="{StaticResource AccentButtonStyle}" />
<StackPanel Spacing="16">
<Border
Padding="6,2"
HorizontalAlignment="Left"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="4"
Visibility="{x:Bind ViewModel.IsPrereleaseUpdate, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock
x:Uid="General_PreviewBadge"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}" />
</Border>
<Button
x:Uid="General_InstallNow"
Margin="0,0,0,16"
Command="{Binding UpdateNowButtonEventHandler}"
IsEnabled="{Binding IsDownloadAllowed}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</InfoBar.Content>
<InfoBar.ActionButton>
<HyperlinkButton

View File

@@ -1425,6 +1425,12 @@ opera.exe</value>
<data name="GeneralPage_AutoDownloadAndInstallUpdates.Description" xml:space="preserve">
<value>Except on metered connections</value>
</data>
<data name="GeneralPage_IncludePrereleaseUpdates.Header" xml:space="preserve">
<value>Include prerelease updates</value>
</data>
<data name="GeneralPage_IncludePrereleaseUpdates.Description" xml:space="preserve">
<value>Allow update checks to include preview builds. You may stay ahead of stable until a later stable release is available.</value>
</data>
<data name="GeneralSettings_AlwaysRunAsAdminText.Header" xml:space="preserve">
<value>Always run as administrator</value>
</data>
@@ -2418,6 +2424,27 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="General_NewVersionAvailable.Title" xml:space="preserve">
<value>An update is available:</value>
</data>
<data name="General_UpdateAvailableTitle" xml:space="preserve">
<value>An update is available:</value>
</data>
<data name="General_PreviewUpdateAvailableTitle" xml:space="preserve">
<value>Preview update available:</value>
</data>
<data name="General_UpdateReadyToInstallTitle" xml:space="preserve">
<value>An update is ready to install:</value>
</data>
<data name="General_PreviewUpdateReadyToInstallTitle" xml:space="preserve">
<value>A Preview update is ready to install:</value>
</data>
<data name="General_PreviewBadge.Text" xml:space="preserve">
<value>Preview</value>
</data>
<data name="ScoobeReleaseGroup_Preview" xml:space="preserve">
<value>Preview</value>
</data>
<data name="ScoobeReleaseNotes_PreviewBadge" xml:space="preserve">
<value>Preview release</value>
</data>
<data name="General_Downloading.Text" xml:space="preserve">
<value>Downloading...</value>
</data>

View File

@@ -167,6 +167,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_showThemeAdaptiveSysTrayIcon = GeneralSettingsConfig.ShowThemeAdaptiveTrayIcon;
_showNewUpdatesToastNotification = GeneralSettingsConfig.ShowNewUpdatesToastNotification;
_autoDownloadUpdates = GeneralSettingsConfig.AutoDownloadUpdates;
_includePrereleaseUpdates = GeneralSettingsConfig.IncludePrereleaseUpdates;
_showWhatsNewAfterUpdates = GeneralSettingsConfig.ShowWhatsNewAfterUpdates;
_enableExperimentation = GeneralSettingsConfig.EnableExperimentation;
@@ -188,10 +189,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_updatingState = UpdatingSettingsConfig.State;
_newAvailableVersion = UpdatingSettingsConfig.NewVersion;
_newAvailableVersionLink = UpdatingSettingsConfig.ReleasePageLink;
_isPrereleaseUpdate = UpdatingSettingsConfig.IsPrerelease;
_updateCheckedDate = FriendlyDateHelper.Format(UpdatingSettingsConfig.LastCheckedDateTime);
_newUpdatesToastIsGpoDisabled = GPOWrapper.GetDisableNewUpdateToastValue() == GpoRuleConfigured.Enabled;
_autoDownloadUpdatesIsGpoDisabled = GPOWrapper.GetDisableAutomaticUpdateDownloadValue() == GpoRuleConfigured.Enabled;
_includePrereleaseUpdatesIsGpoDisabled = GPOWrapper.GetDisablePreviewUpdatesValue() == GpoRuleConfigured.Enabled;
_experimentationIsGpoDisallowed = GPOWrapper.GetAllowExperimentationValue() == GpoRuleConfigured.Disabled;
_showWhatsNewAfterUpdatesIsGpoDisabled = GPOWrapper.GetDisableShowWhatsNewAfterUpdatesValue() == GpoRuleConfigured.Enabled;
_enableDataDiagnosticsIsGpoDisallowed = GPOWrapper.GetAllowDataDiagnosticsValue() == GpoRuleConfigured.Disabled;
@@ -269,6 +272,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _newUpdatesToastIsGpoDisabled;
private bool _autoDownloadUpdates;
private bool _autoDownloadUpdatesIsGpoDisabled;
private bool _includePrereleaseUpdatesIsGpoDisabled;
private bool _includePrereleaseUpdates;
private bool _showWhatsNewAfterUpdates;
private bool _showWhatsNewAfterUpdatesIsGpoDisabled;
private bool _enableExperimentation;
@@ -282,6 +287,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private UpdatingSettings.UpdatingState _updatingState = UpdatingSettings.UpdatingState.UpToDate;
private string _newAvailableVersion = string.Empty;
private string _newAvailableVersionLink = string.Empty;
private bool _isPrereleaseUpdate;
private string _updateCheckedDate = string.Empty;
private bool _isNewVersionDownloading;
@@ -318,7 +324,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
string installScope = GetCurrentInstallScope() == InstallScope.PerMachine ? "per machine (system)" : "per user";
var info = $"OS Version: {GetOSVersion()} \n.NET Version: {GetDotNetVersion()}\n{isElevatedString}\nInstall scope: {installScope}\nOperating System Language: {CultureInfo.InstalledUICulture.DisplayName}\nSystem locale: {CultureInfo.InstalledUICulture.Name}";
var info = $"OS Version: {GetOSVersion()} \n.NET Version: {GetDotNetVersion()}\n{isElevatedString}\nInstall scope: {installScope}\nVersion channel: {GetPowerToysVersionChannel()}\nSource commit: {GetPowerToysSourceCommit()}\nOperating System Language: {CultureInfo.InstalledUICulture.DisplayName}\nSystem locale: {CultureInfo.InstalledUICulture.Name}";
var gitHubURL = "https://github.com/microsoft/PowerToys/issues/new?template=bug_report.yml&labels=Issue-Bug%2CTriage-Needed" +
"&version=" + version + "&additionalInfo=" + System.Web.HttpUtility.UrlEncode(info);
@@ -331,6 +337,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return Helper.GetProductVersion().TrimStart('v');
}
private string GetPowerToysVersionChannel()
{
return global::PowerToys.Interop.CommonManaged.GetProductVersionChannel();
}
private string GetPowerToysSourceCommit()
{
return global::PowerToys.Interop.CommonManaged.GetProductVersionSourceCommit();
}
private string GetOSVersion()
{
return Environment.OSVersion.VersionString;
@@ -582,6 +598,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
return _newUpdatesToastIsGpoDisabled ||
(_isAdmin && _autoDownloadUpdatesIsGpoDisabled) ||
_includePrereleaseUpdatesIsGpoDisabled ||
_showWhatsNewAfterUpdatesIsGpoDisabled;
}
}
@@ -632,6 +649,27 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
get => !_isDevBuild && !_autoDownloadUpdatesIsGpoDisabled;
}
public bool IncludePrereleaseUpdates
{
get => _includePrereleaseUpdates && !_includePrereleaseUpdatesIsGpoDisabled;
set
{
if (_includePrereleaseUpdates != value)
{
_includePrereleaseUpdates = value;
GeneralSettingsConfig.IncludePrereleaseUpdates = value;
NotifyPropertyChanged();
CheckForUpdatesClick();
}
}
}
public bool IsIncludePrereleaseUpdatesCardEnabled
{
get => !_isDevBuild && !_includePrereleaseUpdatesIsGpoDisabled;
}
public bool ShowWhatsNewAfterUpdates
{
get
@@ -798,6 +836,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool IsCurrentVersionPreview => string.Equals(GetPowerToysVersionChannel(), "preview", StringComparison.OrdinalIgnoreCase);
public string NewVersionAvailableTitle => GetResourceString(IsPrereleaseUpdate ? "General_PreviewUpdateAvailableTitle" : "General_UpdateAvailableTitle");
public string NewVersionReadyToInstallTitle => GetResourceString(IsPrereleaseUpdate ? "General_PreviewUpdateReadyToInstallTitle" : "General_UpdateReadyToInstallTitle");
public string UpdateCheckedDate
{
get
@@ -1016,6 +1060,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool IsPrereleaseUpdate
{
get
{
return _isPrereleaseUpdate;
}
private set
{
if (value != _isPrereleaseUpdate)
{
_isPrereleaseUpdate = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(NewVersionAvailableTitle));
NotifyPropertyChanged(nameof(NewVersionReadyToInstallTitle));
}
}
}
public bool IsNewVersionDownloading
{
get
@@ -1391,6 +1454,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
PowerToysUpdatingState = UpdatingSettingsConfig.State;
PowerToysNewAvailableVersion = UpdatingSettingsConfig.NewVersion;
PowerToysNewAvailableVersionLink = UpdatingSettingsConfig.ReleasePageLink;
IsPrereleaseUpdate = UpdatingSettingsConfig.IsPrerelease;
UpdateCheckedDate = FriendlyDateHelper.Format(UpdatingSettingsConfig.LastCheckedDateTime);
_isNoNetwork = PowerToysUpdatingState == UpdatingSettings.UpdatingState.NetworkError;

View File

@@ -281,8 +281,8 @@ try {
$versionPropsPath = Join-Path $repoRoot "src\Version.props"
[xml]$versionProps = Get-Content $versionPropsPath
$ptVersion = $versionProps.Project.PropertyGroup.Version
# Directory.Build.props appends .0 to the version for .csproj files
$ptVersionFull = "$ptVersion.0"
# Package versions need four components. Preserve explicit preview build components.
$ptVersionFull = if (($ptVersion.ToCharArray() | Where-Object { $_ -eq '.' }).Count -eq 2) { "$ptVersion.0" } else { $ptVersion }
# 2. Build the Generator
$generatorProj = Join-Path $repoRoot "src\dsc\PowerToys.Settings.DSC.Schema.Generator\PowerToys.Settings.DSC.Schema.Generator.csproj"

View File

@@ -5,37 +5,81 @@ Param(
[Parameter(Mandatory=$True,Position=2)]
[AllowEmptyString()]
[string]$DevEnvironment = "Local"
[string]$DevEnvironment = "Local",
[ValidateSet("stable", "preview", "private")]
[string]$Channel = "stable",
[string]$SourceCommit = $env:BUILD_SOURCEVERSION,
[string]$BuildNumber = $env:BUILD_BUILDNUMBER
)
Write-Host $PSScriptRoot
$versionRegex = "(\d+)\.(\d+)\.(\d+)"
if($versionNumber -match $versionRegEx)
{
$buildDayOfYear = (Get-Date).DayofYear;
$buildTime = Get-Date -Format HH;
# $buildTime = Get-Date -Format HHmmss;
# $buildYear = Get-Date -Format yy;
# $revision = [string]::Format("{0}{1}{2}", $buildYear, $buildDayOfYear, $buildTime )
function Get-NormalizedVersion {
param(
[Parameter(Mandatory = $true)]
[string]$InputVersion,
[Parameter(Mandatory = $true)]
[string]$ReleaseChannel,
[string]$PipelineBuildNumber
)
# max UInt16, 65535
#$revision = [string]::Format("{0}{1}", $buildDayOfYear, $buildTime )
#Write-Host "Revision" $revision
if ($InputVersion -match "^(?<numeric>\d+\.\d+(?:\.\d+){0,2})-(?<suffix>preview)$") {
if ($ReleaseChannel -ne "preview") {
throw "Version suffix '-preview' can only be used with the preview release channel"
}
$versionNumber = [int]::Parse($matches[1]).ToString() + "." + [int]::Parse($matches[2]).ToString() + "." + [int]::Parse($matches[3]).ToString() # + "." + $revision
Write-Host "Version Number" $versionNumber
$InputVersion = $matches["numeric"]
}
if ($ReleaseChannel -eq "preview" -and $InputVersion -match "^(\d+)\.(\d+)$") {
$major = [int]::Parse($matches[1])
$minor = [int]::Parse($matches[2])
$now = Get-Date
$yyMM = [int]::Parse($now.ToString("yyMM"))
$day = $now.ToString("dd")
$rev = "001"
if ($PipelineBuildNumber -match "_(?<yyMM>\d{4})\.(?<day>\d{2})(?<rev>\d{3})") {
$yyMM = [int]::Parse($matches["yyMM"])
$day = $matches["day"]
$rev = $matches["rev"]
}
$build = [int]::Parse("$day$rev")
return "$major.$minor.$yyMM.$build"
}
if ($InputVersion -match "^(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?$") {
$versionParts = @([int]::Parse($matches[1]), [int]::Parse($matches[2]), [int]::Parse($matches[3]))
if ($matches[4]) {
$versionParts += [int]::Parse($matches[4])
}
return $versionParts -join "."
}
throw "Build format does not match the expected pattern (w.x, w.x.y, w.x.y.z, or w.x.y.z-preview for preview channel)"
}
else{
throw "Build format does not match the expected pattern (buildName_w.x.y.z)"
$versionNumber = Get-NormalizedVersion -InputVersion $versionNumber -ReleaseChannel $Channel -PipelineBuildNumber $BuildNumber
foreach ($part in ($versionNumber -split '\.')) {
$value = [int]::Parse($part)
if ($value -lt 0 -or $value -gt [UInt16]::MaxValue) {
throw "Version component '$value' is outside the supported Windows version range 0-65535"
}
}
Write-Host "Version Number" $versionNumber
$verPropWriteFileLocation = $PSScriptRoot + '/../../src/Version.props';
$verPropReadFileLocation = $verPropWriteFileLocation;
[XML]$verProps = Get-Content $verPropReadFileLocation
$verProps.Project.PropertyGroup.Version = $versionNumber;
$verProps.Project.PropertyGroup.VersionChannel = $Channel;
$verProps.Project.PropertyGroup.SourceCommit = if ([string]::IsNullOrWhiteSpace($SourceCommit)) { "" } else { $SourceCommit };
$verProps.Project.PropertyGroup.DevEnvironment = $DevEnvironment;
Write-Host "xml" $verProps.Project.PropertyGroup.Version
$verProps.Save($verPropWriteFileLocation);
$verProps.Save($verPropWriteFileLocation);