diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d226df..5fdafed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.6.9] - 2026-08-14
+
+### Changed
+- Updated NuGet dependencies: Microsoft.Extensions.* and System.Threading.RateLimiting 10.0.9 → 10.0.11, Microsoft.AspNetCore.* 2.3.11 → 2.3.12, Microsoft.IdentityModel.Tokens and System.IdentityModel.Tokens.Jwt 8.19.1 → 8.22.0, System.Linq.Dynamic.Core 1.7.2 → 1.7.3, Microsoft.NET.Test.Sdk 18.7.0 → 18.9.0
+- Synced RepoUtils: fail-fast external commands (`ThrowOnError`), exact plugin success checks (avoid polluted PowerShell return arrays), discovered plugin group loading, and removed `Update-RepoUtils` (local-copy sync only)
+
## [1.6.8] - 2026-06-27
### Changed
diff --git a/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj b/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj
index 3bcfac8..c0120d1 100644
--- a/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj
+++ b/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj
@@ -14,8 +14,8 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj
index 2b09506..ca1e04c 100644
--- a/src/MaksIT.Core/MaksIT.Core.csproj
+++ b/src/MaksIT.Core/MaksIT.Core.csproj
@@ -12,7 +12,7 @@
MaksIT.Core
- 1.6.8
+ 1.6.9
Maksym Sadovnychyy
MAKS-IT
MaksIT.Core
@@ -42,16 +42,16 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
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 4ad9cbc..b788172 100644
--- a/utils/engines/release/Invoke-ReleasePackage.ps1
+++ b/utils/engines/release/Invoke-ReleasePackage.ps1
@@ -80,8 +80,9 @@ else {
}
}
- $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
}
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/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/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 e13050c..8677946 100644
--- a/utils/modules/GitTools.psm1
+++ b/utils/modules/GitTools.psm1
@@ -53,21 +53,18 @@ function Invoke-GitInternal {
if (-not (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path $PSScriptRoot -Parent
- $externalCandidates = @(
- (Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1'),
- (Join-Path $srcDir 'plugins' 'Shared' 'ExternalCommandSupport.psm1')
- )
- foreach ($externalModule in $externalCandidates) {
- if (Test-Path -LiteralPath $externalModule -PathType Leaf) {
- Import-Module $externalModule -Global
- break
- }
+ $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
+ $output = Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput -ThrowOnError:$false
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Error "$ErrorMessage (exit code: $exitCode)"
@@ -96,7 +93,7 @@ function Invoke-GitInternal {
}
if (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue) {
- Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput | Out-Null
+ Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput -ThrowOnError:$false | Out-Null
}
else {
& git @Arguments
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/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/tools/Update-RepoUtils/Update-RepoUtils.ps1 b/utils/tools/Update-RepoUtils/Update-RepoUtils.ps1
deleted file mode 100644
index 00b3ce0..0000000
--- a/utils/tools/Update-RepoUtils/Update-RepoUtils.ps1
+++ /dev/null
@@ -1,439 +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 (preserve dest)
- - repository.omittedRelativeDirectories: Relative directories to delete from dest and never copy from source
- (product repos: ["tests"] — RepoUtils self-tests stay only in maksit-repoutils / enterprise)
-#>
-
-[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
-}
-
-function Get-ReleaseDeployPreserveFiles {
- param(
- [Parameter(Mandatory = $true)]
- [string]$TargetDirectory
- )
-
- return @()
-}
-
-function Add-PreservedFileBackup {
- param(
- [Parameter(Mandatory = $true)]
- [System.Collections.IList]$PreservedFiles,
-
- [Parameter(Mandatory = $true)]
- [System.IO.FileInfo]$File,
-
- [Parameter(Mandatory = $true)]
- [string]$TargetDirectory,
-
- [Parameter(Mandatory = $true)]
- [string]$TemporaryRoot,
-
- [Parameter(Mandatory = $true)]
- [bool]$DryRun
- )
-
- $relativePath = [System.IO.Path]::GetRelativePath($TargetDirectory, $File.FullName)
- foreach ($existing in $PreservedFiles) {
- if ($existing.RelativePath.Equals($relativePath, [System.StringComparison]::OrdinalIgnoreCase)) {
- return
- }
- }
-
- $backupPath = Join-Path $TemporaryRoot ("preserved-" + ($relativePath -replace '[\\/:*?""<>|]', '_'))
- $PreservedFiles.Add([pscustomobject]@{
- RelativePath = $relativePath
- BackupPath = $backupPath
- }) | Out-Null
-
- 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
- }
-}
-
-#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')
- )
-}
-[string[]]$omittedRelativeDirectories = if ($settings.repository.omittedRelativeDirectories) {
- @(
- $settings.repository.omittedRelativeDirectories |
- ForEach-Object {
- ConvertTo-NormalizedRelativePath -Path ([string]$_)
- } |
- Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
- )
-}
-else {
- @()
-}
-
-#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 = [System.Collections.ArrayList]@()
- [string[]]$updatePhaseSkippedDirectories = @($skippedRelativeDirectories) + $selfUpdateDirectory
- $existingPreservedFiles = Get-ChildItem -Path $targetDirectory -Recurse -File -Filter $preserveFileName -ErrorAction SilentlyContinue
- if ($existingPreservedFiles) {
- foreach ($file in $existingPreservedFiles) {
- Add-PreservedFileBackup -PreservedFiles $preservedFiles -File $file -TargetDirectory $targetDirectory -TemporaryRoot $temporaryRoot -DryRun $dryRun
- }
- Write-Log -Level "OK" -Message "Preserved $($existingPreservedFiles.Count) existing $preserveFileName file(s)"
- }
- else {
- Write-Log -Level "WARN" -Message "No existing $preserveFileName files found in subfolders"
- }
-
- $releaseDeployPreserveFiles = @(Get-ReleaseDeployPreserveFiles -TargetDirectory $targetDirectory)
- if ($releaseDeployPreserveFiles.Count -gt 0) {
- foreach ($file in $releaseDeployPreserveFiles) {
- Add-PreservedFileBackup -PreservedFiles $preservedFiles -File $file -TargetDirectory $targetDirectory -TemporaryRoot $temporaryRoot -DryRun $dryRun
- }
- Write-Log -Level "OK" -Message "Preserved $($releaseDeployPreserveFiles.Count) release deploy file(s)"
- }
-
- $preservedRelativePaths = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
- foreach ($preservedFile in $preservedFiles) {
- [void]$preservedRelativePaths.Add($preservedFile.RelativePath)
- }
-
- if ($dryRun) {
- Write-LogStep "Dry run summary"
- Write-Log -Level "INFO" -Message "Would remove all files under target except preserved $preserveFileName and release deploy files"
- Write-Log -Level "INFO" -Message "Would skip phase-two refresh for: $($updatePhaseSkippedDirectories -join ', ')"
- if ($omittedRelativeDirectories.Count -gt 0) {
- Write-Log -Level "INFO" -Message "Would omit (delete dest + skip copy): $($omittedRelativeDirectories -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
- $isInOmittedDirectory = ($omittedRelativeDirectories.Count -gt 0) -and
- (Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $omittedRelativeDirectories)
-
- $_.Name -ne $preserveFileName -and
- -not $preservedRelativePaths.Contains($relativePath) -and
- (-not $isInSkippedDirectory -or $isInOmittedDirectory)
- }
-
- 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)
- $isInOmittedDirectory = ($omittedRelativeDirectories.Count -gt 0) -and
- (Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $omittedRelativeDirectories)
- if ((Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $updatePhaseSkippedDirectories) -and
- -not $isInOmittedDirectory) {
- 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
- $isInOmittedDirectory = ($omittedRelativeDirectories.Count -gt 0) -and
- (Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $omittedRelativeDirectories)
-
- -not $isInSkippedDirectory -and -not $isInOmittedDirectory
- }
-
- 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"
- }
- }
- foreach ($omittedDirectory in $omittedRelativeDirectories) {
- Write-Log -Level "INFO" -Message "Omitted (not shipped): $omittedDirectory"
- }
- 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 "Preserved files restored ($($preservedFiles.Count))"
- }
-
- 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 d79ebc5..0000000
--- a/utils/tools/Update-RepoUtils/scriptSettings.json
+++ /dev/null
@@ -1,19 +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"
- ],
- "omittedRelativeDirectories": [
- "tests"
- ]
- }
-}