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"
- ]
- }
-}