Request-InfisicalCertificate + PKI lifecycle, MAML help for all 39 cmdlets, chain-store routing fix

Cmdlets added: Request-InfisicalCertificate, Get-InfisicalCertificate, Get-InfisicalCertificates. Request supports BySubscriber/ByCa parameter sets, BouncyCastle CSR generation (RSA/ECDSA/Ed25519), local-key generation, -Install/-InstallChain (chain certs routed to Root vs CertificateAuthority by self-signed status), idempotency reuse with -AllowRenewal/-RenewalThresholdDays, local chain reconstruction with -LocalChainOnly opt-out, Infisical bundle fallback when local stores are incomplete, and private-key protection modes (Exportable/LocalOnly/NonExportable/Ephemeral) via -PrivateKeyProtection plus -PersistKey/-MachineKey/-PrivateKeyPath.

Install-InfisicalCertificate fix: chain certs were previously dumped into CertificateAuthority unconditionally. They are now routed by Subject==Issuer (self-signed -> Root, otherwise -> CertificateAuthority), matching Request-InfisicalCertificate. Routing centralized in InfisicalCertificateRequestHelpers.GetChainCertificateTargetStore and a new InstallChain(IEnumerable<X509Certificate2>,...) overload.

Help: authored Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml covering all 39 cmdlets (synopsis, description, notes, two examples per cmdlet: one-liner + OrderedDictionary splat with preceding Get- resolvers for IDs/slugs).

Build pipeline: build.ps1 stages the help XML into bin/<culture>/ next to the DLL during publish (hard-fails if missing or has zero <command:command> entries). Test-ModuleImports now enumerates every exported cmdlet via Get-Command, cross-checks against expected names, and asserts non-empty synopsis (rejecting auto-generated cmdlet-name fallback), non-empty description, and at least one example with a non-empty <dev:code> block.

Tests: 230/230 passing (up from 190).
This commit is contained in:
GraceSolutions
2026-06-04 14:26:40 -04:00
parent 19615363e3
commit 51bf819c37
28 changed files with 5192 additions and 44 deletions
+74 -7
View File
@@ -129,7 +129,10 @@ function Write-Manifest {
'Update-InfisicalTag',
'Remove-InfisicalTag',
'Get-InfisicalCertificateAuthority',
'Get-InfisicalCertificate',
'Get-InfisicalCertificates',
'Search-InfisicalCertificate',
'Request-InfisicalCertificate',
'ConvertTo-InfisicalCertificate',
'Install-InfisicalCertificate',
'Uninstall-InfisicalCertificate',
@@ -193,15 +196,50 @@ if (`$null -eq `$manifest) {
Import-Module -Name '$($ModuleDirectory.FullName)' -Force
`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Search-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate')
foreach (`$c in `$cmds) {
if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$c"
`$cmds = @(Get-Command -Module PSInfisicalAPI -CommandType Cmdlet)
if (`$cmds.Count -eq 0) {
throw "No cmdlets were exported by the PSInfisicalAPI module."
}
`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalCertificate','Get-InfisicalCertificates','Search-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate')
foreach (`$expected in `$expectedCmds) {
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$expected"
}
}
foreach (`$cmd in `$cmds) {
`$name = `$cmd.Name
`$help = Get-Help -Name `$name -Full -ErrorAction SilentlyContinue
if (`$null -eq `$help) {
throw "Get-Help returned nothing for cmdlet: `$name"
}
`$help = Get-Help -Name `$c -ErrorAction SilentlyContinue
if (`$null -eq `$help) {
throw "Get-Help returned nothing for cmdlet: `$c"
`$synopsis = (`$help.Synopsis | Out-String).Trim()
if ([string]::IsNullOrWhiteSpace(`$synopsis) -or `$synopsis.StartsWith(`$name, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Get-Help synopsis is missing or auto-generated for cmdlet: `$name"
}
`$description = (`$help.description | Out-String).Trim()
if ([string]::IsNullOrWhiteSpace(`$description)) {
throw "Get-Help description is empty for cmdlet: `$name"
}
`$examples = Get-Help -Name `$name -Examples -ErrorAction SilentlyContinue
if (`$null -eq `$examples -or `$null -eq `$examples.examples -or `$null -eq `$examples.examples.example) {
throw "Get-Help -Examples returned no examples for cmdlet: `$name"
}
`$exampleNodes = @(`$examples.examples.example)
if (`$exampleNodes.Count -lt 1) {
throw "Get-Help -Examples returned zero examples for cmdlet: `$name"
}
foreach (`$example in `$exampleNodes) {
`$code = (`$example.code | Out-String).Trim()
if ([string]::IsNullOrWhiteSpace(`$code)) {
throw "Example with empty code block found for cmdlet: `$name"
}
}
}
@@ -297,6 +335,35 @@ foreach ($assembly in $desiredAssemblies) {
}
}
Write-Step "Staging cmdlet help XML next to module binary"
$moduleCultureDirs = Get-ChildItem -LiteralPath $ModuleRoot.FullName -Directory -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '^[a-z]{2}(-[A-Za-z0-9]+)*$' }
foreach ($cultureDir in $moduleCultureDirs) {
$helpXmlSource = [System.IO.FileInfo][System.IO.Path]::Combine($cultureDir.FullName, 'PSInfisicalAPI.dll-Help.xml')
if (-not $helpXmlSource.Exists) { continue }
$binCultureDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ModuleBinDir.FullName, $cultureDir.Name)
Ensure-Directory -Directory $binCultureDir
Copy-Item -LiteralPath $helpXmlSource.FullName -Destination $binCultureDir.FullName -Force
}
$primaryHelpXml = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleBinDir.FullName, 'en-US', 'PSInfisicalAPI.dll-Help.xml')
if (-not $primaryHelpXml.Exists) {
throw "Help XML not found at '$($primaryHelpXml.FullName)'. Ensure Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml exists."
}
try {
[xml]$helpDocument = Get-Content -LiteralPath $primaryHelpXml.FullName -Raw
} catch {
throw "Help XML at '$($primaryHelpXml.FullName)' failed to parse as XML: $_"
}
$helpCommandCount = @($helpDocument.helpItems.command).Count
if ($helpCommandCount -lt 1) {
throw "Help XML at '$($primaryHelpXml.FullName)' contains no <command:command> entries."
}
Write-Step "Help XML contains $helpCommandCount cmdlet entries."
$manifestPath = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleRoot.FullName, 'PSInfisicalAPI.psd1')
Write-Manifest -Path $manifestPath -ModuleVersion $buildVersion -CommitHash $commitHash