Skip to content

Commit 3c4d789

Browse files
authored
feat(#96): centralize configuration and secret resolution for NovaMod… (#126)
* feat(#96): centralize configuration and secret resolution for NovaModuleTools - Implement explicit precedence model for delivery configuration resolution - Ensure raw package upload, update notification settings, and PSGallery publishing follow the new model - Introduce functions for environment variable handling and secret resolution - Update documentation to reflect changes in upload and secret precedence * feat(#96): centralize configuration handling and enhance tests - Refactor Get-NovaSettingsRootPath to accept platform parameter - Update copyright information in project.json - Add tests for Get-NovaSettingsRootPath and environment variable functions
1 parent e17800b commit 3c4d789

19 files changed

Lines changed: 477 additions & 54 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8181

8282
### Fixed
8383

84+
- Centralize delivery configuration resolution so raw package upload, update notification settings, and PSGallery
85+
publishing now follow one explicit precedence model without surfacing configured secrets in error text.
86+
- Raw upload now resolves command overrides before named repository settings, then package defaults.
87+
- Secret lookup now resolves explicit values before environment-variable indirection, then configured literal
88+
fallbacks.
8489
- Keep CI-oriented publish and bump workflows bound to the freshly built module so follow-up `publish`, `release`, and
8590
`bump` steps no longer lose private helpers after module re-imports in the same session.
8691
- PowerShell now supports `New-NovaModulePackage -SkipTests`, `Publish-NovaModule -SkipTests`, and

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ PS> Update-NovaModuleTools # alias
9696
Use `% nova notification` when you want the CLI-oriented workflow and the `Set-` / `Get-` cmdlets when you want the
9797
PowerShell function form in scripts.
9898

99+
Update notification preferences use one shared settings location:
100+
101+
- Windows: `%APPDATA%/NovaModuleTools/settings.json`
102+
- macOS/Linux with `XDG_CONFIG_HOME`: `$XDG_CONFIG_HOME/NovaModuleTools/settings.json`
103+
- macOS/Linux fallback: `~/.config/NovaModuleTools/settings.json`
104+
99105
`Update-NovaModuleTool` (and its `Update-NovaModuleTools` alias), CLI:`% nova update` use that stored prerelease
100106
preference to decide whether prerelease self-updates are eligible. When prerelease self-updates are disabled,
101107
self-update stays on stable releases. When they are enabled, self-update may target a prerelease, but it asks for
@@ -339,8 +345,12 @@ Use this `project.json` shape when you want Nova to resolve upload targets from
339345
- `Deploy-NovaPackage` uploads existing package files only; it does not build, test, or create packages.
340346
- `% nova deploy` is the CLI entrypoint for the same raw upload workflow and uses POSIX/GNU-style options such as
341347
`--repository`, `--url`, and `--token`.
342-
- `-Url` overrides repository or package-level upload settings and is the simplest CI/CD path for the PowerShell cmdlet
343-
form.
348+
- Upload target precedence is explicit: `-Url` / `--url` and `-UploadPath` / `--upload-path` win first, then matching
349+
`Package.Repositories[]` values, then package-level `Package.RepositoryUrl` / `Package.RawRepositoryUrl` and
350+
`Package.UploadPath`.
351+
- Secret precedence is explicit too: `-Token` / `--token` wins first, then `-TokenEnvironmentVariable` /
352+
`--token-env`, then merged repository/package `Auth.TokenEnvironmentVariable`, then merged repository/package
353+
`Auth.Token`.
344354
- When `-PackagePath` is omitted, Nova resolves package files from `Package.OutputDirectory.Path`.
345355
- `Package.FileNamePattern` overrides the default upload discovery pattern. When omitted, Nova falls back to
346356
`<Package.Id>*` and then applies the selected package type extension.
@@ -354,6 +364,9 @@ Use this `project.json` shape when you want Nova to resolve upload targets from
354364
- `Package.Headers`, `Package.Auth`, `Package.RepositoryUrl`, and repository-specific overrides remain generic so the
355365
workflow works with raw endpoints such as Nexus or Artifactory without turning `Publish-NovaModule` into a vendor-
356366
specific upload command.
367+
- `Publish-NovaModule` and `Invoke-NovaRelease` keep a matching secret rule for PowerShell repositories: `-ApiKey` /
368+
`--api-key` overrides any fallback, and `PSGallery` still checks `PSGALLERY_API` when no explicit API key was
369+
provided.
357370

358371
For module publishing and release flows, the same opt-in skip-tests behavior is available when tests already ran earlier
359372
in the pipeline:

docs/commands.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,11 @@ <h3><code>Deploy-NovaPackage</code> / <code>% nova deploy</code></h3>
363363
% nova deploy --url https://packages.example/raw/ --token $env:NOVA_PACKAGE_TOKEN</code></pre>
364364
</div>
365365
</div>
366+
<p>Resolution order stays explicit: command-line <code>url</code> / <code>upload-path</code>
367+
overrides win first, then the selected <code>Package.Repositories[]</code> entry, then
368+
package-level defaults. Secrets follow the same pattern: explicit <code>token</code>, then
369+
explicit <code>token-env</code>, then configured <code>Auth.TokenEnvironmentVariable</code>,
370+
then configured <code>Auth.Token</code>.</p>
366371
<p><a class="text-link" href="./packaging-and-delivery.html#upload">See raw upload details</a>
367372
</p>
368373
</article>
@@ -404,6 +409,9 @@ <h3><code>Publish-NovaModule</code> / <code>% nova publish</code></h3>
404409
% nova publish --repository PSGallery --api-key $env:PSGALLERY_API --continuous-integration</code></pre>
405410
</div>
406411
</div>
412+
<p>When publishing to <code>PSGallery</code>, an explicit <code>-ApiKey</code> /
413+
<code>--api-key</code> wins first. If it is omitted, Nova still checks
414+
<code>PSGALLERY_API</code> as the repository-specific fallback.</p>
407415
<p><a class="text-link" href="./packaging-and-delivery.html#publish">See publish details</a></p>
408416
</article>
409417

@@ -608,6 +616,9 @@ <h3><code>Get-NovaUpdateNotificationPreference</code>, <code>Set-NovaUpdateNotif
608616
% nova notification --enable</code></pre>
609617
</div>
610618
</div>
619+
<p>The shared preference file lives under <code>%APPDATA%</code> on Windows, otherwise under
620+
<code>$XDG_CONFIG_HOME</code> when it is set, and finally under
621+
<code>~/.config/NovaModuleTools/settings.json</code>.</p>
611622
<p><a class="text-link" href="./versioning-and-updates.html#notification-preferences">See update
612623
preference guidance</a></p>
613624
</article>

docs/project-json-reference.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,15 @@ <h3>Raw upload settings</h3>
532532
</tbody>
533533
</table>
534534
</div>
535+
<p>Nova resolves raw-upload settings in a fixed order so CI/CD overrides stay predictable. Command
536+
arguments such as <code>-Url</code> / <code>--url</code> and <code>-UploadPath</code> /
537+
<code>--upload-path</code> win first, then the selected <code>Package.Repositories[]</code> entry,
538+
then package-level defaults such as <code>Package.RepositoryUrl</code>,
539+
<code>Package.RawRepositoryUrl</code>, and <code>Package.UploadPath</code>.</p>
540+
<p>Secrets follow a matching precedence model. An explicit token value wins first, then an explicit
541+
token-environment-variable name, then the merged repository/package
542+
<code>Auth.TokenEnvironmentVariable</code>, and finally the merged literal
543+
<code>Auth.Token</code>. Prefer environment-variable indirection for real credentials.</p>
535544

536545
<h3>Authentication examples</h3>
537546
<pre><code>{
@@ -677,7 +686,8 @@ <h3>Use named raw repositories with shared defaults</h3>
677686
}
678687
}</code></pre>
679688
<p>Use package-level values for the shared default behavior, then override only the repository-specific
680-
parts that need to change.</p>
689+
parts that need to change. Repository-specific <code>Headers</code>, <code>Auth</code>, and
690+
<code>UploadPath</code> override package defaults only for the selected named target.</p>
681691
</section>
682692
</div>
683693
</section>

project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
],
99
"Manifest": {
1010
"Author": "Stiwi Gabriel Courage",
11-
"Copyright": "Copyright (c) 2026 Terra Nova.",
11+
"Copyright": "Copyright (c) 2026 Stiwi Gabriel Courage",
1212
"PowerShellHostVersion": "7.4",
1313
"GUID": "6b9202c8-0353-473b-b73c-afab632125a6",
1414
"RequiredModules": [

src/private/package/GetNovaPackageUploadPath.ps1

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@ function Get-NovaPackageUploadPath {
66
[string]$UploadPath
77
)
88

9-
if (-not [string]::IsNullOrWhiteSpace($UploadPath)) {
10-
return $UploadPath.Trim()
11-
}
12-
13-
$repositoryUploadPath = Get-NovaPackageSettingValue -InputObject $RepositorySettings -Name 'UploadPath'
14-
if (-not [string]::IsNullOrWhiteSpace("$repositoryUploadPath")) {
15-
return "$repositoryUploadPath".Trim()
16-
}
9+
$resolvedUploadPath = Get-NovaFirstConfiguredValue -CandidateList @(
10+
$UploadPath
11+
(Get-NovaPackageSettingValue -InputObject $RepositorySettings -Name 'UploadPath')
12+
(Get-NovaPackageSettingValue -InputObject $PackageSettings -Name 'UploadPath')
13+
)
1714

18-
$packageUploadPath = Get-NovaPackageSettingValue -InputObject $PackageSettings -Name 'UploadPath'
19-
return "$( $packageUploadPath )".Trim()
15+
return "$( $resolvedUploadPath )".Trim()
2016
}
2117

src/private/package/GetNovaPackageUploadTargetUrl.ps1

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,13 @@ function Get-NovaPackageUploadTargetUrl {
66
[string]$Url
77
)
88

9-
$resolvedUrl = $Url
10-
if ( [string]::IsNullOrWhiteSpace($resolvedUrl)) {
11-
$resolvedUrl = Get-NovaPackageSettingValue -InputObject $RepositorySettings -Name 'Url'
12-
}
13-
if ( [string]::IsNullOrWhiteSpace("$resolvedUrl")) {
14-
$resolvedUrl = Get-NovaPackageSettingValue -InputObject $PackageSettings -Name 'RepositoryUrl'
15-
}
16-
if ( [string]::IsNullOrWhiteSpace("$resolvedUrl")) {
17-
$resolvedUrl = Get-NovaPackageSettingValue -InputObject $PackageSettings -Name 'RawRepositoryUrl'
18-
}
19-
if ( [string]::IsNullOrWhiteSpace("$resolvedUrl")) {
9+
$resolvedUrl = Get-NovaFirstConfiguredValue -CandidateList @(
10+
$Url
11+
(Get-NovaPackageSettingValue -InputObject $RepositorySettings -Name 'Url')
12+
(Get-NovaPackageSettingValue -InputObject $PackageSettings -Name 'RepositoryUrl')
13+
(Get-NovaPackageSettingValue -InputObject $PackageSettings -Name 'RawRepositoryUrl')
14+
)
15+
if (-not (Test-NovaConfiguredValue -Value $resolvedUrl)) {
2016
Stop-NovaOperation -Message 'Upload target URL is missing. Provide -Url or configure Package.RepositoryUrl or Package.Repositories[].Url.' -ErrorId 'Nova.Configuration.PackageUploadTargetUrlMissing' -Category InvalidData -TargetObject 'Url'
2117
}
2218

src/private/package/GetNovaPackageUploadToken.ps1

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,11 @@ function Get-NovaPackageUploadToken {
66
[string]$TokenEnvironmentVariable
77
)
88

9-
if (-not [string]::IsNullOrWhiteSpace($Token)) {
10-
return $Token
11-
}
12-
13-
$environmentVariableName = $TokenEnvironmentVariable
14-
if ( [string]::IsNullOrWhiteSpace($environmentVariableName)) {
15-
$environmentVariableName = Get-NovaPackageSettingValue -InputObject $AuthSettings -Name 'TokenEnvironmentVariable'
16-
}
17-
if (-not [string]::IsNullOrWhiteSpace("$environmentVariableName")) {
18-
return [System.Environment]::GetEnvironmentVariable("$environmentVariableName")
19-
}
20-
21-
return Get-NovaPackageSettingValue -InputObject $AuthSettings -Name 'Token'
9+
return Resolve-NovaSecretValue -SecretSources ([pscustomobject]@{
10+
ExplicitValue = $Token
11+
ExplicitEnvironmentVariableName = $TokenEnvironmentVariable
12+
ConfiguredEnvironmentVariableName = Get-NovaPackageSettingValue -InputObject $AuthSettings -Name 'TokenEnvironmentVariable'
13+
ConfiguredValue = Get-NovaPackageSettingValue -InputObject $AuthSettings -Name 'Token'
14+
})
2215
}
2316

src/private/release/GetLocalModulePathEntryList.ps1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
function Get-LocalModulePathEntryList {
22
$separator = [IO.Path]::PathSeparator
3+
$modulePath = Get-NovaEnvironmentVariableValue -Name 'PSModulePath'
4+
5+
if ( [string]::IsNullOrWhiteSpace($modulePath)) {
6+
return @()
7+
}
38

49
return @(
5-
$env:PSModulePath -split $separator |
10+
$modulePath -split $separator |
611
ForEach-Object {$_.Trim()} |
712
Where-Object {-not [string]::IsNullOrWhiteSpace($_)} |
813
Select-Object -Unique

src/private/release/PublishNovaBuiltModuleToRepository.ps1

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
function Get-NovaPublishRepositoryDefaultApiKeyEnvironmentVariable {
2+
[CmdletBinding()]
3+
param(
4+
[Parameter(Mandatory)][string]$Repository
5+
)
6+
7+
if ( $Repository.Equals('PSGallery', [System.StringComparison]::OrdinalIgnoreCase)) {
8+
return 'PSGALLERY_API'
9+
}
10+
11+
return $null
12+
}
13+
114
function Publish-NovaBuiltModuleToRepository {
215
[CmdletBinding(SupportsShouldProcess = $true)]
316
param(
@@ -10,10 +23,10 @@ function Publish-NovaBuiltModuleToRepository {
1023
[string]$ApiKey
1124
)
1225

13-
$resolvedApiKey = $ApiKey
14-
if ($Repository -eq 'PSGallery' -and [string]::IsNullOrWhiteSpace($resolvedApiKey)) {
15-
$resolvedApiKey = $env:PSGALLERY_API
16-
}
26+
$resolvedApiKey = Resolve-NovaSecretValue -SecretSources ([pscustomobject]@{
27+
ExplicitValue = $ApiKey
28+
DefaultEnvironmentVariableName = Get-NovaPublishRepositoryDefaultApiKeyEnvironmentVariable -Repository $Repository
29+
})
1730

1831
$publishParams = @{
1932
Path = $ProjectInfo.OutputModuleDir

0 commit comments

Comments
 (0)