Merge remote-tracking branch 'origin/main' into dev

This commit is contained in:
Maksym Sadovnychyy 2026-02-28 22:22:14 +01:00
commit f749de4c79
7 changed files with 208 additions and 40 deletions

View File

@ -5,7 +5,7 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v1.6.3 - 2026-02-21 ## v1.6.4 - 2026-02-21
### Added ### Added
- New shared utility modules under `utils/`: - New shared utility modules under `utils/`:

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 49.6%"> <svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 49.6%">
<title>Branch Coverage: 49.6%</title> <title>Branch Coverage: 49.6%</title>
<linearGradient id="s" x2="0" y2="100%"> <linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="134.5" height="20" role="img" aria-label="Line Coverage: 60%"> <svg xmlns="http://www.w3.org/2000/svg" width="134.5" height="20" role="img" aria-label="Line Coverage: 60%">
<title>Line Coverage: 60%</title> <title>Line Coverage: 60%</title>
<linearGradient id="s" x2="0" y2="100%"> <linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 69.2%"> <svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 69.2%">
<title>Method Coverage: 69.2%</title> <title>Method Coverage: 69.2%</title>
<linearGradient id="s" x2="0" y2="100%"> <linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -12,7 +12,7 @@
<!-- NuGet package metadata --> <!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId> <PackageId>MaksIT.Core</PackageId>
<Version>1.6.3</Version> <Version>1.6.4</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product> <Product>MaksIT.Core</Product>

View File

@ -7,9 +7,9 @@
.DESCRIPTION .DESCRIPTION
This script clones the configured repository into a temporary directory, This script clones the configured repository into a temporary directory,
removes the current working directory contents, preserves an existing refreshes the parent directory of this script, preserves existing
scriptsettings.json file, and copies the cloned src contents into the scriptsettings.json files in subfolders, and copies the cloned source
current working directory. contents into that parent directory.
All configuration is stored in scriptsettings.json. All configuration is stored in scriptsettings.json.
@ -18,14 +18,20 @@
.NOTES .NOTES
CONFIGURATION (scriptsettings.json): CONFIGURATION (scriptsettings.json):
- dryRun: If true, logs the planned update without modifying files
- repository.url: Git repository to clone - repository.url: Git repository to clone
- repository.sourceSubdirectory: Folder copied into the target directory - repository.sourceSubdirectory: Folder copied into the target directory
- repository.preserveFileName: Existing file in the target directory to keep - repository.preserveFileName: Existing file name to preserve in subfolders
- repository.cloneDepth: Depth used for git clone - repository.cloneDepth: Depth used for git clone
#> #>
[CmdletBinding()] [CmdletBinding()]
param() param(
[switch]$ContinueAfterSelfUpdate,
[string]$TargetDirectoryOverride,
[string]$ClonedSourceDirectoryOverride,
[string]$TemporaryRootOverride
)
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@ -34,8 +40,18 @@ $ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$utilsDir = Split-Path $scriptDir -Parent $utilsDir = Split-Path $scriptDir -Parent
# The target is the current working directory, not the script directory. # Refresh the parent directory that contains the shared modules and sibling tools.
$targetDirectory = (Get-Location).Path $targetDirectory = if ([string]::IsNullOrWhiteSpace($TargetDirectoryOverride)) {
Split-Path $scriptDir -Parent
}
else {
[System.IO.Path]::GetFullPath($TargetDirectoryOverride)
}
$currentScriptPath = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Path)
$selfUpdateDirectory = 'Update-RepoUtils'
$skippedRelativeDirectories = @(
[System.IO.Path]::Combine('Release-Package', 'CustomPlugins')
)
#region Import Modules #region Import Modules
@ -65,16 +81,17 @@ $settings = Get-ScriptSettings -ScriptDir $scriptDir
#region Configuration #region Configuration
$repositoryUrl = $settings.repository.url $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' } $sourceSubdirectory = if ($settings.repository.sourceSubdirectory) { $settings.repository.sourceSubdirectory } else { 'src' }
$preserveFileName = if ($settings.repository.preserveFileName) { $settings.repository.preserveFileName } else { 'scriptsettings.json' } $preserveFileName = if ($settings.repository.preserveFileName) { $settings.repository.preserveFileName } else { 'scriptsettings.json' }
$cloneDepth = if ($settings.repository.cloneDepth) { [int]$settings.repository.cloneDepth } else { 1 } $cloneDepth = if ($settings.repository.cloneDepth) { [int]$settings.repository.cloneDepth } else { 1 }
$currentScriptName = Split-Path -Leaf $MyInvocation.MyCommand.Path
#endregion #endregion
#region Validate CLI Dependencies #region Validate CLI Dependencies
Assert-Command git Assert-Command git
Assert-Command pwsh
if ([string]::IsNullOrWhiteSpace($repositoryUrl)) { if ([string]::IsNullOrWhiteSpace($repositoryUrl)) {
Write-Error "repository.url is required in scriptsettings.json." Write-Error "repository.url is required in scriptsettings.json."
@ -89,10 +106,18 @@ Write-Log -Level "INFO" -Message "========================================"
Write-Log -Level "INFO" -Message "Update RepoUtils Script" Write-Log -Level "INFO" -Message "Update RepoUtils Script"
Write-Log -Level "INFO" -Message "========================================" Write-Log -Level "INFO" -Message "========================================"
Write-Log -Level "INFO" -Message "Target directory: $targetDirectory" Write-Log -Level "INFO" -Message "Target directory: $targetDirectory"
Write-Log -Level "INFO" -Message "Dry run: $dryRun"
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("maksit-repoutils-update-" + [System.Guid]::NewGuid().ToString('N')) $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 { try {
$clonedSourceDirectory = if ([string]::IsNullOrWhiteSpace($ClonedSourceDirectoryOverride)) {
Write-LogStep "Cloning latest repository snapshot..." Write-LogStep "Cloning latest repository snapshot..."
& git clone --depth $cloneDepth $repositoryUrl $temporaryRoot & git clone --depth $cloneDepth $repositoryUrl $temporaryRoot
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
@ -100,43 +125,182 @@ try {
} }
Write-Log -Level "OK" -Message "Repository cloned" Write-Log -Level "OK" -Message "Repository cloned"
$clonedSourceDirectory = Join-Path $temporaryRoot $sourceSubdirectory Join-Path $temporaryRoot $sourceSubdirectory
}
else {
[System.IO.Path]::GetFullPath($ClonedSourceDirectoryOverride)
}
if (-not (Test-Path -Path $clonedSourceDirectory -PathType Container)) { if (-not (Test-Path -Path $clonedSourceDirectory -PathType Container)) {
throw "The cloned repository does not contain the expected source directory: $clonedSourceDirectory" throw "The cloned repository does not contain the expected source directory: $clonedSourceDirectory"
} }
$existingPreservedFile = Join-Path $targetDirectory $preserveFileName if (-not $ContinueAfterSelfUpdate) {
$preservedFileBackup = $null if ($dryRun) {
if (Test-Path -Path $existingPreservedFile -PathType Leaf) { Write-LogStep "Dry run self-update summary"
$preservedFileBackup = Join-Path $temporaryRoot ("preserved-" + $preserveFileName) Write-Log -Level "INFO" -Message "Would refresh shared modules and $selfUpdateDirectory before relaunching the updater"
Copy-Item -Path $existingPreservedFile -Destination $preservedFileBackup -Force
Write-Log -Level "OK" -Message "Preserved existing $preserveFileName"
} }
else { else {
Write-Log -Level "WARN" -Message "No existing $preserveFileName found in target directory" 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 = @()
$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) {
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..." Write-LogStep "Cleaning target directory..."
$itemsToRemove = Get-ChildItem -Path $targetDirectory -Force | $filesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -File |
Where-Object { Where-Object {
$_.Name -ne $preserveFileName -and $relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $_.FullName)
$_.Name -ne $currentScriptName $isInSkippedDirectory = $false
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
if ($relativePath.StartsWith($skippedDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
$isInSkippedDirectory = $true
break
}
} }
foreach ($item in $itemsToRemove) { $_.Name -ne $preserveFileName -and
Remove-Item -Path $item.FullName -Recurse -Force -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) {
$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-Log -Level "OK" -Message "Target directory cleaned"
Write-LogStep "Copying refreshed source files..." Write-LogStep "Copying refreshed source files..."
Get-ChildItem -Path $clonedSourceDirectory -Force | ForEach-Object { $sourceFilesToCopy = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File |
Copy-Item -Path $_.FullName -Destination $targetDirectory -Recurse -Force Where-Object {
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName)
$isInSkippedDirectory = $false
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
if ($relativePath.StartsWith($skippedDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
$isInSkippedDirectory = $true
break
}
}
-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" Write-Log -Level "OK" -Message "Source files copied"
if ($preservedFileBackup -and (Test-Path -Path $preservedFileBackup -PathType Leaf)) { if ($preservedFiles.Count -gt 0) {
Copy-Item -Path $preservedFileBackup -Destination $existingPreservedFile -Force foreach ($preservedFile in $preservedFiles) {
Write-Log -Level "OK" -Message "$preserveFileName restored" 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 "========================================"
@ -144,7 +308,7 @@ try {
Write-Log -Level "OK" -Message "========================================" Write-Log -Level "OK" -Message "========================================"
} }
finally { finally {
if (Test-Path -Path $temporaryRoot) { if ($ownsTemporaryRoot -and (Test-Path -Path $temporaryRoot)) {
Remove-Item -Path $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
} }
} }

View File

@ -2,10 +2,14 @@
"$schema": "https://json-schema.org/draft-07/schema", "$schema": "https://json-schema.org/draft-07/schema",
"title": "Update RepoUtils Script Settings", "title": "Update RepoUtils Script Settings",
"description": "Configuration for the Update-RepoUtils utility.", "description": "Configuration for the Update-RepoUtils utility.",
"dryRun": true,
"repository": { "repository": {
"url": "https://github.com/MAKS-IT-COM/maksit-repoutils.git", "url": "https://github.com/MAKS-IT-COM/maksit-repoutils.git",
"sourceSubdirectory": "src", "sourceSubdirectory": "src",
"preserveFileName": "scriptsettings.json", "preserveFileName": "scriptsettings.json",
"cloneDepth": 1 "cloneDepth": 1,
"skippedRelativeDirectories": [
"Release-Package/CustomPlugins"
]
} }
} }