diff --git a/AGENTS.md b/AGENTS.md index b499572c..2e6ef192 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,33 @@ For faster iteration when ProfileExplorer is running: The Core project build is much faster (~2-3s) and doesn't require ProfileExplorer to be closed. +## Bumping the App Version + +The app version lives in 3 files that must stay in lockstep (history shows they've drifted apart and needed follow-up commits). Use the bump script — never edit these by hand: + +```powershell +# Print current version across all files +.\scripts\Bump-Version.ps1 -Show + +# Bump components (1.2.1 -> 1.2.2 / 1.3.0 / 2.0.0) +.\scripts\Bump-Version.ps1 -Bump Patch +.\scripts\Bump-Version.ps1 -Bump Minor +.\scripts\Bump-Version.ps1 -Bump Major + +# Set an explicit version +.\scripts\Bump-Version.ps1 -Version 1.5.0 + +# Preview without writing +.\scripts\Bump-Version.ps1 -Bump Patch -WhatIf +``` + +The script refuses to bump when the files disagree on the current version; pass `-Version X.Y.Z -Force` to force a known-good value. See the script's header comment for full help. + +The files it updates: +- `src/ProfileExplorerUI/ProfileExplorerUI.csproj` (`AssemblyVersion`, `FileVersion`, `Version`) +- `installer/x64/prepare-installer.cmd` +- `installer/arm64/prepare-installer.cmd` + ## Diagnostic Logging Enable diagnostic logging with: diff --git a/installer/arm64/prepare-installer.cmd b/installer/arm64/prepare-installer.cmd index 7beffe65..728b7b69 100644 --- a/installer/arm64/prepare-installer.cmd +++ b/installer/arm64/prepare-installer.cmd @@ -1,3 +1,3 @@ -set %_VERSION="1.2.1" +set %_VERSION="1.3.0" iscc.exe installer.iss /DAPP_VERSION=%_VERSION% /O%cd% diff --git a/installer/x64/prepare-installer.cmd b/installer/x64/prepare-installer.cmd index 7beffe65..728b7b69 100644 --- a/installer/x64/prepare-installer.cmd +++ b/installer/x64/prepare-installer.cmd @@ -1,3 +1,3 @@ -set %_VERSION="1.2.1" +set %_VERSION="1.3.0" iscc.exe installer.iss /DAPP_VERSION=%_VERSION% /O%cd% diff --git a/scripts/Bump-Version.ps1 b/scripts/Bump-Version.ps1 new file mode 100644 index 00000000..25a9e471 --- /dev/null +++ b/scripts/Bump-Version.ps1 @@ -0,0 +1,319 @@ +<# +.SYNOPSIS + Bumps the Profile Explorer application version across all files that hold it. + +.DESCRIPTION + Updates the app version in: + - src/ProfileExplorerUI/ProfileExplorerUI.csproj (3 elements: AssemblyVersion, FileVersion, Version) + - installer/x64/prepare-installer.cmd + - installer/arm64/prepare-installer.cmd + + Refuses to operate when the files disagree on the current version, unless + -Force is supplied alongside -Version. Preserves file encoding (UTF-8 BOM + where present) and line endings. + +.PARAMETER Bump + Auto-increment a single semver component. Major resets minor and patch to 0; + Minor resets patch to 0; Patch bumps the patch only. + +.PARAMETER Version + Set an explicit version in the form Major.Minor.Patch (e.g. 1.5.0). + +.PARAMETER Show + Print the current version found in each file and exit. + +.PARAMETER Force + Bypass the drift check. Only valid with -Version; rewrites every file to the + given version regardless of what it currently holds. + +.EXAMPLE + .\Bump-Version.ps1 -Show + Print the current version of each file. + +.EXAMPLE + .\Bump-Version.ps1 -Bump Patch + Bump 1.2.1 -> 1.2.2 across all files. + +.EXAMPLE + .\Bump-Version.ps1 -Bump Minor -WhatIf + Preview a 1.2.1 -> 1.3.0 bump without writing. + +.EXAMPLE + .\Bump-Version.ps1 -Version 1.5.0 + Set every file's version to 1.5.0 (requires all files to currently agree). + +.EXAMPLE + .\Bump-Version.ps1 -Version 1.5.0 -Force + Set every file's version to 1.5.0 even if the files currently disagree. +#> + +[CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Bump')] +param( + [Parameter(ParameterSetName = 'Bump', Mandatory = $true)] + [ValidateSet('Major', 'Minor', 'Patch')] + [string]$Bump, + + [Parameter(ParameterSetName = 'Version', Mandatory = $true)] + [string]$Version, + + [Parameter(ParameterSetName = 'Show', Mandatory = $true)] + [switch]$Show, + + [Parameter(ParameterSetName = 'Version')] + [switch]$Force +) + +$ErrorActionPreference = 'Stop' + +# Repository root is the parent of the scripts/ directory holding this file. +$RepoRoot = Split-Path -Parent $PSScriptRoot +if (-not $RepoRoot) { + $RepoRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +} + +# File targets. Each entry describes one file and the regex patterns that +# locate the version string within it. ExpectedHits is asserted before we +# write -- a mismatch means the file's schema changed and the script needs +# updating. +$Targets = @( + [pscustomobject]@{ + Name = 'src/ProfileExplorerUI/ProfileExplorerUI.csproj' + Path = Join-Path $RepoRoot 'src\ProfileExplorerUI\ProfileExplorerUI.csproj' + # Anchored on the specific element names so PackageReference + # elements (none today, but possible) cannot be matched by accident. + Patterns = @( + '(?)(?\d+\.\d+\.\d+)(?)', + '(?)(?\d+\.\d+\.\d+)(?)', + '(?)(?\d+\.\d+\.\d+)(?)' + ) + ExpectedHits = 3 + }, + [pscustomobject]@{ + Name = 'installer/x64/prepare-installer.cmd' + Path = Join-Path $RepoRoot 'installer\x64\prepare-installer.cmd' + Patterns = @('(?set %_VERSION=")(?\d+\.\d+\.\d+)(?")') + ExpectedHits = 1 + }, + [pscustomobject]@{ + Name = 'installer/arm64/prepare-installer.cmd' + Path = Join-Path $RepoRoot 'installer\arm64\prepare-installer.cmd' + Patterns = @('(?set %_VERSION=")(?\d+\.\d+\.\d+)(?")') + ExpectedHits = 1 + } +) + +function Read-FileWithEncoding { + param([string]$Path) + + $bytes = [System.IO.File]::ReadAllBytes($Path) + $hasBom = $bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF + $encoding = New-Object System.Text.UTF8Encoding($hasBom) + $text = $encoding.GetString($bytes) + if ($hasBom) { + # GetString includes the BOM as U+FEFF when the bytes are present; strip + # it from the content view but remember to re-add via the encoding. + $text = $text.TrimStart([char]0xFEFF) + } + return [pscustomobject]@{ Text = $text; Encoding = $encoding } +} + +function Write-FileWithEncoding { + param( + [string]$Path, + [string]$Text, + [System.Text.Encoding]$Encoding + ) + [System.IO.File]::WriteAllText($Path, $Text, $Encoding) +} + +function Get-VersionHits { + param([pscustomobject]$Target, [string]$Text) + + $hits = @() + foreach ($pattern in $Target.Patterns) { + $matchResults = [regex]::Matches($Text, $pattern) + foreach ($m in $matchResults) { + # Compute 1-based line number for human-readable output. + $line = ($Text.Substring(0, $m.Index) -split "`n").Count + $hits += [pscustomobject]@{ + Pattern = $pattern + Match = $m + Version = $m.Groups['ver'].Value + Prefix = $m.Groups['prefix'].Value + Suffix = $m.Groups['suffix'].Value + Line = $line + } + } + } + return ,$hits +} + +function Test-SemVer { + param([string]$Value) + return $Value -match '^\d+\.\d+\.\d+$' +} + +function Get-BumpedVersion { + param([string]$Current, [string]$Component) + + $parts = $Current -split '\.' + [int]$major = $parts[0] + [int]$minor = $parts[1] + [int]$patch = $parts[2] + + switch ($Component) { + 'Major' { $major++; $minor = 0; $patch = 0 } + 'Minor' { $minor++; $patch = 0 } + 'Patch' { $patch++ } + } + return "$major.$minor.$patch" +} + +# ----- Phase 1: read every file, collect current versions ------------------- + +$fileStates = @() +foreach ($target in $Targets) { + if (-not (Test-Path $target.Path)) { + Write-Host "ERROR: File not found: $($target.Path)" -ForegroundColor Red + exit 1 + } + + $read = Read-FileWithEncoding -Path $target.Path + $hits = Get-VersionHits -Target $target -Text $read.Text + + if ($hits.Count -ne $target.ExpectedHits) { + Write-Host "ERROR: $($target.Name) -- expected $($target.ExpectedHits) version match(es), found $($hits.Count)." -ForegroundColor Red + Write-Host " The file's schema may have changed. Update the Patterns/ExpectedHits in Bump-Version.ps1." -ForegroundColor Red + exit 1 + } + + $distinct = @($hits.Version | Select-Object -Unique) + if ($distinct.Count -gt 1) { + Write-Host "ERROR: $($target.Name) -- internal version mismatch: $($distinct -join ', ')" -ForegroundColor Red + exit 1 + } + + $fileStates += [pscustomobject]@{ + Target = $target + Text = $read.Text + Encoding = $read.Encoding + Hits = $hits + Version = $distinct[0] + } +} + +# ----- Phase 2: handle -Show ------------------------------------------------ + +if ($Show) { + $allAgree = @($fileStates.Version | Select-Object -Unique).Count -eq 1 + if ($allAgree) { + Write-Host "Current version: $($fileStates[0].Version) (consistent across $($fileStates.Count) files)" -ForegroundColor Green + } else { + Write-Host "Version drift detected:" -ForegroundColor Yellow + } + foreach ($state in $fileStates) { + $color = if ($allAgree) { 'Gray' } else { 'Yellow' } + Write-Host (" {0,-55} -> {1}" -f $state.Target.Name, $state.Version) -ForegroundColor $color + } + exit 0 +} + +# ----- Phase 3: drift check (skippable via -Force + -Version) --------------- + +$distinctVersions = @($fileStates.Version | Select-Object -Unique) +$drift = $distinctVersions.Count -gt 1 + +if ($drift -and -not ($Force -and $PSCmdlet.ParameterSetName -eq 'Version')) { + Write-Host "ERROR: Version drift detected across files:" -ForegroundColor Red + foreach ($state in $fileStates) { + Write-Host (" {0,-55} -> {1}" -f $state.Target.Name, $state.Version) -ForegroundColor Yellow + } + Write-Host "" + Write-Host "Refusing to bump from an inconsistent state. Options:" -ForegroundColor Red + Write-Host " 1. Resolve drift manually, then re-run." -ForegroundColor Yellow + Write-Host " 2. Pin an explicit target with -Version -Force" -ForegroundColor Yellow + Write-Host " (this will rewrite all files to the given version regardless of current value)" -ForegroundColor Yellow + exit 1 +} + +if ($Force -and -not $drift) { + Write-Host "Note: -Force supplied but no drift detected; proceeding normally." -ForegroundColor DarkGray +} + +# ----- Phase 4: compute target version -------------------------------------- + +$currentVersion = $fileStates[0].Version # When drift+Force, "current" is meaningless; only -Version is allowed in that case. + +if ($PSCmdlet.ParameterSetName -eq 'Bump') { + $newVersion = Get-BumpedVersion -Current $currentVersion -Component $Bump +} else { + if (-not (Test-SemVer $Version)) { + Write-Host "ERROR: -Version must be in the form Major.Minor.Patch (e.g. 1.2.3). Got: $Version" -ForegroundColor Red + exit 1 + } + $newVersion = $Version +} + +if (-not $drift -and $newVersion -eq $currentVersion) { + Write-Host "Nothing to do: target version $newVersion already matches current version." -ForegroundColor Yellow + exit 0 +} + +# ----- Phase 5: report / apply ---------------------------------------------- + +if (-not $drift) { + Write-Host "Current version: $currentVersion (consistent across $($fileStates.Count) files)" -ForegroundColor Cyan +} else { + Write-Host "Current version: (drift -- see below)" -ForegroundColor Yellow +} +$dryRunSuffix = if ($WhatIfPreference) { ' (dry run, no files modified)' } else { '' } +Write-Host "New version: $newVersion$dryRunSuffix" -ForegroundColor Cyan +Write-Host "" + +if ($WhatIfPreference) { + Write-Host "Would update:" -ForegroundColor Cyan +} else { + Write-Host "Updated:" -ForegroundColor Green +} + +foreach ($state in $fileStates) { + $target = $state.Target + $text = $state.Text + + if ($WhatIfPreference) { + Write-Host " $($target.Name)" -ForegroundColor Gray + foreach ($hit in $state.Hits) { + $before = "$($hit.Prefix)$($hit.Version)$($hit.Suffix)" + $after = "$($hit.Prefix)$newVersion$($hit.Suffix)" + Write-Host (" line {0,3}: {1} -> {2}" -f $hit.Line, $before, $after) -ForegroundColor DarkGray + } + continue + } + + # Apply replacements. Re-run regex against the live text on each iteration + # to keep indices accurate after each substitution. + $newText = $text + $totalReplacements = 0 + foreach ($pattern in $target.Patterns) { + $newText = [regex]::Replace($newText, $pattern, { + param($m) + $script:totalReplacements++ + return "$($m.Groups['prefix'].Value)$newVersion$($m.Groups['suffix'].Value)" + }) + } + + if ($totalReplacements -ne $target.ExpectedHits) { + Write-Host "ERROR: $($target.Name) -- expected $($target.ExpectedHits) replacement(s), made $totalReplacements. Aborting before writing." -ForegroundColor Red + exit 1 + } + + Write-FileWithEncoding -Path $target.Path -Text $newText -Encoding $state.Encoding + Write-Host (" {0,-55} ({1} replacement{2})" -f $target.Name, $target.ExpectedHits, $(if ($target.ExpectedHits -eq 1) { '' } else { 's' })) -ForegroundColor Gray +} + +Write-Host "" +if ($WhatIfPreference) { + Write-Host "Dry run complete. Re-run without -WhatIf to apply." -ForegroundColor Cyan +} else { + Write-Host "Done. Review with: git diff" -ForegroundColor Green +} diff --git a/src/ProfileExplorerUI/ProfileExplorerUI.csproj b/src/ProfileExplorerUI/ProfileExplorerUI.csproj index 8801c812..3a9763af 100644 --- a/src/ProfileExplorerUI/ProfileExplorerUI.csproj +++ b/src/ProfileExplorerUI/ProfileExplorerUI.csproj @@ -5,9 +5,9 @@ net8.0-windows true main.ico - 1.2.1 - 1.2.1 - 1.2.1 + 1.3.0 + 1.3.0 + 1.3.0 Microsoft Corporation Profile Explorer