Compare commits

...

1 Commits
v2.0.0 ... main

Author SHA1 Message Date
Maksym Sadovnychyy
1103c502ca (chore): release 2.0.1 with deps, RepoUtils sync, and nuget OIDC 2026-10-27 20:45:57 +01:00
30 changed files with 1579 additions and 947 deletions

34
.github/workflows/publish-nuget.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Publish to NuGet
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Pack
run: |
dotnet pack src/MaksIT.Persistence/MaksIT.Persistence.csproj -c Release -o artifacts
- name: NuGet login (OIDC)
id: login
uses: NuGet/login@v1
with:
user: maks-it
- name: Push to NuGet
run: dotnet nuget push artifacts/*.nupkg --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate

View File

@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.0.1] - 2026-10-27
### Changed
- Bumped package dependencies: linq2db / linq2db.PostgreSQL 6.5.1, MaksIT.Core 1.6.14, MaksIT.Results 2.0.6, Microsoft.Extensions.* 10.0.14 (tests: coverlet.mtp, xunit.v3).
- Synced RepoUtils utils: ContainerRegistry JSON catalog (PascalCase Harbor / InCluster keys); `CleanupArtifacts` replaces `DotNetCleanupArtifacts`; GitHub `githubSecret` is `GitClone`; release `scriptSettings` declares `repoUtilsSecretsShared` / `repoUtilsSecrets`; dropped unused Npm plugins from release settings.
- NuGet publish moved to GitHub Actions Trusted Publishing (OIDC on `v*` tags); removed `DotNetNuGet` from the RepoUtils release pipeline.
- Docs: CONTRIBUTING points at RepoUtils Origin; README HAMode note is **maksit-buoy**.
## [2.0.0] - 2026-08-21 ## [2.0.0] - 2026-08-21
### Removed ### Removed

View File

@ -94,7 +94,7 @@ Optional SemVer prerelease (`0.1.0-alpha.1`, `0.1.0-beta.1`, `0.1.0-rc.1`) is va
## Utility Scripts ## Utility Scripts
Automation lives under `utils/` (Community profile from [maksit-repoutils](https://github.com/MAKS-IT-COM/maksit-repoutils), local-copy sync). Product-specific settings are in each engine’s `scriptSettings.json`. Automation lives under `utils/` (Community profile from [maksit-repoutils](https://git.maks-it.com/MAKS-IT/maksit-repoutils), local-copy sync). Product-specific settings are in each engine’s `scriptSettings.json`.
### Run tests and coverage badges ### Run tests and coverage badges

View File

@ -1,8 +1,8 @@
# MaksIT.Persistence # MaksIT.Persistence
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-36.8%25-yellow) ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-38.8%25-yellow)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-41.4%25-yellowgreen) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-41.7%25-yellowgreen)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-48.6%25-yellowgreen) ![Method Coverage](https://img.shields.io/badge/Method%20Coverage-50%25-yellowgreen)
NuGet facade over [linq2db](https://linq2db.github.io/) + PostgreSQL for ASP.NET Core Engines: connection factory, FluentMigrator startup, and `MaksIT.Results` guards. NuGet facade over [linq2db](https://linq2db.github.io/) + PostgreSQL for ASP.NET Core Engines: connection factory, FluentMigrator startup, and `MaksIT.Results` guards.
@ -29,7 +29,7 @@ Linq2Db queries stay in the product: persist/query services still call `GetTable
| Scoped `DataConnection` | `RegisterLinq2Db(connectionString, mappingSchema?)` | `ILinq2DbConnectionFactory` | | Scoped `DataConnection` | `RegisterLinq2Db(connectionString, mappingSchema?)` | `ILinq2DbConnectionFactory` |
| FluentMigrator wait / create-DB / retry MigrateUp | `RegisterPostgresMigrations(connectionString, assembly, options?)` | `IRunMigrationsService` | | FluentMigrator wait / create-DB / retry MigrateUp | `RegisterPostgresMigrations(connectionString, assembly, options?)` | `IRunMigrationsService` |
Work leases are **not** in this package: use **MaksIT.Dapr** (`RegisterWorkLeases`) for most first-party apps, or **MaksIT.HAMode** for **maksit-nats** only. Work leases are **not** in this package: use **MaksIT.Dapr** (`RegisterWorkLeases`) for most first-party apps, or **MaksIT.HAMode** for **maksit-buoy** only.
## Result conventions ## Result conventions
@ -54,7 +54,7 @@ dotnet add package MaksIT.Persistence
Or in your `.csproj`: Or in your `.csproj`:
```xml ```xml
<PackageReference Include="MaksIT.Persistence" Version="2.0.0" /> <PackageReference Include="MaksIT.Persistence" Version="2.0.1" />
``` ```
## Registering services ## Registering services

View File

@ -12,11 +12,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.MTP" Version="10.0.1" /> <PackageReference Include="coverlet.MTP" Version="10.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.14" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.11" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.14" />
<PackageReference Include="Moq" Version="4.*" /> <PackageReference Include="Moq" Version="4.*" />
<PackageReference Include="xunit.v3" Version="4.0.0" /> <PackageReference Include="xunit.v3" Version="4.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,7 +7,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>MaksIT.Persistence</PackageId> <PackageId>MaksIT.Persistence</PackageId>
<Version>2.0.0</Version> <Version>2.0.1</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>MaksIT.Persistence</Product> <Product>MaksIT.Persistence</Product>
@ -23,12 +23,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentMigrator" Version="8.0.1" /> <PackageReference Include="FluentMigrator" Version="8.0.1" />
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="8.0.1" /> <PackageReference Include="FluentMigrator.Runner.Postgres" Version="8.0.1" />
<PackageReference Include="linq2db" Version="6.4.0" /> <PackageReference Include="linq2db" Version="6.5.1" />
<PackageReference Include="linq2db.PostgreSQL" Version="6.4.0" /> <PackageReference Include="linq2db.PostgreSQL" Version="6.5.1" />
<PackageReference Include="MaksIT.Core" Version="1.6.9" /> <PackageReference Include="MaksIT.Core" Version="1.6.14" />
<PackageReference Include="MaksIT.Results" Version="2.0.4" /> <PackageReference Include="MaksIT.Results" Version="2.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.14" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.11" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.14" />
<PackageReference Include="Npgsql" Version="10.0.3" /> <PackageReference Include="Npgsql" Version="10.0.3" />
</ItemGroup> </ItemGroup>

View File

@ -8,6 +8,10 @@
.NOTES .NOTES
Per-plugin dryRun on mutatesRemote plugins validates without remote mutation. Per-plugin dryRun on mutatesRemote plugins validates without remote mutation.
There is no engine-wide dryRun switch — set dryRun on each plugin as needed. There is no engine-wide dryRun switch — set dryRun on each plugin as needed.
-InstallOnly (Install-Helm-*.bat) keeps the version plugin and HelmSelfDeploy.
Compile, test, image build, image push, and HelmPush are skipped. The chart
must already be in OCI at the version the version plugin reports.
#> #>
# Keep a plain param block so unbound launcher arguments remain in $args for # Keep a plain param block so unbound launcher arguments remain in $args for
@ -17,6 +21,17 @@ param()
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot Set-Location $PSScriptRoot
$installOnly = $false
$extensionArgs = [System.Collections.Generic.List[object]]::new()
foreach ($argument in @($args)) {
if ([string]$argument -eq '-InstallOnly') {
$installOnly = $true
continue
}
$extensionArgs.Add($argument)
}
$srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path $srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1') . (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
@ -24,7 +39,7 @@ Import-EngineModules -Engine Release
$releaseExtension = $null $releaseExtension = $null
if (Get-Command Initialize-ReleaseExtension -ErrorAction SilentlyContinue) { if (Get-Command Initialize-ReleaseExtension -ErrorAction SilentlyContinue) {
$releaseExtension = Initialize-ReleaseExtension -ScriptDir $PSScriptRoot -ArgumentList $args $releaseExtension = Initialize-ReleaseExtension -ScriptDir $PSScriptRoot -ArgumentList @($extensionArgs)
} }
$settings = if ($null -ne $releaseExtension) { $settings = if ($null -ne $releaseExtension) {
@ -35,8 +50,29 @@ else {
} }
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings $configuredPlugins = Get-ConfiguredPlugins -Settings $settings
if ($installOnly) {
$enabledBefore = @($configuredPlugins | Where-Object { $_.enabled } | ForEach-Object { [string]$_.name })
$configuredPlugins = @(Select-HelmInstallPlugins -Plugins $configuredPlugins -ScriptDir $PSScriptRoot)
$kept = @($configuredPlugins | ForEach-Object { [string]$_.name })
$skipped = @($enabledBefore | Where-Object { $_ -notin $kept })
if ($skipped.Count -gt 0) {
Write-Log -Level 'INFO' -Message "Install-only: skipping $($skipped -join ', '). Chart and images are taken from OCI."
}
$releaseBanner = if ($null -ne $releaseExtension) { Write-Log -Level 'INFO' -Message "Install-only: running $($kept -join ', ')."
}
Initialize-RepoUtilsVaultSecrets -Settings $settings -Plugins $configuredPlugins
$releaseBanner = if ($installOnly) {
$modeLabel = 'ha'
if ($null -ne $releaseExtension -and $releaseExtension.ContextData.PSObject.Properties.Name -contains 'deployMode') {
$modeLabel = [string]$releaseExtension.ContextData.deployMode
}
"HELM INSTALL (deploy mode: $modeLabel; OCI chart, no build or push)"
}
elseif ($null -ne $releaseExtension) {
$releaseExtension.StepBanner $releaseExtension.StepBanner
} }
else { else {
@ -96,7 +132,11 @@ if (-not $releaseStageInitialized) {
Write-Log -Level 'OK' -Message '==================================================' Write-Log -Level 'OK' -Message '=================================================='
if ($releaseHadPluginFailures) { if ($releaseHadPluginFailures) {
Write-Log -Level 'ERROR' -Message 'RELEASE FAILED' $failureLabel = if ($installOnly) { 'HELM INSTALL FAILED' } else { 'RELEASE FAILED' }
Write-Log -Level 'ERROR' -Message $failureLabel
}
elseif ($installOnly) {
Write-Log -Level 'OK' -Message 'HELM INSTALL COMPLETE'
} }
elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins) { elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins) {
Write-Log -Level 'OK' -Message 'RUN COMPLETE (publish skipped by ReleasePublishGuard)' Write-Log -Level 'OK' -Message 'RUN COMPLETE (publish skipped by ReleasePublishGuard)'
@ -109,7 +149,7 @@ else {
} }
Write-Log -Level 'OK' -Message '==================================================' Write-Log -Level 'OK' -Message '=================================================='
if ($engineContext.isNonReleaseBranch -and -not ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins)) { if (-not $installOnly -and $engineContext.isNonReleaseBranch -and -not ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins)) {
$preferredReleaseBranch = Get-PreferredReleaseBranch -EngineContext $engineContext $preferredReleaseBranch = Get-PreferredReleaseBranch -EngineContext $engineContext
Write-Log -Level 'INFO' -Message "For publish, use an allowed branch (see ReleasePublishGuard.branches), e.g. '$preferredReleaseBranch', and satisfy the guard requirements." Write-Log -Level 'INFO' -Message "For publish, use an allowed branch (see ReleasePublishGuard.branches), e.g. '$preferredReleaseBranch', and satisfy the guard requirements."
} }

View File

@ -2,6 +2,8 @@
"$schema": "https://json-schema.org/draft-07/schema", "$schema": "https://json-schema.org/draft-07/schema",
"title": "Release Package Script Settings", "title": "Release Package Script Settings",
"description": "Invoke-ReleasePackage.ps1 plugin settings for maksit-persistence.", "description": "Invoke-ReleasePackage.ps1 plugin settings for maksit-persistence.",
"repoUtilsSecretsShared": "RepoUtilsSecretsShared",
"repoUtilsSecrets": "RepoUtilsSecrets",
"plugins": [ "plugins": [
{ {
"name": "DotNetReleaseVersion", "name": "DotNetReleaseVersion",
@ -61,45 +63,13 @@
"name": "GitHub", "name": "GitHub",
"stageLabel": "release", "stageLabel": "release",
"enabled": true, "enabled": true,
"githubSecret": "GitHub", "githubSecret": "GitClone",
"repository": "https://github.com/MAKS-IT-COM/maksit-persistence", "repository": "https://github.com/MAKS-IT-COM/maksit-persistence",
"releaseNotesFile": "..\\..\\..\\CHANGELOG.md", "releaseNotesFile": "..\\..\\..\\CHANGELOG.md",
"releaseTitlePattern": "Release {version}" "releaseTitlePattern": "Release {version}"
}, },
{ {
"name": "DotNetNuGet", "name": "CleanupArtifacts",
"stageLabel": "release",
"enabled": true,
"nugetSecret": "NuGet",
"source": "https://api.nuget.org/v3/index.json"
},
{
"name": "NpmReleaseVersion",
"stageLabel": "build",
"enabled": false,
"packageJsonPath": "..\\..\\..\\src\\package.json",
"syncWorkspaceVersions": true
},
{
"name": "NpmBuild",
"stageLabel": "build",
"enabled": false,
"workspaceRoot": "..\\..\\..\\src",
"useCi": true,
"buildScript": "build"
},
{
"name": "NpmPublish",
"stageLabel": "release",
"enabled": false,
"npmSecret": "Npm",
"registry": "https://registry.npmjs.org",
"access": "public",
"workspaceRoot": "..\\..\\..\\src",
"publishOrder": []
},
{
"name": "DotNetCleanupArtifacts",
"stageLabel": "release", "stageLabel": "release",
"enabled": true, "enabled": true,
"includePatterns": [ "includePatterns": [

View File

@ -321,6 +321,37 @@ function Get-EngineState {
return $null return $null
} }
function Add-ReleaseAssetPaths {
param(
[Parameter(Mandatory = $true)]
[psobject]$Context,
[Parameter(Mandatory = $true)]
[AllowEmptyCollection()]
[string[]]$Path
)
$list = [System.Collections.Generic.List[string]]::new()
$existing = Get-EngineFact -Context $Context -Namespace 'release' -Name 'assetPaths' -Default @() -LegacyProperty @('releaseAssetPaths')
foreach ($item in @($existing)) {
$value = [string]$item
if (-not [string]::IsNullOrWhiteSpace($value) -and -not $list.Contains($value)) {
$list.Add($value)
}
}
foreach ($item in @($Path)) {
$value = [string]$item
if ([string]::IsNullOrWhiteSpace($value) -or $list.Contains($value)) {
continue
}
$list.Add($value)
}
Set-EngineFact -Context $Context -Namespace 'release' -Name 'assetPaths' -Value @($list) -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
}
Export-ModuleMember -Function ` Export-ModuleMember -Function `
Resolve-RelativePaths, ` Resolve-RelativePaths, `
Initialize-EngineFactsBag, ` Initialize-EngineFactsBag, `
@ -329,4 +360,5 @@ Export-ModuleMember -Function `
Test-EngineFact, ` Test-EngineFact, `
Set-EngineState, ` Set-EngineState, `
Add-EnginePublishCompletion, ` Add-EnginePublishCompletion, `
Add-ReleaseAssetPaths, `
Get-EngineState Get-EngineState

View File

@ -15,6 +15,7 @@ function Import-EngineModules {
(Join-Path $modulesDir 'ScriptConfig.psm1'), (Join-Path $modulesDir 'ScriptConfig.psm1'),
(Join-Path $modulesDir 'Logging.psm1'), (Join-Path $modulesDir 'Logging.psm1'),
(Join-Path $engineModuleDir 'PluginSupport.psm1'), (Join-Path $engineModuleDir 'PluginSupport.psm1'),
(Join-Path $engineModuleDir 'VaultSupport.psm1'),
(Join-Path $engineModuleDir 'EngineContext.psm1') (Join-Path $engineModuleDir 'EngineContext.psm1')
) )

View File

@ -33,6 +33,7 @@ function Test-IsEngineRuntimeModuleName {
'TestRunner', 'TestRunner',
'EngineContext', 'EngineContext',
'PluginSupport', 'PluginSupport',
'VaultSupport',
'ReleaseSupport', 'ReleaseSupport',
'TestSupport' 'TestSupport'
), ),
@ -53,7 +54,7 @@ function Get-PluginDependencyGroupDirectories {
} }
# Prefer Shared (helpers), then stock host groups; any other plugins/{Group}/ is discovered. # Prefer Shared (helpers), then stock host groups; any other plugins/{Group}/ is discovered.
$preferred = @('Shared', 'Platform', 'DotNet', 'Npm') $preferred = @('Shared', 'Platform', 'DotNet', 'Npm', 'Desktop')
$dirs = [System.Collections.Generic.List[string]]::new() $dirs = [System.Collections.Generic.List[string]]::new()
foreach ($name in $preferred) { foreach ($name in $preferred) {
$path = Join-Path $PluginsRoot $name $path = Join-Path $PluginsRoot $name
@ -293,131 +294,588 @@ function Test-PluginMutatesRemote {
return $false return $false
} }
function Get-SecretEnvironmentValue { function Get-RepoUtilsEnvironmentVariable {
<# <#
.SYNOPSIS .SYNOPSIS
Reads a secret value from an environment variable by logical name. Reads a named environment variable from Process, then Windows User, then Machine.
.DESCRIPTION .DESCRIPTION
Plugins never store secret material in scriptSettings.json. Settings hold a Process wins (CICD sandbox inject, `$env:Name`, explicit session values). When the
logical name (e.g. "GitHub", "NuGet"); the process environment variable with current process was started before a User-level pack was set, User/Machine still
that same name must be set before the engine runs. apply so laptop releases do not require a new shell. An explicit process value —
including empty JSON `{}` — shadows User/Machine.
.PARAMETER Name
Logical secret name — also the environment variable name to read.
.OUTPUTS
System.String. The environment variable value, or $null when unset.
.EXAMPLE
$token = Get-SecretEnvironmentValue -Name 'GitHub'
#> #>
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$Name [string]$Name
) )
return [Environment]::GetEnvironmentVariable($Name) if ([string]::IsNullOrWhiteSpace($Name)) {
} throw "Environment variable name is required."
}
function Resolve-PluginSecretName { $processValue = [Environment]::GetEnvironmentVariable($Name, 'Process')
<# # Empty string is what SetEnvironmentVariable($null, Process) leaves behind;
.SYNOPSIS # treat it as unset so Windows User packs still apply. Explicit '{}' shadows User.
Resolves a logical secret name from a plugin's scriptSettings entry. if (-not [string]::IsNullOrWhiteSpace($processValue)) {
return $processValue
}
.DESCRIPTION foreach ($target in @('User', 'Machine')) {
Reads a string property such as githubSecret / nugetSecret / npmSecret / try {
containerRegistrySecret from the plugin settings object. Returns $null when $value = [Environment]::GetEnvironmentVariable($Name, $target)
the property is missing or blank. }
catch {
continue
}
.PARAMETER PluginSettings
Plugin settings object from scriptSettings.json (the enabled plugin entry).
.PARAMETER PropertyName
Settings property that holds the logical secret name (e.g. 'githubSecret').
.OUTPUTS
System.String. Trimmed logical secret name, or $null.
.EXAMPLE
$name = Resolve-PluginSecretName -PluginSettings $plugin -PropertyName 'nugetSecret'
$key = Get-SecretEnvironmentValue -Name $name
#>
param(
[Parameter(Mandatory = $true)]
$PluginSettings,
[Parameter(Mandatory = $true)]
[string]$PropertyName
)
if ($PluginSettings.PSObject.Properties.Name -contains $PropertyName) {
$value = [string]$PluginSettings.$PropertyName
if (-not [string]::IsNullOrWhiteSpace($value)) { if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim() return $value
} }
} }
return $null return $null
} }
function Get-RegistryCredentialsFromRuntime { function Get-RepoUtilsSecretsEnvNames {
<# <#
.SYNOPSIS .SYNOPSIS
Loads container-registry username/password from a logical secret name. Reads declared pack environment variable names from scriptSettings / engine context.
.DESCRIPTION
Looks up the environment variable named by SecretName. The value must be
Base64(UTF8('username:password')). Used by registry login and image-pull
secret creation — never pass the password itself as a parameter.
.PARAMETER SecretName
Logical secret name (environment variable name), not a password or token.
.PARAMETER SharedSettings
Optional engine shared context (reserved for callers that thread context).
.OUTPUTS
Hashtable with User and Password keys (decoded credential material).
.EXAMPLE
$creds = Get-RegistryCredentialsFromRuntime -SecretName 'ContainerRegistry'
# $creds.User / $creds.Password
#> #>
# SecretName is a logical env-var name from scriptSettings, not a password value.
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidUsingPlainTextForPassword',
'SecretName',
Justification = 'Logical secret name for env lookup (Base64 username:password); not a credential value.'
)]
param( param(
[Parameter(Mandatory = $true)]
[string]$SecretName,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[psobject]$SharedSettings $Settings
) )
$raw = Get-SecretEnvironmentValue -Name $SecretName $sharedName = $null
if ([string]::IsNullOrWhiteSpace($raw)) { $packName = $null
throw "Environment variable '$SecretName' is not set." if ($null -ne $Settings) {
if ($Settings.PSObject.Properties.Name -contains 'repoUtilsSecretsShared') {
$sharedName = [string]$Settings.repoUtilsSecretsShared
}
if ($Settings.PSObject.Properties.Name -contains 'repoUtilsSecrets') {
$packName = [string]$Settings.repoUtilsSecrets
}
}
$sharedName = if ($null -eq $sharedName) { '' } else { $sharedName.Trim() }
$packName = if ($null -eq $packName) { '' } else { $packName.Trim() }
if ([string]::IsNullOrWhiteSpace($sharedName) -or [string]::IsNullOrWhiteSpace($packName)) {
throw "scriptSettings.json must declare repoUtilsSecretsShared and repoUtilsSecrets (environment variable names, e.g. RepoUtilsSecretsShared / RepoUtilsSecrets)."
}
return [pscustomobject]@{
SharedEnv = $sharedName
PackEnv = $packName
}
}
function ConvertFrom-RepoUtilsSecretsPackJson {
<#
.SYNOPSIS
Parses a RepoUtilsSecrets JSON object. Empty input is {}. Bare non-JSON throws.
#>
param(
[Parameter(Mandatory = $false)]
[AllowEmptyString()]
[string]$Raw,
[Parameter(Mandatory = $true)]
[string]$SourceName
)
if ([string]::IsNullOrWhiteSpace($Raw)) {
return [pscustomobject]@{}
}
$trimmed = $Raw.Trim()
if (-not $trimmed.StartsWith('{')) {
throw "${SourceName} must be a JSON object (RepoUtilsSecrets pack), not a bare string."
} }
try { try {
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($raw)) $parsed = $trimmed | ConvertFrom-Json -ErrorAction Stop
} }
catch { catch {
throw "Failed to decode '$SecretName' as Base64 (expected base64('username:password')): $($_.Exception.Message)" throw "${SourceName} is not valid JSON: $($_.Exception.Message)"
}
if ($null -eq $parsed -or $parsed -is [System.Collections.IEnumerable]) {
throw "${SourceName} JSON must be an object."
}
return $parsed
}
function ConvertTo-OrdinalPropertyMap {
param($Object)
$map = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal)
if ($null -eq $Object) {
return $map
}
if ($Object -is [System.Collections.IDictionary]) {
foreach ($key in @($Object.Keys)) {
$name = [string]$key
if ([string]::IsNullOrWhiteSpace($name)) {
continue
}
$map[$name] = $Object[$key]
}
return $map
}
foreach ($property in $Object.PSObject.Properties) {
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
continue
}
$map[[string]$property.Name] = $property.Value
}
return $map
}
function ConvertFrom-OrdinalPropertyMap {
param(
[Parameter(Mandatory = $true)]
[System.Collections.Generic.Dictionary[string, object]]$Map
)
$properties = [ordered]@{}
foreach ($key in $Map.Keys) {
$properties[$key] = $Map[$key]
}
return [pscustomobject]$properties
}
function Merge-RepoUtilsSecretsPackObjects {
<#
.SYNOPSIS
Appsettings-style merge: org-pack first, slug overlay. Nested merge for any object-valued slot (typically ContainerRegistry).
#>
param(
$Base,
$Overlay
)
$result = ConvertTo-OrdinalPropertyMap -Object $Base
$overlayMap = ConvertTo-OrdinalPropertyMap -Object $Overlay
foreach ($key in @($overlayMap.Keys)) {
if (-not (Test-ContainerRegistryCatalogKey -Key $key)) {
throw "RepoUtilsSecrets pack key '$key' must be PascalCase (e.g. GitClone, ContainerRegistry)."
}
$overlayValue = $overlayMap[$key]
$overlayIsObject = ($null -ne $overlayValue) -and -not ($overlayValue -is [string]) -and -not ($overlayValue -is [ValueType]) -and -not ($overlayValue -is [System.Collections.IEnumerable])
if ($overlayIsObject) {
$baseCatalog = $null
if ($result.ContainsKey($key)) {
$baseCatalog = $result[$key]
}
$mergedCatalog = ConvertTo-OrdinalPropertyMap -Object $baseCatalog
$overlayCatalogMap = ConvertTo-OrdinalPropertyMap -Object $overlayValue
foreach ($catalogKey in @($overlayCatalogMap.Keys)) {
if (-not (Test-ContainerRegistryCatalogKey -Key $catalogKey)) {
throw "Catalog key '$catalogKey' in pack slot '$key' must be PascalCase (e.g. Harbor, InCluster)."
}
$mergedCatalog[$catalogKey] = $overlayCatalogMap[$catalogKey]
}
$result[$key] = ConvertFrom-OrdinalPropertyMap -Map $mergedCatalog
continue
}
$result[$key] = $overlayValue
}
foreach ($key in @($result.Keys)) {
if (-not (Test-ContainerRegistryCatalogKey -Key $key)) {
throw "RepoUtilsSecrets pack key '$key' must be PascalCase (e.g. GitClone, ContainerRegistry)."
}
}
return ConvertFrom-OrdinalPropertyMap -Map $result
}
function Get-MergedRepoUtilsSecretsPack {
<#
.SYNOPSIS
Merges $env:RepoUtilsSecretsShared then $env:RepoUtilsSecrets (names from settings).
#>
param(
[Parameter(Mandatory = $false)]
$Settings
)
$names = Get-RepoUtilsSecretsEnvNames -Settings $Settings
$sharedRaw = Get-RepoUtilsEnvironmentVariable -Name $names.SharedEnv
$packRaw = Get-RepoUtilsEnvironmentVariable -Name $names.PackEnv
$sharedObject = ConvertFrom-RepoUtilsSecretsPackJson -Raw $sharedRaw -SourceName $names.SharedEnv
$packObject = ConvertFrom-RepoUtilsSecretsPackJson -Raw $packRaw -SourceName $names.PackEnv
return Merge-RepoUtilsSecretsPackObjects -Base $sharedObject -Overlay $packObject
}
function Get-RepoUtilsSecretSlot {
<#
.SYNOPSIS
Reads a scalar pack slot (GitClone, NuGet, Npm, CosignKey, …) after merge.
#>
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $false)]
$Settings,
[switch]$AllowMissing
)
if ([string]::IsNullOrWhiteSpace($Name) -or -not (Test-ContainerRegistryCatalogKey -Key $Name)) {
throw "RepoUtilsSecrets slot '$Name' must be PascalCase (e.g. GitClone, NuGet)."
}
$merged = Get-MergedRepoUtilsSecretsPack -Settings $Settings
$match = $null
foreach ($property in $merged.PSObject.Properties) {
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
continue
}
if ([string]::Equals([string]$property.Name, $Name, [System.StringComparison]::Ordinal)) {
$match = $property
break
}
}
if ($null -eq $match) {
if ($AllowMissing) {
return $null
}
throw "RepoUtilsSecrets slot '$Name' is missing after merging org-pack and slug-pack."
}
$value = $match.Value
if ($value -is [string] -or $null -eq $value -or $value -is [ValueType]) {
$text = if ($null -eq $value) { '' } else { [string]$value }
if ([string]::IsNullOrWhiteSpace($text) -and -not $AllowMissing) {
throw "RepoUtilsSecrets slot '$Name' is empty."
}
if ([string]::IsNullOrWhiteSpace($text)) {
return $null
}
return $text
}
throw "RepoUtilsSecrets slot '$Name' must be a string (nested maps are only allowed for ContainerRegistry)."
}
function Copy-RepoUtilsSecretsEnvNamesToContext {
param(
[Parameter(Mandatory = $true)]
$Context,
[Parameter(Mandatory = $false)]
$Settings
)
if ($null -eq $Settings) {
return $Context
}
foreach ($name in @('repoUtilsSecretsShared', 'repoUtilsSecrets', 'vaultRepoUtilsApplication')) {
if ($Settings.PSObject.Properties.Name -contains $name -and -not [string]::IsNullOrWhiteSpace([string]$Settings.$name)) {
$Context | Add-Member -NotePropertyName $name -NotePropertyValue ([string]$Settings.$name).Trim() -Force
}
}
return $Context
}
function Test-ContainerRegistryCatalogKey {
<#
.SYNOPSIS
True when Key is PascalCase (Harbor, InCluster), matching env-slot names.
#>
param(
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$Key
)
return $Key -cmatch '^[A-Z][A-Za-z0-9]*$'
}
function Resolve-PluginSecretName {
<#
.SYNOPSIS
Reads a plugin setting that names a RepoUtilsSecrets pack subkey.
.DESCRIPTION
Settings such as githubSecret / nugetSecret / npmSecret / containerRegistrySecret /
cosignKeySecret hold the PascalCase pack slot to read (e.g. GitClone, NuGet).
They are not environment variable names.
.PARAMETER PluginSettings
Plugin settings object from scriptSettings.json.
.PARAMETER PropertyName
Settings property that holds the pack subkey name (e.g. 'githubSecret').
.PARAMETER PluginDisplayName
Plugin name used in required/validation errors.
.PARAMETER Required
Throw when the property is missing or blank.
#>
param(
[Parameter(Mandatory = $true)]
$PluginSettings,
[Parameter(Mandatory = $true)]
[string]$PropertyName,
[Parameter(Mandatory = $false)]
[string]$PluginDisplayName,
[switch]$Required
)
$display = if ([string]::IsNullOrWhiteSpace($PluginDisplayName)) { 'Plugin' } else { $PluginDisplayName }
$value = $null
if ($null -ne $PluginSettings -and $PluginSettings.PSObject.Properties.Name -contains $PropertyName) {
$raw = [string]$PluginSettings.$PropertyName
if (-not [string]::IsNullOrWhiteSpace($raw)) {
$value = $raw.Trim()
}
}
if ([string]::IsNullOrWhiteSpace($value)) {
if ($Required) {
throw "$display requires '$PropertyName' (PascalCase pack subkey, e.g. GitClone, NuGet, ContainerRegistry)."
}
return $null
}
if (-not (Test-ContainerRegistryCatalogKey -Key $value)) {
throw "$display '$PropertyName' '$value' must be PascalCase (e.g. GitClone, NuGet, ContainerRegistry)."
}
return $value
}
function Assert-RetiredContainerRegistrySettingsAbsent {
<#
.SYNOPSIS
Throws when obsolete per-registry secret settings are present.
#>
param(
$Object,
[Parameter(Mandatory = $true)]
[string]$Context
)
if ($null -eq $Object) {
return
}
$retired = @(
'imagesCredentialsSecret',
'additionalImageRegistries',
'additionalImageRegistryUrls',
'additionalImagesCredentialsSecret',
'helmOciCredentialsSecret'
)
foreach ($name in $retired) {
$present = $false
if ($Object -is [System.Collections.IDictionary]) {
$present = $Object.Contains($name)
}
elseif ($Object.PSObject.Properties.Name -contains $name) {
$present = $true
}
if ($present) {
throw "${Context}: '$name' is not supported. Use the ContainerRegistry JSON catalog with PascalCase containerRegistryKey / helmRegistryKey."
}
}
}
function ConvertFrom-RegistryCredentialPayload {
param(
[Parameter(Mandatory = $true)]
[string]$Payload,
[Parameter(Mandatory = $true)]
[string]$ContextName
)
try {
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Payload.Trim()))
}
catch {
throw "Failed to decode '$ContextName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
} }
$parts = $decoded -split ':', 2 $parts = $decoded -split ':', 2
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) { if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
throw "Decoded '$SecretName' must be in the form 'username:password'." throw "Decoded '$ContextName' must be in the form 'username:password'."
} }
return @{ User = $parts[0]; Password = $parts[1] } return @{ User = $parts[0]; Password = $parts[1] }
} }
function Get-ContainerRegistryCatalogObject {
<#
.SYNOPSIS
Validates a PascalCase JSON map of Base64(username:password) values.
#>
param(
[Parameter(Mandatory = $true)]
$Catalog,
[Parameter(Mandatory = $false)]
[string]$SourceName = 'ContainerRegistry'
)
if ($Catalog -is [string]) {
$raw = [string]$Catalog
$trimmed = $raw.Trim()
if (-not $trimmed.StartsWith('{')) {
throw "$SourceName must be a JSON catalog object { `"Harbor`": `"<Base64>`", `"InCluster`": `"<Base64>`" }."
}
try {
$Catalog = $trimmed | ConvertFrom-Json -ErrorAction Stop
}
catch {
throw "$SourceName is not valid JSON: $($_.Exception.Message)"
}
}
if ($null -eq $Catalog -or $Catalog -is [string] -or $Catalog -is [ValueType] -or $Catalog -is [System.Collections.IEnumerable]) {
throw "$SourceName JSON catalog must be an object of PascalCase keys to Base64(username:password)."
}
$keyCount = 0
foreach ($property in $Catalog.PSObject.Properties) {
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
continue
}
$keyCount++
$keyName = [string]$property.Name
if (-not (Test-ContainerRegistryCatalogKey -Key $keyName)) {
throw "Catalog key '$keyName' in '$SourceName' must be PascalCase (e.g. Harbor, InCluster)."
}
}
if ($keyCount -eq 0) {
throw "$SourceName JSON catalog has no PascalCase keys."
}
return $Catalog
}
function Get-RegistryCredentialsFromRuntime {
<#
.SYNOPSIS
Loads container-registry username/password from the merged RepoUtilsSecrets pack.
.DESCRIPTION
Reads a nested catalog slot (named by -Slot, typically ContainerRegistry) after
org-pack + slug-pack merge. -Key (PascalCase, ordinal match) selects an entry.
.PARAMETER Key
PascalCase catalog key (e.g. Harbor). Required.
.PARAMETER Slot
PascalCase pack subkey that holds the catalog object (from containerRegistrySecret).
.PARAMETER SharedSettings
Engine shared context (must declare repoUtilsSecretsShared / repoUtilsSecrets).
.OUTPUTS
Hashtable with User and Password keys (decoded credential material).
#>
param(
# Omitted arguments must throw. Mandatory prompts on an interactive host and stalls tests and release runs.
[AllowEmptyString()]
[string]$Key,
[AllowEmptyString()]
[string]$Slot,
[psobject]$SharedSettings
)
if ([string]::IsNullOrWhiteSpace($Slot) -or -not (Test-ContainerRegistryCatalogKey -Key $Slot)) {
throw "containerRegistrySecret '$Slot' must be PascalCase (e.g. ContainerRegistry)."
}
if ([string]::IsNullOrWhiteSpace($Key)) {
throw "Pass -Key (PascalCase, e.g. Harbor) to select an entry in pack slot '$Slot'."
}
if (-not (Test-ContainerRegistryCatalogKey -Key $Key)) {
throw "containerRegistryKey '$Key' must be PascalCase (e.g. Harbor, InCluster)."
}
$merged = Get-MergedRepoUtilsSecretsPack -Settings $SharedSettings
$catalogProperty = $null
foreach ($property in $merged.PSObject.Properties) {
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
continue
}
if ([string]::Equals([string]$property.Name, $Slot, [System.StringComparison]::Ordinal)) {
$catalogProperty = $property
break
}
}
if ($null -eq $catalogProperty -or $null -eq $catalogProperty.Value) {
throw "RepoUtilsSecrets slot '$Slot' is missing after merging org-pack and slug-pack."
}
$catalog = Get-ContainerRegistryCatalogObject -Catalog $catalogProperty.Value -SourceName $Slot
$match = $null
foreach ($property in $catalog.PSObject.Properties) {
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
continue
}
if ([string]::Equals([string]$property.Name, $Key, [System.StringComparison]::Ordinal)) {
$match = $property
break
}
}
if ($null -eq $match) {
throw "Catalog key '$Key' was not found in '$Slot' (ordinal PascalCase match)."
}
$payload = [string]$match.Value
if ([string]::IsNullOrWhiteSpace($payload)) {
throw "Catalog key '$Key' in '$Slot' is empty."
}
return ConvertFrom-RegistryCredentialPayload -Payload $payload -ContextName "$Slot.$Key"
}
function Resolve-EngineDirectoryFromSharedSettings { function Resolve-EngineDirectoryFromSharedSettings {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -596,7 +1054,7 @@ function Resolve-PluginModulePath {
$candidatePaths = [System.Collections.Generic.List[string]]::new() $candidatePaths = [System.Collections.Generic.List[string]]::new()
$candidatePaths.Add((Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName)) $candidatePaths.Add((Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName))
$preferredGroups = @('Platform', 'DotNet', 'Npm') $preferredGroups = @('Platform', 'DotNet', 'Npm', 'Desktop')
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $preferredGroups[0]) $pluginFileName)) $candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $preferredGroups[0]) $pluginFileName))
if (Get-Command Get-ExtensionPluginModulePaths -ErrorAction SilentlyContinue) { if (Get-Command Get-ExtensionPluginModulePaths -ErrorAction SilentlyContinue) {
@ -756,4 +1214,4 @@ function Invoke-ConfiguredPlugin {
} }
} }
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, Get-PluginMetadataObject, Test-PluginCompatible, Test-PluginMutatesRemote, Resolve-PluginSecretName, Get-SecretEnvironmentValue, Get-RegistryCredentialsFromRuntime, Test-PluginSkipsRemoteMutation, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, Get-PluginMetadataObject, Test-PluginCompatible, Test-PluginMutatesRemote, Get-RepoUtilsEnvironmentVariable, Get-RepoUtilsSecretsEnvNames, ConvertFrom-RepoUtilsSecretsPackJson, Merge-RepoUtilsSecretsPackObjects, Get-MergedRepoUtilsSecretsPack, Get-RepoUtilsSecretSlot, Copy-RepoUtilsSecretsEnvNamesToContext, Resolve-PluginSecretName, Test-ContainerRegistryCatalogKey, Assert-RetiredContainerRegistrySettingsAbsent, Get-ContainerRegistryCatalogObject, Get-RegistryCredentialsFromRuntime, Test-PluginSkipsRemoteMutation, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin

View File

@ -213,6 +213,7 @@ function New-EngineContext {
skipPublishPlugins = $false skipPublishPlugins = $false
facts = [ordered]@{} facts = [ordered]@{}
} }
$context = Copy-RepoUtilsSecretsEnvNamesToContext -Context $context -Settings $Settings
$versionSource = Resolve-EngineContextVersion -Plugins $Plugins -Context $context -ScriptDir $ScriptDir $versionSource = Resolve-EngineContextVersion -Plugins $Plugins -Context $context -ScriptDir $ScriptDir
$version = [string](Get-EngineState -Context $context -Name 'version' -Required) $version = [string](Get-EngineState -Context $context -Name 'version' -Required)
@ -265,4 +266,61 @@ function Get-PreferredReleaseBranch {
return "main" return "main"
} }
Export-ModuleMember -Function Assert-WorkingTreeClean, Initialize-ReleaseStageContext, New-EngineContext, Get-PreferredReleaseBranch function Select-HelmInstallPlugins {
<#
.SYNOPSIS
Keeps the release version plugin and HelmSelfDeploy for an OCI install without build or push.
.DESCRIPTION
Install-Helm-*.bat passes -InstallOnly. The chart and images are already in OCI, so compile,
test, image build, image push, and HelmPush are not run. The version plugin still supplies
the chart tag (shared.version). HelmSelfDeploy dryRun is rejected: that path needs the local
package HelmPush keeps in the same run.
#>
param(
[Parameter(Mandatory = $true)]
[AllowEmptyCollection()]
[object[]]$Plugins,
[Parameter(Mandatory = $true)]
[string]$ScriptDir
)
$versionPlugins = @(Get-EnabledVersionPlugins -Plugins $Plugins -ScriptDir $ScriptDir)
if ($versionPlugins.Count -eq 0) {
throw "Install-only Helm requires exactly one enabled release version plugin (providesVersion) so the OCI chart tag can be resolved. Build and push plugins are not run."
}
if ($versionPlugins.Count -gt 1) {
$names = ($versionPlugins | ForEach-Object { [string]$_.name }) -join ', '
throw "Install-only Helm requires exactly one enabled release version plugin. Found: $names."
}
$deployPlugins = @($Plugins | Where-Object {
$null -ne $_ -and
$_.enabled -and
[string]$_.name -eq 'HelmSelfDeploy'
})
if ($deployPlugins.Count -eq 0) {
throw "Install-only Helm requires an enabled HelmSelfDeploy plugin. The chart is installed from OCI; image build, image push, and HelmPush are not run."
}
if ($deployPlugins.Count -gt 1) {
throw "Install-only Helm found multiple enabled HelmSelfDeploy plugins."
}
$deployPlugin = $deployPlugins[0]
if ($deployPlugin.PSObject.Properties.Name -contains 'dryRun' -and $null -ne $deployPlugin.dryRun -and [bool]$deployPlugin.dryRun) {
throw "Install-only Helm cannot use HelmSelfDeploy dryRun. That path needs the local package from HelmPush in the same run. Omit dryRun to install the chart already published to OCI."
}
$versionName = [string]$versionPlugins[0].name
return @($Plugins | Where-Object {
$null -ne $_ -and
$_.enabled -and
([string]$_.name -eq $versionName -or [string]$_.name -eq 'HelmSelfDeploy')
})
}
Export-ModuleMember -Function Assert-WorkingTreeClean, Initialize-ReleaseStageContext, New-EngineContext, Get-PreferredReleaseBranch, Select-HelmInstallPlugins

View File

@ -38,6 +38,7 @@ function New-EngineContext {
utilsDir = $SrcDir utilsDir = $SrcDir
facts = [ordered]@{} facts = [ordered]@{}
} }
$context = Copy-RepoUtilsSecretsEnvNamesToContext -Context $context -Settings $Settings
$expandContext = Get-Command Expand-ExtensionEngineContext -ErrorAction SilentlyContinue $expandContext = Get-Command Expand-ExtensionEngineContext -ErrorAction SilentlyContinue
if ($expandContext) { if ($expandContext) {

View File

@ -0,0 +1,373 @@
#requires -Version 7.0
#requires -PSEdition Core
function Test-RepoUtilsUseVaultEnabled {
param(
[Parameter(Mandatory = $true)]
$Settings
)
if ($null -eq $Settings -or -not ($Settings.PSObject.Properties.Name -contains 'useVault')) {
return $false
}
$value = $Settings.useVault
if ($value -is [bool]) {
return $value
}
return [string]$value -eq 'true'
}
function ConvertFrom-VaultConnectionSecret {
param(
[Parameter(Mandatory = $true)]
[string]$Raw
)
$trimmed = $Raw.Trim()
$separator = $trimmed.IndexOf('|')
if ($separator -lt 1 -or $separator -ge ($trimmed.Length - 1)) {
throw "Vault connection must be 'baseAddress|apiKey' (pipe separator). Example: http://172.16.0.14|your-key"
}
$baseAddress = $trimmed.Substring(0, $separator).Trim().TrimEnd('/')
$apiKey = $trimmed.Substring($separator + 1).Trim()
if ([string]::IsNullOrWhiteSpace($baseAddress) -or [string]::IsNullOrWhiteSpace($apiKey)) {
throw "Vault connection is missing base address or API key."
}
return [pscustomobject]@{
BaseAddress = $baseAddress
ApiKey = $apiKey
}
}
function Get-RepoUtilsVaultScope {
param(
[Parameter(Mandatory = $true)]
$Settings
)
$organization = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_ORGANIZATION')
if ([string]::IsNullOrWhiteSpace($organization) -and ($Settings.PSObject.Properties.Name -contains 'vaultOrganization')) {
$organization = [string]$Settings.vaultOrganization
}
$application = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_APPLICATION')
if ([string]::IsNullOrWhiteSpace($application) -and ($Settings.PSObject.Properties.Name -contains 'vaultApplication')) {
$application = [string]$Settings.vaultApplication
}
$organization = if ($null -eq $organization) { '' } else { $organization.Trim() }
$application = if ($null -eq $application) { '' } else { $application.Trim() }
if ([string]::IsNullOrWhiteSpace($organization) -or [string]::IsNullOrWhiteSpace($application)) {
throw "useVault is true but vault organization/application are not set. Set vaultOrganization/vaultApplication in scriptSettings.json or MAKSIT_VAULT_ORGANIZATION / MAKSIT_VAULT_APPLICATION."
}
return [pscustomobject]@{
Organization = $organization
Application = $application
}
}
function Get-RepoUtilsVaultConnectionSecretName {
param(
[Parameter(Mandatory = $true)]
$Settings
)
if (($Settings.PSObject.Properties.Name -contains 'vaultConnectionSecret') -and
-not [string]::IsNullOrWhiteSpace([string]$Settings.vaultConnectionSecret)) {
return ([string]$Settings.vaultConnectionSecret).Trim()
}
return 'MAKSIT_VAULT'
}
function Get-RepoUtilsVaultOrgPackApplicationName {
param(
[Parameter(Mandatory = $true)]
$Settings
)
$application = $null
if ($Settings.PSObject.Properties.Name -contains 'vaultRepoUtilsApplication') {
$application = [string]$Settings.vaultRepoUtilsApplication
}
$application = if ($null -eq $application) { '' } else { $application.Trim() }
if ([string]::IsNullOrWhiteSpace($application)) {
throw "useVault is true but vaultRepoUtilsApplication is not set in scriptSettings.json (Vault application for the org-pack, e.g. Shared)."
}
return $application
}
function Get-VaultNameFilterExpression {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
$escaped = $Name.Replace('\', '\\').Replace('"', '\"')
return "Name == `"$escaped`""
}
function Import-RepoUtilsVaultClientModule {
$modulePath = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_MODULE_PATH')
if (-not [string]::IsNullOrWhiteSpace($modulePath)) {
Import-Module $modulePath -Force -ErrorAction Stop
return
}
if (Get-Command Connect-Vault -ErrorAction SilentlyContinue) {
return
}
Import-Module 'MaksIT.Vault.Client.PowerShell' -ErrorAction SilentlyContinue
}
function Find-RepoUtilsVaultSecretMatch {
param(
[Parameter(Mandatory = $true)]
[string]$OrganizationName,
[Parameter(Mandatory = $true)]
[string]$ApplicationName,
[Parameter(Mandatory = $true)]
[string]$SecretName,
[Parameter(Mandatory = $true)]
[pscustomobject]$Connection,
[Parameter(Mandatory = $true)]
[ValidateSet('Cmdlet', 'Rest')]
[string]$Mode
)
$orgFilter = Get-VaultNameFilterExpression -Name $OrganizationName
$appFilter = Get-VaultNameFilterExpression -Name $ApplicationName
$pageNumber = 1
while ($true) {
$items = @()
$hasNext = $false
if ($Mode -eq 'Cmdlet') {
$response = Search-VaultSecrets -PageNumber $pageNumber -PageSize 100 `
-OrganizationFilters $orgFilter -ApplicationFilters $appFilter
if ($null -ne $response -and $null -ne $response.Items) {
$items = @($response.Items)
}
if ($null -ne $response) {
$hasNext = [bool]$response.HasNextPage
}
}
else {
$body = @{
pageNumber = $pageNumber
pageSize = 100
organizationFilters = $orgFilter
applicationFilters = $appFilter
} | ConvertTo-Json -Compress
$uri = "$($Connection.BaseAddress)/api/vault/secrets"
$headers = @{ 'X-API-KEY' = $Connection.ApiKey }
$response = Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -ContentType 'application/json' -Body $body
if ($null -ne $response.items) {
$items = @($response.items)
}
elseif ($null -ne $response.Items) {
$items = @($response.Items)
}
$hasNext = [bool]($response.hasNextPage)
if (-not $hasNext) {
$hasNext = [bool]($response.HasNextPage)
}
}
foreach ($item in $items) {
$itemName = [string]$(if ($item.PSObject.Properties.Name -contains 'Name') { $item.Name } else { $item.name })
if ([string]::Equals($itemName, $SecretName, [System.StringComparison]::Ordinal)) {
return $item
}
}
if (-not $hasNext) {
break
}
$pageNumber++
}
return $null
}
function Get-RepoUtilsVaultSecretValue {
param(
[Parameter(Mandatory = $true)]
$Match,
[Parameter(Mandatory = $true)]
[pscustomobject]$Connection,
[Parameter(Mandatory = $true)]
[ValidateSet('Cmdlet', 'Rest')]
[string]$Mode
)
$organizationId = if ($Match.PSObject.Properties.Name -contains 'OrganizationId') { $Match.OrganizationId } else { $Match.organizationId }
$applicationId = if ($Match.PSObject.Properties.Name -contains 'ApplicationId') { $Match.ApplicationId } else { $Match.applicationId }
$secretId = if ($Match.PSObject.Properties.Name -contains 'Id') { $Match.Id } else { $Match.id }
if ($Mode -eq 'Cmdlet') {
$version = Get-VaultSecret -OrganizationId $organizationId -ApplicationId $applicationId -SecretId $secretId -SecretVersion 'current'
if ($null -eq $version -or [string]::IsNullOrWhiteSpace([string]$version.Value)) {
return $null
}
return [string]$version.Value
}
$uri = "$($Connection.BaseAddress)/api/vault/organization/$organizationId/application/$applicationId/secret/$secretId" +
'?secretVersion=current'
$headers = @{ 'X-API-KEY' = $Connection.ApiKey }
$version = Invoke-RestMethod -Method Get -Uri $uri -Headers $headers
$value = if ($version.PSObject.Properties.Name -contains 'Value') { $version.Value } else { $version.value }
if ([string]::IsNullOrWhiteSpace([string]$value)) {
return $null
}
return [string]$value
}
function Resolve-RepoUtilsVaultSecretValue {
param(
[Parameter(Mandatory = $true)]
[string]$OrganizationName,
[Parameter(Mandatory = $true)]
[string]$ApplicationName,
[Parameter(Mandatory = $true)]
[string]$SecretName,
[Parameter(Mandatory = $true)]
[pscustomobject]$Connection,
[Parameter(Mandatory = $true)]
[string]$Mode
)
$match = Find-RepoUtilsVaultSecretMatch `
-OrganizationName $OrganizationName `
-ApplicationName $ApplicationName `
-SecretName $SecretName `
-Connection $Connection `
-Mode $Mode
if ($null -eq $match) {
return $null
}
$value = Get-RepoUtilsVaultSecretValue -Match $match -Connection $Connection -Mode $Mode
if ([string]::IsNullOrWhiteSpace($value)) {
return $null
}
Write-Log -Level 'INFO' -Message "Resolved Vault secret '$SecretName' from $OrganizationName/$ApplicationName"
return $value
}
function Initialize-RepoUtilsVaultSecrets {
param(
[Parameter(Mandatory = $true)]
$Settings,
[Parameter(Mandatory = $true)]
$Plugins
)
if (-not (Test-RepoUtilsUseVaultEnabled -Settings $Settings)) {
return
}
$names = Get-RepoUtilsSecretsEnvNames -Settings $Settings
$sharedRaw = Get-RepoUtilsEnvironmentVariable -Name $names.SharedEnv
$packRaw = Get-RepoUtilsEnvironmentVariable -Name $names.PackEnv
$sharedPresent = -not [string]::IsNullOrWhiteSpace($sharedRaw)
$packPresent = -not [string]::IsNullOrWhiteSpace($packRaw)
if ($sharedPresent -and $packPresent) {
[void](ConvertFrom-RepoUtilsSecretsPackJson -Raw $sharedRaw -SourceName $names.SharedEnv)
[void](ConvertFrom-RepoUtilsSecretsPackJson -Raw $packRaw -SourceName $names.PackEnv)
Write-Log -Level 'INFO' -Message "Vault mode: pack env vars '$($names.SharedEnv)' and '$($names.PackEnv)' already set; skipping Vault fetch."
return
}
$connectionName = Get-RepoUtilsVaultConnectionSecretName -Settings $Settings
$raw = [Environment]::GetEnvironmentVariable($connectionName)
if ([string]::IsNullOrWhiteSpace($raw)) {
throw "useVault is true but environment variable '$connectionName' is not set (expected baseAddress|apiKey)."
}
$connection = ConvertFrom-VaultConnectionSecret -Raw $raw
$scope = Get-RepoUtilsVaultScope -Settings $Settings
$orgPackApplication = Get-RepoUtilsVaultOrgPackApplicationName -Settings $Settings
Write-Log -Level 'INFO' -Message "Vault mode: loading RepoUtilsSecrets packs for $($scope.Organization)/$orgPackApplication and $($scope.Organization)/$($scope.Application)"
$mode = 'Rest'
try {
Import-RepoUtilsVaultClientModule
if (Get-Command Connect-Vault -ErrorAction SilentlyContinue) {
Connect-Vault -BaseAddress $connection.BaseAddress -ApiKey $connection.ApiKey
$mode = 'Cmdlet'
Write-Log -Level 'INFO' -Message 'Connected to Vault with MaksIT.Vault.Client.PowerShell'
}
else {
Write-Log -Level 'INFO' -Message 'MaksIT.Vault.Client.PowerShell not found; using Vault HTTP API'
}
}
catch {
Write-Log -Level 'INFO' -Message "Vault PowerShell module not loaded ($($_.Exception.Message)); using Vault HTTP API"
$mode = 'Rest'
}
if (-not $sharedPresent) {
$sharedValue = Resolve-RepoUtilsVaultSecretValue `
-OrganizationName $scope.Organization `
-ApplicationName $orgPackApplication `
-SecretName $names.SharedEnv `
-Connection $connection `
-Mode $mode
if ([string]::IsNullOrWhiteSpace($sharedValue)) {
$sharedValue = '{}'
Write-Log -Level 'INFO' -Message "Vault secret '$($names.SharedEnv)' was not found for $($scope.Organization)/$orgPackApplication; using empty org-pack."
}
[Environment]::SetEnvironmentVariable($names.SharedEnv, $sharedValue, 'Process')
}
if (-not $packPresent) {
$packValue = Resolve-RepoUtilsVaultSecretValue `
-OrganizationName $scope.Organization `
-ApplicationName $scope.Application `
-SecretName $names.PackEnv `
-Connection $connection `
-Mode $mode
if ([string]::IsNullOrWhiteSpace($packValue)) {
$packValue = '{}'
Write-Log -Level 'INFO' -Message "Vault secret '$($names.PackEnv)' was not found for $($scope.Organization)/$($scope.Application); using empty slug-pack."
}
[Environment]::SetEnvironmentVariable($names.PackEnv, $packValue, 'Process')
}
}
Export-ModuleMember -Function @(
'Test-RepoUtilsUseVaultEnabled',
'ConvertFrom-VaultConnectionSecret',
'Get-RepoUtilsVaultScope',
'Get-RepoUtilsVaultConnectionSecretName',
'Get-RepoUtilsVaultOrgPackApplicationName',
'Initialize-RepoUtilsVaultSecrets'
)

View File

@ -27,6 +27,10 @@ function Clear-ExternalCommandTestHandler {
$script:ExternalCommandTestHandler = $null $script:ExternalCommandTestHandler = $null
} }
function Test-ExternalCommandTestHandler {
return ($null -ne $script:ExternalCommandTestHandler)
}
function Set-ExternalCommandAvailability { function Set-ExternalCommandAvailability {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -107,7 +111,7 @@ function Invoke-ExternalCommand {
if ($ThrowOnError -and $exitCode -ne 0) { if ($ThrowOnError -and $exitCode -ne 0) {
$preview = ($output | ForEach-Object { $preview = ($output | ForEach-Object {
if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { [string]$_ } if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { [string]$_ }
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 8) -join ' ' } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Last 12) -join ' '
if ([string]::IsNullOrWhiteSpace($preview)) { if ([string]::IsNullOrWhiteSpace($preview)) {
throw "External command '$Name' failed with exit code $exitCode." throw "External command '$Name' failed with exit code $exitCode."
} }
@ -128,4 +132,5 @@ Export-ModuleMember -Function `
Invoke-ExternalCommand, ` Invoke-ExternalCommand, `
Set-ExternalCommandTestHandler, ` Set-ExternalCommandTestHandler, `
Clear-ExternalCommandTestHandler, ` Clear-ExternalCommandTestHandler, `
Test-ExternalCommandTestHandler, `
Set-ExternalCommandAvailability Set-ExternalCommandAvailability

View File

@ -1,122 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
.NET artifact cleanup plugin — remove NuGet build outputs after release.
.DESCRIPTION
Removes files from the configured artifacts directory using glob patterns.
Defaults target NuGet outputs (*.nupkg, *.snupkg). Typically placed at the
end of the Release stage after DotNetCreateArchive or publish plugins.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Get-CleanupPatternsInternal {
param(
[Parameter(Mandatory = $false)]
$ConfiguredPatterns
)
if ($null -eq $ConfiguredPatterns) {
return @('*.nupkg', '*.snupkg')
}
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
}
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
return @('*.nupkg', '*.snupkg')
}
return @([string]$ConfiguredPatterns)
}
function Get-ExcludePatternsInternal {
param(
[Parameter(Mandatory = $false)]
$ConfiguredPatterns
)
if ($null -eq $ConfiguredPatterns) {
return @()
}
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
}
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
return @()
}
return @([string]$ConfiguredPatterns)
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$artifactsDirectory = $sharedSettings.artifactsDirectory
$patterns = Get-CleanupPatternsInternal -ConfiguredPatterns $pluginSettings.includePatterns
$excludePatterns = Get-ExcludePatternsInternal -ConfiguredPatterns $pluginSettings.excludePatterns
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
throw "DotNetCleanupArtifacts plugin requires an artifacts directory in the shared context."
}
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
Write-Log -Level "WARN" -Message " Artifacts directory not found: $artifactsDirectory"
return
}
Write-Log -Level "STEP" -Message "Cleaning generated artifacts..."
$itemsToRemove = @()
foreach ($pattern in $patterns) {
$matchedItems = @(
Get-ChildItem -Path $artifactsDirectory -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like $pattern }
)
if ($excludePatterns.Count -gt 0) {
$matchedItems = @(
$matchedItems |
Where-Object {
$item = $_
-not ($excludePatterns | Where-Object { $item.Name -like $_ } | Select-Object -First 1)
}
)
}
$itemsToRemove += @($matchedItems)
}
$itemsToRemove = @($itemsToRemove | Sort-Object FullName -Unique)
if ($itemsToRemove.Count -eq 0) {
Write-Log -Level "INFO" -Message " No artifacts matched cleanup rules."
return
}
foreach ($item in $itemsToRemove) {
Remove-Item -Path $item.FullName -Recurse -Force -ErrorAction SilentlyContinue
Write-Log -Level "OK" -Message " Removed: $($item.Name)"
}
}
Export-ModuleMember -Function Invoke-Plugin

View File

@ -9,6 +9,10 @@
This plugin compresses .NET release artifact inputs prepared by an earlier This plugin compresses .NET release artifact inputs prepared by an earlier
DotNet plugin (DotNetPack or DotNetPublish) into a zip file DotNet plugin (DotNetPack or DotNetPublish) into a zip file
and exposes the resulting release assets for later publisher plugins. and exposes the resulting release assets for later publisher plugins.
For desktop apps with per-RID publish folders, the zip is the portable
build only (default win-x64). Windows installer and Flatpak artifacts are
separate GitHub assets and are never added to this zip.
#> #>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) { if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
@ -26,7 +30,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" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-ReleaseAssetPaths"
$pluginSettings = $Settings $pluginSettings = $Settings
$sharedSettings = $Settings.context $sharedSettings = $Settings.context
@ -49,6 +53,34 @@ function Invoke-Plugin {
} }
} }
$portableRid = $null
if ($pluginSettings.PSObject.Properties['portableRuntimeIdentifier'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.portableRuntimeIdentifier)) {
$portableRid = ([string]$pluginSettings.portableRuntimeIdentifier).Trim()
}
$publishOutputs = @(Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'publishOutputs' -Default @())
if ($publishOutputs.Count -gt 0) {
if ([string]::IsNullOrWhiteSpace($portableRid)) {
$portableRid = 'win-x64'
}
$portableOutputs = @(
$publishOutputs |
Where-Object { [string]$_.runtimeIdentifier -eq $portableRid }
)
if ($portableOutputs.Count -eq 0) {
$portableOutputs = @(
$publishOutputs |
Where-Object { ([string]$_.runtimeIdentifier).StartsWith('win-', [System.StringComparison]::OrdinalIgnoreCase) }
)
}
if ($portableOutputs.Count -gt 0) {
$archiveInputs = @($portableOutputs | ForEach-Object { [string]$_.directory })
Write-Log -Level "INFO" -Message " Portable zip uses runtime '$portableRid' only (installer/Flatpak stay out of the zip)."
}
}
if ($archiveInputs.Count -eq 0) { if ($archiveInputs.Count -eq 0) {
throw "DotNetCreateArchive plugin requires prepared artifacts. Run DotNetPack or DotNetPublish first." throw "DotNetCreateArchive plugin requires prepared artifacts. Run DotNetPack or DotNetPublish first."
} }
@ -84,19 +116,20 @@ function Invoke-Plugin {
Write-Log -Level "OK" -Message " Release archive ready: $zipPath" Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
$releaseAssetPaths = @($zipPath) $newAssets = [System.Collections.Generic.List[string]]::new()
$newAssets.Add($zipPath)
$packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile') $packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile')
if ($null -ne $packageFile) { if ($null -ne $packageFile) {
$releaseAssetPaths += $packageFile.FullName $newAssets.Add($packageFile.FullName)
} }
$symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile') $symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile')
if ($null -ne $symbolsPackageFile) { if ($null -ne $symbolsPackageFile) {
$releaseAssetPaths += $symbolsPackageFile.FullName $newAssets.Add($symbolsPackageFile.FullName)
} }
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archivePath' -Value $zipPath -Overwrite Replace -LegacyProperty 'releaseArchivePath' Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archivePath' -Value $zipPath -Overwrite Replace -LegacyProperty 'releaseArchivePath'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths' Add-ReleaseAssetPaths -Context $sharedSettings -Path @($newAssets)
} }
Export-ModuleMember -Function Invoke-Plugin Export-ModuleMember -Function Invoke-Plugin

View File

@ -29,10 +29,10 @@ function Invoke-Plugin {
$pluginSettings = $Settings $pluginSettings = $Settings
$sharedSettings = $Settings.context $sharedSettings = $Settings.context
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret'
$packageFile = $sharedSettings.packageFile $packageFile = $sharedSettings.packageFile
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings $dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret' -PluginDisplayName 'DotNetNuGet' -Required
Assert-Command dotnet Assert-Command dotnet
@ -52,13 +52,9 @@ function Invoke-Plugin {
return return
} }
if ([string]::IsNullOrWhiteSpace($nugetSecret)) { $nugetKey = Get-RepoUtilsSecretSlot -Name $nugetSecret -Settings $sharedSettings
throw "DotNetNuGet plugin requires 'nugetSecret' in scriptSettings.json (logical secret name, e.g. NuGet)."
}
$nugetKey = Get-SecretEnvironmentValue -Name $nugetSecret
if ([string]::IsNullOrWhiteSpace($nugetKey)) { if ([string]::IsNullOrWhiteSpace($nugetKey)) {
throw "NuGet API key is not set. Set environment variable '$nugetSecret'." throw "NuGet API key is not set. Set RepoUtilsSecrets slot '$nugetSecret' (nugetSecret)."
} }
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) { $nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {

View File

@ -6,9 +6,14 @@
.NET publish plugin for producing application release artifacts. .NET publish plugin for producing application release artifacts.
.DESCRIPTION .DESCRIPTION
This plugin publishes the configured .NET project into a release output This plugin publishes configured .NET projects into the artifacts directory
directory and exposes that published directory to the shared release and appends those publish folders to shared archive inputs so later plugins
context so later release-stage plugins can archive and publish it. can zip them next to any earlier pack outputs. Existing NuGet package facts
(packageFile) are left unchanged.
Optional runtimeIdentifiers (or runtimeIdentifier) produce per-RID folders
under artifactsDir/<ProjectName>/<rid> for desktop installers (Windows MSI /
Linux Flatpak). When RIDs are set, selfContained defaults to true.
#> #>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) { if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
@ -19,6 +24,28 @@ if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
} }
} }
function Get-DotNetPublishRuntimeIdentifiers {
param(
[Parameter(Mandatory = $true)]
$PluginSettings
)
$rids = [System.Collections.Generic.List[string]]::new()
if ($PluginSettings.PSObject.Properties['runtimeIdentifiers'] -and $null -ne $PluginSettings.runtimeIdentifiers) {
foreach ($item in @($PluginSettings.runtimeIdentifiers)) {
$value = [string]$item
if (-not [string]::IsNullOrWhiteSpace($value)) {
$rids.Add($value.Trim())
}
}
}
elseif ($PluginSettings.PSObject.Properties['runtimeIdentifier'] -and -not [string]::IsNullOrWhiteSpace([string]$PluginSettings.runtimeIdentifier)) {
$rids.Add(([string]$PluginSettings.runtimeIdentifier).Trim())
}
return @($rids)
}
function Invoke-Plugin { function Invoke-Plugin {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
@ -28,48 +55,145 @@ 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" Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand"
$pluginSettings = $Settings
$sharedSettings = $Settings.context $sharedSettings = $Settings.context
$projectFiles = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles') $scriptDir = $sharedSettings.scriptDir
$artifactsDirectory = $sharedSettings.artifactsDirectory $projectFiles = @()
$publishProjectPath = $null
Assert-Command dotnet Assert-Command dotnet
if ($null -eq $projectFiles -or @($projectFiles).Count -eq 0) { if ($pluginSettings.PSObject.Properties['projectFiles'] -and $null -ne $pluginSettings.projectFiles) {
throw "DotNetPublish plugin requires project files in the shared context." $projectFiles = @(Resolve-RelativePaths -Value $pluginSettings.projectFiles -BasePath $scriptDir)
}
else {
$fromFact = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles')
if ($null -ne $fromFact) {
$projectFiles = @($fromFact)
}
elseif ($sharedSettings.PSObject.Properties['projectFiles'] -and $null -ne $sharedSettings.projectFiles) {
$projectFiles = @($sharedSettings.projectFiles)
}
} }
$projectFiles = @($projectFiles) if ($projectFiles.Count -eq 0) {
throw "DotNetPublish plugin requires projectFiles in plugin settings or projectFiles on shared context."
}
if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) {
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$pluginSettings.artifactsDir)))
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
}
else {
$artifactsDirectory = $sharedSettings.artifactsDirectory
}
if ([string]::IsNullOrWhiteSpace([string]$artifactsDirectory)) {
throw "DotNetPublish plugin requires artifactsDir in plugin settings or artifactsDirectory on shared context."
}
if (!(Test-Path $artifactsDirectory)) { if (!(Test-Path $artifactsDirectory)) {
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
} }
# The first configured project remains the canonical release artifact source. $existing = Get-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -LegacyProperty @('releaseArchiveInputs')
$publishProjectPath = $projectFiles[0] $archiveInputs = [System.Collections.Generic.List[object]]::new()
$publishDir = Join-Path $artifactsDirectory ([System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath)) if ($null -ne $existing) {
foreach ($item in @($existing)) {
if (Test-Path $publishDir) { if ($null -ne $item) {
Remove-Item -Path $publishDir -Recurse -Force $archiveInputs.Add($item)
}
}
} }
Write-Log -Level "STEP" -Message "Publishing release artifact..." $runtimeIdentifiers = @(Get-DotNetPublishRuntimeIdentifiers -PluginSettings $pluginSettings)
dotnet publish $publishProjectPath -c Release -o $publishDir --nologo $selfContained = $null
if ($LASTEXITCODE -ne 0) { if ($pluginSettings.PSObject.Properties['selfContained'] -and $null -ne $pluginSettings.selfContained) {
throw "dotnet publish failed for $publishProjectPath." $selfContained = [bool]$pluginSettings.selfContained
}
elseif ($runtimeIdentifiers.Count -gt 0) {
$selfContained = $true
} }
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue) $publishSingleFile = $false
if ($publishedItems.Count -eq 0) { if ($pluginSettings.PSObject.Properties['publishSingleFile'] -and $null -ne $pluginSettings.publishSingleFile) {
throw "dotnet publish completed, but no files were produced in: $publishDir" $publishSingleFile = [bool]$pluginSettings.publishSingleFile
} }
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir" $additionalArguments = @()
if ($pluginSettings.PSObject.Properties['additionalPublishArguments'] -and $null -ne $pluginSettings.additionalPublishArguments) {
$additionalArguments = @($pluginSettings.additionalPublishArguments | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $null -Overwrite Replace -LegacyProperty 'packageFile' $publishOutputs = [System.Collections.Generic.List[object]]::new()
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $null -Overwrite Replace -LegacyProperty 'symbolsPackageFile' $ridLoop = @($null)
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($publishDir) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs' if ($runtimeIdentifiers.Count -gt 0) {
$ridLoop = @($runtimeIdentifiers)
}
foreach ($publishProjectPath in $projectFiles) {
$projectName = [System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath)
$projectRoot = Join-Path $artifactsDirectory $projectName
foreach ($runtimeIdentifier in $ridLoop) {
$publishDir = if ([string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) {
$projectRoot
}
else {
Join-Path $projectRoot ([string]$runtimeIdentifier)
}
if (Test-Path $publishDir) {
Remove-Item -Path $publishDir -Recurse -Force
}
$ridLabel = if ([string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) { 'current runtime' } else { [string]$runtimeIdentifier }
Write-Log -Level "STEP" -Message "Publishing release artifact ($projectName / $ridLabel)..."
$dotnetPublishArguments = @(
'publish', $publishProjectPath, '-c', 'Release', '-o', $publishDir, '--nologo'
)
if (-not [string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) {
$dotnetPublishArguments += @('-r', [string]$runtimeIdentifier)
}
if ($null -ne $selfContained) {
if ($selfContained) {
$dotnetPublishArguments += '--self-contained'
}
else {
$dotnetPublishArguments += '--no-self-contained'
}
}
if ($publishSingleFile) {
$dotnetPublishArguments += '-p:PublishSingleFile=true'
}
if ($additionalArguments.Count -gt 0) {
$dotnetPublishArguments += $additionalArguments
}
Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetPublishArguments | Out-Null
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue)
if ($publishedItems.Count -eq 0) {
throw "dotnet publish completed, but no files were produced in: $publishDir"
}
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
$archiveInputs.Add($publishDir)
$publishOutputs.Add([pscustomobject]@{
projectName = $projectName
runtimeIdentifier = [string]$runtimeIdentifier
directory = $publishDir
})
}
}
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($archiveInputs) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'publishOutputs' -Value @($publishOutputs) -Overwrite Replace
} }
Export-ModuleMember -Function Invoke-Plugin Export-ModuleMember -Function Invoke-Plugin, Get-DotNetPublishRuntimeIdentifiers

View File

@ -7,12 +7,15 @@
.DESCRIPTION .DESCRIPTION
Resolves one or more .NET test projects (`project` or `projects`), runs tests once Resolves one or more .NET test projects (`project` or `projects`), runs tests once
via TestRunner, then publishes metrics on the shared engine context for any later via TestRunner (Microsoft Testing Platform + coverlet.MTP only — no VSTest /
coverlet.collector), then publishes metrics on the shared engine context for any later
plugin: `qualityLineCoverage`, `testResult`, `coverageLineRate` / `coverageBranchRate` / `coverageMethodRate`, plugin: `qualityLineCoverage`, `testResult`, `coverageLineRate` / `coverageBranchRate` / `coverageMethodRate`,
method counts, `testResultsDirectory`, `coverageCoberturaPaths`. Quality gates read method counts, `testResultsDirectory`, `coverageCoberturaPaths`. Quality gates read
those keys generically (not tied to this plugin by name). When `resultsDir` (or the those keys generically (not tied to this plugin by name). When `resultsDir` (or the
multi-project default TestResults folder) is used, Cobertura output is kept on disk multi-project default TestResults folder) is used, Cobertura output is kept on disk
via TestRunner `-KeepResults` so repo-root `test-results/` persists after the run. via TestRunner `-KeepResults` so repo-root `test-results/` persists after the run.
Product solutions must place `src/global.json` with `test.runner` =
Microsoft.Testing.Platform next to the `.sln`/`.slnx`.
#> #>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) { if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {

View File

@ -1,89 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Builds an npm workspace (install + build script).
.DESCRIPTION
Runs npm ci (or npm install when useCi is false) and npm run build in the
configured workspace root. Requires NpmReleaseVersion to have set
shared npmWorkspaceRoot unless workspaceRoot is configured explicitly.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
$pluginSettings = $Settings
$shared = $Settings.context
Assert-Command npm
$workspaceRoot = $null
if ($pluginSettings.workspaceRoot) {
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $shared.scriptDir)
$workspaceRoot = $workspaceRoots[0]
}
elseif ($shared.PSObject.Properties['npmWorkspaceRoot'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.npmWorkspaceRoot)) {
$workspaceRoot = [string]$shared.npmWorkspaceRoot
}
else {
throw "NpmBuild plugin requires 'workspaceRoot' or a prior NpmReleaseVersion plugin run."
}
$useCi = $true
if ($null -ne $pluginSettings.useCi) {
$useCi = [bool]$pluginSettings.useCi
}
$buildScript = 'build'
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.buildScript)) {
$buildScript = [string]$pluginSettings.buildScript
}
Push-Location $workspaceRoot
try {
if ($useCi) {
Write-Log -Level "STEP" -Message "Running npm ci in '$workspaceRoot'..."
npm ci
if ($LASTEXITCODE -ne 0) {
throw "npm ci failed with exit code $LASTEXITCODE."
}
}
else {
Write-Log -Level "STEP" -Message "Running npm install in '$workspaceRoot'..."
npm install
if ($LASTEXITCODE -ne 0) {
throw "npm install failed with exit code $LASTEXITCODE."
}
}
Write-Log -Level "STEP" -Message "Running npm run $buildScript..."
npm run $buildScript
if ($LASTEXITCODE -ne 0) {
throw "npm run $buildScript failed with exit code $LASTEXITCODE."
}
Write-Log -Level "OK" -Message " npm build completed."
}
finally {
Pop-Location
}
}
Export-ModuleMember -Function Invoke-Plugin

View File

@ -1,75 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
npm/Jest test plugin for the test engine.
.DESCRIPTION
Runs Jest with coverage via TestRunner.Invoke-NpmJestTestsWithCoverage and publishes
normalized metrics on the shared engine context for downstream plugins.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Invoke-NpmJestTestsWithCoverage"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
Assert-Command npm
if (-not $pluginSettings.workspaceRoot) {
throw "NpmJestTest plugin requires 'workspaceRoot' in scriptSettings.json."
}
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $scriptDir)
$workspaceRoot = $workspaceRoots[0]
$testScript = 'test'
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.testScript)) {
$testScript = [string]$pluginSettings.testScript
}
$coverageDirectory = 'coverage'
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.coverageDirectory)) {
$coverageDirectory = [string]$pluginSettings.coverageDirectory
}
$testResult = Invoke-NpmJestTestsWithCoverage -WorkspaceRoot $workspaceRoot -TestScript $testScript -CoverageDirectory $coverageDirectory
if (-not $testResult.Success) {
throw "Tests failed. $($testResult.Error)"
}
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Publish-CoverageMetricsToSharedContext"
Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'workspaceRoot' -Value $workspaceRoot -Overwrite Replace -LegacyProperty 'npmWorkspaceRoot'
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
if (($testResult.PSObject.Properties.Name -contains 'CoverageSummaryFile') -and $testResult.CoverageSummaryFile) {
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 "INFO" -Message " Line Coverage: $($testResult.LineRate)%"
Write-Log -Level "INFO" -Message " Branch Coverage: $($testResult.BranchRate)%"
Write-Log -Level "INFO" -Message " Method Coverage: $($testResult.MethodRate)%"
}
Export-ModuleMember -Function Invoke-Plugin

View File

@ -1,139 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Packs npm workspace packages into .tgz release artifacts.
.DESCRIPTION
Runs npm pack for each configured workspace package and publishes the
resulting tarball paths into shared release context (releaseAssetPaths)
for the GitHub release plugin.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
$pluginSettings = $Settings
$shared = $Settings.context
Assert-Command npm
$workspaceRoot = $null
if ($pluginSettings.workspaceRoot) {
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $shared.scriptDir)
$workspaceRoot = $workspaceRoots[0]
}
elseif ($shared.PSObject.Properties['npmWorkspaceRoot'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.npmWorkspaceRoot)) {
$workspaceRoot = [string]$shared.npmWorkspaceRoot
}
else {
throw "NpmPack plugin requires 'workspaceRoot' or a prior NpmReleaseVersion plugin run."
}
$artifactsDirectory = $null
if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) {
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $shared.scriptDir ([string]$pluginSettings.artifactsDir)))
Set-EngineState -Context $shared -Name 'artifactsDirectory' -Value $artifactsDirectory
}
elseif ($shared.PSObject.Properties['artifactsDirectory'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.artifactsDirectory)) {
$artifactsDirectory = [string]$shared.artifactsDirectory
}
else {
throw "NpmPack plugin requires release-stage artifactsDirectory."
}
$packOrder = @()
if ($pluginSettings.publishOrder) {
if ($pluginSettings.publishOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.publishOrder -is [string])) {
$packOrder = @($pluginSettings.publishOrder | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
}
elseif (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.publishOrder)) {
$packOrder = @([string]$pluginSettings.publishOrder)
}
}
elseif ($pluginSettings.packOrder) {
if ($pluginSettings.packOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.packOrder -is [string])) {
$packOrder = @($pluginSettings.packOrder | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
}
elseif (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.packOrder)) {
$packOrder = @([string]$pluginSettings.packOrder)
}
}
if ($packOrder.Count -eq 0) {
throw "NpmPack plugin requires non-empty 'publishOrder' or 'packOrder' (workspace package names)."
}
Import-Module (Join-Path $PSScriptRoot 'NpmPackageSupport.psm1') -Force
$useWorkspaces = Test-NpmWorkspacesConfigured -WorkspaceRoot $workspaceRoot
if (-not $useWorkspaces -and $packOrder.Count -gt 1) {
throw "NpmPack plugin requires npm workspaces when packing more than one package."
}
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
}
$releaseAssetPaths = @()
Push-Location $workspaceRoot
try {
foreach ($packageName in $packOrder) {
Write-Log -Level "STEP" -Message "Packing npm package '$packageName'..."
if ($useWorkspaces) {
$tarballName = (npm pack -w $packageName --pack-destination $artifactsDirectory 2>$null | Select-Object -Last 1)
}
else {
Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName
$tarballName = (npm pack --pack-destination $artifactsDirectory 2>$null | Select-Object -Last 1)
}
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace([string]$tarballName)) {
throw "npm pack failed for '$packageName'."
}
$tarballPath = Join-Path $artifactsDirectory ([string]$tarballName).Trim()
if (-not (Test-Path $tarballPath -PathType Leaf)) {
throw "Could not locate pack output for '$packageName' at: $tarballPath"
}
$strayTarballPath = Join-Path $workspaceRoot ([string]$tarballName).Trim()
if ((Test-Path $strayTarballPath -PathType Leaf) -and $strayTarballPath -ne $tarballPath) {
Remove-Item -LiteralPath $strayTarballPath -Force -ErrorAction SilentlyContinue
}
Write-Log -Level "OK" -Message " Package ready: $tarballPath"
$releaseAssetPaths += $tarballPath
}
}
finally {
Pop-Location
}
Set-EngineState -Context $shared -Name 'releaseDir' -Value $artifactsDirectory
Set-EngineFact -Context $shared -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
if ($releaseAssetPaths.Count -gt 0) {
$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))."
}
Export-ModuleMember -Function Invoke-Plugin

View File

@ -1,62 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
function Get-NpmRootPackageJson {
param(
[Parameter(Mandatory = $true)]
[string]$WorkspaceRoot
)
$packageJsonPath = Join-Path $WorkspaceRoot 'package.json'
if (-not (Test-Path $packageJsonPath -PathType Leaf)) {
throw "package.json not found at '$packageJsonPath'."
}
return Get-Content -Path $packageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json
}
function Test-NpmWorkspacesConfigured {
param(
[Parameter(Mandatory = $true)]
[string]$WorkspaceRoot
)
$json = Get-NpmRootPackageJson -WorkspaceRoot $WorkspaceRoot
if (-not $json.PSObject.Properties['workspaces'] -or $null -eq $json.workspaces) {
return $false
}
$workspaces = $json.workspaces
if ($workspaces -is [string]) {
return -not [string]::IsNullOrWhiteSpace($workspaces)
}
if ($workspaces -is [System.Collections.IEnumerable]) {
$entries = @($workspaces | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
return $entries.Count -gt 0
}
return $false
}
function Assert-NpmRootPackageName {
param(
[Parameter(Mandatory = $true)]
[string]$WorkspaceRoot,
[Parameter(Mandatory = $true)]
[string]$ExpectedPackageName
)
$json = Get-NpmRootPackageJson -WorkspaceRoot $WorkspaceRoot
$rootPackageName = [string]$json.name
if ([string]::IsNullOrWhiteSpace($rootPackageName)) {
throw "Root package.json at '$WorkspaceRoot' is missing 'name'."
}
if ($rootPackageName -ne $ExpectedPackageName) {
throw "publishOrder package '$ExpectedPackageName' does not match root package name '$rootPackageName' (npm workspaces not configured)."
}
}
Export-ModuleMember -Function Test-NpmWorkspacesConfigured, Assert-NpmRootPackageName, Get-NpmRootPackageJson

View File

@ -1,162 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Publishes npm workspace packages to the npm registry.
.DESCRIPTION
Publishes packages in configured order using npmSecret (logical secret name).
Pass the token via an environment variable named like the configured npm secret (e.g. Npm).
Uses a temporary .npmrc in the workspace root.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-ReleaseSemverPrereleaseLabel"
$pluginSettings = $Settings
$shared = $Settings.context
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
Assert-Command npm
$workspaceRoot = $null
if ($pluginSettings.workspaceRoot) {
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $shared.scriptDir)
$workspaceRoot = $workspaceRoots[0]
}
elseif ($shared.PSObject.Properties['npmWorkspaceRoot'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.npmWorkspaceRoot)) {
$workspaceRoot = [string]$shared.npmWorkspaceRoot
}
else {
throw "NpmPublish plugin requires 'workspaceRoot' or a prior NpmReleaseVersion plugin run."
}
$registry = if ([string]::IsNullOrWhiteSpace([string]$pluginSettings.registry)) {
'https://registry.npmjs.org'
}
else {
[string]$pluginSettings.registry
}
$access = if ([string]::IsNullOrWhiteSpace([string]$pluginSettings.access)) {
'public'
}
else {
[string]$pluginSettings.access
}
$npmDistTag = $null
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.npmDistTag)) {
$npmDistTag = [string]$pluginSettings.npmDistTag
}
else {
$npmDistTag = Get-ReleaseSemverPrereleaseLabel -Version ([string]$shared.version)
}
$publishOrder = @()
if ($pluginSettings.publishOrder) {
if ($pluginSettings.publishOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.publishOrder -is [string])) {
$publishOrder = @($pluginSettings.publishOrder | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
}
elseif (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.publishOrder)) {
$publishOrder = @([string]$pluginSettings.publishOrder)
}
}
if ($publishOrder.Count -eq 0) {
throw "NpmPublish plugin requires non-empty 'publishOrder' (workspace package names)."
}
Import-Module (Join-Path $PSScriptRoot 'NpmPackageSupport.psm1') -Force
$useWorkspaces = Test-NpmWorkspacesConfigured -WorkspaceRoot $workspaceRoot
if (-not $useWorkspaces -and $publishOrder.Count -gt 1) {
throw "NpmPublish plugin requires npm workspaces when publishing more than one package."
}
if ($dryRun) {
foreach ($packageName in $publishOrder) {
$tagNote = if ([string]::IsNullOrWhiteSpace($npmDistTag)) { 'latest' } else { $npmDistTag }
Write-Log -Level "INFO" -Message "Dry run: would publish npm package '$packageName' to $registry (dist-tag $tagNote)"
}
return
}
$npmSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'npmSecret'
if ([string]::IsNullOrWhiteSpace($npmSecret)) {
throw "NpmPublish plugin requires 'npmSecret' in scriptSettings.json (logical secret name, e.g. Npm)."
}
$npmToken = Get-SecretEnvironmentValue -Name $npmSecret
if ([string]::IsNullOrWhiteSpace($npmToken)) {
throw "npm API key is not set. Set environment variable '$npmSecret'."
}
$registryHost = ([uri]$registry).Host
$tempNpmRcPath = Join-Path $workspaceRoot ".npmrc.release-temp"
$npmRcContent = @"
registry=$registry
//$registryHost/:_authToken=$npmToken
"@
Push-Location $workspaceRoot
try {
Set-Content -Path $tempNpmRcPath -Value $npmRcContent -Encoding UTF8 -NoNewline
foreach ($packageName in $publishOrder) {
Write-Log -Level "STEP" -Message "Publishing npm package '$packageName'..."
$publishArgs = @('publish')
if ($useWorkspaces) {
$publishArgs += @('-w', $packageName)
}
else {
Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName
}
$publishArgs += @('--access', $access, '--userconfig', $tempNpmRcPath)
if (-not [string]::IsNullOrWhiteSpace($npmDistTag)) {
$publishArgs += @('--tag', $npmDistTag)
Write-Log -Level "INFO" -Message " Using npm dist-tag '$npmDistTag' (prerelease)."
}
npm @publishArgs
if ($LASTEXITCODE -ne 0) {
throw "Failed to publish npm package '$packageName'."
}
Write-Log -Level "OK" -Message " Published $packageName."
}
Write-Log -Level "OK" -Message " npm publish completed."
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-EnginePublishCompletion"
Add-EnginePublishCompletion -Context $shared -Publisher 'NpmPublish'
}
finally {
if (Test-Path $tempNpmRcPath -PathType Leaf) {
Remove-Item -Path $tempNpmRcPath -Force -ErrorAction SilentlyContinue
}
Pop-Location
}
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -1,106 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Loads npm workspace release version into shared context.
.DESCRIPTION
Reads semver from the configured workspace package.json and writes it to
shared context version. Optionally synchronizes version fields across
workspace package manifests before build/publish.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Get-PackageJsonVersionInternal {
param(
[Parameter(Mandatory = $true)]
[string]$PackageJsonPath
)
if (-not (Test-Path $PackageJsonPath -PathType Leaf)) {
throw "NpmReleaseVersion: package.json not found at '$PackageJsonPath'."
}
$json = Get-Content -Path $PackageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json
$version = [string]$json.version
if ([string]::IsNullOrWhiteSpace($version)) {
throw "NpmReleaseVersion: 'version' is missing in '$PackageJsonPath'."
}
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
if (-not (Test-ReleaseSemver -Version $version)) {
throw "NpmReleaseVersion: version '$version' in '$PackageJsonPath' is not a valid semver (X.Y.Z or X.Y.Z-prerelease)."
}
return $version
}
function Set-PackageJsonVersionInternal {
param(
[Parameter(Mandatory = $true)]
[string]$PackageJsonPath,
[Parameter(Mandatory = $true)]
[string]$Version
)
$raw = Get-Content -Path $PackageJsonPath -Raw -Encoding UTF8
$json = $raw | ConvertFrom-Json
$json.version = $Version
($json | ConvertTo-Json -Depth 100) + [Environment]::NewLine | Set-Content -Path $PackageJsonPath -Encoding UTF8 -NoNewline
}
function Get-PluginMetadata {
return [pscustomobject]@{ providesVersion = $true }
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
$pluginSettings = $Settings
$shared = $Settings.context
$packageJsonPaths = @(Resolve-RelativePaths -Value $pluginSettings.packageJsonPath -BasePath $shared.scriptDir)
if ($packageJsonPaths.Count -eq 0) {
throw "NpmReleaseVersion plugin requires 'packageJsonPath' in scriptSettings.json."
}
$packageJsonPath = $packageJsonPaths[0]
$version = Get-PackageJsonVersionInternal -PackageJsonPath $packageJsonPath
$syncWorkspaceVersions = $false
if ($null -ne $pluginSettings.syncWorkspaceVersions) {
$syncWorkspaceVersions = [bool]$pluginSettings.syncWorkspaceVersions
}
if ($syncWorkspaceVersions) {
$workspaceRoot = Split-Path -Parent $packageJsonPath
$packageManifests = Get-ChildItem -Path (Join-Path $workspaceRoot 'packages') -Recurse -Filter package.json -File -ErrorAction SilentlyContinue
foreach ($manifest in $packageManifests) {
Set-PackageJsonVersionInternal -PackageJsonPath $manifest.FullName -Version $version
}
Write-Log -Level "OK" -Message " Synchronized workspace package versions to $version."
}
$npmWorkspaceRoot = Split-Path -Parent $packageJsonPath
Set-EngineState -Context $shared -Name 'version' -Value $version
Set-EngineFact -Context $shared -Namespace 'npm' -Name 'workspaceRoot' -Value $npmWorkspaceRoot -Overwrite Replace -LegacyProperty 'npmWorkspaceRoot'
Set-EngineFact -Context $shared -Namespace 'npm' -Name 'packageJsonPath' -Value $packageJsonPath -Overwrite Replace -LegacyProperty 'npmPackageJsonPath'
Write-Log -Level "OK" -Message " Release version loaded by NpmReleaseVersion plugin: $version"
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,78 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Collects coverage metrics from an existing test results directory.
.DESCRIPTION
Stack-agnostic parser for recovered container or host test output. Auto-detects .NET
Cobertura (coverage.cobertura.xml) or Jest (coverage-summary.json) and publishes the
same shared-context keys as DotNetTest / NpmJestTest for QualityGate.
Use after DockerContainerBuilder / PodmanContainerBuilder when tests run inside a container image.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Get-CoverageFromResultsDirectory'
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Publish-CoverageMetricsToSharedContext'
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
$resultsDirSetting = $pluginSettings.resultsDir
if ([string]::IsNullOrWhiteSpace($resultsDirSetting)) {
if ($sharedSettings.PSObject.Properties.Name -contains 'testResultsDirectory' -and $sharedSettings.testResultsDirectory) {
$resultsDirSetting = $sharedSettings.testResultsDirectory
}
else {
throw "CollectCoverage requires 'resultsDir' in plugin settings or testResultsDirectory on shared context (from ContainerBuilder)."
}
}
$resultsDirectory = if ([System.IO.Path]::IsPathRooted([string]$resultsDirSetting)) {
[string]$resultsDirSetting
}
else {
[System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$resultsDirSetting)))
}
$format = 'auto'
if ($pluginSettings.PSObject.Properties.Name -contains 'format' -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.format)) {
$format = [string]$pluginSettings.format
}
Write-Log -Level 'STEP' -Message "Collecting coverage from $resultsDirectory ..."
$testResult = Get-CoverageFromResultsDirectory -ResultsDirectory $resultsDirectory -Format $format -Silent:$true
if (-not $testResult.Success) {
throw $testResult.Error
}
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
$formatLabel = if ($testResult.CoverageFormat) { $testResult.CoverageFormat } else { 'unknown' }
Write-Log -Level 'OK' -Message " Coverage collected ($formatLabel)."
Write-Log -Level 'INFO' -Message " Line Coverage: $($testResult.LineRate)%"
Write-Log -Level 'INFO' -Message " Branch Coverage: $($testResult.BranchRate)%"
Write-Log -Level 'INFO' -Message " Method Coverage: $($testResult.MethodRate)%"
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $false }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,52 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Probes docker/podman availability and sets mandatory shared engine state for the pipeline.
.DESCRIPTION
Must be the first enabled plugin. Sets containerEngineProbeCompleted, activeContainerEngine,
dockerEngineAvailable, podmanEngineAvailable, and compose CLI fields on shared context.
Fails the pipeline when no usable engine is found.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'ContainerEngineSupport' -RequiredCommand 'Resolve-ActiveContainerEngineState'
$shared = $Settings.context
Write-Log -Level 'STEP' -Message 'Probing container engine availability...'
$state = Resolve-ActiveContainerEngineState
Set-ContainerEngineSharedState -SharedSettings $shared -State $state
$display = Get-ContainerEngineDisplayName -Engine (Get-SharedContainerEngineInvocation -SharedSettings $shared)
Write-Log -Level 'OK' -Message " Active engine: $display"
Write-Log -Level 'INFO' -Message " docker CLI available: $($state.DockerEngineAvailable); podman CLI available: $($state.PodmanEngineAvailable)"
if ([string]::IsNullOrWhiteSpace([string]$state.ComposeExecutable)) {
Write-Log -Level 'INFO' -Message ' Compose CLI: not available (Compose plugins will fail if enabled).'
}
else {
Write-Log -Level 'INFO' -Message " Compose CLI: $($state.ComposeExecutable) $($state.ComposeBaseArgs -join ' ')"
}
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $false }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,124 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Discovers package artifacts produced by pack plugins or container build pipelines.
.DESCRIPTION
Stack-agnostic scanner for artifactsDir. Detects .NET .nupkg/.snupkg or npm .tgz files
and populates shared context (packageFile, releaseAssetPaths, releaseArchiveInputs).
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'DotNetArtifactSupport' -RequiredCommand 'Resolve-DotNetPackageArtifacts'
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
$version = $sharedSettings.version
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
}
else {
$sharedSettings | Add-Member -NotePropertyName artifactsDirectory -NotePropertyValue $artifactsDirectory -Force
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
}
}
else {
$artifactsDirectory = $sharedSettings.artifactsDirectory
}
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
throw 'DiscoverPackageArtifacts requires artifactsDir in plugin settings or artifactsDirectory on shared context.'
}
$nupkgCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.nupkg' -ErrorAction SilentlyContinue | Where-Object {
$_.Name -notlike '*.symbols.nupkg' -and $_.Name -notlike '*.snupkg'
})
$tgzCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.tgz' -ErrorAction SilentlyContinue)
if ($nupkgCandidates.Count -gt 0 -and $tgzCandidates.Count -gt 0) {
throw "DiscoverPackageArtifacts found both NuGet and npm artifacts in $artifactsDirectory. Use one package stack per repository."
}
if ($nupkgCandidates.Count -gt 0) {
if ([string]::IsNullOrWhiteSpace([string]$version)) {
throw 'DiscoverPackageArtifacts requires release version on shared context when discovering .nupkg artifacts.'
}
Write-Log -Level 'STEP' -Message "Discovering NuGet package artifacts in $artifactsDirectory ..."
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
Write-Log -Level 'OK' -Message " Package ready: $($resolved.PackageFile.FullName)"
if ($resolved.SymbolsPackageFile) {
Write-Log -Level 'OK' -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
}
else {
Write-Log -Level 'WARN' -Message " Symbols package (.snupkg) not found for version $version."
}
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
$sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $resolved.SymbolsPackageFile -Force
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
}
return
}
if ($tgzCandidates.Count -gt 0) {
Import-PluginDependency -ModuleName 'NpmArtifactSupport' -RequiredCommand 'Resolve-NpmPackageArtifacts'
Write-Log -Level 'STEP' -Message "Discovering npm package artifacts in $artifactsDirectory ..."
$resolved = Resolve-NpmPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
foreach ($tarball in $resolved.PackageFiles) {
Write-Log -Level 'OK' -Message " Package ready: $($tarball.FullName)"
}
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseAssetPaths -Force
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
}
}
return
}
throw "DiscoverPackageArtifacts found no .nupkg or .tgz files in: $artifactsDirectory"
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $false }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -101,7 +101,6 @@ function Invoke-Plugin {
$pluginSettings = $Settings $pluginSettings = $Settings
$sharedSettings = $Settings.context $sharedSettings = $Settings.context
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret'
$configuredRepository = $pluginSettings.repository $configuredRepository = $pluginSettings.repository
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile $releaseNotesFileSetting = $pluginSettings.releaseNotesFile
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern $releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
@ -112,6 +111,7 @@ function Invoke-Plugin {
$releaseAssetPaths = @() $releaseAssetPaths = @()
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings $dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret' -PluginDisplayName 'GitHub' -Required
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) { if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json." throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json."
@ -138,13 +138,9 @@ function Invoke-Plugin {
Assert-Command gh Assert-Command gh
if ([string]::IsNullOrWhiteSpace($githubSecret)) { $ghToken = Get-RepoUtilsSecretSlot -Name $githubSecret -Settings $sharedSettings
throw "GitHub plugin requires 'githubSecret' in scriptSettings.json (logical secret name, e.g. GitHub)."
}
$ghToken = Get-SecretEnvironmentValue -Name $githubSecret
if ([string]::IsNullOrWhiteSpace($ghToken)) { if ([string]::IsNullOrWhiteSpace($ghToken)) {
throw "GitHub token is not set. Set environment variable '$githubSecret'." throw "GitHub token is not set. Set RepoUtilsSecrets slot '$githubSecret' (githubSecret)."
} }
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) { if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
@ -235,7 +231,7 @@ function Invoke-Plugin {
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" } $authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
} }
throw "GitHub CLI authentication failed for repository '$repo'. Ensure secret '$githubSecret' is valid and has access to this repository." throw "GitHub CLI authentication failed for repository '$repo'. Ensure RepoUtilsSecrets slot '$githubSecret' is valid and has access to this repository."
} }
Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)" Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)"