mirror of
https://github.com/MAKS-IT-COM/maksit-cluster-console.git
synced 2026-09-30 00:38:10 +02:00
(feature): persist operator settings in appdata; sync repoutils secret packs
This commit is contained in:
parent
7da30fd459
commit
0ee07f7246
@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.2] - 2026-09-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Operator settings (layout, port-forwards, Chat) are written to `%AppData%/MaksIT/Cluster Console/settings.json` (WiX `installFolderName`). Shipped `appsettings.json` next to the exe keeps host logging only; a leftover `Configuration` block is copied once into the user file.
|
||||
- Synced RepoUtils: ContainerRegistry JSON catalog (PascalCase Harbor / InCluster keys) and `RepoUtilsSecrets` pack slots instead of per-plugin `*Secret` env names.
|
||||
|
||||
## [0.6.1] - 2026-08-30
|
||||
|
||||
### Changed
|
||||
@ -133,3 +140,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- Helm releases, Dapr CRDs, Applications view, force-delete, volume file browse, resource-limit patches, and a read-only local Ollama **Chat** tab.
|
||||
|
||||
See [README.md](README.md) for the full feature list.
|
||||
|
||||
|
||||
@ -61,7 +61,9 @@ Connect a context from the catalog, pick a navigator item, then use the table, d
|
||||
|
||||
## Configuration
|
||||
|
||||
Defaults live in `src/MaksIT.ClusterConsole.Shared/appsettings.json` (copied next to the UI). Notable keys under `Configuration`:
|
||||
Host logging lives in `src/MaksIT.ClusterConsole.Shared/appsettings.json` (copied next to the UI under Program Files; normal users cannot write it). Operator layout, open clusters, port-forwards, and Chat settings are saved to `%AppData%/MaksIT/Cluster Console/settings.json` (same folder name as WiX: `Program Files\MaksIT\Cluster Console`). On first launch, a leftover `Configuration` block next to the exe is copied once into that user file.
|
||||
|
||||
Notable keys under `Configuration` in the user file:
|
||||
|
||||
| Key | Role |
|
||||
|-----|------|
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>0.6.1</Version>
|
||||
<Version>0.6.2</Version>
|
||||
<Product>MaksIT.ClusterConsole</Product>
|
||||
<AssemblyTitle>MaksIT.ClusterConsole</AssemblyTitle>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -5,20 +5,36 @@ using System.Text.Json.Nodes;
|
||||
namespace MaksIT.ClusterConsole.Shared;
|
||||
|
||||
public sealed class ConfigurationFileService {
|
||||
public const string ProductFolder = "Cluster Console";
|
||||
public const string SeedFileName = "appsettings.json";
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new() {
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = null
|
||||
};
|
||||
|
||||
private readonly string? _seedPath;
|
||||
private Configuration _current;
|
||||
|
||||
public string FilePath { get; }
|
||||
|
||||
public Configuration Current => _current;
|
||||
|
||||
public ConfigurationFileService(string? configurationPath = null) {
|
||||
FilePath = configurationPath ?? Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
||||
public ConfigurationFileService(string? configurationPath = null, string? seedPath = null) {
|
||||
if (!string.IsNullOrWhiteSpace(configurationPath))
|
||||
FilePath = configurationPath;
|
||||
else
|
||||
FilePath = UserSettingsPath.Get(ProductFolder);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(seedPath))
|
||||
_seedPath = seedPath;
|
||||
else if (string.IsNullOrWhiteSpace(configurationPath))
|
||||
_seedPath = Path.Combine(AppContext.BaseDirectory, SeedFileName);
|
||||
else
|
||||
_seedPath = null;
|
||||
|
||||
_current = LoadFromDisk();
|
||||
CopySeedIfNeeded();
|
||||
}
|
||||
|
||||
public Configuration Reload() {
|
||||
@ -29,24 +45,23 @@ public sealed class ConfigurationFileService {
|
||||
public void Save(Configuration configuration) {
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
configuration.EnsureDefaults();
|
||||
JsonObject root;
|
||||
if (File.Exists(FilePath)) {
|
||||
root = JsonNode.Parse(File.ReadAllText(FilePath)) as JsonObject ?? [];
|
||||
}
|
||||
else {
|
||||
root = [];
|
||||
}
|
||||
|
||||
var dir = Path.GetDirectoryName(FilePath);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var root = ReadRoot(File.Exists(FilePath) ? FilePath : null) ?? [];
|
||||
root["Configuration"] = JsonSerializer.SerializeToNode(configuration, SerializerOptions);
|
||||
File.WriteAllText(FilePath, root.ToJsonString(SerializerOptions));
|
||||
_current = configuration;
|
||||
}
|
||||
|
||||
private Configuration LoadFromDisk() {
|
||||
if (!File.Exists(FilePath))
|
||||
var path = ResolveReadPath();
|
||||
if (path is null)
|
||||
return new Configuration();
|
||||
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(FilePath));
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (!document.RootElement.TryGetProperty("Configuration", out var value))
|
||||
return new Configuration();
|
||||
|
||||
@ -54,4 +69,31 @@ public sealed class ConfigurationFileService {
|
||||
configuration.EnsureDefaults();
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private static JsonObject? ReadRoot(string? path) {
|
||||
if (path is null || !File.Exists(path))
|
||||
return null;
|
||||
|
||||
return JsonNode.Parse(File.ReadAllText(path)) as JsonObject;
|
||||
}
|
||||
|
||||
private void CopySeedIfNeeded() {
|
||||
if (File.Exists(FilePath) || _seedPath is null || !File.Exists(_seedPath) || !HasConfiguration(_seedPath))
|
||||
return;
|
||||
|
||||
Save(_current);
|
||||
}
|
||||
|
||||
private static bool HasConfiguration(string path) {
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
return document.RootElement.TryGetProperty("Configuration", out _);
|
||||
}
|
||||
|
||||
private string? ResolveReadPath() {
|
||||
if (File.Exists(FilePath))
|
||||
return FilePath;
|
||||
if (_seedPath is not null && File.Exists(_seedPath))
|
||||
return _seedPath;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
17
src/MaksIT.ClusterConsole.Shared/UserSettingsPath.cs
Normal file
17
src/MaksIT.ClusterConsole.Shared/UserSettingsPath.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace MaksIT.ClusterConsole.Shared;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// User-writable operator settings under AppData. The product folder must match
|
||||
/// the WiX <c>installFolderName</c> (or <c>Get-DesktopInstallFolderName</c> from
|
||||
/// <c>appName</c> + <c>manufacturer</c>), e.g. <c>%AppData%/MaksIT/Cluster Console</c>.
|
||||
/// Host logging stays in shipped <c>appsettings.json</c> next to the exe.
|
||||
/// </summary>
|
||||
public static class UserSettingsPath {
|
||||
public static string Get(string product, string fileName = "settings.json") =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"MaksIT",
|
||||
product,
|
||||
fileName);
|
||||
}
|
||||
@ -5,22 +5,5 @@
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"Configuration": {
|
||||
"SelectedNamespace": "all",
|
||||
"OllamaEndpoint": "http://127.0.0.1:11434",
|
||||
"OllamaModel": "qwen3:8b",
|
||||
"NavigatorExpanded": {},
|
||||
"Layout": {
|
||||
"WindowWidth": 1400,
|
||||
"WindowHeight": 860,
|
||||
"WindowState": "Normal",
|
||||
"CatalogWidth": 248,
|
||||
"NavigatorWidth": 228,
|
||||
"DetailsWidth": 380,
|
||||
"SelectedNavId": "pods",
|
||||
"Tables": {}
|
||||
},
|
||||
"PortForwards": []
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,9 +247,76 @@ public class ConfigurationFileServiceTests {
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_path_is_appsettings_beside_the_executable() {
|
||||
public void Default_path_is_appdata_under_maksit() {
|
||||
var service = new ConfigurationFileService();
|
||||
Assert.Equal(Path.Combine(AppContext.BaseDirectory, "appsettings.json"), service.FilePath);
|
||||
Assert.Equal(UserSettingsPath.Get(ConfigurationFileService.ProductFolder), service.FilePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_to_new_file_writes_only_configuration() {
|
||||
var path = Path.Combine(Path.GetTempPath(), $"maksit-cluster-console-{Guid.NewGuid():N}.json");
|
||||
try {
|
||||
var service = new ConfigurationFileService(path);
|
||||
service.Save(new Configuration { SelectedNamespace = "kube-system" });
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
Assert.Contains("\"Configuration\"", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\"Logging\"", json, StringComparison.Ordinal);
|
||||
Assert.Equal("kube-system", new ConfigurationFileService(path).Current.SelectedNamespace);
|
||||
}
|
||||
finally {
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Copies_seed_configuration_without_logging() {
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"maksit-cluster-console-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
var seed = Path.Combine(dir, "appsettings.json");
|
||||
var user = Path.Combine(dir, "settings.json");
|
||||
File.WriteAllText(seed, """
|
||||
{
|
||||
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||
"Configuration": { "SelectedNamespace": "kube-system" }
|
||||
}
|
||||
""");
|
||||
|
||||
try {
|
||||
var service = new ConfigurationFileService(user, seed);
|
||||
Assert.True(File.Exists(user));
|
||||
Assert.Equal("kube-system", service.Current.SelectedNamespace);
|
||||
|
||||
var json = File.ReadAllText(user);
|
||||
Assert.Contains("\"Configuration\"", json, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\"Logging\"", json, StringComparison.Ordinal);
|
||||
}
|
||||
finally {
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logging_only_seed_does_not_create_user_file() {
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"maksit-cluster-console-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
var seed = Path.Combine(dir, "appsettings.json");
|
||||
var user = Path.Combine(dir, "settings.json");
|
||||
File.WriteAllText(seed, """
|
||||
{
|
||||
"Logging": { "LogLevel": { "Default": "Information" } }
|
||||
}
|
||||
""");
|
||||
|
||||
try {
|
||||
var service = new ConfigurationFileService(user, seed);
|
||||
Assert.False(File.Exists(user));
|
||||
Assert.Equal(Configuration.AllNamespaces, service.Current.SelectedNamespace);
|
||||
}
|
||||
finally {
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@ -22,6 +22,10 @@ public partial class App : Application {
|
||||
.ConfigureAppConfiguration(builder => {
|
||||
builder.SetBasePath(AppContext.BaseDirectory);
|
||||
builder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
|
||||
builder.AddJsonFile(
|
||||
UserSettingsPath.Get(ConfigurationFileService.ProductFolder),
|
||||
optional: true,
|
||||
reloadOnChange: true);
|
||||
})
|
||||
.ConfigureServices((_, services) => {
|
||||
services.AddSingleton(_ => new ConfigurationFileService());
|
||||
|
||||
@ -98,9 +98,9 @@
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"githubSecret": "GitClone",
|
||||
"stageLabel": "release",
|
||||
"enabled": true,
|
||||
"githubSecret": "GitHub",
|
||||
"repository": "https://github.com/MAKS-IT-COM/maksit-cluster-console",
|
||||
"releaseNotesFile": "..\\..\\..\\CHANGELOG.md",
|
||||
"releaseTitlePattern": "Release {version}",
|
||||
@ -119,5 +119,8 @@
|
||||
"*.flatpak"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"repoUtilsSecretsShared": "RepoUtilsSecretsShared",
|
||||
"repoUtilsSecrets": "RepoUtilsSecrets",
|
||||
"vaultRepoUtilsApplication": "Shared"
|
||||
}
|
||||
|
||||
@ -46,5 +46,8 @@
|
||||
"red": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"repoUtilsSecretsShared": "RepoUtilsSecretsShared",
|
||||
"repoUtilsSecrets": "RepoUtilsSecrets",
|
||||
"vaultRepoUtilsApplication": "Shared"
|
||||
}
|
||||
|
||||
@ -294,129 +294,586 @@ function Test-PluginMutatesRemote {
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-SecretEnvironmentValue {
|
||||
function Get-RepoUtilsEnvironmentVariable {
|
||||
<#
|
||||
.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
|
||||
Plugins never store secret material in scriptSettings.json. Settings hold a
|
||||
logical name (e.g. "GitHub", "NuGet"); the process environment variable with
|
||||
that same name must be set before the engine runs.
|
||||
|
||||
.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'
|
||||
Process wins (CICD sandbox inject, `$env:Name`, explicit session values). When the
|
||||
current process was started before a User-level pack was set, User/Machine still
|
||||
apply so laptop releases do not require a new shell. An explicit process value —
|
||||
including empty JSON `{}` — shadows User/Machine.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
return [Environment]::GetEnvironmentVariable($Name)
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($Name)) {
|
||||
throw "Environment variable name is required."
|
||||
}
|
||||
|
||||
function Resolve-PluginSecretName {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves a logical secret name from a plugin's scriptSettings entry.
|
||||
$processValue = [Environment]::GetEnvironmentVariable($Name, 'Process')
|
||||
# Empty string is what SetEnvironmentVariable($null, Process) leaves behind;
|
||||
# treat it as unset so Windows User packs still apply. Explicit '{}' shadows User.
|
||||
if (-not [string]::IsNullOrWhiteSpace($processValue)) {
|
||||
return $processValue
|
||||
}
|
||||
|
||||
.DESCRIPTION
|
||||
Reads a string property such as githubSecret / nugetSecret / npmSecret /
|
||||
containerRegistrySecret from the plugin settings object. Returns $null when
|
||||
the property is missing or blank.
|
||||
foreach ($target in @('User', 'Machine')) {
|
||||
try {
|
||||
$value = [Environment]::GetEnvironmentVariable($Name, $target)
|
||||
}
|
||||
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)) {
|
||||
return $value.Trim()
|
||||
return $value
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-RepoUtilsSecretsEnvNames {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reads declared pack environment variable names from scriptSettings / engine context.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
$sharedName = $null
|
||||
$packName = $null
|
||||
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 {
|
||||
$parsed = $trimmed | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
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
|
||||
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
|
||||
throw "Decoded '$ContextName' must be in the form 'username:password'."
|
||||
}
|
||||
|
||||
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 a logical secret name.
|
||||
Loads container-registry username/password from the merged RepoUtilsSecrets pack.
|
||||
|
||||
.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.
|
||||
Reads a nested catalog slot (named by -Slot, typically ContainerRegistry) after
|
||||
org-pack + slug-pack merge. -Key (PascalCase, ordinal match) selects an entry.
|
||||
|
||||
.PARAMETER SecretName
|
||||
Logical secret name (environment variable name), not a password or token.
|
||||
.PARAMETER Key
|
||||
PascalCase catalog key (e.g. Harbor). Required.
|
||||
|
||||
.PARAMETER Slot
|
||||
PascalCase pack subkey that holds the catalog object (from containerRegistrySecret).
|
||||
|
||||
.PARAMETER SharedSettings
|
||||
Optional engine shared context (reserved for callers that thread context).
|
||||
Engine shared context (must declare repoUtilsSecretsShared / repoUtilsSecrets).
|
||||
|
||||
.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(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SecretName,
|
||||
[string]$Key,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Slot,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[psobject]$SharedSettings
|
||||
)
|
||||
|
||||
$raw = Get-SecretEnvironmentValue -Name $SecretName
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||
throw "Environment variable '$SecretName' is not set."
|
||||
if ([string]::IsNullOrWhiteSpace($Slot) -or -not (Test-ContainerRegistryCatalogKey -Key $Slot)) {
|
||||
throw "containerRegistrySecret '$Slot' must be PascalCase (e.g. ContainerRegistry)."
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($raw))
|
||||
}
|
||||
catch {
|
||||
throw "Failed to decode '$SecretName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
|
||||
if ([string]::IsNullOrWhiteSpace($Key)) {
|
||||
throw "Pass -Key (PascalCase, e.g. Harbor) to select an entry in pack slot '$Slot'."
|
||||
}
|
||||
|
||||
$parts = $decoded -split ':', 2
|
||||
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'."
|
||||
if (-not (Test-ContainerRegistryCatalogKey -Key $Key)) {
|
||||
throw "containerRegistryKey '$Key' must be PascalCase (e.g. Harbor, InCluster)."
|
||||
}
|
||||
|
||||
return @{ User = $parts[0]; Password = $parts[1] }
|
||||
$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 {
|
||||
@ -757,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
|
||||
|
||||
@ -213,6 +213,7 @@ function New-EngineContext {
|
||||
skipPublishPlugins = $false
|
||||
facts = [ordered]@{}
|
||||
}
|
||||
$context = Copy-RepoUtilsSecretsEnvNamesToContext -Context $context -Settings $Settings
|
||||
|
||||
$versionSource = Resolve-EngineContextVersion -Plugins $Plugins -Context $context -ScriptDir $ScriptDir
|
||||
$version = [string](Get-EngineState -Context $context -Name 'version' -Required)
|
||||
|
||||
@ -38,6 +38,7 @@ function New-EngineContext {
|
||||
utilsDir = $SrcDir
|
||||
facts = [ordered]@{}
|
||||
}
|
||||
$context = Copy-RepoUtilsSecretsEnvNamesToContext -Context $context -Settings $Settings
|
||||
|
||||
$expandContext = Get-Command Expand-ExtensionEngineContext -ErrorAction SilentlyContinue
|
||||
if ($expandContext) {
|
||||
|
||||
@ -86,89 +86,23 @@ function Get-RepoUtilsVaultConnectionSecretName {
|
||||
return 'MAKSIT_VAULT'
|
||||
}
|
||||
|
||||
function Add-RepoUtilsSecretNamesFromObject {
|
||||
param(
|
||||
$Object,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyCollection()]
|
||||
[System.Collections.Generic.HashSet[string]]$Names
|
||||
)
|
||||
|
||||
if ($null -eq $Object -or $Object -is [string] -or $Object -is [ValueType]) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($Object -is [System.Collections.IDictionary]) {
|
||||
foreach ($key in @($Object.Keys)) {
|
||||
$name = [string]$key
|
||||
$value = $Object[$key]
|
||||
if ($name -like '*Secret') {
|
||||
$trimmed = ([string]$value).Trim()
|
||||
if (-not [string]::IsNullOrWhiteSpace($trimmed) -and
|
||||
-not [string]::Equals($trimmed, 'WebhookSecret', [System.StringComparison]::Ordinal)) {
|
||||
[void]$Names.Add($trimmed)
|
||||
}
|
||||
}
|
||||
else {
|
||||
Add-RepoUtilsSecretNamesFromObject -Object $value -Names $Names
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ($Object -is [System.Collections.IEnumerable]) {
|
||||
foreach ($item in @($Object)) {
|
||||
Add-RepoUtilsSecretNamesFromObject -Object $item -Names $Names
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ($null -eq $Object.PSObject) {
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($property in $Object.PSObject.Properties) {
|
||||
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||
continue
|
||||
}
|
||||
|
||||
$value = $property.Value
|
||||
if ($property.Name -like '*Secret') {
|
||||
$trimmed = ([string]$value).Trim()
|
||||
if (-not [string]::IsNullOrWhiteSpace($trimmed) -and
|
||||
-not [string]::Equals($trimmed, 'WebhookSecret', [System.StringComparison]::Ordinal)) {
|
||||
[void]$Names.Add($trimmed)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
Add-RepoUtilsSecretNamesFromObject -Object $value -Names $Names
|
||||
}
|
||||
}
|
||||
|
||||
function Get-EnabledPluginSecretNames {
|
||||
function Get-RepoUtilsVaultOrgPackApplicationName {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugins
|
||||
$Settings
|
||||
)
|
||||
|
||||
$names = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($plugin in @($Plugins)) {
|
||||
if ($null -eq $plugin) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (($plugin.PSObject.Properties.Name -contains 'enabled') -and ($plugin.enabled -eq $false)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Add-RepoUtilsSecretNamesFromObject -Object $plugin -Names $names
|
||||
$application = $null
|
||||
if ($Settings.PSObject.Properties.Name -contains 'vaultRepoUtilsApplication') {
|
||||
$application = [string]$Settings.vaultRepoUtilsApplication
|
||||
}
|
||||
|
||||
return @($names)
|
||||
$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 {
|
||||
@ -325,25 +259,23 @@ function Resolve-RepoUtilsVaultSecretValue {
|
||||
[string]$Mode
|
||||
)
|
||||
|
||||
foreach ($application in @($ApplicationName, 'Shared')) {
|
||||
$match = Find-RepoUtilsVaultSecretMatch `
|
||||
-OrganizationName $OrganizationName `
|
||||
-ApplicationName $application `
|
||||
-SecretName $SecretName `
|
||||
-Connection $Connection `
|
||||
-Mode $Mode
|
||||
if ($null -eq $match) {
|
||||
continue
|
||||
}
|
||||
|
||||
$value = Get-RepoUtilsVaultSecretValue -Match $match -Connection $Connection -Mode $Mode
|
||||
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||
Write-Log -Level 'INFO' -Message "Resolved Vault secret '$SecretName' from $OrganizationName/$application"
|
||||
return $value
|
||||
}
|
||||
$match = Find-RepoUtilsVaultSecretMatch `
|
||||
-OrganizationName $OrganizationName `
|
||||
-ApplicationName $ApplicationName `
|
||||
-SecretName $SecretName `
|
||||
-Connection $Connection `
|
||||
-Mode $Mode
|
||||
if ($null -eq $match) {
|
||||
return $null
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -359,6 +291,18 @@ function Initialize-RepoUtilsVaultSecrets {
|
||||
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)) {
|
||||
@ -367,9 +311,9 @@ function Initialize-RepoUtilsVaultSecrets {
|
||||
|
||||
$connection = ConvertFrom-VaultConnectionSecret -Raw $raw
|
||||
$scope = Get-RepoUtilsVaultScope -Settings $Settings
|
||||
$secretNames = @(Get-EnabledPluginSecretNames -Plugins $Plugins)
|
||||
$orgPackApplication = Get-RepoUtilsVaultOrgPackApplicationName -Settings $Settings
|
||||
|
||||
Write-Log -Level 'INFO' -Message "Vault mode: loading $($secretNames.Count) plugin secret(s) for $($scope.Organization)/$($scope.Application)"
|
||||
Write-Log -Level 'INFO' -Message "Vault mode: loading RepoUtilsSecrets packs for $($scope.Organization)/$orgPackApplication and $($scope.Organization)/$($scope.Application)"
|
||||
|
||||
$mode = 'Rest'
|
||||
try {
|
||||
@ -388,18 +332,34 @@ function Initialize-RepoUtilsVaultSecrets {
|
||||
$mode = 'Rest'
|
||||
}
|
||||
|
||||
foreach ($secretName in $secretNames) {
|
||||
$value = Resolve-RepoUtilsVaultSecretValue `
|
||||
if (-not $sharedPresent) {
|
||||
$sharedValue = Resolve-RepoUtilsVaultSecretValue `
|
||||
-OrganizationName $scope.Organization `
|
||||
-ApplicationName $scope.Application `
|
||||
-SecretName $secretName `
|
||||
-ApplicationName $orgPackApplication `
|
||||
-SecretName $names.SharedEnv `
|
||||
-Connection $connection `
|
||||
-Mode $mode
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
throw "Vault secret '$secretName' was not found for $($scope.Organization)/$($scope.Application) (or Shared) or has no current version."
|
||||
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($secretName, $value, 'Process')
|
||||
[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')
|
||||
}
|
||||
}
|
||||
|
||||
@ -408,6 +368,6 @@ Export-ModuleMember -Function @(
|
||||
'ConvertFrom-VaultConnectionSecret',
|
||||
'Get-RepoUtilsVaultScope',
|
||||
'Get-RepoUtilsVaultConnectionSecretName',
|
||||
'Get-EnabledPluginSecretNames',
|
||||
'Get-RepoUtilsVaultOrgPackApplicationName',
|
||||
'Initialize-RepoUtilsVaultSecrets'
|
||||
)
|
||||
|
||||
@ -55,6 +55,37 @@ function Get-DesktopInstallFolderName {
|
||||
return $name
|
||||
}
|
||||
|
||||
function Get-WixArchitectureFromRuntimeIdentifier {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$RuntimeIdentifier = ''
|
||||
)
|
||||
|
||||
$rid = [string]$RuntimeIdentifier
|
||||
if ($rid -match '(?i)arm64') {
|
||||
return 'arm64'
|
||||
}
|
||||
|
||||
if ($rid -match '(?i)(^|-)x86($|-)') {
|
||||
return 'x86'
|
||||
}
|
||||
|
||||
return 'x64'
|
||||
}
|
||||
|
||||
function Get-WixPerMachineProgramFilesFolderId {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Architecture = 'x64'
|
||||
)
|
||||
|
||||
if ($Architecture -eq 'x86') {
|
||||
return 'ProgramFilesFolder'
|
||||
}
|
||||
|
||||
return 'ProgramFiles64Folder'
|
||||
}
|
||||
|
||||
function Get-MsiProductVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@ -248,6 +279,9 @@ function New-WixPackageXml {
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$InstallFolderName,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Architecture = 'x64',
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$IconPath
|
||||
)
|
||||
@ -297,7 +331,12 @@ function New-WixPackageXml {
|
||||
-InstallFolderName $InstallFolderName
|
||||
|
||||
$stdLocal = $xml.CreateElement('StandardDirectory', $ns)
|
||||
$rootFolderId = if ($scope -eq 'perMachine') { 'ProgramFiles6432Folder' } else { 'LocalAppDataFolder' }
|
||||
$rootFolderId = if ($scope -eq 'perMachine') {
|
||||
Get-WixPerMachineProgramFilesFolderId -Architecture $Architecture
|
||||
}
|
||||
else {
|
||||
'LocalAppDataFolder'
|
||||
}
|
||||
$null = $stdLocal.SetAttribute('Id', $rootFolderId)
|
||||
$null = $package.AppendChild($stdLocal)
|
||||
|
||||
@ -890,7 +929,10 @@ function New-WixBundleXml {
|
||||
[string]$InstallScope = 'perMachine',
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$InstallFolderName
|
||||
[string]$InstallFolderName,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Architecture = 'x64'
|
||||
)
|
||||
|
||||
$escapedName = [System.Security.SecurityElement]::Escape($AppName)
|
||||
@ -923,7 +965,7 @@ function New-WixBundleXml {
|
||||
}
|
||||
|
||||
# Type=formatted so WixStdBA expands well-known folders in the InstallFolder edit box.
|
||||
# Type=string shows the raw token, e.g. [ProgramFiles6432Folder]MaksIT\Cluster Console.
|
||||
# Type=string shows the raw token, e.g. [ProgramFiles64Folder]MaksIT\Cluster Console.
|
||||
# Burn CSIDL folders already end with a backslash, so do not insert another one.
|
||||
# Layout is {ProgramFiles|LocalAppData}\{Manufacturer}\{product} — product folder is the
|
||||
# internal name (appName with manufacturer prefix stripped, or installFolderName).
|
||||
@ -931,7 +973,7 @@ function New-WixBundleXml {
|
||||
'[LocalAppDataFolder]'
|
||||
}
|
||||
else {
|
||||
'[ProgramFiles6432Folder]'
|
||||
'[' + (Get-WixPerMachineProgramFilesFolderId -Architecture $Architecture) + ']'
|
||||
}
|
||||
|
||||
$folderPath = if ([string]::IsNullOrWhiteSpace($Manufacturer)) {
|
||||
@ -968,6 +1010,8 @@ Export-ModuleMember -Function `
|
||||
ConvertTo-WixIdentifier, `
|
||||
Get-MsiProductVersion, `
|
||||
Get-DesktopInstallFolderName, `
|
||||
Get-WixArchitectureFromRuntimeIdentifier, `
|
||||
Get-WixPerMachineProgramFilesFolderId, `
|
||||
Get-PluginPropertyValue, `
|
||||
Resolve-DesktopPublishDirectory, `
|
||||
Resolve-DesktopExecutablePath, `
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
Harvests a win-* (or sole) DotNetPublish folder into a WiX MSI, then wraps
|
||||
it in a Burn bootstrapper .exe. The .exe is the GitHub asset; the MSI/WXS
|
||||
stay in a staging folder and are not added to the portable zip.
|
||||
`wix build -arch` follows `runtimeIdentifier` (default win-x64 → x64) so
|
||||
per-machine installs go to `C:\Program Files`, not Program Files (x86).
|
||||
Requires the WiX CLI (`dotnet tool install -g wix`). WiX v7: accept the
|
||||
OSMF EULA (`wix eula accept wix7` or `-acceptEula wix7`) and
|
||||
`wix extension add -g WixToolset.BootstrapperApplications.wixext`.
|
||||
@ -102,6 +104,8 @@ function Invoke-Plugin {
|
||||
$upgradeCode = [guid]$upgradeCodeRaw
|
||||
$manufacturer = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'manufacturer' -Default 'MaksIT')
|
||||
$runtimeIdentifier = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeIdentifier' -Default 'win-x64')
|
||||
$wixArch = Get-WixArchitectureFromRuntimeIdentifier -RuntimeIdentifier $runtimeIdentifier
|
||||
$archArgs = @('-arch', $wixArch)
|
||||
$installScope = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'installScope' -Default 'perMachine')
|
||||
$installFolderName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'installFolderName')
|
||||
$executableName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'executableName')
|
||||
@ -209,7 +213,7 @@ function Invoke-Plugin {
|
||||
$msiPath = Join-Path $stageDir ($safeName + '-' + $version + '.msi')
|
||||
$bundleWxsPath = Join-Path $stageDir ($safeName + '-' + $version + '-bundle.wxs')
|
||||
|
||||
Write-Log -Level "STEP" -Message "Generating WiX source for '$appName' from $publishDirectory"
|
||||
Write-Log -Level "STEP" -Message "Generating WiX source for '$appName' from $publishDirectory ($wixArch)"
|
||||
$xml = New-WixPackageXml `
|
||||
-AppName $appName `
|
||||
-Manufacturer $manufacturer `
|
||||
@ -219,6 +223,7 @@ function Invoke-Plugin {
|
||||
-ExecutablePath $executablePath `
|
||||
-InstallScope $installScope `
|
||||
-InstallFolderName $installFolderName `
|
||||
-Architecture $wixArch `
|
||||
-IconPath $iconPath
|
||||
|
||||
$xml.Save($wxsPath)
|
||||
@ -238,9 +243,9 @@ function Invoke-Plugin {
|
||||
$eulaArgs = @(Get-WixAcceptEulaArguments -VersionText $wixVersion)
|
||||
$bundleExt = Get-WixBundleExtensionName -VersionText $wixVersion
|
||||
|
||||
Write-Log -Level "STEP" -Message "Building MSI with WiX..."
|
||||
Write-Log -Level "STEP" -Message "Building MSI with WiX ($wixArch)..."
|
||||
try {
|
||||
Invoke-ExternalCommand -Name wix -ArgumentList (@('build') + $eulaArgs + @($wxsPath, '-o', $msiPath)) | Out-Null
|
||||
Invoke-ExternalCommand -Name wix -ArgumentList (@('build') + $eulaArgs + $archArgs + @($wxsPath, '-o', $msiPath)) | Out-Null
|
||||
}
|
||||
catch {
|
||||
if (Test-WixMissingException -ErrorRecord $_) {
|
||||
@ -265,16 +270,17 @@ function Invoke-Plugin {
|
||||
-LogoSidePath $logoSidePath `
|
||||
-ThemePath $themePath `
|
||||
-InstallScope $installScope `
|
||||
-InstallFolderName $installFolderName
|
||||
-InstallFolderName $installFolderName `
|
||||
-Architecture $wixArch
|
||||
[System.IO.File]::WriteAllText($bundleWxsPath, $bundleXml, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
if (Test-Path -LiteralPath $exePath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $exePath -Force
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Building Windows installer exe..."
|
||||
Write-Log -Level "STEP" -Message "Building Windows installer exe ($wixArch)..."
|
||||
try {
|
||||
Invoke-ExternalCommand -Name wix -ArgumentList (@('build') + $eulaArgs + @($bundleWxsPath, '-ext', $bundleExt, '-o', $exePath)) | Out-Null
|
||||
Invoke-ExternalCommand -Name wix -ArgumentList (@('build') + $eulaArgs + $archArgs + @($bundleWxsPath, '-ext', $bundleExt, '-o', $exePath)) | Out-Null
|
||||
}
|
||||
catch {
|
||||
if (Test-WixMissingException -ErrorRecord $_) {
|
||||
|
||||
@ -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
|
||||
@ -29,10 +29,10 @@ function Invoke-Plugin {
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret'
|
||||
$packageFile = $sharedSettings.packageFile
|
||||
|
||||
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret' -PluginDisplayName 'DotNetNuGet' -Required
|
||||
|
||||
Assert-Command dotnet
|
||||
|
||||
@ -52,13 +52,9 @@ function Invoke-Plugin {
|
||||
return
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($nugetSecret)) {
|
||||
throw "DotNetNuGet plugin requires 'nugetSecret' in scriptSettings.json (logical secret name, e.g. NuGet)."
|
||||
}
|
||||
|
||||
$nugetKey = Get-SecretEnvironmentValue -Name $nugetSecret
|
||||
$nugetKey = Get-RepoUtilsSecretSlot -Name $nugetSecret -Settings $sharedSettings
|
||||
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)) {
|
||||
|
||||
@ -6,8 +6,7 @@
|
||||
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).
|
||||
Publishes packages in configured order using RepoUtilsSecrets slot Npm.
|
||||
Uses a temporary .npmrc in the workspace root.
|
||||
#>
|
||||
|
||||
@ -85,6 +84,8 @@ function Invoke-Plugin {
|
||||
throw "NpmPublish plugin requires non-empty 'publishOrder' (workspace package names)."
|
||||
}
|
||||
|
||||
$npmSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'npmSecret' -PluginDisplayName 'NpmPublish' -Required
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'NpmPackageSupport.psm1') -Force
|
||||
$useWorkspaces = Test-NpmWorkspacesConfigured -WorkspaceRoot $workspaceRoot
|
||||
if (-not $useWorkspaces -and $publishOrder.Count -gt 1) {
|
||||
@ -99,14 +100,9 @@ function Invoke-Plugin {
|
||||
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
|
||||
$npmToken = Get-RepoUtilsSecretSlot -Name $npmSecret -Settings $shared
|
||||
if ([string]::IsNullOrWhiteSpace($npmToken)) {
|
||||
throw "npm API key is not set. Set environment variable '$npmSecret'."
|
||||
throw "npm API key is not set. Set RepoUtilsSecrets slot '$npmSecret' (npmSecret)."
|
||||
}
|
||||
|
||||
$registryHost = ([uri]$registry).Host
|
||||
|
||||
@ -101,7 +101,6 @@ function Invoke-Plugin {
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret'
|
||||
$configuredRepository = $pluginSettings.repository
|
||||
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
|
||||
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
|
||||
@ -112,6 +111,7 @@ function Invoke-Plugin {
|
||||
$releaseAssetPaths = @()
|
||||
|
||||
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret' -PluginDisplayName 'GitHub' -Required
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||
throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json."
|
||||
@ -138,13 +138,9 @@ function Invoke-Plugin {
|
||||
|
||||
Assert-Command gh
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($githubSecret)) {
|
||||
throw "GitHub plugin requires 'githubSecret' in scriptSettings.json (logical secret name, e.g. GitHub)."
|
||||
}
|
||||
|
||||
$ghToken = Get-SecretEnvironmentValue -Name $githubSecret
|
||||
$ghToken = Get-RepoUtilsSecretSlot -Name $githubSecret -Settings $sharedSettings
|
||||
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)) {
|
||||
@ -235,7 +231,7 @@ function Invoke-Plugin {
|
||||
$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)"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user