diff --git a/CHANGELOG.md b/CHANGELOG.md index 8854442..fd105eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.1] - 2026-08-14 + +### Changed + +- Updated **MaksIT.Results** (2.0.4), **Microsoft.Extensions.*** (10.0.11), and **System.Management.Automation** (7.6.5) package references. +- Updated test dependencies: **Microsoft.NET.Test.Sdk** (18.9.0), **Microsoft.Extensions.Logging.Console** (10.0.11). +- Synced **RepoUtils** (`utils/`): engine/plugin updates; removed obsolete `Update-RepoUtils` tooling (local-copy sync only). + ## [1.3.0] - 2026-07-27 ### Added diff --git a/src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj b/src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj index ebaab68..70a9d1f 100644 --- a/src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj +++ b/src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj @@ -7,7 +7,7 @@ MaksIT.PodmanClientDotNet.PowerShell MaksIT.PodmanClientDotNet.PowerShell PowerShell module with cmdlets for PodmanClient.DotNet. - 1.3.0 + 1.3.1 Maksym Sadovnychyy MAKS-IT true @@ -16,8 +16,8 @@ - - + + diff --git a/src/PodmanClient/PodmanClientDotNet.csproj b/src/PodmanClient/PodmanClientDotNet.csproj index 19be550..6f5f121 100644 --- a/src/PodmanClient/PodmanClientDotNet.csproj +++ b/src/PodmanClient/PodmanClientDotNet.csproj @@ -12,7 +12,7 @@ PodmanClient.DotNet - 1.3.0 + 1.3.1 Maksym Sadovnychyy MAKS-IT PodmanClient.DotNet @@ -49,11 +49,11 @@ - - - - - + + + + + diff --git a/src/PodmanClientDotNet.Tests/PodmanClientDotNet.Tests.csproj b/src/PodmanClientDotNet.Tests/PodmanClientDotNet.Tests.csproj index 8c4346e..ecf131e 100644 --- a/src/PodmanClientDotNet.Tests/PodmanClientDotNet.Tests.csproj +++ b/src/PodmanClientDotNet.Tests/PodmanClientDotNet.Tests.csproj @@ -14,8 +14,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/utils/Update-RepoUtils.bat b/utils/Update-RepoUtils.bat deleted file mode 100644 index 048e3fb..0000000 --- a/utils/Update-RepoUtils.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\Update-RepoUtils\Update-RepoUtils.ps1" %* -pause diff --git a/utils/engines/release/Invoke-ReleasePackage.ps1 b/utils/engines/release/Invoke-ReleasePackage.ps1 index dd59c4f..b788172 100644 --- a/utils/engines/release/Invoke-ReleasePackage.ps1 +++ b/utils/engines/release/Invoke-ReleasePackage.ps1 @@ -5,21 +5,14 @@ .SYNOPSIS Plugin-driven release engine entry script. -.PARAMETER DryRun - When set, plugins that declare mutatesRemote in Get-PluginMetadata validate only (no registry push, GitHub release, or cluster deploy). - -.PARAMETER Mode - Optional override for HelmSelfDeploy values resolution (single or ha). - When omitted, HelmSelfDeploy.deployMode from scriptSettings.json is used (CI/CD default). - Manual installs: Invoke-ReleasePackage-Single.bat / Invoke-ReleasePackage-HA.bat. +.NOTES + Per-plugin dryRun on mutatesRemote plugins validates without remote mutation. + There is no engine-wide dryRun switch — set dryRun on each plugin as needed. #> -[CmdletBinding()] -param( - [switch]$DryRun, - [ValidateSet('single', 'ha')] - [string]$Mode -) +# Keep a plain param block so unbound launcher arguments remain in $args for +# optional Initialize-ReleaseExtension (advanced binding would reject them). +param() $ErrorActionPreference = 'Stop' Set-Location $PSScriptRoot @@ -29,20 +22,43 @@ $srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path . (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1') Import-EngineModules -Engine Release -$settings = Get-ScriptSettings -ScriptDir $PSScriptRoot -$resolveModeParams = @{ Settings = $settings } -if ($PSBoundParameters.ContainsKey('Mode')) { - $resolveModeParams['ModeOverride'] = $Mode +$releaseExtension = $null +if (Get-Command Initialize-ReleaseExtension -ErrorAction SilentlyContinue) { + $releaseExtension = Initialize-ReleaseExtension -ScriptDir $PSScriptRoot -ArgumentList $args } -$deployMode = Get-DeployModeFromSettings @resolveModeParams + +$settings = if ($null -ne $releaseExtension) { + $releaseExtension.Settings +} +else { + Get-ScriptSettings -ScriptDir $PSScriptRoot +} + $configuredPlugins = Get-ConfiguredPlugins -Settings $settings +$releaseBanner = if ($null -ne $releaseExtension) { + $releaseExtension.StepBanner +} +else { + 'RELEASE ENGINE' +} + Write-Log -Level 'STEP' -Message '==================================================' -Write-Log -Level 'STEP' -Message "RELEASE ENGINE (deploy mode: $deployMode)" +Write-Log -Level 'STEP' -Message $releaseBanner Write-Log -Level 'STEP' -Message '==================================================' $plugins = $configuredPlugins -$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $PSScriptRoot -SrcDir $srcDir -Settings $settings -DryRun:$DryRun -DeployMode $deployMode +$newContextParams = @{ + Plugins = $plugins + ScriptDir = $PSScriptRoot + SrcDir = $srcDir + Settings = $settings +} +if ($null -ne $releaseExtension) { + $newContextParams['ExtensionData'] = $releaseExtension.ContextData +} + +$engineContext = New-EngineContext @newContextParams Write-Log -Level 'OK' -Message 'All pre-flight checks passed!' $sharedPluginSettings = $engineContext @@ -59,14 +75,14 @@ else { if ((Test-IsPublishPlugin -Plugin $plugin -EngineDirectory $PSScriptRoot) -and -not $releaseStageInitialized) { if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -WriteLogs:$false) { - $remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)]) - Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory -Version $engineContext.version + Initialize-ReleaseStageContext -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory $releaseStageInitialized = $true } } - $pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -ContinueOnError:$false - if (-not $pluginSucceeded) { + $pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot + # Exact $true only: polluted arrays (CLI stdout + $false) are truthy under -not. + if ($pluginSucceeded -ne $true) { $releaseHadPluginFailures = $true break } @@ -88,9 +104,6 @@ elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' - elseif ($engineContext.isNonReleaseBranch) { Write-Log -Level 'OK' -Message 'NON-RELEASE RUN COMPLETE' } -elseif ($engineContext.dryRun) { - Write-Log -Level 'OK' -Message 'DRY RUN COMPLETE' -} else { Write-Log -Level 'OK' -Message 'RELEASE COMPLETE' } diff --git a/utils/engines/test/Invoke-TestEngine.ps1 b/utils/engines/test/Invoke-TestEngine.ps1 index f4da98e..7d06532 100644 --- a/utils/engines/test/Invoke-TestEngine.ps1 +++ b/utils/engines/test/Invoke-TestEngine.ps1 @@ -6,6 +6,8 @@ Plugin-driven test and coverage engine entry script. #> +$ErrorActionPreference = 'Stop' + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path @@ -29,8 +31,9 @@ if ($configuredPlugins.Count -eq 0) { $testHadPluginFailures = $false foreach ($plugin in $configuredPlugins) { - $pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $scriptDir -ContinueOnError:$false - if (-not $pluginSucceeded) { + $pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $scriptDir + # Exact $true only: polluted arrays (CLI stdout + $false) are truthy under -not. + if ($pluginSucceeded -ne $true) { $testHadPluginFailures = $true break } diff --git a/utils/modules/Engine/EngineContext.psm1 b/utils/modules/Engine/EngineContext.psm1 index 19ad750..3b94fa9 100644 --- a/utils/modules/Engine/EngineContext.psm1 +++ b/utils/modules/Engine/EngineContext.psm1 @@ -3,13 +3,14 @@ <# .SYNOPSIS - Helpers to resolve engine semver and relative paths from plugin configuration. + Generic engine helpers: path resolution and the shared context facts API. .DESCRIPTION - Used by New-EngineContext and version plugins: - - DotNetReleaseVersion plugin -> projectFiles (.csproj ) - - NpmReleaseVersion plugin -> packageJsonPath (package.json version) - - FileReleaseVersion plugin -> versionFilePath (repo-root VERSION file) + Engine-owned state stays on the context object (version, tag, skipPublishPlugins, …). + Plugin-published values go under $context.facts[namespace][name] via Set/Get/Test-EngineFact. + During migration, -LegacyProperty on Set dual-writes flat properties; Get falls back to them. + + Version plugins declare providesVersion = $true; New-EngineContext discovers the single enabled one. #> if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) { @@ -19,6 +20,16 @@ if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) { } } +$script:EngineStateAllowlist = @( + 'version' + 'tag' + 'skipPublishPlugins' + 'releaseDir' + 'artifactsDirectory' + 'deployMode' + 'orchestrator' +) + function Resolve-RelativePaths { param( [Parameter(Mandatory = $true)] @@ -52,251 +63,270 @@ function Resolve-RelativePaths { return @($resolved) } -function Get-CsprojPropertyValue { +function Initialize-EngineFactsBag { param( [Parameter(Mandatory = $true)] - [xml]$Csproj, - - [Parameter(Mandatory = $true)] - [string]$PropertyName + [psobject]$Context ) - # SDK-style .csproj files can have multiple PropertyGroup nodes. - # Use the first group that defines the requested property. - $propNode = $Csproj.Project.PropertyGroup | - Where-Object { $_.$PropertyName } | - Select-Object -First 1 + if (-not ($Context.PSObject.Properties.Name -contains 'facts') -or $null -eq $Context.facts) { + $Context | Add-Member -NotePropertyName facts -NotePropertyValue ([ordered]@{}) -Force + } +} - if ($propNode) { - return $propNode.$PropertyName +function Assert-EngineFactNamespace { + param( + [Parameter(Mandatory = $true)] + [string]$Namespace + ) + + if ($Namespace -eq 'engine') { + throw "Namespace 'engine' is reserved; use Set-EngineState / Get-EngineState for engine-owned fields." + } + + if ($Namespace -cnotmatch '^[a-z][a-z0-9]*$') { + throw "Invalid facts namespace '$Namespace'. Use lowercase alphanumeric starting with a letter (e.g. 'test', 'dotnet')." + } +} + +function Assert-EngineFactName { + param( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + if ($Name -cnotmatch '^[a-zA-Z][a-zA-Z0-9_]*$') { + throw "Invalid fact name '$Name'. Use letters, digits, underscore; must start with a letter." + } +} + +function Test-EngineFactValuePresent { + param( + [AllowNull()] + $Value + ) + + if ($null -eq $Value) { + return $false + } + + if ($Value -is [string] -and [string]::IsNullOrWhiteSpace($Value)) { + return $false + } + + return $true +} + +function Set-EngineFact { + param( + [Parameter(Mandatory = $true)] + [psobject]$Context, + + [Parameter(Mandatory = $true)] + [string]$Namespace, + + [Parameter(Mandatory = $true)] + [string]$Name, + + [Parameter(Mandatory = $true)] + [AllowNull()] + $Value, + + [ValidateSet('Error', 'Replace', 'Keep')] + [string]$Overwrite = 'Error', + + [Parameter(Mandatory = $false)] + [string]$LegacyProperty + ) + + Assert-EngineFactNamespace -Namespace $Namespace + Assert-EngineFactName -Name $Name + Initialize-EngineFactsBag -Context $Context + + if (-not $Context.facts.Contains($Namespace)) { + $Context.facts[$Namespace] = [ordered]@{} + } + + $bag = $Context.facts[$Namespace] + $exists = $bag.Contains($Name) + if ($exists) { + if ($Overwrite -eq 'Keep') { + if (-not [string]::IsNullOrWhiteSpace($LegacyProperty) -and -not ($Context.PSObject.Properties.Name -contains $LegacyProperty)) { + $Context | Add-Member -NotePropertyName $LegacyProperty -NotePropertyValue $bag[$Name] -Force + } + return + } + + if ($Overwrite -eq 'Error') { + throw "Fact ${Namespace}.${Name} already set (use -Overwrite Replace or Keep)." + } + } + + $bag[$Name] = $Value + + if (-not [string]::IsNullOrWhiteSpace($LegacyProperty)) { + $Context | Add-Member -NotePropertyName $LegacyProperty -NotePropertyValue $Value -Force + } +} + +function Get-EngineFact { + param( + [Parameter(Mandatory = $true)] + [psobject]$Context, + + [Parameter(Mandatory = $true)] + [string]$Namespace, + + [Parameter(Mandatory = $true)] + [string]$Name, + + [Parameter(Mandatory = $false)] + $Default, + + [switch]$Required, + + [Parameter(Mandatory = $false)] + [string[]]$LegacyProperty + ) + + Assert-EngineFactNamespace -Namespace $Namespace + Assert-EngineFactName -Name $Name + Initialize-EngineFactsBag -Context $Context + + if ($Context.facts.Contains($Namespace) -and $Context.facts[$Namespace].Contains($Name)) { + $value = $Context.facts[$Namespace][$Name] + if (Test-EngineFactValuePresent -Value $value) { + return $value + } + } + + foreach ($propertyName in @($LegacyProperty)) { + if ([string]::IsNullOrWhiteSpace($propertyName)) { + continue + } + + if ($Context.PSObject.Properties.Name -contains $propertyName) { + $legacyValue = $Context.$propertyName + if (Test-EngineFactValuePresent -Value $legacyValue) { + return $legacyValue + } + } + } + + if ($Required) { + throw "Required fact ${Namespace}.${Name} is missing." + } + + if ($PSBoundParameters.ContainsKey('Default')) { + return $Default } return $null } -function Get-CsprojVersions { +function Test-EngineFact { param( [Parameter(Mandatory = $true)] - [string[]]$ProjectFiles + [psobject]$Context, + + [Parameter(Mandatory = $true)] + [string]$Namespace, + + [Parameter(Mandatory = $true)] + [string]$Name, + + [Parameter(Mandatory = $false)] + [string[]]$LegacyProperty ) - Write-Log -Level "INFO" -Message "Reading version(s) from SDK-style project files (projectFiles)..." - $projectVersions = @{} + $value = Get-EngineFact -Context $Context -Namespace $Namespace -Name $Name -LegacyProperty $LegacyProperty + return (Test-EngineFactValuePresent -Value $value) +} - foreach ($projectPath in $ProjectFiles) { - if (-not (Test-Path $projectPath -PathType Leaf)) { - Write-Error "Project file not found at: $projectPath" - exit 1 +function Set-EngineState { + param( + [Parameter(Mandatory = $true)] + [psobject]$Context, + + [Parameter(Mandatory = $true)] + [string]$Name, + + [Parameter(Mandatory = $true)] + [AllowNull()] + $Value + ) + + if ($script:EngineStateAllowlist -notcontains $Name) { + throw "Engine state key '$Name' is not allowlisted. Use Set-EngineFact for plugin outputs, or extend the engine allowlist for documented engine fields." + } + + $Context | Add-Member -NotePropertyName $Name -NotePropertyValue $Value -Force +} + +function Add-EnginePublishCompletion { + param( + [Parameter(Mandatory = $true)] + [psobject]$Context, + + [Parameter(Mandatory = $true)] + [string]$Publisher + ) + + if ([string]::IsNullOrWhiteSpace($Publisher)) { + throw "Publisher name is required." + } + + $completedBy = @(Get-EngineFact -Context $Context -Namespace 'publish' -Name 'completedBy' -Default @()) + if ($completedBy -notcontains $Publisher) { + $completedBy += $Publisher + } + + Set-EngineFact -Context $Context -Namespace 'publish' -Name 'completedBy' -Value $completedBy -Overwrite Replace + Set-EngineFact -Context $Context -Namespace 'publish' -Name 'completed' -Value $true -Overwrite Replace -LegacyProperty 'publishCompleted' +} + +function Get-EngineState { + param( + [Parameter(Mandatory = $true)] + [psobject]$Context, + + [Parameter(Mandatory = $true)] + [string]$Name, + + [Parameter(Mandatory = $false)] + $Default, + + [switch]$Required + ) + + if ($script:EngineStateAllowlist -notcontains $Name) { + throw "Engine state key '$Name' is not allowlisted." + } + + if ($Context.PSObject.Properties.Name -contains $Name) { + $value = $Context.$Name + if (Test-EngineFactValuePresent -Value $value) { + return $value } - - if ([System.IO.Path]::GetExtension($projectPath) -ne ".csproj") { - Write-Error "Configured project file is not a .csproj file: $projectPath" - exit 1 - } - - [xml]$csproj = Get-Content $projectPath - $version = Get-CsprojPropertyValue -Csproj $csproj -PropertyName "Version" - - if (-not $version) { - Write-Error "Version not found in $projectPath" - exit 1 - } - - $projectVersions[$projectPath] = $version - Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($projectPath)): $version" } - return $projectVersions + if ($Required) { + throw "Required engine state '$Name' is missing." + } + + if ($PSBoundParameters.ContainsKey('Default')) { + return $Default + } + + return $null } -function Resolve-DotNetReleaseVersion { - param( - [Parameter(Mandatory = $true)] - [object[]]$Plugins, - - [Parameter(Mandatory = $true)] - [string]$ScriptDir - ) - - $releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'DotNetReleaseVersion' } | Select-Object -First 1) - if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) { - Write-Error "Configure a DotNetReleaseVersion plugin in scriptSettings.json with projectFiles." - exit 1 - } - - $releaseVersionSettings = $releaseVersionPlugin[0] - $projectFiles = @(Resolve-RelativePaths -Value $releaseVersionSettings.projectFiles -BasePath $ScriptDir) - - if ($projectFiles.Count -eq 0) { - Write-Error "Configure release version via DotNetReleaseVersion.projectFiles (first .csproj with )." - exit 1 - } - - $projectVersions = Get-CsprojVersions -ProjectFiles $projectFiles - $version = $projectVersions[$projectFiles[0]] - - return [pscustomobject]@{ - version = $version - source = 'DotNetReleaseVersion' - } -} - -function Resolve-NpmReleaseVersion { - param( - [Parameter(Mandatory = $true)] - [object[]]$Plugins, - - [Parameter(Mandatory = $true)] - [string]$ScriptDir - ) - - $releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'NpmReleaseVersion' } | Select-Object -First 1) - if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) { - Write-Error "Configure an NpmReleaseVersion plugin in scriptSettings.json with packageJsonPath." - exit 1 - } - - $releaseVersionSettings = $releaseVersionPlugin[0] - $packageJsonPaths = @(Resolve-RelativePaths -Value $releaseVersionSettings.packageJsonPath -BasePath $ScriptDir) - - if ($packageJsonPaths.Count -eq 0) { - Write-Error "Configure release version via NpmReleaseVersion.packageJsonPath." - exit 1 - } - - $packageJsonPath = $packageJsonPaths[0] - if (-not (Test-Path $packageJsonPath -PathType Leaf)) { - Write-Error "NpmReleaseVersion: package.json not found at: $packageJsonPath" - exit 1 - } - - Write-Log -Level "INFO" -Message "Reading version from npm package.json (packageJsonPath)..." - $json = Get-Content -Path $packageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json - $version = [string]$json.version - if ([string]::IsNullOrWhiteSpace($version)) { - Write-Error "NpmReleaseVersion: 'version' is missing in '$packageJsonPath'." - exit 1 - } - - if ($version -notmatch '^\d+\.\d+\.\d+') { - Write-Error "NpmReleaseVersion: version '$version' in '$packageJsonPath' is not a valid semver." - exit 1 - } - - Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($packageJsonPath)): $version" - - return [pscustomobject]@{ - version = $version - source = 'NpmReleaseVersion' - } -} - -function Get-VersionFileSemver { - param( - [Parameter(Mandatory = $true)] - [string]$VersionFilePath - ) - - if (-not (Test-Path $VersionFilePath -PathType Leaf)) { - Write-Error "FileReleaseVersion: VERSION file not found at: $VersionFilePath" - exit 1 - } - - $version = (Get-Content -Path $VersionFilePath -Raw -Encoding UTF8).Trim() - if ([string]::IsNullOrWhiteSpace($version)) { - Write-Error "FileReleaseVersion: VERSION file is empty at: $VersionFilePath" - exit 1 - } - - $version = $version -replace '^[vV]', '' - if ($version -notmatch '^\d+\.\d+\.\d+') { - Write-Error "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver." - exit 1 - } - - return $version -} - -function Resolve-FileReleaseVersion { - param( - [Parameter(Mandatory = $true)] - [object[]]$Plugins, - - [Parameter(Mandatory = $true)] - [string]$ScriptDir - ) - - $releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'FileReleaseVersion' } | Select-Object -First 1) - if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) { - Write-Error "Configure a FileReleaseVersion plugin in scriptSettings.json with versionFilePath." - exit 1 - } - - $releaseVersionSettings = $releaseVersionPlugin[0] - $versionFileSetting = if ($releaseVersionSettings.versionFilePath) { - $releaseVersionSettings.versionFilePath - } - else { - '..\\..\\..\\VERSION' - } - - $versionFilePaths = @(Resolve-RelativePaths -Value $versionFileSetting -BasePath $ScriptDir) - if ($versionFilePaths.Count -eq 0) { - Write-Error "Configure release version via FileReleaseVersion.versionFilePath (repo-root VERSION file)." - exit 1 - } - - $versionFilePath = $versionFilePaths[0] - Write-Log -Level "INFO" -Message "Reading version from VERSION file (versionFilePath)..." - $version = Get-VersionFileSemver -VersionFilePath $versionFilePath - Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($versionFilePath)): $version" - - return [pscustomobject]@{ - version = $version - source = 'FileReleaseVersion' - } -} - -function Resolve-ReleaseVersion { - param( - [Parameter(Mandatory = $true)] - [object[]]$Plugins, - - [Parameter(Mandatory = $true)] - [string]$ScriptDir - ) - - $dotnetPlugin = @($Plugins | Where-Object { $_.name -eq 'DotNetReleaseVersion' -and $_.enabled -ne $false }) - $npmPlugin = @($Plugins | Where-Object { $_.name -eq 'NpmReleaseVersion' -and $_.enabled -ne $false }) - $filePlugin = @($Plugins | Where-Object { $_.name -eq 'FileReleaseVersion' -and $_.enabled -ne $false }) - - $enabledVersionPlugins = @() - if ($dotnetPlugin.Count -gt 0) { $enabledVersionPlugins += 'DotNetReleaseVersion' } - if ($npmPlugin.Count -gt 0) { $enabledVersionPlugins += 'NpmReleaseVersion' } - if ($filePlugin.Count -gt 0) { $enabledVersionPlugins += 'FileReleaseVersion' } - - if ($enabledVersionPlugins.Count -gt 1) { - Write-Error "Configure only one release version plugin: $($enabledVersionPlugins -join ', ')." - exit 1 - } - - if ($dotnetPlugin.Count -gt 0) { - return Resolve-DotNetReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir - } - - if ($npmPlugin.Count -gt 0) { - return Resolve-NpmReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir - } - - if ($filePlugin.Count -gt 0) { - return Resolve-FileReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir - } - - Write-Error "Configure a DotNetReleaseVersion (projectFiles), NpmReleaseVersion (packageJsonPath), or FileReleaseVersion (versionFilePath) plugin in scriptSettings.json." - exit 1 -} - -Export-ModuleMember -Function Get-CsprojPropertyValue, Get-CsprojVersions, Get-VersionFileSemver, Resolve-RelativePaths, Resolve-DotNetReleaseVersion, Resolve-NpmReleaseVersion, Resolve-FileReleaseVersion, Resolve-ReleaseVersion - - - +Export-ModuleMember -Function ` + Resolve-RelativePaths, ` + Initialize-EngineFactsBag, ` + Set-EngineFact, ` + Get-EngineFact, ` + Test-EngineFact, ` + Set-EngineState, ` + Add-EnginePublishCompletion, ` + Get-EngineState diff --git a/utils/modules/Engine/Import-EngineModules.ps1 b/utils/modules/Engine/Import-EngineModules.ps1 index 5c43ed7..a557a58 100644 --- a/utils/modules/Engine/Import-EngineModules.ps1 +++ b/utils/modules/Engine/Import-EngineModules.ps1 @@ -32,4 +32,9 @@ function Import-EngineModules { Import-Module $modulePath -Force } + + $extensionImport = Join-Path $modulesDir 'Extensions' 'Import-ExtensionModules.ps1' + if (Test-Path -LiteralPath $extensionImport -PathType Leaf) { + . $extensionImport + } } diff --git a/utils/modules/Engine/PluginSupport.psm1 b/utils/modules/Engine/PluginSupport.psm1 index 8d8c482..f95f58d 100644 --- a/utils/modules/Engine/PluginSupport.psm1 +++ b/utils/modules/Engine/PluginSupport.psm1 @@ -22,7 +22,7 @@ function Test-IsEngineRuntimeModuleName { [string]$ModuleName ) - # Engine runtime under modules/ only — never dual-homed under plugins/. + # Host engine runtime under modules/ (and optional modules/Extensions/) — never dual-homed under plugins/. $engineNames = [System.Collections.Generic.HashSet[string]]::new( [string[]]@( 'ChangelogSupport', @@ -34,11 +34,7 @@ function Test-IsEngineRuntimeModuleName { 'EngineContext', 'PluginSupport', 'ReleaseSupport', - 'TestSupport', - 'DeployConfig', - 'EngineContextSupport', - 'OrchestratorSupport', - 'PluginPathSupport' + 'TestSupport' ), [System.StringComparer]::OrdinalIgnoreCase ) @@ -46,6 +42,34 @@ function Test-IsEngineRuntimeModuleName { return $engineNames.Contains($ModuleName) } +function Get-PluginDependencyGroupDirectories { + param( + [Parameter(Mandatory = $true)] + [string]$PluginsRoot + ) + + if (-not (Test-Path -LiteralPath $PluginsRoot -PathType Container)) { + return @() + } + + # Prefer Shared (helpers), then stock host groups; any other plugins/{Group}/ is discovered. + $preferred = @('Shared', 'Platform', 'DotNet', 'Npm') + $dirs = [System.Collections.Generic.List[string]]::new() + foreach ($name in $preferred) { + $path = Join-Path $PluginsRoot $name + if (Test-Path -LiteralPath $path -PathType Container) { + $dirs.Add($path) + } + } + + Get-ChildItem -LiteralPath $PluginsRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notin $preferred } | + Sort-Object Name | + ForEach-Object { $dirs.Add($_.FullName) } + + return @($dirs) +} + function Import-PluginDependency { param( [Parameter(Mandatory = $true)] @@ -66,16 +90,16 @@ function Import-PluginDependency { $candidatePaths = [System.Collections.Generic.List[string]]::new() if (Test-IsEngineRuntimeModuleName -ModuleName $ModuleName) { - # Engine runtime: modules/ only (no plugins/ fallback). + # Engine runtime: modules/ only (no plugins/ fallback). Optional Extensions/ for layered hosts. $candidatePaths.Add((Join-Path $modulesDir "$ModuleName.psm1")) $candidatePaths.Add((Join-Path $engineModuleDir "$ModuleName.psm1")) $extensionsDir = Join-Path $modulesDir 'Extensions' $candidatePaths.Add((Join-Path $extensionsDir "$ModuleName.psm1")) } else { - # Plugin helpers: plugins/ only (no modules/ legacy shadow). - foreach ($group in @('Shared', 'Platform', 'DotNet', 'Npm', 'Helm', 'Docker', 'Podman')) { - $candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $group) "$ModuleName.psm1")) + # Plugin helpers: plugins/{Group}/ only (no modules/ legacy shadow). Groups are discovered. + foreach ($groupDir in Get-PluginDependencyGroupDirectories -PluginsRoot $pluginsRoot) { + $candidatePaths.Add((Join-Path $groupDir "$ModuleName.psm1")) } } @@ -344,8 +368,8 @@ function Get-RegistryCredentialsFromRuntime { .DESCRIPTION Looks up the environment variable named by SecretName. The value must be - Base64(UTF8('username:password')). Used by Docker/Podman/Helm registry login - and image-pull secret creation — never pass the password itself as a parameter. + Base64(UTF8('username:password')). Used by registry login and image-pull + secret creation — never pass the password itself as a parameter. .PARAMETER SecretName Logical secret name (environment variable name), not a password or token. @@ -671,10 +695,7 @@ function Invoke-ConfiguredPlugin { [psobject]$SharedSettings, [Parameter(Mandatory = $true)] - [string]$EngineDirectory, - - [Parameter(Mandatory = $false)] - [bool]$ContinueOnError = $false + [string]$EngineDirectory ) if (-not (Test-PluginRunnable -Plugin $Plugin -SharedSettings $SharedSettings -EngineDirectory $EngineDirectory -WriteLogs:$true)) { @@ -716,12 +737,16 @@ function Invoke-ConfiguredPlugin { $pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.name)'..." + # Sink plugin success-stream output to the host so it cannot pollute this + # function's return value. Otherwise `return $false` after CLI stdout becomes + # @("helm-line…", $false), which is truthy under `if (-not $result)` and + # causes RELEASE COMPLETE / exit 0 after a failed plugin. try { $moduleInfo = Import-Module $pluginModulePath -Force -PassThru -ErrorAction Stop $invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop $pluginSettings = New-PluginInvocationSettings -Plugin $Plugin -SharedSettings $SharedSettings - & $invokeCommand -Settings $pluginSettings + & $invokeCommand -Settings $pluginSettings | ForEach-Object { Write-Host $_ } Write-Log -Level "OK" -Message " Plugin '$($Plugin.name)' completed." return $true } diff --git a/utils/modules/Engine/ReleaseSupport.psm1 b/utils/modules/Engine/ReleaseSupport.psm1 index 853eb74..0de88e4 100644 --- a/utils/modules/Engine/ReleaseSupport.psm1 +++ b/utils/modules/Engine/ReleaseSupport.psm1 @@ -24,13 +24,101 @@ if (-not (Get-Command Get-PluginStageLabel -ErrorAction SilentlyContinue) -or -n } } -if (-not (Get-Command Resolve-ReleaseVersion -ErrorAction SilentlyContinue)) { +if (-not (Get-Command Resolve-RelativePaths -ErrorAction SilentlyContinue) -or -not (Get-Command Set-EngineState -ErrorAction SilentlyContinue)) { $engineContextModulePath = Join-Path $PSScriptRoot "EngineContext.psm1" if (Test-Path $engineContextModulePath -PathType Leaf) { Import-Module $engineContextModulePath -Force } } +function Get-EnabledVersionPlugins { + param( + [Parameter(Mandatory = $true)] + [object[]]$Plugins, + + [Parameter(Mandatory = $true)] + [string]$ScriptDir + ) + + $versionPlugins = @() + foreach ($plugin in $Plugins) { + if ($null -eq $plugin -or [string]::IsNullOrWhiteSpace([string]$plugin.name)) { + continue + } + + if (-not $plugin.enabled) { + continue + } + + $metadata = Get-PluginMetadataObject -Plugin $plugin -EngineDirectory $ScriptDir + if ($null -eq $metadata) { + continue + } + + if (($metadata.PSObject.Properties.Name -contains 'providesVersion') -and [bool]$metadata.providesVersion) { + $versionPlugins += $plugin + } + } + + return @($versionPlugins) +} + +function Invoke-VersionPlugin { + param( + [Parameter(Mandatory = $true)] + $Plugin, + + [Parameter(Mandatory = $true)] + [psobject]$Context, + + [Parameter(Mandatory = $true)] + [string]$EngineDirectory + ) + + $modulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory + if (-not (Test-Path $modulePath -PathType Leaf)) { + throw "Version plugin '$($Plugin.name)' module not found at: $modulePath" + } + + $moduleInfo = Import-Module $modulePath -Force -PassThru -ErrorAction Stop + $invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop + $pluginSettings = New-PluginInvocationSettings -Plugin $Plugin -SharedSettings $Context + & $invokeCommand -Settings $pluginSettings +} + +function Resolve-EngineContextVersion { + param( + [Parameter(Mandatory = $true)] + [object[]]$Plugins, + + [Parameter(Mandatory = $true)] + [psobject]$Context, + + [Parameter(Mandatory = $true)] + [string]$ScriptDir + ) + + $versionPlugins = @(Get-EnabledVersionPlugins -Plugins $Plugins -ScriptDir $ScriptDir) + if ($versionPlugins.Count -eq 0) { + throw "Configure exactly one enabled release version plugin (declares providesVersion in Get-PluginMetadata), e.g. DotNetReleaseVersion (projectFiles), NpmReleaseVersion (packageJsonPath), or FileReleaseVersion (versionFilePath)." + } + + if ($versionPlugins.Count -gt 1) { + $names = ($versionPlugins | ForEach-Object { [string]$_.name }) -join ', ' + throw "Configure only one enabled release version plugin. Found: $names." + } + + $versionPlugin = $versionPlugins[0] + Invoke-VersionPlugin -Plugin $versionPlugin -Context $Context -EngineDirectory $ScriptDir + + $version = Get-EngineState -Context $Context -Name 'version' + if ($null -eq $version -or [string]::IsNullOrWhiteSpace([string]$version)) { + throw "Version plugin '$($versionPlugin.name)' did not set a version on the engine context." + } + + return [string]$versionPlugin.name +} + function Assert-WorkingTreeClean { $gitStatus = Get-GitStatusShort if (-not [string]::IsNullOrWhiteSpace([string]$gitStatus)) { @@ -48,21 +136,20 @@ function Assert-WorkingTreeClean { function Initialize-ReleaseStageContext { param( - [Parameter(Mandatory = $true)] - [object[]]$RemainingPlugins, - [Parameter(Mandatory = $true)] [psobject]$SharedSettings, [Parameter(Mandatory = $true)] - [string]$ArtifactsDirectory, - - [Parameter(Mandatory = $true)] - [string]$Version + [string]$ArtifactsDirectory ) if (-not $SharedSettings.PSObject.Properties['releaseDir'] -or [string]::IsNullOrWhiteSpace([string]$SharedSettings.releaseDir)) { - $SharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $ArtifactsDirectory -Force + if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) { + Set-EngineState -Context $SharedSettings -Name 'releaseDir' -Value $ArtifactsDirectory + } + else { + $SharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $ArtifactsDirectory -Force + } } } @@ -80,15 +167,10 @@ function New-EngineContext { [Parameter(Mandatory = $false)] [psobject]$Settings, - [switch]$DryRun, - - [ValidateSet('single', 'ha')] - [string]$DeployMode = 'ha' + [Parameter(Mandatory = $false)] + [psobject]$ExtensionData ) - $resolvedVersion = Resolve-ReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir - $version = $resolvedVersion.version - $versionSource = $resolvedVersion.source $releaseRelative = '..\..\..\releases' $artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $releaseRelative)) @@ -116,48 +198,58 @@ function New-EngineContext { $releaseBranches = @('main') } - $isReleaseBranch = $releaseBranches -contains $currentBranch - $isNonReleaseBranch = -not $isReleaseBranch + $isNonReleaseBranch = -not ($releaseBranches -contains $currentBranch) Assert-WorkingTreeClean - $tag = "v$version" - Write-Log -Level "INFO" -Message " Release tag default from ${versionSource}: $tag (ReleasePublishGuard may replace from git when publish is allowed)." - - $dryRun = $false - if ($DryRun) { - $dryRun = $true - } - else { - $dryRun = Get-EngineDryRun -Settings $Settings - } - Write-Log -Level "INFO" -Message " Dry run (remote mutations only): $dryRun" - - $orchestrator = Get-MaksitOrchestrator - if ($orchestrator) { - Write-Log -Level "INFO" -Message " Orchestrator: $orchestrator (plugin profile filtering active)" - } - else { - Write-Log -Level "INFO" -Message " Orchestrator: not set (dev mode — all plugins eligible; engine probe still selects docker vs podman)" - } - - return [pscustomobject]@{ + $context = [pscustomobject]@{ scriptDir = $ScriptDir srcDir = $SrcDir utilsDir = $SrcDir currentBranch = $currentBranch - version = $version - tag = $tag artifactsDirectory = $artifactsDirectory - isReleaseBranch = $isReleaseBranch isNonReleaseBranch = $isNonReleaseBranch releaseBranches = $releaseBranches - publishCompleted = $false skipPublishPlugins = $false - dryRun = $dryRun - deployMode = $DeployMode - orchestrator = $orchestrator + facts = [ordered]@{} } + + $versionSource = Resolve-EngineContextVersion -Plugins $Plugins -Context $context -ScriptDir $ScriptDir + $version = [string](Get-EngineState -Context $context -Name 'version' -Required) + $tag = "v$version" + Set-EngineState -Context $context -Name 'tag' -Value $tag + Write-Log -Level "INFO" -Message " Release tag default from ${versionSource}: $tag (ReleasePublishGuard may replace from git when publish is allowed)." + + $dryRunPlugins = @( + $Plugins | + Where-Object { + $_.enabled -and + ($_.PSObject.Properties.Name -contains 'dryRun') -and + $null -ne $_.dryRun -and + [bool]$_.dryRun -and + (Test-PluginMutatesRemote -Plugin $_ -EngineDirectory $ScriptDir) + } | + ForEach-Object { [string]$_.name } + ) + if ($dryRunPlugins.Count -gt 0) { + Write-Log -Level "INFO" -Message " Plugin dryRun (validate only): $($dryRunPlugins -join ', ')" + } + + $expandContext = Get-Command Expand-ExtensionEngineContext -ErrorAction SilentlyContinue + if ($expandContext) { + $expandParams = @{ + Context = $context + ScriptDir = $ScriptDir + Settings = $Settings + } + if ($PSBoundParameters.ContainsKey('ExtensionData')) { + $expandParams['ExtensionData'] = $ExtensionData + } + + return & $expandContext @expandParams + } + + return $context } function Get-PreferredReleaseBranch { diff --git a/utils/modules/Engine/TestSupport.psm1 b/utils/modules/Engine/TestSupport.psm1 index 03aeb46..b49b60b 100644 --- a/utils/modules/Engine/TestSupport.psm1 +++ b/utils/modules/Engine/TestSupport.psm1 @@ -10,6 +10,13 @@ if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) { } } +if (-not (Get-Command Initialize-EngineFactsBag -ErrorAction SilentlyContinue)) { + $engineContextModulePath = Join-Path $PSScriptRoot "EngineContext.psm1" + if (Test-Path $engineContextModulePath -PathType Leaf) { + Import-Module $engineContextModulePath -Force + } +} + function New-EngineContext { param( [Parameter(Mandatory = $true)] @@ -21,22 +28,32 @@ function New-EngineContext { [Parameter(Mandatory = $false)] [psobject]$Settings, - [ValidateSet('single', 'ha')] - [string]$DeployMode = 'ha' + [Parameter(Mandatory = $false)] + [psobject]$ExtensionData ) - $badgesDir = $null - if ($Settings -and $Settings.PSObject.Properties['paths'] -and $Settings.paths.badgesDir) { - $badgesDir = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir ([string]$Settings.paths.badgesDir))) - } - - return [pscustomobject]@{ + $context = [pscustomobject]@{ scriptDir = $ScriptDir srcDir = $SrcDir utilsDir = $SrcDir - badgesDir = $badgesDir - deployMode = $DeployMode + facts = [ordered]@{} } + + $expandContext = Get-Command Expand-ExtensionEngineContext -ErrorAction SilentlyContinue + if ($expandContext) { + $expandParams = @{ + Context = $context + ScriptDir = $ScriptDir + Settings = $Settings + } + if ($PSBoundParameters.ContainsKey('ExtensionData')) { + $expandParams['ExtensionData'] = $ExtensionData + } + + return & $expandContext @expandParams + } + + return $context } Export-ModuleMember -Function New-EngineContext diff --git a/utils/modules/ExternalCommandSupport.psm1 b/utils/modules/ExternalCommandSupport.psm1 index b6c05f1..6c86899 100644 --- a/utils/modules/ExternalCommandSupport.psm1 +++ b/utils/modules/ExternalCommandSupport.psm1 @@ -1,6 +1,16 @@ #requires -Version 7.0 #requires -PSEdition Core +<# + Runs native CLIs (dotnet, git, helm, …) and keeps $LASTEXITCODE intact. + By default throws on non-zero exit so callers cannot forget to check. + Pass -ThrowOnError:$false when you need the exit code / output yourself + (e.g. TestRunner Success objects, logging full container build output first). + + Test hooks: Set-ExternalCommandTestHandler / Set-ExternalCommandAvailability + let Pester stub CLIs without touching PATH. +#> + $script:ExternalCommandTestHandler = $null $script:ExternalCommandAvailability = @{} @@ -40,7 +50,10 @@ function Invoke-ExternalCommand { [string]$InputObject, - [switch]$MergeErrorOutput + [switch]$MergeErrorOutput, + + # Default true: fail fast. Soft callers (tests, nested loggers) pass $false. + [bool]$ThrowOnError = $true ) $previousLocation = $null @@ -51,6 +64,7 @@ function Invoke-ExternalCommand { try { $effectiveWorkingDirectory = (Get-Location).Path + $output = @() if ($null -ne $script:ExternalCommandTestHandler) { $handlerResult = & $script:ExternalCommandTestHandler ` @@ -62,31 +76,46 @@ function Invoke-ExternalCommand { $global:LASTEXITCODE = [int]$handlerResult.ExitCode if ($null -eq $handlerResult.Output) { - return @() + $output = @() } - - if ($handlerResult.Output -is [System.Collections.IEnumerable] -and -not ($handlerResult.Output -is [string])) { - return @($handlerResult.Output) + elseif ($handlerResult.Output -is [System.Collections.IEnumerable] -and -not ($handlerResult.Output -is [string])) { + $output = @($handlerResult.Output) + } + else { + $output = @([string]$handlerResult.Output) } - - return @([string]$handlerResult.Output) - } - - if ($script:ExternalCommandAvailability.ContainsKey($Name) -and -not $script:ExternalCommandAvailability[$Name]) { - throw "External command '$Name' is marked unavailable." - } - - if (-not [string]::IsNullOrWhiteSpace($InputObject)) { - $output = $InputObject | & $Name @ArgumentList 2>&1 - } - elseif ($MergeErrorOutput) { - $output = & $Name @ArgumentList 2>&1 } else { - $output = & $Name @ArgumentList + if ($script:ExternalCommandAvailability.ContainsKey($Name) -and -not $script:ExternalCommandAvailability[$Name]) { + throw "External command '$Name' is marked unavailable." + } + + if (-not [string]::IsNullOrWhiteSpace($InputObject)) { + $raw = $InputObject | & $Name @ArgumentList 2>&1 + } + elseif ($MergeErrorOutput) { + $raw = & $Name @ArgumentList 2>&1 + } + else { + $raw = & $Name @ArgumentList + } + + $output = @($raw) } - return @($output) + $exitCode = [int]$global:LASTEXITCODE + if ($ThrowOnError -and $exitCode -ne 0) { + $preview = ($output | ForEach-Object { + if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { [string]$_ } + } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 8) -join ' ' + if ([string]::IsNullOrWhiteSpace($preview)) { + throw "External command '$Name' failed with exit code $exitCode." + } + + throw "External command '$Name' failed with exit code $exitCode. $preview" + } + + return $output } finally { if ($null -ne $previousLocation) { diff --git a/utils/modules/GitTools.psm1 b/utils/modules/GitTools.psm1 index be56c94..8677946 100644 --- a/utils/modules/GitTools.psm1 +++ b/utils/modules/GitTools.psm1 @@ -51,7 +51,33 @@ function Invoke-GitInternal { [string]$ErrorMessage = "Git command failed" ) + if (-not (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue)) { + $srcDir = Split-Path $PSScriptRoot -Parent + $externalModule = Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1' + if (Test-Path -LiteralPath $externalModule -PathType Leaf) { + Import-Module $externalModule -Global + } + elseif (Test-Path -LiteralPath (Join-Path $srcDir 'modules' 'ExternalCommandSupport.psm1') -PathType Leaf) { + Import-Module (Join-Path $srcDir 'modules' 'ExternalCommandSupport.psm1') -Global + } + } + if ($CaptureOutput) { + if (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue) { + $output = Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput -ThrowOnError:$false + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + Write-Error "$ErrorMessage (exit code: $exitCode)" + exit 1 + } + + if ($null -eq $output) { + return "" + } + + return ($output -join "`n").Trim() + } + $output = & git @Arguments 2>&1 $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { @@ -66,7 +92,13 @@ function Invoke-GitInternal { return ($output -join "`n").Trim() } - & git @Arguments + if (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue) { + Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput -ThrowOnError:$false | Out-Null + } + else { + & git @Arguments + } + $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { Write-Error "$ErrorMessage (exit code: $exitCode)" diff --git a/utils/modules/ScriptConfig.psm1 b/utils/modules/ScriptConfig.psm1 index b8895b1..3cdbb35 100644 --- a/utils/modules/ScriptConfig.psm1 +++ b/utils/modules/ScriptConfig.psm1 @@ -7,25 +7,11 @@ function Get-ScriptSettings { [Parameter(Mandatory = $true)] [string]$ScriptDir, - [ValidateSet('single', 'ha')] - [string]$Mode, - [Parameter(Mandatory = $false)] [string]$SettingsFileName = 'scriptSettings.json' ) - $settingsPath = if ($PSBoundParameters.ContainsKey('Mode')) { - $modePath = Join-Path $ScriptDir "scriptSettings.$Mode.json" - if (Test-Path -LiteralPath $modePath -PathType Leaf) { - $modePath - } - else { - Join-Path $ScriptDir $SettingsFileName - } - } - else { - Join-Path $ScriptDir $SettingsFileName - } + $settingsPath = Join-Path $ScriptDir $SettingsFileName if (-not (Test-Path -LiteralPath $settingsPath -PathType Leaf)) { throw "Settings file not found: $settingsPath" @@ -34,92 +20,6 @@ function Get-ScriptSettings { return Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json } -function Get-HelmSelfDeployPluginFromSettings { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [psobject]$Settings - ) - - if (-not ($Settings.PSObject.Properties.Name -contains 'plugins') -or -not $Settings.plugins) { - return $null - } - - foreach ($plugin in @($Settings.plugins)) { - if ($plugin.name -eq 'HelmSelfDeploy') { - return $plugin - } - } - - return $null -} - -function Get-DeployModeFromSettings { - <# - .SYNOPSIS - Resolves cluster deploy profile from HelmSelfDeploy plugin settings or an explicit CLI override. - - .DESCRIPTION - CI/CD pipelines omit -Mode and read deployMode on the HelmSelfDeploy plugin. - Manual installs use Invoke-ReleasePackage-Single.bat / Invoke-ReleasePackage-HA.bat. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [psobject]$Settings, - - [ValidateSet('single', 'ha')] - [string]$ModeOverride - ) - - if ($PSBoundParameters.ContainsKey('ModeOverride')) { - return $ModeOverride - } - - $helmSelfDeploy = Get-HelmSelfDeployPluginFromSettings -Settings $Settings - if ($null -ne $helmSelfDeploy -and $helmSelfDeploy.PSObject.Properties.Name -contains 'deployMode') { - $mode = [string]$helmSelfDeploy.deployMode - if (-not [string]::IsNullOrWhiteSpace($mode)) { - $mode = $mode.Trim().ToLowerInvariant() - if ($mode -in @('single', 'ha')) { - return $mode - } - - throw "HelmSelfDeploy.deployMode must be 'single' or 'ha' (got '$mode')." - } - } - - return 'ha' -} - -function Resolve-DeployValuesFilePath { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$DeploySettingsDir, - - [Parameter(Mandatory = $true)] - [string]$ValuesFile, - - [Parameter(Mandatory = $true)] - [ValidateSet('single', 'ha')] - [string]$DeployMode - ) - - $baseName = [System.IO.Path]::GetFileNameWithoutExtension($ValuesFile) - $extension = [System.IO.Path]::GetExtension($ValuesFile) - if ([string]::IsNullOrEmpty($extension)) { - $extension = '.yaml' - } - - $valuesPath = Join-Path $DeploySettingsDir "$baseName.$DeployMode$extension" - if (Test-Path -LiteralPath $valuesPath -PathType Leaf) { - return $valuesPath - } - - throw "Deploy values file not found: '$valuesPath'. HelmSelfDeploy expects values.single.yaml or values.ha.yaml beside scriptSettings.json." -} - function Assert-Command { [CmdletBinding()] param( @@ -132,4 +32,4 @@ function Assert-Command { } } -Export-ModuleMember -Function Get-ScriptSettings, Get-DeployModeFromSettings, Resolve-DeployValuesFilePath, Assert-Command +Export-ModuleMember -Function Get-ScriptSettings, Assert-Command diff --git a/utils/modules/TestRunner.psm1 b/utils/modules/TestRunner.psm1 index b1935e2..aac3308 100644 --- a/utils/modules/TestRunner.psm1 +++ b/utils/modules/TestRunner.psm1 @@ -19,10 +19,8 @@ function Import-ExternalCommandSupportInternal { return } - $srcDir = Split-Path $PSScriptRoot -Parent $candidates = @( - (Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1'), - (Join-Path $srcDir 'plugins' 'Shared' 'ExternalCommandSupport.psm1') + (Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1') ) foreach ($modulePath in $candidates) { if (Test-Path -LiteralPath $modulePath -PathType Leaf) { @@ -175,10 +173,10 @@ function Invoke-TestsWithCoverage { Import-ExternalCommandSupportInternal if ($Silent) { - $null = Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -MergeErrorOutput + $null = Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -MergeErrorOutput -ThrowOnError:$false } else { - Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs | Out-Default + Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -ThrowOnError:$false | Out-Default } $testExitCode = $LASTEXITCODE @@ -356,10 +354,10 @@ function Invoke-NpmJestTestsWithCoverage { $npmArgs = @('run', $TestScript, '--', '--coverage', '--coverageReporters=json-summary', '--coverageReporters=text') Import-ExternalCommandSupportInternal if ($Silent) { - $null = Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -MergeErrorOutput + $null = Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -MergeErrorOutput -ThrowOnError:$false } else { - Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs | Out-Default + Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -ThrowOnError:$false | Out-Default } if ($LASTEXITCODE -ne 0) { diff --git a/utils/plugins/DotNet/DotNetCreateArchive.psm1 b/utils/plugins/DotNet/DotNetCreateArchive.psm1 index 92b34bd..e921189 100644 --- a/utils/plugins/DotNet/DotNetCreateArchive.psm1 +++ b/utils/plugins/DotNet/DotNetCreateArchive.psm1 @@ -26,6 +26,7 @@ function Invoke-Plugin { ) Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact" $pluginSettings = $Settings $sharedSettings = $Settings.context @@ -33,13 +34,18 @@ function Invoke-Plugin { $version = $sharedSettings.version $archiveInputs = @() - if ($sharedSettings.PSObject.Properties['releaseArchiveInputs'] -and $sharedSettings.releaseArchiveInputs) { - $archiveInputs = @($sharedSettings.releaseArchiveInputs) + $fromFact = Get-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -LegacyProperty @('releaseArchiveInputs') + if ($null -ne $fromFact) { + $archiveInputs = @($fromFact) } - elseif ($sharedSettings.PSObject.Properties['packageFile'] -and $sharedSettings.packageFile) { - $archiveInputs = @($sharedSettings.packageFile.FullName) - if ($sharedSettings.PSObject.Properties['symbolsPackageFile'] -and $sharedSettings.symbolsPackageFile) { - $archiveInputs += $sharedSettings.symbolsPackageFile.FullName + else { + $packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile') + if ($null -ne $packageFile) { + $archiveInputs = @($packageFile.FullName) + $symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile') + if ($null -ne $symbolsPackageFile) { + $archiveInputs += $symbolsPackageFile.FullName + } } } @@ -79,16 +85,18 @@ function Invoke-Plugin { Write-Log -Level "OK" -Message " Release archive ready: $zipPath" $releaseAssetPaths = @($zipPath) - if ($sharedSettings.PSObject.Properties['packageFile'] -and $sharedSettings.packageFile) { - $releaseAssetPaths += $sharedSettings.packageFile.FullName + $packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile') + if ($null -ne $packageFile) { + $releaseAssetPaths += $packageFile.FullName } - if ($sharedSettings.PSObject.Properties['symbolsPackageFile'] -and $sharedSettings.symbolsPackageFile) { - $releaseAssetPaths += $sharedSettings.symbolsPackageFile.FullName + $symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile') + if ($null -ne $symbolsPackageFile) { + $releaseAssetPaths += $symbolsPackageFile.FullName } - $sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force - $sharedSettings | Add-Member -NotePropertyName releaseArchivePath -NotePropertyValue $zipPath -Force - $sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $releaseAssetPaths -Force + Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory + Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archivePath' -Value $zipPath -Overwrite Replace -LegacyProperty 'releaseArchivePath' + Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths' } Export-ModuleMember -Function Invoke-Plugin diff --git a/utils/plugins/DotNet/DotNetNuGet.psm1 b/utils/plugins/DotNet/DotNetNuGet.psm1 index 535fae4..ef08a81 100644 --- a/utils/plugins/DotNet/DotNetNuGet.psm1 +++ b/utils/plugins/DotNet/DotNetNuGet.psm1 @@ -76,7 +76,8 @@ function Invoke-Plugin { } Write-Log -Level "OK" -Message " NuGet push completed." - $sharedSettings | Add-Member -NotePropertyName publishCompleted -NotePropertyValue $true -Force + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-EnginePublishCompletion" + Add-EnginePublishCompletion -Context $sharedSettings -Publisher 'DotNetNuGet' } function Get-PluginMetadata { diff --git a/utils/plugins/DotNet/DotNetPack.psm1 b/utils/plugins/DotNet/DotNetPack.psm1 index bffc191..5929045 100644 --- a/utils/plugins/DotNet/DotNetPack.psm1 +++ b/utils/plugins/DotNet/DotNetPack.psm1 @@ -30,7 +30,8 @@ function Invoke-Plugin { Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command" - Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact" + Import-PluginDependency -ModuleName "DotNetArtifactSupport" -RequiredCommand "Resolve-DotNetPackageArtifacts" $sharedSettings = $Settings.context $scriptDir = $sharedSettings.scriptDir @@ -48,12 +49,12 @@ function Invoke-Plugin { if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) { $artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir))) + Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory + Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory } else { $artifactsDirectory = $sharedSettings.artifactsDirectory } - $packageProjectPath = $null - $releaseArchiveInputs = @() Assert-Command dotnet @@ -79,50 +80,19 @@ function Invoke-Plugin { throw "dotnet pack failed for $packageProjectPath." } - # dotnet pack can leave older packages in the artifacts directory. - # Pick the newest file matching the current version rather than assuming a clean folder. - $packageFile = $null - $newestNupkgWrite = [datetime]::MinValue - $nupkgCandidates = Get-ChildItem -Path $outputDir -Filter "*.nupkg" - foreach ($candidate in $nupkgCandidates) { - if (($candidate.Name -like "*$version*.nupkg") -and ($candidate.Name -notlike "*.symbols.nupkg") -and ($candidate.Name -notlike "*.snupkg")) { - if ($candidate.LastWriteTime -gt $newestNupkgWrite) { - $newestNupkgWrite = $candidate.LastWriteTime - $packageFile = $candidate - } - } - } + $resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $outputDir -Version $version - if (-not $packageFile) { - throw "Could not locate generated NuGet package for version $version in: $outputDir" - } - - Write-Log -Level "OK" -Message " Package ready: $($packageFile.FullName)" - $releaseArchiveInputs = @($packageFile.FullName) - - $symbolsPackageFile = $null - $newestSnupkgWrite = [datetime]::MinValue - $snupkgCandidates = Get-ChildItem -Path $outputDir -Filter "*.snupkg" - foreach ($candidate in $snupkgCandidates) { - if ($candidate.Name -like "*$version*.snupkg") { - if ($candidate.LastWriteTime -gt $newestSnupkgWrite) { - $newestSnupkgWrite = $candidate.LastWriteTime - $symbolsPackageFile = $candidate - } - } - } - - if ($symbolsPackageFile) { - Write-Log -Level "OK" -Message " Symbols package ready: $($symbolsPackageFile.FullName)" - $releaseArchiveInputs += $symbolsPackageFile.FullName + Write-Log -Level "OK" -Message " Package ready: $($resolved.PackageFile.FullName)" + if ($resolved.SymbolsPackageFile) { + Write-Log -Level "OK" -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)" } else { Write-Log -Level "WARN" -Message " Symbols package (.snupkg) not found for version $version." } - $sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $packageFile -Force - $sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $symbolsPackageFile -Force - $sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $releaseArchiveInputs -Force + Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile' + Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile' + Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs' } Export-ModuleMember -Function Invoke-Plugin diff --git a/utils/plugins/DotNet/DotNetPublish.psm1 b/utils/plugins/DotNet/DotNetPublish.psm1 index 84c4ec2..d2dacba 100644 --- a/utils/plugins/DotNet/DotNetPublish.psm1 +++ b/utils/plugins/DotNet/DotNetPublish.psm1 @@ -27,18 +27,21 @@ function Invoke-Plugin { Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact" $sharedSettings = $Settings.context - $projectFiles = $sharedSettings.projectFiles + $projectFiles = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles') $artifactsDirectory = $sharedSettings.artifactsDirectory $publishProjectPath = $null Assert-Command dotnet - if (-not $sharedSettings.PSObject.Properties['projectFiles'] -or $projectFiles.Count -eq 0) { + if ($null -eq $projectFiles -or @($projectFiles).Count -eq 0) { throw "DotNetPublish plugin requires project files in the shared context." } + $projectFiles = @($projectFiles) + if (!(Test-Path $artifactsDirectory)) { New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null } @@ -64,9 +67,9 @@ function Invoke-Plugin { Write-Log -Level "OK" -Message " Published artifact ready: $publishDir" - $sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $null -Force - $sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $null -Force - $sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue @($publishDir) -Force + Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $null -Overwrite Replace -LegacyProperty 'packageFile' + Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $null -Overwrite Replace -LegacyProperty 'symbolsPackageFile' + Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($publishDir) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs' } Export-ModuleMember -Function Invoke-Plugin diff --git a/utils/plugins/DotNet/DotNetReleaseVersion.psm1 b/utils/plugins/DotNet/DotNetReleaseVersion.psm1 index 66ac1b6..3cb01fd 100644 --- a/utils/plugins/DotNet/DotNetReleaseVersion.psm1 +++ b/utils/plugins/DotNet/DotNetReleaseVersion.psm1 @@ -3,11 +3,13 @@ <# .SYNOPSIS - Loads release version into shared context. + Loads release version from an SDK-style .csproj into shared context. .DESCRIPTION - Dedicated version-loading plugin. It reads .csproj version via - EngineContext helpers and writes Version into the shared runtime context. + Dedicated version-loading plugin. Reads from the first configured + projectFiles entry and writes it (plus the resolved projectFiles) to the + shared runtime context. Declares providesVersion = $true so the engine can + discover it as the single release version source. #> if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) { @@ -18,6 +20,56 @@ if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) { } } +function Get-CsprojPropertyValueInternal { + param( + [Parameter(Mandatory = $true)] + [xml]$Csproj, + + [Parameter(Mandatory = $true)] + [string]$PropertyName + ) + + # SDK-style .csproj files can have multiple PropertyGroup nodes. + # Use the first group that defines the requested property. + $propNode = $Csproj.Project.PropertyGroup | + Where-Object { $_.$PropertyName } | + Select-Object -First 1 + + if ($propNode) { + return $propNode.$PropertyName + } + + return $null +} + +function Get-CsprojVersionInternal { + param( + [Parameter(Mandatory = $true)] + [string]$ProjectPath + ) + + if (-not (Test-Path $ProjectPath -PathType Leaf)) { + throw "DotNetReleaseVersion: project file not found at '$ProjectPath'." + } + + if ([System.IO.Path]::GetExtension($ProjectPath) -ne ".csproj") { + throw "DotNetReleaseVersion: configured project file is not a .csproj file: '$ProjectPath'." + } + + [xml]$csproj = Get-Content $ProjectPath + $version = Get-CsprojPropertyValueInternal -Csproj $csproj -PropertyName "Version" + + if ([string]::IsNullOrWhiteSpace([string]$version)) { + throw "DotNetReleaseVersion: not found in '$ProjectPath'." + } + + return [string]$version +} + +function Get-PluginMetadata { + return [pscustomobject]@{ providesVersion = $true } +} + function Invoke-Plugin { param( [Parameter(Mandatory = $true)] @@ -25,17 +77,20 @@ function Invoke-Plugin { ) Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" - Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-DotNetReleaseVersion" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState" $shared = $Settings.context - $resolved = Resolve-DotNetReleaseVersion -Plugins @($Settings) -ScriptDir $shared.scriptDir $projectFiles = @(Resolve-RelativePaths -Value $Settings.projectFiles -BasePath $shared.scriptDir) + if ($projectFiles.Count -eq 0) { + throw "DotNetReleaseVersion plugin requires 'projectFiles' (first .csproj with ) in scriptSettings.json." + } - $shared | Add-Member -NotePropertyName version -NotePropertyValue $resolved.version -Force - $shared | Add-Member -NotePropertyName projectFiles -NotePropertyValue $projectFiles -Force - Write-Log -Level "OK" -Message " Release version loaded by DotNetReleaseVersion plugin: $($shared.version)" + Write-Log -Level "INFO" -Message "Reading version from SDK-style project file (projectFiles)..." + $version = Get-CsprojVersionInternal -ProjectPath $projectFiles[0] + + Set-EngineState -Context $shared -Name 'version' -Value $version + Set-EngineFact -Context $shared -Namespace 'dotnet' -Name 'projectFiles' -Value $projectFiles -Overwrite Replace -LegacyProperty 'projectFiles' + Write-Log -Level "OK" -Message " Release version loaded by DotNetReleaseVersion plugin: $version" } -Export-ModuleMember -Function Invoke-Plugin - - +Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata diff --git a/utils/plugins/Npm/NpmJestTest.psm1 b/utils/plugins/Npm/NpmJestTest.psm1 index 82803c7..e89f604 100644 --- a/utils/plugins/Npm/NpmJestTest.psm1 +++ b/utils/plugins/Npm/NpmJestTest.psm1 @@ -58,20 +58,12 @@ function Invoke-Plugin { throw "Tests failed. $($testResult.Error)" } - $sharedSettings | Add-Member -NotePropertyName npmWorkspaceRoot -NotePropertyValue $workspaceRoot -Force - $sharedSettings | Add-Member -NotePropertyName testResult -NotePropertyValue $testResult -Force - $sharedSettings | Add-Member -NotePropertyName qualityLineCoverage -NotePropertyValue $testResult.LineRate -Force - $sharedSettings | Add-Member -NotePropertyName coverageLineRate -NotePropertyValue $testResult.LineRate -Force - $sharedSettings | Add-Member -NotePropertyName coverageBranchRate -NotePropertyValue $testResult.BranchRate -Force - $sharedSettings | Add-Member -NotePropertyName coverageMethodRate -NotePropertyValue $testResult.MethodRate -Force - $sharedSettings | Add-Member -NotePropertyName coverageTotalMethods -NotePropertyValue $testResult.TotalMethods -Force - $sharedSettings | Add-Member -NotePropertyName coverageCoveredMethods -NotePropertyValue $testResult.CoveredMethods -Force - - if (($testResult.PSObject.Properties.Name -contains 'ResultsDirectory') -and $testResult.ResultsDirectory) { - $sharedSettings | Add-Member -NotePropertyName testResultsDirectory -NotePropertyValue $testResult.ResultsDirectory -Force - } + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact" + Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Publish-CoverageMetricsToSharedContext" + Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'workspaceRoot' -Value $workspaceRoot -Overwrite Replace -LegacyProperty 'npmWorkspaceRoot' + Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult if (($testResult.PSObject.Properties.Name -contains 'CoverageSummaryFile') -and $testResult.CoverageSummaryFile) { - $sharedSettings | Add-Member -NotePropertyName coverageSummaryFile -NotePropertyValue $testResult.CoverageSummaryFile -Force + Set-EngineFact -Context $sharedSettings -Namespace 'test' -Name 'coverageSummaryFile' -Value $testResult.CoverageSummaryFile -Overwrite Replace -LegacyProperty 'coverageSummaryFile' } Write-Log -Level "OK" -Message " All tests passed!" diff --git a/utils/plugins/Npm/NpmPack.psm1 b/utils/plugins/Npm/NpmPack.psm1 index f8350ba..7ec9945 100644 --- a/utils/plugins/Npm/NpmPack.psm1 +++ b/utils/plugins/Npm/NpmPack.psm1 @@ -27,7 +27,7 @@ function Invoke-Plugin { Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command" - Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact" $pluginSettings = $Settings $shared = $Settings.context @@ -49,6 +49,7 @@ function Invoke-Plugin { $artifactsDirectory = $null if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) { $artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $shared.scriptDir ([string]$pluginSettings.artifactsDir))) + Set-EngineState -Context $shared -Name 'artifactsDirectory' -Value $artifactsDirectory } elseif ($shared.PSObject.Properties['artifactsDirectory'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.artifactsDirectory)) { $artifactsDirectory = [string]$shared.artifactsDirectory @@ -125,10 +126,11 @@ function Invoke-Plugin { Pop-Location } - $shared | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force - $shared | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $releaseAssetPaths -Force + Set-EngineState -Context $shared -Name 'releaseDir' -Value $artifactsDirectory + Set-EngineFact -Context $shared -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths' if ($releaseAssetPaths.Count -gt 0) { - $shared | Add-Member -NotePropertyName packageFile -NotePropertyValue (Get-Item -LiteralPath $releaseAssetPaths[0]) -Force + $packageItem = Get-Item -LiteralPath $releaseAssetPaths[0] + Set-EngineFact -Context $shared -Namespace 'npm' -Name 'packageFile' -Value $packageItem -Overwrite Replace -LegacyProperty 'packageFile' } Write-Log -Level "OK" -Message " npm pack completed ($($releaseAssetPaths.Count) tarball(s))." diff --git a/utils/plugins/Npm/NpmPublish.psm1 b/utils/plugins/Npm/NpmPublish.psm1 index 146fe85..db6b0ec 100644 --- a/utils/plugins/Npm/NpmPublish.psm1 +++ b/utils/plugins/Npm/NpmPublish.psm1 @@ -127,7 +127,8 @@ registry=$registry } Write-Log -Level "OK" -Message " npm publish completed." - $shared | Add-Member -NotePropertyName publishCompleted -NotePropertyValue $true -Force + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-EnginePublishCompletion" + Add-EnginePublishCompletion -Context $shared -Publisher 'NpmPublish' } finally { if (Test-Path $tempNpmRcPath -PathType Leaf) { diff --git a/utils/plugins/Npm/NpmReleaseVersion.psm1 b/utils/plugins/Npm/NpmReleaseVersion.psm1 index 3020c78..ad48e46 100644 --- a/utils/plugins/Npm/NpmReleaseVersion.psm1 +++ b/utils/plugins/Npm/NpmReleaseVersion.psm1 @@ -57,6 +57,10 @@ function Set-PackageJsonVersionInternal { ($json | ConvertTo-Json -Depth 100) + [Environment]::NewLine | Set-Content -Path $PackageJsonPath -Encoding UTF8 -NoNewline } +function Get-PluginMetadata { + return [pscustomobject]@{ providesVersion = $true } +} + function Invoke-Plugin { param( [Parameter(Mandatory = $true)] @@ -64,7 +68,7 @@ function Invoke-Plugin { ) Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" - Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState" $pluginSettings = $Settings $shared = $Settings.context @@ -90,10 +94,11 @@ function Invoke-Plugin { Write-Log -Level "OK" -Message " Synchronized workspace package versions to $version." } - $shared | Add-Member -NotePropertyName version -NotePropertyValue $version -Force - $shared | Add-Member -NotePropertyName npmWorkspaceRoot -NotePropertyValue (Split-Path -Parent $packageJsonPath) -Force - $shared | Add-Member -NotePropertyName npmPackageJsonPath -NotePropertyValue $packageJsonPath -Force + $npmWorkspaceRoot = Split-Path -Parent $packageJsonPath + Set-EngineState -Context $shared -Name 'version' -Value $version + Set-EngineFact -Context $shared -Namespace 'npm' -Name 'workspaceRoot' -Value $npmWorkspaceRoot -Overwrite Replace -LegacyProperty 'npmWorkspaceRoot' + Set-EngineFact -Context $shared -Namespace 'npm' -Name 'packageJsonPath' -Value $packageJsonPath -Overwrite Replace -LegacyProperty 'npmPackageJsonPath' Write-Log -Level "OK" -Message " Release version loaded by NpmReleaseVersion plugin: $version" } -Export-ModuleMember -Function Invoke-Plugin +Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata diff --git a/utils/plugins/Platform/FileReleaseVersion.psm1 b/utils/plugins/Platform/FileReleaseVersion.psm1 index e0b7457..c7bb3bd 100644 --- a/utils/plugins/Platform/FileReleaseVersion.psm1 +++ b/utils/plugins/Platform/FileReleaseVersion.psm1 @@ -6,8 +6,10 @@ Loads release version from a repo-root VERSION file into shared context. .DESCRIPTION - Reads a single-line semver from the configured versionFilePath (default repo-root VERSION). - Used by container-only repos without .csproj or package.json. + Reads a single-line semver from the configured versionFilePath (default + repo-root VERSION). Useful for repositories without .csproj or package.json + version metadata. Declares providesVersion = $true so the engine can + discover it as the single release version source. #> if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) { @@ -18,6 +20,33 @@ if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) { } } +function Get-VersionFileSemverInternal { + param( + [Parameter(Mandatory = $true)] + [string]$VersionFilePath + ) + + if (-not (Test-Path $VersionFilePath -PathType Leaf)) { + throw "FileReleaseVersion: VERSION file not found at '$VersionFilePath'." + } + + $version = (Get-Content -Path $VersionFilePath -Raw -Encoding UTF8).Trim() + if ([string]::IsNullOrWhiteSpace($version)) { + throw "FileReleaseVersion: VERSION file is empty at '$VersionFilePath'." + } + + $version = $version -replace '^[vV]', '' + if ($version -notmatch '^\d+\.\d+\.\d+') { + throw "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver." + } + + return $version +} + +function Get-PluginMetadata { + return [pscustomobject]@{ providesVersion = $true } +} + function Invoke-Plugin { param( [Parameter(Mandatory = $true)] @@ -25,17 +54,28 @@ function Invoke-Plugin { ) Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" - Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-FileReleaseVersion" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState" $shared = $Settings.context - $resolved = Resolve-FileReleaseVersion -Plugins @($Settings) -ScriptDir $shared.scriptDir - $versionFilePaths = @(Resolve-RelativePaths -Value $Settings.versionFilePath -BasePath $shared.scriptDir) - - $shared | Add-Member -NotePropertyName version -NotePropertyValue $resolved.version -Force - if ($versionFilePaths.Count -gt 0) { - $shared | Add-Member -NotePropertyName versionFilePath -NotePropertyValue $versionFilePaths[0] -Force + $versionFileSetting = if ($Settings.versionFilePath) { + $Settings.versionFilePath } - Write-Log -Level "OK" -Message " Release version loaded by FileReleaseVersion plugin: $($shared.version)" + else { + '..\..\..\VERSION' + } + + $versionFilePaths = @(Resolve-RelativePaths -Value $versionFileSetting -BasePath $shared.scriptDir) + if ($versionFilePaths.Count -eq 0) { + throw "FileReleaseVersion plugin requires 'versionFilePath' (repo-root VERSION file) in scriptSettings.json." + } + + $versionFilePath = $versionFilePaths[0] + Write-Log -Level "INFO" -Message "Reading version from VERSION file (versionFilePath)..." + $version = Get-VersionFileSemverInternal -VersionFilePath $versionFilePath + + Set-EngineState -Context $shared -Name 'version' -Value $version + Set-EngineFact -Context $shared -Namespace 'release' -Name 'versionFilePath' -Value $versionFilePath -Overwrite Replace -LegacyProperty 'versionFilePath' + Write-Log -Level "OK" -Message " Release version loaded by FileReleaseVersion plugin: $version" } -Export-ModuleMember -Function Invoke-Plugin +Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata diff --git a/utils/plugins/Platform/PesterTest.psm1 b/utils/plugins/Platform/PesterTest.psm1 index 1dbc1e4..1fa1942 100644 --- a/utils/plugins/Platform/PesterTest.psm1 +++ b/utils/plugins/Platform/PesterTest.psm1 @@ -6,7 +6,7 @@ Pester test plugin for the RepoUtils test engine. .DESCRIPTION - Runs the community Pester suite and publishes normalized coverage metrics on the + Runs the RepoUtils Pester suite and publishes normalized coverage metrics on the shared engine context for QualityGate. #> diff --git a/utils/plugins/Platform/QualityGate.psm1 b/utils/plugins/Platform/QualityGate.psm1 index da18d38..a81dfde 100644 --- a/utils/plugins/Platform/QualityGate.psm1 +++ b/utils/plugins/Platform/QualityGate.psm1 @@ -10,9 +10,9 @@ shared engine context (same object passed to every plugin as .context). Line coverage for threshold checks is resolved in order (first present wins): - - qualityLineCoverage (generic; any plugin may set this) - - coverageLineRate (conventional flat metric) - - testResult.LineRate (object from a test plugin; property name is conventional) + - facts test.coverageLineRate (via Get-EngineFact) + - legacy qualityLineCoverage / coverageLineRate flat properties + - testResult.LineRate (object from a test plugin) Configure coverageThreshold > 0 to require one of those inputs. With coverageThreshold 0 and scanVulnerabilities false, the plugin is a no-op. @@ -64,6 +64,20 @@ function Get-LineCoveragePercentFromSharedContext { $Shared ) + if (Get-Command Get-EngineFact -ErrorAction SilentlyContinue) { + $fromFact = Get-EngineFact -Context $Shared -Namespace 'test' -Name 'coverageLineRate' -LegacyProperty @('qualityLineCoverage', 'coverageLineRate') + if ($null -ne $fromFact -and -not [string]::IsNullOrWhiteSpace([string]$fromFact)) { + return [double]$fromFact + } + + $testResult = Get-EngineFact -Context $Shared -Namespace 'test' -Name 'testResult' -LegacyProperty @('testResult') + if ($null -ne $testResult -and ($testResult.PSObject.Properties.Name -contains 'LineRate')) { + return [double]$testResult.LineRate + } + + return $null + } + foreach ($prop in @('qualityLineCoverage', 'coverageLineRate')) { if ($Shared.PSObject.Properties.Name -contains $prop) { $raw = $Shared.$prop @@ -92,7 +106,7 @@ function Invoke-Plugin { Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command" - Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Get-EngineFact" $pluginSettings = $Settings $sharedSettings = $Settings.context @@ -107,11 +121,9 @@ function Invoke-Plugin { if ($pluginSettings.PSObject.Properties['projectFiles'] -and $null -ne $pluginSettings.projectFiles) { $projectFiles = @(Resolve-RelativePaths -Value $pluginSettings.projectFiles -BasePath $scriptDir) } - elseif ($sharedSettings.PSObject.Properties['projectFiles'] -and $null -ne $sharedSettings.projectFiles) { - $projectFiles = @($sharedSettings.projectFiles) - } else { - $projectFiles = @() + $fromContext = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles') + $projectFiles = if ($null -ne $fromContext) { @($fromContext) } else { @() } } $coverageThreshold = 0 @@ -129,7 +141,7 @@ function Invoke-Plugin { if ($needCoverageCheck) { $lineRate = Get-LineCoveragePercentFromSharedContext -Shared $sharedSettings if ($null -eq $lineRate) { - throw "coverageThreshold is $coverageThreshold but shared context has no line coverage. Set one of: qualityLineCoverage, coverageLineRate, or testResult.LineRate (from an earlier plugin)." + throw "coverageThreshold is $coverageThreshold but shared context has no line coverage. Set test.coverageLineRate (or legacy qualityLineCoverage / coverageLineRate / testResult.LineRate) from an earlier plugin." } Write-Log -Level "STEP" -Message "Checking line coverage threshold against shared context..." diff --git a/utils/plugins/Platform/ReleasePublishGuard.psm1 b/utils/plugins/Platform/ReleasePublishGuard.psm1 index f1eb5e3..43b6dfe 100644 --- a/utils/plugins/Platform/ReleasePublishGuard.psm1 +++ b/utils/plugins/Platform/ReleasePublishGuard.psm1 @@ -50,10 +50,15 @@ function Invoke-NotMetInternal { [string]$Reason ) - $Shared | Add-Member -NotePropertyName skipPublishPlugins -NotePropertyValue $true -Force + if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) { + Set-EngineState -Context $Shared -Name 'skipPublishPlugins' -Value $true + } + else { + $Shared | Add-Member -NotePropertyName skipPublishPlugins -NotePropertyValue $true -Force + } + if ($When -eq 'fail') { - Write-Log -Level "ERROR" -Message "ReleasePublishGuard: $Reason" - exit 1 + throw "ReleasePublishGuard: $Reason" } Write-Log -Level "WARN" -Message " Publish suppressed: $Reason" @@ -67,6 +72,7 @@ function Invoke-Plugin { Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "PluginSupport" -RequiredCommand "Get-PluginBranches" + Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState" Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Get-GitStatusShort" Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Test-RemoteTagExists" @@ -82,15 +88,10 @@ function Invoke-Plugin { throw "ReleasePublishGuard: whenRequirementsNotMet must be 'skip' or 'fail'." } - $shared | Add-Member -NotePropertyName skipPublishPlugins -NotePropertyValue $false -Force + Set-EngineState -Context $shared -Name 'skipPublishPlugins' -Value $false Write-Log -Level "STEP" -Message "Release publish guard..." - if ($shared.PSObject.Properties.Name -contains 'dryRun' -and [bool]$shared.dryRun) { - Write-Log -Level "INFO" -Message " Dry run: publish guard relaxed; publish plugins will validate only." - return - } - $allowed = @(Get-PluginBranches -Plugin $pluginSettings) if ($allowed.Count -gt 0 -and $allowed -notcontains '*' -and $allowed -notcontains $shared.currentBranch) { Invoke-NotMetInternal -Shared $shared -When $when -Reason "branch '$($shared.currentBranch)' is not in the guard branches list." @@ -143,7 +144,7 @@ function Invoke-Plugin { return } - $shared | Add-Member -NotePropertyName tag -NotePropertyValue $tag -Force + Set-EngineState -Context $shared -Name 'tag' -Value $tag } $ensureRemote = $true diff --git a/utils/tools/Update-RepoUtils/Update-RepoUtils.ps1 b/utils/tools/Update-RepoUtils/Update-RepoUtils.ps1 deleted file mode 100644 index 9548ede..0000000 --- a/utils/tools/Update-RepoUtils/Update-RepoUtils.ps1 +++ /dev/null @@ -1,363 +0,0 @@ -#requires -Version 7.0 -#requires -PSEdition Core - -<# -.SYNOPSIS - Refreshes a local maksit-repoutils copy from GitHub. - -.DESCRIPTION - This script clones the configured repository into a temporary directory, - refreshes the parent directory of this script, preserves existing - scriptSettings.json files in subfolders, and copies the cloned source - contents into that parent directory. - - All configuration is stored in scriptSettings.json. - -.EXAMPLE - pwsh -File .\Update-RepoUtils.ps1 - -.NOTES - CONFIGURATION (scriptSettings.json): - - dryRun: If true, logs the planned update without modifying files - - repository.url: Git repository to clone - - repository.sourceSubdirectory: Folder copied into the target directory - - repository.preserveFileName: Existing file name to preserve in subfolders - - repository.cloneDepth: Depth used for git clone - - repository.skippedRelativeDirectories: Relative directories to exclude from phase-two refresh -#> - -[CmdletBinding()] -param( - [switch]$ContinueAfterSelfUpdate, - [string]$TargetDirectoryOverride, - [string]$ClonedSourceDirectoryOverride, - [string]$TemporaryRootOverride -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$srcDir = Split-Path (Split-Path $scriptDir -Parent) -Parent -$modulesDir = Join-Path $srcDir 'modules' - -# Refresh the src directory that contains modules, engines, plugins, and tools. -$targetDirectory = if ([string]::IsNullOrWhiteSpace($TargetDirectoryOverride)) { - $srcDir -} -else { - [System.IO.Path]::GetFullPath($TargetDirectoryOverride) -} -$currentScriptPath = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Path) -$selfUpdateDirectory = [System.IO.Path]::Combine('tools', 'Update-RepoUtils') - -function ConvertTo-NormalizedRelativePath { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $normalizedPath = $Path.Replace('/', [System.IO.Path]::DirectorySeparatorChar).Replace('\', [System.IO.Path]::DirectorySeparatorChar) - return $normalizedPath.TrimStart('.', [System.IO.Path]::DirectorySeparatorChar).TrimEnd([System.IO.Path]::DirectorySeparatorChar) -} - -function Test-IsInRelativeDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$RelativePath, - - [Parameter(Mandatory = $true)] - [string[]]$Directories - ) - - $normalizedRelativePath = ConvertTo-NormalizedRelativePath -Path $RelativePath - foreach ($directory in $Directories) { - $normalizedDirectory = ConvertTo-NormalizedRelativePath -Path $directory - if ([string]::IsNullOrWhiteSpace($normalizedDirectory)) { - continue - } - - if ( - $normalizedRelativePath.Equals($normalizedDirectory, [System.StringComparison]::OrdinalIgnoreCase) -or - $normalizedRelativePath.StartsWith($normalizedDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase) - ) { - return $true - } - } - - return $false -} - -#region Import Modules - -$scriptConfigModulePath = Join-Path $modulesDir "ScriptConfig.psm1" -if (-not (Test-Path $scriptConfigModulePath)) { - Write-Error "ScriptConfig module not found at: $scriptConfigModulePath" - exit 1 -} - -$loggingModulePath = Join-Path $modulesDir "Logging.psm1" -if (-not (Test-Path $loggingModulePath)) { - Write-Error "Logging module not found at: $loggingModulePath" - exit 1 -} - -Import-Module $scriptConfigModulePath -Force -Import-Module $loggingModulePath -Force - -#endregion - -#region Load Settings - -$settings = Get-ScriptSettings -ScriptDir $scriptDir - -#endregion - -#region Configuration - -$repositoryUrl = $settings.repository.url -$dryRun = if ($null -ne $settings.dryRun) { [bool]$settings.dryRun } else { $false } -$sourceSubdirectory = if ($settings.repository.sourceSubdirectory) { $settings.repository.sourceSubdirectory } else { 'src' } -$preserveFileName = if ($settings.repository.preserveFileName) { $settings.repository.preserveFileName } else { 'scriptSettings.json' } -$cloneDepth = if ($settings.repository.cloneDepth) { [int]$settings.repository.cloneDepth } else { 1 } -[string[]]$skippedRelativeDirectories = if ($settings.repository.skippedRelativeDirectories) { - @( - $settings.repository.skippedRelativeDirectories | - ForEach-Object { - ConvertTo-NormalizedRelativePath -Path ([string]$_) - } - ) -} -else { - @( - [System.IO.Path]::Combine('engines', 'release', 'custom'), - [System.IO.Path]::Combine('engines', 'test', 'custom') - ) -} - -#endregion - -#region Validate CLI Dependencies - -Assert-Command git -Assert-Command pwsh - -if ([string]::IsNullOrWhiteSpace($repositoryUrl)) { - Write-Error "repository.url is required in scriptSettings.json." - exit 1 -} - -#endregion - -#region Main - -Write-Log -Level "INFO" -Message "========================================" -Write-Log -Level "INFO" -Message "Update RepoUtils Script" -Write-Log -Level "INFO" -Message "========================================" -Write-Log -Level "INFO" -Message "Target directory: $targetDirectory" -Write-Log -Level "INFO" -Message "Dry run: $dryRun" - -$ownsTemporaryRoot = [string]::IsNullOrWhiteSpace($TemporaryRootOverride) -$temporaryRoot = if ($ownsTemporaryRoot) { - Join-Path ([System.IO.Path]::GetTempPath()) ("maksit-repoutils-update-" + [System.Guid]::NewGuid().ToString('N')) -} -else { - [System.IO.Path]::GetFullPath($TemporaryRootOverride) -} - -try { - $clonedSourceDirectory = if ([string]::IsNullOrWhiteSpace($ClonedSourceDirectoryOverride)) { - Write-LogStep "Cloning latest repository snapshot..." - & git clone --depth $cloneDepth $repositoryUrl $temporaryRoot - if ($LASTEXITCODE -ne 0) { - throw "git clone failed with exit code $LASTEXITCODE." - } - Write-Log -Level "OK" -Message "Repository cloned" - - Join-Path $temporaryRoot $sourceSubdirectory - } - else { - [System.IO.Path]::GetFullPath($ClonedSourceDirectoryOverride) - } - - if (-not (Test-Path -Path $clonedSourceDirectory -PathType Container)) { - throw "The cloned repository does not contain the expected source directory: $clonedSourceDirectory" - } - - if (-not $ContinueAfterSelfUpdate) { - if ($dryRun) { - Write-LogStep "Dry run self-update summary" - Write-Log -Level "INFO" -Message "Would refresh shared modules and $selfUpdateDirectory before relaunching the updater" - } - else { - Write-LogStep "Refreshing updater files..." - $selfUpdateFiles = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File | - Where-Object { - $relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName) - $isRootFile = -not $relativePath.Contains([System.IO.Path]::DirectorySeparatorChar) - $isUpdaterFile = $relativePath.StartsWith($selfUpdateDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase) - - $_.Name -ne $preserveFileName -and - ($isRootFile -or $isUpdaterFile) - } - - foreach ($sourceFile in $selfUpdateFiles) { - $relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $sourceFile.FullName) - $destinationPath = Join-Path $targetDirectory $relativePath - $destinationDirectory = Split-Path -Parent $destinationPath - if (-not (Test-Path -Path $destinationDirectory -PathType Container)) { - New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null - } - - Copy-Item -Path $sourceFile.FullName -Destination $destinationPath -Force - } - - Write-Log -Level "OK" -Message "Updater files refreshed" - } - - if ($dryRun) { - Write-LogStep "Dry run bootstrap completed" - Write-Log -Level "INFO" -Message "Continuing with phase two in the current process because no files were changed" - } - else { - Write-LogStep "Relaunching the updated updater..." - & pwsh -File $currentScriptPath ` - -ContinueAfterSelfUpdate ` - -TargetDirectoryOverride $targetDirectory ` - -ClonedSourceDirectoryOverride $clonedSourceDirectory ` - -TemporaryRootOverride $temporaryRoot - if ($LASTEXITCODE -ne 0) { - throw "Relaunched updater failed with exit code $LASTEXITCODE." - } - - Write-Log -Level "OK" -Message "Bootstrap phase completed" - return - } - } - - $preservedFiles = @() - [string[]]$updatePhaseSkippedDirectories = @($skippedRelativeDirectories) + $selfUpdateDirectory - $existingPreservedFiles = Get-ChildItem -Path $targetDirectory -Recurse -File -Filter $preserveFileName -ErrorAction SilentlyContinue - if ($existingPreservedFiles) { - foreach ($file in $existingPreservedFiles) { - $relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $file.FullName) - $backupPath = Join-Path $temporaryRoot ("preserved-" + ($relativePath -replace '[\\/:*?""<>|]', '_')) - $preservedFiles += [pscustomobject]@{ - RelativePath = $relativePath - BackupPath = $backupPath - } - - if (-not $dryRun) { - $backupDirectory = Split-Path -Parent $backupPath - if (-not (Test-Path -Path $backupDirectory -PathType Container)) { - New-Item -ItemType Directory -Path $backupDirectory -Force | Out-Null - } - - Copy-Item -Path $file.FullName -Destination $backupPath -Force - } - } - Write-Log -Level "OK" -Message "Preserved $($preservedFiles.Count) existing $preserveFileName file(s)" - } - else { - Write-Log -Level "WARN" -Message "No existing $preserveFileName files found in subfolders" - } - - if ($dryRun) { - Write-LogStep "Dry run summary" - Write-Log -Level "INFO" -Message "Would remove all files under target except preserved $preserveFileName files" - Write-Log -Level "INFO" -Message "Would skip phase-two refresh for: $($updatePhaseSkippedDirectories -join ', ')" - Write-Log -Level "INFO" -Message "Would copy refreshed files from: $clonedSourceDirectory" - if ($preservedFiles.Count -gt 0) { - $preservedList = ($preservedFiles | ForEach-Object { $_.RelativePath }) -join ", " - Write-Log -Level "INFO" -Message "Would restore preserved files: $preservedList" - } - Write-Log -Level "OK" -Message "Dry run completed. No files were modified." - return - } - - Write-LogStep "Cleaning target directory..." - $filesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -File | - Where-Object { - $relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $_.FullName) - $isInSkippedDirectory = Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $updatePhaseSkippedDirectories - - $_.Name -ne $preserveFileName -and - -not $isInSkippedDirectory - } - - foreach ($file in $filesToRemove) { - Remove-Item -Path $file.FullName -Force - } - - $directoriesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -Directory | - Sort-Object { $_.FullName.Length } -Descending - - foreach ($directory in $directoriesToRemove) { - $relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $directory.FullName) - if (Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $updatePhaseSkippedDirectories) { - continue - } - - $remainingItems = Get-ChildItem -Path $directory.FullName -Force -ErrorAction SilentlyContinue - if (-not $remainingItems) { - Remove-Item -Path $directory.FullName -Force - } - } - Write-Log -Level "OK" -Message "Target directory cleaned" - - Write-LogStep "Copying refreshed source files..." - $sourceFilesToCopy = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File | - Where-Object { - $relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName) - $isInSkippedDirectory = Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $updatePhaseSkippedDirectories - - -not $isInSkippedDirectory - } - - foreach ($sourceFile in $sourceFilesToCopy) { - $relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $sourceFile.FullName) - $destinationPath = Join-Path $targetDirectory $relativePath - $destinationDirectory = Split-Path -Parent $destinationPath - if (-not (Test-Path -Path $destinationDirectory -PathType Container)) { - New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null - } - - Copy-Item -Path $sourceFile.FullName -Destination $destinationPath -Force - } - - foreach ($skippedDirectory in $updatePhaseSkippedDirectories) { - $skippedSourcePath = Join-Path $clonedSourceDirectory $skippedDirectory - if (Test-Path -Path $skippedSourcePath) { - Write-Log -Level "INFO" -Message "Skipped refresh for $skippedDirectory" - } - } - Write-Log -Level "OK" -Message "Source files copied" - - if ($preservedFiles.Count -gt 0) { - foreach ($preservedFile in $preservedFiles) { - if (-not (Test-Path -Path $preservedFile.BackupPath -PathType Leaf)) { - continue - } - - $restorePath = Join-Path $targetDirectory $preservedFile.RelativePath - $restoreDirectory = Split-Path -Parent $restorePath - if (-not (Test-Path -Path $restoreDirectory -PathType Container)) { - New-Item -ItemType Directory -Path $restoreDirectory -Force | Out-Null - } - - Copy-Item -Path $preservedFile.BackupPath -Destination $restorePath -Force - } - Write-Log -Level "OK" -Message "$preserveFileName files restored" - } - - Write-Log -Level "OK" -Message "========================================" - Write-Log -Level "OK" -Message "Update completed successfully!" - Write-Log -Level "OK" -Message "========================================" -} -finally { - if ($ownsTemporaryRoot -and (Test-Path -Path $temporaryRoot)) { - Remove-Item -Path $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue - } -} - -#endregion diff --git a/utils/tools/Update-RepoUtils/scriptSettings.json b/utils/tools/Update-RepoUtils/scriptSettings.json deleted file mode 100644 index de67aab..0000000 --- a/utils/tools/Update-RepoUtils/scriptSettings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft-07/schema", - "title": "Update RepoUtils Script Settings", - "description": "Configuration for the Update-RepoUtils utility.", - "dryRun": true, - "repository": { - "url": "https://github.com/MAKS-IT-COM/maksit-repoutils.git", - "sourceSubdirectory": "src", - "preserveFileName": "scriptSettings.json", - "cloneDepth": 1, - "skippedRelativeDirectories": [ - "engines/release/custom", - "engines/test/custom" - ] - } -}