scripts/internal/version-check.ps1
|
Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $sharedGovernancePath = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'extensions\specrew-speckit\scripts\shared-governance.ps1' if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) { throw "Missing shared governance helper '$sharedGovernancePath'." } . $sharedGovernancePath function Get-SpecrewVersionConfigValue { param( [Parameter(Mandatory = $true)] [string]$ProjectRoot, [Parameter(Mandatory = $true)] [string]$Key ) $configPath = Join-Path (Resolve-ProjectPath -Path $ProjectRoot) '.specrew\config.yml' if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { return $null } foreach ($line in Get-Content -LiteralPath $configPath -Encoding UTF8) { if ($line -match ('^\s*{0}:\s*"?(?<value>[^"#]+?)"?\s*$' -f [regex]::Escape($Key))) { return $Matches['value'].Trim() } } return $null } function Get-SpecrewInstalledVersion { param([Parameter(Mandatory = $true)][string]$ProjectRoot) # Step 1: Get-Module -ListAvailable. SilentlyContinue + try/catch because on Linux, # PSModulePath often contains directories with malformed modules or permission # issues; without SilentlyContinue, those produce non-terminating errors that # $ErrorActionPreference='Stop' (set at the top of this script) turns into # terminating exceptions, which silently fail the whole function via outer catch. try { $module = @(Get-Module -Name Specrew -ListAvailable -ErrorAction SilentlyContinue | Sort-Object Version -Descending | Select-Object -First 1) if ($module.Count -gt 0 -and $module[0].Version) { return $module[0].Version.ToString() } } catch { # Fall through to manifest check. } # Step 2: manifest path search. Always try the repo-root manifest (two parents # up from this script). Add ProjectRoot manifest only if Resolve-ProjectPath # succeeds (it normally does; defensive). $manifestCandidates = @( (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'Specrew.psd1') ) try { $resolvedProjectRoot = Resolve-ProjectPath -Path $ProjectRoot $manifestCandidates += (Join-Path $resolvedProjectRoot 'Specrew.psd1') } catch { # ProjectRoot may be unresolvable; the repo-root manifest is still tried. } $manifestCandidates = @($manifestCandidates | Select-Object -Unique) foreach ($manifestPath in $manifestCandidates) { try { if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { continue } $manifest = Import-PowerShellDataFile -LiteralPath $manifestPath if ($manifest -and $manifest.ContainsKey('ModuleVersion')) { return [string]$manifest.ModuleVersion } } catch { continue } } return $null } function ConvertTo-SpecrewSemanticVersion { param([AllowNull()][string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $null } $match = [regex]::Match($Value, '(?<version>\d+\.\d+\.\d+(?:\.\d+)?)') if (-not $match.Success) { return $null } try { return [version]$match.Groups['version'].Value } catch { return $null } } function Get-SpecrewSlashCommandMinVersion { return '0.24.0' } function Get-SpecrewVersionCheckCachePath { param([Parameter(Mandatory = $true)][string]$ProjectRoot) return Join-Path (Resolve-ProjectPath -Path $ProjectRoot) '.specrew\version-check-cache.json' } function Test-SpecrewSkipUpdateCheck { param([bool]$SkipUpdateCheck) if ($SkipUpdateCheck) { return $true } return ($env:SPECREW_SKIP_UPDATE_CHECK -eq '1') } function Get-SpecrewVersionCheckCacheState { param([Parameter(Mandatory = $true)][string]$ProjectRoot) $cachePath = Get-SpecrewVersionCheckCachePath -ProjectRoot $ProjectRoot if (-not (Test-Path -LiteralPath $cachePath -PathType Leaf)) { return $null } try { # F-023: Use -AsHashtable for StrictMode compatibility; hashtable indexer tolerates missing fields $cache = Get-Content -LiteralPath $cachePath -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable -Depth 6 # F-023: Legacy schema handling - missing 'schema' field implies v0 $schema = $cache['schema'] if (-not $schema) { Write-Debug "schema-implied-v0 for $cachePath" # v0 behavior: all cache fields are optional } # v1+ behavior: same as v0 for this cache (no behavioral divergence yet) return $cache } catch { return $null } } function Set-SpecrewVersionCheckCacheState { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [Parameter(Mandatory = $true)][string]$LatestVersion, [Parameter(Mandatory = $true)][string]$CheckedAt, [Parameter(Mandatory = $true)][string]$CacheValidUntil, [Parameter(Mandatory = $true)][string]$Source ) $payload = [ordered]@{ schema = 'v1' latest_version = $LatestVersion checked_at = $CheckedAt cache_valid_until = $CacheValidUntil source = $Source } | ConvertTo-Json -Depth 6 Write-Utf8FileAtomic -Path (Get-SpecrewVersionCheckCachePath -ProjectRoot $ProjectRoot) -Content ($payload + [Environment]::NewLine) } function Test-SpecrewVersionCacheValid { param([AllowNull()][object]$CacheState) if ($null -eq $CacheState -or [string]::IsNullOrWhiteSpace([string]$CacheState.cache_valid_until)) { return $false } try { $cacheValidUntil = [datetime]::Parse([string]$CacheState.cache_valid_until, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) return ($cacheValidUntil -gt (Get-Date).ToUniversalTime()) } catch { return $false } } function Invoke-SpecrewPSGalleryLatestVersionQuery { param([int]$TimeoutSeconds = 10) if (-not [string]::IsNullOrWhiteSpace($env:SPECREW_PSGALLERY_LATEST_VERSION)) { return [pscustomobject]@{ LatestVersion = $env:SPECREW_PSGALLERY_LATEST_VERSION Source = 'override' } } if ($env:SPECREW_PSGALLERY_FORCE_FAILURE -eq '1') { throw 'Simulated PSGallery failure.' } $job = Start-Job -ScriptBlock { $module = Find-Module -Name Specrew -Repository PSGallery -ErrorAction Stop [pscustomobject]@{ LatestVersion = [string]$module.Version Source = 'psgallery' } } try { if (-not (Wait-Job -Job $job -Timeout $TimeoutSeconds)) { Stop-Job -Job $job -ErrorAction SilentlyContinue | Out-Null throw ("Timed out after {0} seconds while querying PSGallery." -f $TimeoutSeconds) } $result = Receive-Job -Job $job -ErrorAction Stop if ($null -eq $result -or [string]::IsNullOrWhiteSpace([string]$result.LatestVersion)) { throw 'PSGallery query returned no version.' } return $result } finally { Remove-Job -Job $job -Force -ErrorAction SilentlyContinue | Out-Null } } function Get-PSGalleryLatestVersion { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [switch]$ForceRefresh, [bool]$SkipCheck ) if (Test-SpecrewSkipUpdateCheck -SkipUpdateCheck $SkipCheck) { return [pscustomobject]@{ Skipped = $true LatestVersion = $null Source = 'skipped' } } $cacheState = Get-SpecrewVersionCheckCacheState -ProjectRoot $ProjectRoot if (-not $ForceRefresh -and (Test-SpecrewVersionCacheValid -CacheState $cacheState) -and -not [string]::IsNullOrWhiteSpace([string]$cacheState.latest_version)) { return [pscustomobject]@{ Skipped = $false LatestVersion = [string]$cacheState.latest_version Source = 'cache' CheckedAt = [string]$cacheState.checked_at CacheValidUntil = [string]$cacheState.cache_valid_until } } try { $queryResult = Invoke-SpecrewPSGalleryLatestVersionQuery $checkedAt = (Get-Date).ToUniversalTime().ToString('o') $cacheValidUntil = (Get-Date).ToUniversalTime().AddHours(24).ToString('o') Set-SpecrewVersionCheckCacheState ` -ProjectRoot $ProjectRoot ` -LatestVersion ([string]$queryResult.LatestVersion) ` -CheckedAt $checkedAt ` -CacheValidUntil $cacheValidUntil ` -Source ([string]$queryResult.Source) return [pscustomobject]@{ Skipped = $false LatestVersion = [string]$queryResult.LatestVersion Source = [string]$queryResult.Source CheckedAt = $checkedAt CacheValidUntil = $cacheValidUntil } } catch { Write-Verbose ("PSGallery latest-version query failed: {0}" -f $_.Exception.Message) return $null } } function Get-PSGalleryUpdateWarning { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [bool]$SkipCheck ) if (Test-SpecrewSkipUpdateCheck -SkipUpdateCheck $SkipCheck) { return $null } $installedVersionText = Get-SpecrewInstalledVersion -ProjectRoot $ProjectRoot $installedVersion = ConvertTo-SpecrewSemanticVersion -Value $installedVersionText if ($null -eq $installedVersion) { return $null } $latestState = Get-PSGalleryLatestVersion -ProjectRoot $ProjectRoot -SkipCheck:$SkipCheck if ($null -eq $latestState -or [string]::IsNullOrWhiteSpace([string]$latestState.LatestVersion)) { return $null } $latestVersion = ConvertTo-SpecrewSemanticVersion -Value ([string]$latestState.LatestVersion) if ($null -eq $latestVersion -or $latestVersion -le $installedVersion) { return $null } return "Newer version available: $($latestVersion.ToString()) (current: $($installedVersion.ToString())). To update: Update-Module Specrew" } |