Compare commits

..

1 Commits
v2.2.0 ... main

Author SHA1 Message Date
Maksym Sadovnychyy
8db977da95 (chore): RepoUtils sync and dependency updates 2026-08-14 20:54:34 +02:00
14 changed files with 128 additions and 526 deletions

View File

@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.2.1] - 2026-08-14
### Changed
- **Dependencies:** `MaksIT.Core` `1.6.9`, `MaksIT.Results` `2.0.4`; test SDK `Microsoft.NET.Test.Sdk` `18.9.0`.
- **RepoUtils:** synced Community Non-Helm utils — plugin success checks use exact `$true` (CLI stdout no longer masks failures), `Invoke-ExternalCommand` defaults to throw-on-error with soft callers opting out, plugin helper discovery by group directories.
- Package version **2.2.1**.
### Removed
- `Update-RepoUtils.bat` and `utils/tools/Update-RepoUtils/` (local-copy sync only).
## [2.2.0] - 2026-07-31 ## [2.2.0] - 2026-07-31
### Added ### Added

View File

@ -76,7 +76,7 @@ dotnet add package MaksIT.Dapr
Or in your `.csproj`: Or in your `.csproj`:
```xml ```xml
<PackageReference Include="MaksIT.Dapr" Version="2.2.0" /> <PackageReference Include="MaksIT.Dapr" Version="2.2.1" />
``` ```
## Registering services ## Registering services

View File

@ -13,7 +13,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="Moq" Version="4.*" /> <PackageReference Include="Moq" Version="4.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.*" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.*" />
<PackageReference Include="xunit.v3" Version="3.*" /> <PackageReference Include="xunit.v3" Version="3.*" />

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata --> <!-- NuGet package metadata -->
<PackageId>MaksIT.Dapr</PackageId> <PackageId>MaksIT.Dapr</PackageId>
<Version>2.2.0</Version> <Version>2.2.1</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>MaksIT.Dapr</Product> <Product>MaksIT.Dapr</Product>
@ -25,8 +25,8 @@
<PackageReference Include="Dapr.Actors.AspNetCore" Version="1.18.5" /> <PackageReference Include="Dapr.Actors.AspNetCore" Version="1.18.5" />
<PackageReference Include="Dapr.AspNetCore" Version="1.18.5" /> <PackageReference Include="Dapr.AspNetCore" Version="1.18.5" />
<PackageReference Include="Dapr.Workflow" Version="1.18.5" /> <PackageReference Include="Dapr.Workflow" Version="1.18.5" />
<PackageReference Include="MaksIT.Core" Version="1.6.8" /> <PackageReference Include="MaksIT.Core" Version="1.6.9" />
<PackageReference Include="MaksIT.Results" Version="2.0.3" /> <PackageReference Include="MaksIT.Results" Version="2.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,3 +0,0 @@
@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\Update-RepoUtils\Update-RepoUtils.ps1" %*
pause

View File

@ -80,8 +80,9 @@ else {
} }
} }
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -ContinueOnError:$false $pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot
if (-not $pluginSucceeded) { # Exact $true only: polluted arrays (CLI stdout + $false) are truthy under -not.
if ($pluginSucceeded -ne $true) {
$releaseHadPluginFailures = $true $releaseHadPluginFailures = $true
break break
} }

View File

@ -6,6 +6,8 @@
Plugin-driven test and coverage engine entry script. Plugin-driven test and coverage engine entry script.
#> #>
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path $srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path
@ -29,8 +31,9 @@ if ($configuredPlugins.Count -eq 0) {
$testHadPluginFailures = $false $testHadPluginFailures = $false
foreach ($plugin in $configuredPlugins) { foreach ($plugin in $configuredPlugins) {
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $scriptDir -ContinueOnError:$false $pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $scriptDir
if (-not $pluginSucceeded) { # Exact $true only: polluted arrays (CLI stdout + $false) are truthy under -not.
if ($pluginSucceeded -ne $true) {
$testHadPluginFailures = $true $testHadPluginFailures = $true
break break
} }

View File

@ -22,7 +22,7 @@ function Test-IsEngineRuntimeModuleName {
[string]$ModuleName [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( $engineNames = [System.Collections.Generic.HashSet[string]]::new(
[string[]]@( [string[]]@(
'ChangelogSupport', 'ChangelogSupport',
@ -34,11 +34,7 @@ function Test-IsEngineRuntimeModuleName {
'EngineContext', 'EngineContext',
'PluginSupport', 'PluginSupport',
'ReleaseSupport', 'ReleaseSupport',
'TestSupport', 'TestSupport'
'DeployConfig',
'EngineContextSupport',
'OrchestratorSupport',
'PluginPathSupport'
), ),
[System.StringComparer]::OrdinalIgnoreCase [System.StringComparer]::OrdinalIgnoreCase
) )
@ -46,6 +42,34 @@ function Test-IsEngineRuntimeModuleName {
return $engineNames.Contains($ModuleName) 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 { function Import-PluginDependency {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -66,16 +90,16 @@ function Import-PluginDependency {
$candidatePaths = [System.Collections.Generic.List[string]]::new() $candidatePaths = [System.Collections.Generic.List[string]]::new()
if (Test-IsEngineRuntimeModuleName -ModuleName $ModuleName) { 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 $modulesDir "$ModuleName.psm1"))
$candidatePaths.Add((Join-Path $engineModuleDir "$ModuleName.psm1")) $candidatePaths.Add((Join-Path $engineModuleDir "$ModuleName.psm1"))
$extensionsDir = Join-Path $modulesDir 'Extensions' $extensionsDir = Join-Path $modulesDir 'Extensions'
$candidatePaths.Add((Join-Path $extensionsDir "$ModuleName.psm1")) $candidatePaths.Add((Join-Path $extensionsDir "$ModuleName.psm1"))
} }
else { else {
# Plugin helpers: plugins/ only (no modules/ legacy shadow). # Plugin helpers: plugins/{Group}/ only (no modules/ legacy shadow). Groups are discovered.
foreach ($group in @('Shared', 'Platform', 'DotNet', 'Npm', 'Helm', 'Docker', 'Podman')) { foreach ($groupDir in Get-PluginDependencyGroupDirectories -PluginsRoot $pluginsRoot) {
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $group) "$ModuleName.psm1")) $candidatePaths.Add((Join-Path $groupDir "$ModuleName.psm1"))
} }
} }
@ -344,8 +368,8 @@ function Get-RegistryCredentialsFromRuntime {
.DESCRIPTION .DESCRIPTION
Looks up the environment variable named by SecretName. The value must be Looks up the environment variable named by SecretName. The value must be
Base64(UTF8('username:password')). Used by Docker/Podman/Helm registry login Base64(UTF8('username:password')). Used by registry login and image-pull
and image-pull secret creation never pass the password itself as a parameter. secret creation never pass the password itself as a parameter.
.PARAMETER SecretName .PARAMETER SecretName
Logical secret name (environment variable name), not a password or token. Logical secret name (environment variable name), not a password or token.
@ -671,10 +695,7 @@ function Invoke-ConfiguredPlugin {
[psobject]$SharedSettings, [psobject]$SharedSettings,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$EngineDirectory, [string]$EngineDirectory
[Parameter(Mandatory = $false)]
[bool]$ContinueOnError = $false
) )
if (-not (Test-PluginRunnable -Plugin $Plugin -SharedSettings $SharedSettings -EngineDirectory $EngineDirectory -WriteLogs:$true)) { 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 $pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.name)'..." 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 { try {
$moduleInfo = Import-Module $pluginModulePath -Force -PassThru -ErrorAction Stop $moduleInfo = Import-Module $pluginModulePath -Force -PassThru -ErrorAction Stop
$invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop $invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop
$pluginSettings = New-PluginInvocationSettings -Plugin $Plugin -SharedSettings $SharedSettings $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." Write-Log -Level "OK" -Message " Plugin '$($Plugin.name)' completed."
return $true return $true
} }

View File

@ -1,6 +1,16 @@
#requires -Version 7.0 #requires -Version 7.0
#requires -PSEdition Core #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:ExternalCommandTestHandler = $null
$script:ExternalCommandAvailability = @{} $script:ExternalCommandAvailability = @{}
@ -40,7 +50,10 @@ function Invoke-ExternalCommand {
[string]$InputObject, [string]$InputObject,
[switch]$MergeErrorOutput [switch]$MergeErrorOutput,
# Default true: fail fast. Soft callers (tests, nested loggers) pass $false.
[bool]$ThrowOnError = $true
) )
$previousLocation = $null $previousLocation = $null
@ -51,6 +64,7 @@ function Invoke-ExternalCommand {
try { try {
$effectiveWorkingDirectory = (Get-Location).Path $effectiveWorkingDirectory = (Get-Location).Path
$output = @()
if ($null -ne $script:ExternalCommandTestHandler) { if ($null -ne $script:ExternalCommandTestHandler) {
$handlerResult = & $script:ExternalCommandTestHandler ` $handlerResult = & $script:ExternalCommandTestHandler `
@ -62,31 +76,46 @@ function Invoke-ExternalCommand {
$global:LASTEXITCODE = [int]$handlerResult.ExitCode $global:LASTEXITCODE = [int]$handlerResult.ExitCode
if ($null -eq $handlerResult.Output) { if ($null -eq $handlerResult.Output) {
return @() $output = @()
} }
elseif ($handlerResult.Output -is [System.Collections.IEnumerable] -and -not ($handlerResult.Output -is [string])) {
if ($handlerResult.Output -is [System.Collections.IEnumerable] -and -not ($handlerResult.Output -is [string])) { $output = @($handlerResult.Output)
return @($handlerResult.Output)
} }
else {
return @([string]$handlerResult.Output) $output = @([string]$handlerResult.Output)
} }
}
else {
if ($script:ExternalCommandAvailability.ContainsKey($Name) -and -not $script:ExternalCommandAvailability[$Name]) { if ($script:ExternalCommandAvailability.ContainsKey($Name) -and -not $script:ExternalCommandAvailability[$Name]) {
throw "External command '$Name' is marked unavailable." throw "External command '$Name' is marked unavailable."
} }
if (-not [string]::IsNullOrWhiteSpace($InputObject)) { if (-not [string]::IsNullOrWhiteSpace($InputObject)) {
$output = $InputObject | & $Name @ArgumentList 2>&1 $raw = $InputObject | & $Name @ArgumentList 2>&1
} }
elseif ($MergeErrorOutput) { elseif ($MergeErrorOutput) {
$output = & $Name @ArgumentList 2>&1 $raw = & $Name @ArgumentList 2>&1
} }
else { else {
$output = & $Name @ArgumentList $raw = & $Name @ArgumentList
} }
return @($output) $output = @($raw)
}
$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 { finally {
if ($null -ne $previousLocation) { if ($null -ne $previousLocation) {

View File

@ -53,21 +53,18 @@ function Invoke-GitInternal {
if (-not (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue)) { if (-not (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path $PSScriptRoot -Parent $srcDir = Split-Path $PSScriptRoot -Parent
$externalCandidates = @( $externalModule = Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1'
(Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1'),
(Join-Path $srcDir 'plugins' 'Shared' 'ExternalCommandSupport.psm1')
)
foreach ($externalModule in $externalCandidates) {
if (Test-Path -LiteralPath $externalModule -PathType Leaf) { if (Test-Path -LiteralPath $externalModule -PathType Leaf) {
Import-Module $externalModule -Global Import-Module $externalModule -Global
break
} }
elseif (Test-Path -LiteralPath (Join-Path $srcDir 'modules' 'ExternalCommandSupport.psm1') -PathType Leaf) {
Import-Module (Join-Path $srcDir 'modules' 'ExternalCommandSupport.psm1') -Global
} }
} }
if ($CaptureOutput) { if ($CaptureOutput) {
if (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue) { 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 $exitCode = $LASTEXITCODE
if ($exitCode -ne 0) { if ($exitCode -ne 0) {
Write-Error "$ErrorMessage (exit code: $exitCode)" Write-Error "$ErrorMessage (exit code: $exitCode)"
@ -96,7 +93,7 @@ function Invoke-GitInternal {
} }
if (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue) { 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 { else {
& git @Arguments & git @Arguments

View File

@ -19,10 +19,8 @@ function Import-ExternalCommandSupportInternal {
return return
} }
$srcDir = Split-Path $PSScriptRoot -Parent
$candidates = @( $candidates = @(
(Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1'), (Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1')
(Join-Path $srcDir 'plugins' 'Shared' 'ExternalCommandSupport.psm1')
) )
foreach ($modulePath in $candidates) { foreach ($modulePath in $candidates) {
if (Test-Path -LiteralPath $modulePath -PathType Leaf) { if (Test-Path -LiteralPath $modulePath -PathType Leaf) {
@ -175,10 +173,10 @@ function Invoke-TestsWithCoverage {
Import-ExternalCommandSupportInternal Import-ExternalCommandSupportInternal
if ($Silent) { if ($Silent) {
$null = Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -MergeErrorOutput $null = Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -MergeErrorOutput -ThrowOnError:$false
} }
else { else {
Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs | Out-Default Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -ThrowOnError:$false | Out-Default
} }
$testExitCode = $LASTEXITCODE $testExitCode = $LASTEXITCODE
@ -356,10 +354,10 @@ function Invoke-NpmJestTestsWithCoverage {
$npmArgs = @('run', $TestScript, '--', '--coverage', '--coverageReporters=json-summary', '--coverageReporters=text') $npmArgs = @('run', $TestScript, '--', '--coverage', '--coverageReporters=json-summary', '--coverageReporters=text')
Import-ExternalCommandSupportInternal Import-ExternalCommandSupportInternal
if ($Silent) { if ($Silent) {
$null = Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -MergeErrorOutput $null = Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -MergeErrorOutput -ThrowOnError:$false
} }
else { else {
Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs | Out-Default Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -ThrowOnError:$false | Out-Default
} }
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {

View File

@ -6,7 +6,7 @@
Pester test plugin for the RepoUtils test engine. Pester test plugin for the RepoUtils test engine.
.DESCRIPTION .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. shared engine context for QualityGate.
#> #>

View File

@ -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

View File

@ -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": false,
"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"
]
}
}