Compare commits

...

1 Commits
v1.3.0 ... main

Author SHA1 Message Date
Maksym Sadovnychyy
8f42706221 (chore): update deps and sync RepoUtils 2026-08-14 21:02:41 +02:00
31 changed files with 843 additions and 983 deletions

View File

@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [1.3.0] - 2026-07-27
### Added ### Added

View File

@ -7,7 +7,7 @@
<RootNamespace>MaksIT.PodmanClientDotNet.PowerShell</RootNamespace> <RootNamespace>MaksIT.PodmanClientDotNet.PowerShell</RootNamespace>
<AssemblyName>MaksIT.PodmanClientDotNet.PowerShell</AssemblyName> <AssemblyName>MaksIT.PodmanClientDotNet.PowerShell</AssemblyName>
<Description>PowerShell module with cmdlets for PodmanClient.DotNet.</Description> <Description>PowerShell module with cmdlets for PodmanClient.DotNet.</Description>
<Version>1.3.0</Version> <Version>1.3.1</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -16,8 +16,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Management.Automation" Version="7.6.3" /> <PackageReference Include="System.Management.Automation" Version="7.6.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.11" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -12,7 +12,7 @@
<!-- NuGet package metadata --> <!-- NuGet package metadata -->
<PackageId>PodmanClient.DotNet</PackageId> <PackageId>PodmanClient.DotNet</PackageId>
<Version>1.3.0</Version> <Version>1.3.1</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>PodmanClient.DotNet</Product> <Product>PodmanClient.DotNet</Product>
@ -49,11 +49,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MaksIT.Results" Version="2.0.3" /> <PackageReference Include="MaksIT.Results" Version="2.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.11" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.11" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.11" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.11" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -14,8 +14,8 @@
<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.Extensions.Logging.Console" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.11" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

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

View File

@ -5,21 +5,14 @@
.SYNOPSIS .SYNOPSIS
Plugin-driven release engine entry script. Plugin-driven release engine entry script.
.PARAMETER DryRun .NOTES
When set, plugins that declare mutatesRemote in Get-PluginMetadata validate only (no registry push, GitHub release, or cluster deploy). Per-plugin dryRun on mutatesRemote plugins validates without remote mutation.
There is no engine-wide dryRun switch set dryRun on each plugin as needed.
.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.
#> #>
[CmdletBinding()] # Keep a plain param block so unbound launcher arguments remain in $args for
param( # optional Initialize-ReleaseExtension (advanced binding would reject them).
[switch]$DryRun, param()
[ValidateSet('single', 'ha')]
[string]$Mode
)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot Set-Location $PSScriptRoot
@ -29,20 +22,43 @@ $srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1') . (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
Import-EngineModules -Engine Release Import-EngineModules -Engine Release
$settings = Get-ScriptSettings -ScriptDir $PSScriptRoot $releaseExtension = $null
$resolveModeParams = @{ Settings = $settings } if (Get-Command Initialize-ReleaseExtension -ErrorAction SilentlyContinue) {
if ($PSBoundParameters.ContainsKey('Mode')) { $releaseExtension = Initialize-ReleaseExtension -ScriptDir $PSScriptRoot -ArgumentList $args
$resolveModeParams['ModeOverride'] = $Mode
} }
$deployMode = Get-DeployModeFromSettings @resolveModeParams
$settings = if ($null -ne $releaseExtension) {
$releaseExtension.Settings
}
else {
Get-ScriptSettings -ScriptDir $PSScriptRoot
}
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings $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 '=================================================='
Write-Log -Level 'STEP' -Message "RELEASE ENGINE (deploy mode: $deployMode)" Write-Log -Level 'STEP' -Message $releaseBanner
Write-Log -Level 'STEP' -Message '==================================================' Write-Log -Level 'STEP' -Message '=================================================='
$plugins = $configuredPlugins $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!' Write-Log -Level 'OK' -Message 'All pre-flight checks passed!'
$sharedPluginSettings = $engineContext $sharedPluginSettings = $engineContext
@ -59,14 +75,14 @@ else {
if ((Test-IsPublishPlugin -Plugin $plugin -EngineDirectory $PSScriptRoot) -and -not $releaseStageInitialized) { if ((Test-IsPublishPlugin -Plugin $plugin -EngineDirectory $PSScriptRoot) -and -not $releaseStageInitialized) {
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -WriteLogs:$false) { if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -WriteLogs:$false) {
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)]) Initialize-ReleaseStageContext -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory -Version $engineContext.version
$releaseStageInitialized = $true $releaseStageInitialized = $true
} }
} }
$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
} }
@ -88,9 +104,6 @@ elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -
elseif ($engineContext.isNonReleaseBranch) { elseif ($engineContext.isNonReleaseBranch) {
Write-Log -Level 'OK' -Message 'NON-RELEASE RUN COMPLETE' Write-Log -Level 'OK' -Message 'NON-RELEASE RUN COMPLETE'
} }
elseif ($engineContext.dryRun) {
Write-Log -Level 'OK' -Message 'DRY RUN COMPLETE'
}
else { else {
Write-Log -Level 'OK' -Message 'RELEASE COMPLETE' Write-Log -Level 'OK' -Message 'RELEASE COMPLETE'
} }

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

@ -3,13 +3,14 @@
<# <#
.SYNOPSIS .SYNOPSIS
Helpers to resolve engine semver and relative paths from plugin configuration. Generic engine helpers: path resolution and the shared context facts API.
.DESCRIPTION .DESCRIPTION
Used by New-EngineContext and version plugins: Engine-owned state stays on the context object (version, tag, skipPublishPlugins, ).
- DotNetReleaseVersion plugin -> projectFiles (.csproj <Version>) Plugin-published values go under $context.facts[namespace][name] via Set/Get/Test-EngineFact.
- NpmReleaseVersion plugin -> packageJsonPath (package.json version) During migration, -LegacyProperty on Set dual-writes flat properties; Get falls back to them.
- FileReleaseVersion plugin -> versionFilePath (repo-root VERSION file)
Version plugins declare providesVersion = $true; New-EngineContext discovers the single enabled one.
#> #>
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) { 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 { function Resolve-RelativePaths {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -52,251 +63,270 @@ function Resolve-RelativePaths {
return @($resolved) return @($resolved)
} }
function Get-CsprojPropertyValue { function Initialize-EngineFactsBag {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[xml]$Csproj, [psobject]$Context
[Parameter(Mandatory = $true)]
[string]$PropertyName
) )
# SDK-style .csproj files can have multiple PropertyGroup nodes. if (-not ($Context.PSObject.Properties.Name -contains 'facts') -or $null -eq $Context.facts) {
# Use the first group that defines the requested property. $Context | Add-Member -NotePropertyName facts -NotePropertyValue ([ordered]@{}) -Force
$propNode = $Csproj.Project.PropertyGroup | }
Where-Object { $_.$PropertyName } | }
Select-Object -First 1
if ($propNode) { function Assert-EngineFactNamespace {
return $propNode.$PropertyName 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 return $null
} }
function Get-CsprojVersions { function Test-EngineFact {
param( param(
[Parameter(Mandatory = $true)] [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)..." $value = Get-EngineFact -Context $Context -Namespace $Namespace -Name $Name -LegacyProperty $LegacyProperty
$projectVersions = @{} 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
} }
if ([System.IO.Path]::GetExtension($projectPath) -ne ".csproj") { function Set-EngineState {
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
}
function Resolve-DotNetReleaseVersion {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[object[]]$Plugins, [psobject]$Context,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$ScriptDir [string]$Name,
[Parameter(Mandatory = $true)]
[AllowNull()]
$Value
) )
$releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'DotNetReleaseVersion' } | Select-Object -First 1) if ($script:EngineStateAllowlist -notcontains $Name) {
if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) { throw "Engine state key '$Name' is not allowlisted. Use Set-EngineFact for plugin outputs, or extend the engine allowlist for documented engine fields."
Write-Error "Configure a DotNetReleaseVersion plugin in scriptSettings.json with projectFiles."
exit 1
} }
$releaseVersionSettings = $releaseVersionPlugin[0] $Context | Add-Member -NotePropertyName $Name -NotePropertyValue $Value -Force
$projectFiles = @(Resolve-RelativePaths -Value $releaseVersionSettings.projectFiles -BasePath $ScriptDir)
if ($projectFiles.Count -eq 0) {
Write-Error "Configure release version via DotNetReleaseVersion.projectFiles (first .csproj with <Version>)."
exit 1
} }
$projectVersions = Get-CsprojVersions -ProjectFiles $projectFiles function Add-EnginePublishCompletion {
$version = $projectVersions[$projectFiles[0]]
return [pscustomobject]@{
version = $version
source = 'DotNetReleaseVersion'
}
}
function Resolve-NpmReleaseVersion {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[object[]]$Plugins, [psobject]$Context,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$ScriptDir [string]$Publisher
) )
$releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'NpmReleaseVersion' } | Select-Object -First 1) if ([string]::IsNullOrWhiteSpace($Publisher)) {
if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) { throw "Publisher name is required."
Write-Error "Configure an NpmReleaseVersion plugin in scriptSettings.json with packageJsonPath."
exit 1
} }
$releaseVersionSettings = $releaseVersionPlugin[0] $completedBy = @(Get-EngineFact -Context $Context -Namespace 'publish' -Name 'completedBy' -Default @())
$packageJsonPaths = @(Resolve-RelativePaths -Value $releaseVersionSettings.packageJsonPath -BasePath $ScriptDir) if ($completedBy -notcontains $Publisher) {
$completedBy += $Publisher
if ($packageJsonPaths.Count -eq 0) {
Write-Error "Configure release version via NpmReleaseVersion.packageJsonPath."
exit 1
} }
$packageJsonPath = $packageJsonPaths[0] Set-EngineFact -Context $Context -Namespace 'publish' -Name 'completedBy' -Value $completedBy -Overwrite Replace
if (-not (Test-Path $packageJsonPath -PathType Leaf)) { Set-EngineFact -Context $Context -Namespace 'publish' -Name 'completed' -Value $true -Overwrite Replace -LegacyProperty 'publishCompleted'
Write-Error "NpmReleaseVersion: package.json not found at: $packageJsonPath"
exit 1
} }
Write-Log -Level "INFO" -Message "Reading version from npm package.json (packageJsonPath)..." function Get-EngineState {
$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( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$VersionFilePath [psobject]$Context,
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $false)]
$Default,
[switch]$Required
) )
if (-not (Test-Path $VersionFilePath -PathType Leaf)) { if ($script:EngineStateAllowlist -notcontains $Name) {
Write-Error "FileReleaseVersion: VERSION file not found at: $VersionFilePath" throw "Engine state key '$Name' is not allowlisted."
exit 1
} }
$version = (Get-Content -Path $VersionFilePath -Raw -Encoding UTF8).Trim() if ($Context.PSObject.Properties.Name -contains $Name) {
if ([string]::IsNullOrWhiteSpace($version)) { $value = $Context.$Name
Write-Error "FileReleaseVersion: VERSION file is empty at: $VersionFilePath" if (Test-EngineFactValuePresent -Value $value) {
exit 1 return $value
}
$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 { if ($Required) {
param( throw "Required engine state '$Name' is missing."
[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) { if ($PSBoundParameters.ContainsKey('Default')) {
return Resolve-DotNetReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir return $Default
} }
if ($npmPlugin.Count -gt 0) { return $null
return Resolve-NpmReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
} }
if ($filePlugin.Count -gt 0) { Export-ModuleMember -Function `
return Resolve-FileReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir Resolve-RelativePaths, `
} Initialize-EngineFactsBag, `
Set-EngineFact, `
Write-Error "Configure a DotNetReleaseVersion (projectFiles), NpmReleaseVersion (packageJsonPath), or FileReleaseVersion (versionFilePath) plugin in scriptSettings.json." Get-EngineFact, `
exit 1 Test-EngineFact, `
} Set-EngineState, `
Add-EnginePublishCompletion, `
Export-ModuleMember -Function Get-CsprojPropertyValue, Get-CsprojVersions, Get-VersionFileSemver, Resolve-RelativePaths, Resolve-DotNetReleaseVersion, Resolve-NpmReleaseVersion, Resolve-FileReleaseVersion, Resolve-ReleaseVersion Get-EngineState

View File

@ -32,4 +32,9 @@ function Import-EngineModules {
Import-Module $modulePath -Force Import-Module $modulePath -Force
} }
$extensionImport = Join-Path $modulesDir 'Extensions' 'Import-ExtensionModules.ps1'
if (Test-Path -LiteralPath $extensionImport -PathType Leaf) {
. $extensionImport
}
} }

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

@ -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" $engineContextModulePath = Join-Path $PSScriptRoot "EngineContext.psm1"
if (Test-Path $engineContextModulePath -PathType Leaf) { if (Test-Path $engineContextModulePath -PathType Leaf) {
Import-Module $engineContextModulePath -Force 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 { function Assert-WorkingTreeClean {
$gitStatus = Get-GitStatusShort $gitStatus = Get-GitStatusShort
if (-not [string]::IsNullOrWhiteSpace([string]$gitStatus)) { if (-not [string]::IsNullOrWhiteSpace([string]$gitStatus)) {
@ -48,23 +136,22 @@ function Assert-WorkingTreeClean {
function Initialize-ReleaseStageContext { function Initialize-ReleaseStageContext {
param( param(
[Parameter(Mandatory = $true)]
[object[]]$RemainingPlugins,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[psobject]$SharedSettings, [psobject]$SharedSettings,
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$ArtifactsDirectory, [string]$ArtifactsDirectory
[Parameter(Mandatory = $true)]
[string]$Version
) )
if (-not $SharedSettings.PSObject.Properties['releaseDir'] -or [string]::IsNullOrWhiteSpace([string]$SharedSettings.releaseDir)) { if (-not $SharedSettings.PSObject.Properties['releaseDir'] -or [string]::IsNullOrWhiteSpace([string]$SharedSettings.releaseDir)) {
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
Set-EngineState -Context $SharedSettings -Name 'releaseDir' -Value $ArtifactsDirectory
}
else {
$SharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $ArtifactsDirectory -Force $SharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $ArtifactsDirectory -Force
} }
} }
}
function New-EngineContext { function New-EngineContext {
param( param(
@ -80,15 +167,10 @@ function New-EngineContext {
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[psobject]$Settings, [psobject]$Settings,
[switch]$DryRun, [Parameter(Mandatory = $false)]
[psobject]$ExtensionData
[ValidateSet('single', 'ha')]
[string]$DeployMode = 'ha'
) )
$resolvedVersion = Resolve-ReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
$version = $resolvedVersion.version
$versionSource = $resolvedVersion.source
$releaseRelative = '..\..\..\releases' $releaseRelative = '..\..\..\releases'
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $releaseRelative)) $artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $releaseRelative))
@ -116,48 +198,58 @@ function New-EngineContext {
$releaseBranches = @('main') $releaseBranches = @('main')
} }
$isReleaseBranch = $releaseBranches -contains $currentBranch $isNonReleaseBranch = -not ($releaseBranches -contains $currentBranch)
$isNonReleaseBranch = -not $isReleaseBranch
Assert-WorkingTreeClean Assert-WorkingTreeClean
$tag = "v$version" $context = [pscustomobject]@{
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]@{
scriptDir = $ScriptDir scriptDir = $ScriptDir
srcDir = $SrcDir srcDir = $SrcDir
utilsDir = $SrcDir utilsDir = $SrcDir
currentBranch = $currentBranch currentBranch = $currentBranch
version = $version
tag = $tag
artifactsDirectory = $artifactsDirectory artifactsDirectory = $artifactsDirectory
isReleaseBranch = $isReleaseBranch
isNonReleaseBranch = $isNonReleaseBranch isNonReleaseBranch = $isNonReleaseBranch
releaseBranches = $releaseBranches releaseBranches = $releaseBranches
publishCompleted = $false
skipPublishPlugins = $false skipPublishPlugins = $false
dryRun = $dryRun facts = [ordered]@{}
deployMode = $DeployMode
orchestrator = $orchestrator
} }
$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 { function Get-PreferredReleaseBranch {

View File

@ -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 { function New-EngineContext {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -21,22 +28,32 @@ function New-EngineContext {
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[psobject]$Settings, [psobject]$Settings,
[ValidateSet('single', 'ha')] [Parameter(Mandatory = $false)]
[string]$DeployMode = 'ha' [psobject]$ExtensionData
) )
$badgesDir = $null $context = [pscustomobject]@{
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]@{
scriptDir = $ScriptDir scriptDir = $ScriptDir
srcDir = $SrcDir srcDir = $SrcDir
utilsDir = $SrcDir utilsDir = $SrcDir
badgesDir = $badgesDir facts = [ordered]@{}
deployMode = $DeployMode
} }
$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 Export-ModuleMember -Function New-EngineContext

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

@ -51,7 +51,33 @@ function Invoke-GitInternal {
[string]$ErrorMessage = "Git command failed" [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 ($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 $output = & git @Arguments 2>&1
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
if ($exitCode -ne 0) { if ($exitCode -ne 0) {
@ -66,7 +92,13 @@ function Invoke-GitInternal {
return ($output -join "`n").Trim() return ($output -join "`n").Trim()
} }
if (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue) {
Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput -ThrowOnError:$false | Out-Null
}
else {
& git @Arguments & git @Arguments
}
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
if ($exitCode -ne 0) { if ($exitCode -ne 0) {
Write-Error "$ErrorMessage (exit code: $exitCode)" Write-Error "$ErrorMessage (exit code: $exitCode)"

View File

@ -7,25 +7,11 @@ function Get-ScriptSettings {
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$ScriptDir, [string]$ScriptDir,
[ValidateSet('single', 'ha')]
[string]$Mode,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[string]$SettingsFileName = 'scriptSettings.json' [string]$SettingsFileName = 'scriptSettings.json'
) )
$settingsPath = if ($PSBoundParameters.ContainsKey('Mode')) { $settingsPath = Join-Path $ScriptDir $SettingsFileName
$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
}
if (-not (Test-Path -LiteralPath $settingsPath -PathType Leaf)) { if (-not (Test-Path -LiteralPath $settingsPath -PathType Leaf)) {
throw "Settings file not found: $settingsPath" throw "Settings file not found: $settingsPath"
@ -34,92 +20,6 @@ function Get-ScriptSettings {
return Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json 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 { function Assert-Command {
[CmdletBinding()] [CmdletBinding()]
param( 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

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

@ -26,6 +26,7 @@ function Invoke-Plugin {
) )
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
$pluginSettings = $Settings $pluginSettings = $Settings
$sharedSettings = $Settings.context $sharedSettings = $Settings.context
@ -33,13 +34,18 @@ function Invoke-Plugin {
$version = $sharedSettings.version $version = $sharedSettings.version
$archiveInputs = @() $archiveInputs = @()
if ($sharedSettings.PSObject.Properties['releaseArchiveInputs'] -and $sharedSettings.releaseArchiveInputs) { $fromFact = Get-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -LegacyProperty @('releaseArchiveInputs')
$archiveInputs = @($sharedSettings.releaseArchiveInputs) if ($null -ne $fromFact) {
$archiveInputs = @($fromFact)
}
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
} }
elseif ($sharedSettings.PSObject.Properties['packageFile'] -and $sharedSettings.packageFile) {
$archiveInputs = @($sharedSettings.packageFile.FullName)
if ($sharedSettings.PSObject.Properties['symbolsPackageFile'] -and $sharedSettings.symbolsPackageFile) {
$archiveInputs += $sharedSettings.symbolsPackageFile.FullName
} }
} }
@ -79,16 +85,18 @@ function Invoke-Plugin {
Write-Log -Level "OK" -Message " Release archive ready: $zipPath" Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
$releaseAssetPaths = @($zipPath) $releaseAssetPaths = @($zipPath)
if ($sharedSettings.PSObject.Properties['packageFile'] -and $sharedSettings.packageFile) { $packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile')
$releaseAssetPaths += $sharedSettings.packageFile.FullName if ($null -ne $packageFile) {
$releaseAssetPaths += $packageFile.FullName
} }
if ($sharedSettings.PSObject.Properties['symbolsPackageFile'] -and $sharedSettings.symbolsPackageFile) { $symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile')
$releaseAssetPaths += $sharedSettings.symbolsPackageFile.FullName if ($null -ne $symbolsPackageFile) {
$releaseAssetPaths += $symbolsPackageFile.FullName
} }
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
$sharedSettings | Add-Member -NotePropertyName releaseArchivePath -NotePropertyValue $zipPath -Force Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archivePath' -Value $zipPath -Overwrite Replace -LegacyProperty 'releaseArchivePath'
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $releaseAssetPaths -Force Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
} }
Export-ModuleMember -Function Invoke-Plugin Export-ModuleMember -Function Invoke-Plugin

View File

@ -76,7 +76,8 @@ function Invoke-Plugin {
} }
Write-Log -Level "OK" -Message " NuGet push completed." 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 { function Get-PluginMetadata {

View File

@ -30,7 +30,8 @@ function Invoke-Plugin {
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command" 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 $sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir $scriptDir = $sharedSettings.scriptDir
@ -48,12 +49,12 @@ function Invoke-Plugin {
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) { if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([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 { else {
$artifactsDirectory = $sharedSettings.artifactsDirectory $artifactsDirectory = $sharedSettings.artifactsDirectory
} }
$packageProjectPath = $null
$releaseArchiveInputs = @()
Assert-Command dotnet Assert-Command dotnet
@ -79,50 +80,19 @@ function Invoke-Plugin {
throw "dotnet pack failed for $packageProjectPath." throw "dotnet pack failed for $packageProjectPath."
} }
# dotnet pack can leave older packages in the artifacts directory. $resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $outputDir -Version $version
# 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
}
}
}
if (-not $packageFile) { Write-Log -Level "OK" -Message " Package ready: $($resolved.PackageFile.FullName)"
throw "Could not locate generated NuGet package for version $version in: $outputDir" if ($resolved.SymbolsPackageFile) {
} Write-Log -Level "OK" -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
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
} }
else { else {
Write-Log -Level "WARN" -Message " Symbols package (.snupkg) not found for version $version." Write-Log -Level "WARN" -Message " Symbols package (.snupkg) not found for version $version."
} }
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $packageFile -Force Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
$sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $symbolsPackageFile -Force Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $releaseArchiveInputs -Force Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
} }
Export-ModuleMember -Function Invoke-Plugin Export-ModuleMember -Function Invoke-Plugin

View File

@ -27,18 +27,21 @@ function Invoke-Plugin {
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command" Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
$sharedSettings = $Settings.context $sharedSettings = $Settings.context
$projectFiles = $sharedSettings.projectFiles $projectFiles = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles')
$artifactsDirectory = $sharedSettings.artifactsDirectory $artifactsDirectory = $sharedSettings.artifactsDirectory
$publishProjectPath = $null $publishProjectPath = $null
Assert-Command dotnet 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." throw "DotNetPublish plugin requires project files in the shared context."
} }
$projectFiles = @($projectFiles)
if (!(Test-Path $artifactsDirectory)) { if (!(Test-Path $artifactsDirectory)) {
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
} }
@ -64,9 +67,9 @@ function Invoke-Plugin {
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir" Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $null -Force Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $null -Overwrite Replace -LegacyProperty 'packageFile'
$sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $null -Force Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $null -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue @($publishDir) -Force Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($publishDir) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
} }
Export-ModuleMember -Function Invoke-Plugin Export-ModuleMember -Function Invoke-Plugin

View File

@ -3,11 +3,13 @@
<# <#
.SYNOPSIS .SYNOPSIS
Loads release version into shared context. Loads release version from an SDK-style .csproj into shared context.
.DESCRIPTION .DESCRIPTION
Dedicated version-loading plugin. It reads .csproj version via Dedicated version-loading plugin. Reads <Version> from the first configured
EngineContext helpers and writes Version into the shared runtime context. 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)) { 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: <Version> not found in '$ProjectPath'."
}
return [string]$version
}
function Get-PluginMetadata {
return [pscustomobject]@{ providesVersion = $true }
}
function Invoke-Plugin { function Invoke-Plugin {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -25,17 +77,20 @@ function Invoke-Plugin {
) )
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-DotNetReleaseVersion" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
$shared = $Settings.context $shared = $Settings.context
$resolved = Resolve-DotNetReleaseVersion -Plugins @($Settings) -ScriptDir $shared.scriptDir
$projectFiles = @(Resolve-RelativePaths -Value $Settings.projectFiles -BasePath $shared.scriptDir) $projectFiles = @(Resolve-RelativePaths -Value $Settings.projectFiles -BasePath $shared.scriptDir)
if ($projectFiles.Count -eq 0) {
$shared | Add-Member -NotePropertyName version -NotePropertyValue $resolved.version -Force throw "DotNetReleaseVersion plugin requires 'projectFiles' (first .csproj with <Version>) in scriptSettings.json."
$shared | Add-Member -NotePropertyName projectFiles -NotePropertyValue $projectFiles -Force
Write-Log -Level "OK" -Message " Release version loaded by DotNetReleaseVersion plugin: $($shared.version)"
} }
Export-ModuleMember -Function Invoke-Plugin 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, Get-PluginMetadata

View File

@ -58,20 +58,12 @@ function Invoke-Plugin {
throw "Tests failed. $($testResult.Error)" throw "Tests failed. $($testResult.Error)"
} }
$sharedSettings | Add-Member -NotePropertyName npmWorkspaceRoot -NotePropertyValue $workspaceRoot -Force Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
$sharedSettings | Add-Member -NotePropertyName testResult -NotePropertyValue $testResult -Force Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Publish-CoverageMetricsToSharedContext"
$sharedSettings | Add-Member -NotePropertyName qualityLineCoverage -NotePropertyValue $testResult.LineRate -Force Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'workspaceRoot' -Value $workspaceRoot -Overwrite Replace -LegacyProperty 'npmWorkspaceRoot'
$sharedSettings | Add-Member -NotePropertyName coverageLineRate -NotePropertyValue $testResult.LineRate -Force Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
$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
}
if (($testResult.PSObject.Properties.Name -contains 'CoverageSummaryFile') -and $testResult.CoverageSummaryFile) { 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!" Write-Log -Level "OK" -Message " All tests passed!"

View File

@ -27,7 +27,7 @@ function Invoke-Plugin {
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command" Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
$pluginSettings = $Settings $pluginSettings = $Settings
$shared = $Settings.context $shared = $Settings.context
@ -49,6 +49,7 @@ function Invoke-Plugin {
$artifactsDirectory = $null $artifactsDirectory = $null
if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) { if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) {
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $shared.scriptDir ([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)) { elseif ($shared.PSObject.Properties['artifactsDirectory'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.artifactsDirectory)) {
$artifactsDirectory = [string]$shared.artifactsDirectory $artifactsDirectory = [string]$shared.artifactsDirectory
@ -125,10 +126,11 @@ function Invoke-Plugin {
Pop-Location Pop-Location
} }
$shared | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force Set-EngineState -Context $shared -Name 'releaseDir' -Value $artifactsDirectory
$shared | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $releaseAssetPaths -Force Set-EngineFact -Context $shared -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
if ($releaseAssetPaths.Count -gt 0) { 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))." Write-Log -Level "OK" -Message " npm pack completed ($($releaseAssetPaths.Count) tarball(s))."

View File

@ -127,7 +127,8 @@ registry=$registry
} }
Write-Log -Level "OK" -Message " npm publish completed." 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 { finally {
if (Test-Path $tempNpmRcPath -PathType Leaf) { if (Test-Path $tempNpmRcPath -PathType Leaf) {

View File

@ -57,6 +57,10 @@ function Set-PackageJsonVersionInternal {
($json | ConvertTo-Json -Depth 100) + [Environment]::NewLine | Set-Content -Path $PackageJsonPath -Encoding UTF8 -NoNewline ($json | ConvertTo-Json -Depth 100) + [Environment]::NewLine | Set-Content -Path $PackageJsonPath -Encoding UTF8 -NoNewline
} }
function Get-PluginMetadata {
return [pscustomobject]@{ providesVersion = $true }
}
function Invoke-Plugin { function Invoke-Plugin {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -64,7 +68,7 @@ function Invoke-Plugin {
) )
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
$pluginSettings = $Settings $pluginSettings = $Settings
$shared = $Settings.context $shared = $Settings.context
@ -90,10 +94,11 @@ function Invoke-Plugin {
Write-Log -Level "OK" -Message " Synchronized workspace package versions to $version." Write-Log -Level "OK" -Message " Synchronized workspace package versions to $version."
} }
$shared | Add-Member -NotePropertyName version -NotePropertyValue $version -Force $npmWorkspaceRoot = Split-Path -Parent $packageJsonPath
$shared | Add-Member -NotePropertyName npmWorkspaceRoot -NotePropertyValue (Split-Path -Parent $packageJsonPath) -Force Set-EngineState -Context $shared -Name 'version' -Value $version
$shared | Add-Member -NotePropertyName npmPackageJsonPath -NotePropertyValue $packageJsonPath -Force 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" Write-Log -Level "OK" -Message " Release version loaded by NpmReleaseVersion plugin: $version"
} }
Export-ModuleMember -Function Invoke-Plugin Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -6,8 +6,10 @@
Loads release version from a repo-root VERSION file into shared context. Loads release version from a repo-root VERSION file into shared context.
.DESCRIPTION .DESCRIPTION
Reads a single-line semver from the configured versionFilePath (default repo-root VERSION). Reads a single-line semver from the configured versionFilePath (default
Used by container-only repos without .csproj or package.json. 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)) { 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 { function Invoke-Plugin {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -25,17 +54,28 @@ function Invoke-Plugin {
) )
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-FileReleaseVersion" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
$shared = $Settings.context $shared = $Settings.context
$resolved = Resolve-FileReleaseVersion -Plugins @($Settings) -ScriptDir $shared.scriptDir $versionFileSetting = if ($Settings.versionFilePath) {
$versionFilePaths = @(Resolve-RelativePaths -Value $Settings.versionFilePath -BasePath $shared.scriptDir) $Settings.versionFilePath
$shared | Add-Member -NotePropertyName version -NotePropertyValue $resolved.version -Force
if ($versionFilePaths.Count -gt 0) {
$shared | Add-Member -NotePropertyName versionFilePath -NotePropertyValue $versionFilePaths[0] -Force
} }
Write-Log -Level "OK" -Message " Release version loaded by FileReleaseVersion plugin: $($shared.version)" else {
'..\..\..\VERSION'
} }
Export-ModuleMember -Function Invoke-Plugin $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, Get-PluginMetadata

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

@ -10,9 +10,9 @@
shared engine context (same object passed to every plugin as .context). shared engine context (same object passed to every plugin as .context).
Line coverage for threshold checks is resolved in order (first present wins): Line coverage for threshold checks is resolved in order (first present wins):
- qualityLineCoverage (generic; any plugin may set this) - facts test.coverageLineRate (via Get-EngineFact)
- coverageLineRate (conventional flat metric) - legacy qualityLineCoverage / coverageLineRate flat properties
- testResult.LineRate (object from a test plugin; property name is conventional) - testResult.LineRate (object from a test plugin)
Configure coverageThreshold > 0 to require one of those inputs. With coverageThreshold 0 Configure coverageThreshold > 0 to require one of those inputs. With coverageThreshold 0
and scanVulnerabilities false, the plugin is a no-op. and scanVulnerabilities false, the plugin is a no-op.
@ -64,6 +64,20 @@ function Get-LineCoveragePercentFromSharedContext {
$Shared $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')) { foreach ($prop in @('qualityLineCoverage', 'coverageLineRate')) {
if ($Shared.PSObject.Properties.Name -contains $prop) { if ($Shared.PSObject.Properties.Name -contains $prop) {
$raw = $Shared.$prop $raw = $Shared.$prop
@ -92,7 +106,7 @@ function Invoke-Plugin {
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log" Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command" Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Get-EngineFact"
$pluginSettings = $Settings $pluginSettings = $Settings
$sharedSettings = $Settings.context $sharedSettings = $Settings.context
@ -107,11 +121,9 @@ function Invoke-Plugin {
if ($pluginSettings.PSObject.Properties['projectFiles'] -and $null -ne $pluginSettings.projectFiles) { if ($pluginSettings.PSObject.Properties['projectFiles'] -and $null -ne $pluginSettings.projectFiles) {
$projectFiles = @(Resolve-RelativePaths -Value $pluginSettings.projectFiles -BasePath $scriptDir) $projectFiles = @(Resolve-RelativePaths -Value $pluginSettings.projectFiles -BasePath $scriptDir)
} }
elseif ($sharedSettings.PSObject.Properties['projectFiles'] -and $null -ne $sharedSettings.projectFiles) {
$projectFiles = @($sharedSettings.projectFiles)
}
else { else {
$projectFiles = @() $fromContext = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles')
$projectFiles = if ($null -ne $fromContext) { @($fromContext) } else { @() }
} }
$coverageThreshold = 0 $coverageThreshold = 0
@ -129,7 +141,7 @@ function Invoke-Plugin {
if ($needCoverageCheck) { if ($needCoverageCheck) {
$lineRate = Get-LineCoveragePercentFromSharedContext -Shared $sharedSettings $lineRate = Get-LineCoveragePercentFromSharedContext -Shared $sharedSettings
if ($null -eq $lineRate) { 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..." Write-Log -Level "STEP" -Message "Checking line coverage threshold against shared context..."

View File

@ -50,10 +50,15 @@ function Invoke-NotMetInternal {
[string]$Reason [string]$Reason
) )
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
Set-EngineState -Context $Shared -Name 'skipPublishPlugins' -Value $true
}
else {
$Shared | Add-Member -NotePropertyName skipPublishPlugins -NotePropertyValue $true -Force $Shared | Add-Member -NotePropertyName skipPublishPlugins -NotePropertyValue $true -Force
}
if ($When -eq 'fail') { if ($When -eq 'fail') {
Write-Log -Level "ERROR" -Message "ReleasePublishGuard: $Reason" throw "ReleasePublishGuard: $Reason"
exit 1
} }
Write-Log -Level "WARN" -Message " Publish suppressed: $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 "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "PluginSupport" -RequiredCommand "Get-PluginBranches" 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 "Get-GitStatusShort"
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Test-RemoteTagExists" Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Test-RemoteTagExists"
@ -82,15 +88,10 @@ function Invoke-Plugin {
throw "ReleasePublishGuard: whenRequirementsNotMet must be 'skip' or 'fail'." 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..." 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) $allowed = @(Get-PluginBranches -Plugin $pluginSettings)
if ($allowed.Count -gt 0 -and $allowed -notcontains '*' -and $allowed -notcontains $shared.currentBranch) { 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." 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 return
} }
$shared | Add-Member -NotePropertyName tag -NotePropertyValue $tag -Force Set-EngineState -Context $shared -Name 'tag' -Value $tag
} }
$ensureRemote = $true $ensureRemote = $true

View File

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

View File

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