The subscriber guidance shipped in #18 was wrong for fleet enrollment. signSubscriberCert rejects any CSR whose CN differs from the subscriber's commonName, and allowlists the subscriber's subjectAlternativeNames, so a subscriber is a single named identity rather than a template. Enrolling N machines through subscribers would require N subscribers. Certificate profiles are the correct path: they accept a per-request common name constrained by policy allowed/required/denied lists, and profile issuance is the only path that skips the CA direct-issuance gate (!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate), so a profile issues against a CA whose EnableDirectIssuance is False. Also documents that enableDirectIssuance cannot be changed after CA creation: it appears in no Infisical create or update schema (the generic CA schemas accept only name and status). Migration 20250521110635_add-external-ca-pki.ts renamed requireTemplateForIssuance to enableDirectIssuance and inverted every existing value, so CAs that previously required a template now read False permanently. The remedies are a profile, or a new CA (column defaults to true). README end-to-end example switched from subscriber to profile issuance, and the issuance-path table now leads with whether the common name varies per request. Cmdlet help for Request-InfisicalCertificate and Get-InfisicalCertificateAuthority updated to match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PSInfisicalAPI
A C# binary PowerShell module for interacting with the Infisical REST API. It provides cmdlets for authentication, secret retrieval, structured export, and includes automatic environment-variable discovery so connections can be established with little or no inline configuration.
- License: AGPL-3.0
- Author: Grace Solutions
- Target framework: .NET Standard 2.0 (compatible with Windows PowerShell 5.1 and PowerShell 7+)
Installation
From the PowerShell Gallery
Install-Module -Name PSInfisicalAPI -Scope CurrentUser
Import-Module -Name PSInfisicalAPI
From source
git clone https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI.git
cd PSInfisicalAPI
pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests
Import-Module -Name .\Module\PSInfisicalAPI
Cmdlets
The module exports 51 cmdlets. Discovery cmdlets (Get-Infisical*) use a List (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.
Session
| Cmdlet | Purpose |
|---|---|
Connect-Infisical |
Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. |
Disconnect-Infisical |
Clears the current Infisical session from the module-level session manager. |
Secrets
| Cmdlet | Purpose |
|---|---|
Get-InfisicalSecret |
Lists or retrieves Infisical secrets within a project, environment, and optional folder path. |
New-InfisicalSecret |
Creates a new Infisical secret, with support for SecureString values and bulk creation. |
Update-InfisicalSecret |
Updates an existing Infisical secret value, comment, name, or tags. |
Remove-InfisicalSecret |
Deletes one or many Infisical secrets by name. |
Copy-InfisicalSecret |
Duplicates one or more secrets into a different environment or secret path. |
ConvertTo-InfisicalSecretDictionary |
Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. |
Export-InfisicalSecrets |
Exports InfisicalSecret objects to disk or environment variables in a chosen file format. |
Organizations
| Cmdlet | Purpose |
|---|---|
Get-InfisicalOrganization |
Lists or retrieves Infisical organizations accessible to the current identity. |
New-InfisicalOrganization |
Creates a new Infisical organization. |
Update-InfisicalOrganization |
Updates the name or slug of an existing Infisical organization. |
Remove-InfisicalOrganization |
Deletes an Infisical organization. |
Sub-Organizations
| Cmdlet | Purpose |
|---|---|
Get-InfisicalSubOrganization |
Lists or retrieves Infisical sub-organizations, with optional search, paging, and ordering filters. |
New-InfisicalSubOrganization |
Creates a new Infisical sub-organization. |
Update-InfisicalSubOrganization |
Updates the name or slug of an existing Infisical sub-organization. |
Remove-InfisicalSubOrganization |
Deletes an Infisical sub-organization. |
Projects
| Cmdlet | Purpose |
|---|---|
Get-InfisicalProject |
Lists or retrieves Infisical projects accessible to the current identity. |
New-InfisicalProject |
Creates a new Infisical project in the active organization. |
Update-InfisicalProject |
Updates the name, description, or auto-capitalization flag on an existing project. |
Remove-InfisicalProject |
Deletes an Infisical project. |
Environments
| Cmdlet | Purpose |
|---|---|
Get-InfisicalEnvironment |
Lists or retrieves Infisical environments defined on a project. |
New-InfisicalEnvironment |
Creates a new environment on an Infisical project. |
Update-InfisicalEnvironment |
Updates the name, slug, or sort order of an existing Infisical environment. |
Remove-InfisicalEnvironment |
Deletes an Infisical environment from a project. |
Folders
| Cmdlet | Purpose |
|---|---|
Get-InfisicalFolder |
Lists or retrieves Infisical folders at a given secret path. |
New-InfisicalFolder |
Creates a new Infisical folder under the supplied parent path. |
Update-InfisicalFolder |
Renames an existing Infisical folder. |
Remove-InfisicalFolder |
Deletes an Infisical folder and all secrets it contains. |
Tags
| Cmdlet | Purpose |
|---|---|
Get-InfisicalTag |
Lists or retrieves Infisical tags defined on a project. |
New-InfisicalTag |
Creates a new Infisical tag on a project. |
Update-InfisicalTag |
Updates the slug, name, or color of an existing Infisical tag. |
Remove-InfisicalTag |
Deletes an Infisical tag from a project. |
PKI
| Cmdlet | Purpose |
|---|---|
Get-InfisicalCertificateAuthority |
Lists or retrieves Infisical internal Certificate Authorities. |
Get-InfisicalPkiSubscriber |
Lists or retrieves Infisical PKI subscribers in a project. |
Get-InfisicalCertificate |
Lists or retrieves Infisical certificates in a project, with optional filters and automatic paging. |
Request-InfisicalCertificate |
Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. |
ConvertTo-InfisicalCertificate |
Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. |
Install-InfisicalCertificate |
Installs an Infisical certificate (and optional chain) into a Windows certificate store. |
Uninstall-InfisicalCertificate |
Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. |
Export-InfisicalCertificate |
Exports an Infisical certificate to disk in PEM, PFX, or CER format. |
Get-InfisicalScepMdmProfile |
Projects an Infisical certificate profile into a Windows SCEP MDM profile model. |
Export-InfisicalScepMdmProfile |
Writes a SCEP MDM profile to disk as a SyncML payload suitable for MDM delivery. |
Write-InfisicalScepMdmProfileToWmi |
Submits a SCEP MDM profile to the local MDM Bridge WMI provider to trigger enrollment. |
Get-InfisicalSANList |
Builds a SAN candidate list (device name, <device>.<suffix> per adapter DNS suffix, RFC 1918 + CGNAT IPv4 addresses, IPv4/IPv6 loopback) for Request-InfisicalCertificate -DnsName. |
Process
| Cmdlet | Purpose |
|---|---|
Start-InfisicalProcess |
Launches a child process with Infisical secrets injected directly into its environment block, capturing stdout/stderr and validating the exit code. |
Use Get-Help <Cmdlet> -Full for parameter details and Get-Help about_PSInfisicalAPI for the module overview.
Quick start
$secureSecret = Read-Host -AsSecureString 'Client Secret'
$connection = Connect-Infisical `
-BaseUri 'https://app.infisical.com' `
-OrganizationId '00000000-0000-0000-0000-000000000000' `
-ProjectId '11111111-1111-1111-1111-111111111111' `
-Environment 'dev' `
-ClientId 'machine-identity-client-id' `
-ClientSecret $secureSecret `
-PassThru
Get-InfisicalSecret -SecretPath '/'
Disconnect-Infisical
End-to-end: request and install a chained certificate
Connects, selects a cert-manager project, sources SANs from Get-InfisicalSANList, requests a certificate through a certificate profile, installs it (and its chain) into the current-user store, and disconnects. Each call uses a splatted OrderedDictionary constructed with OrdinalIgnoreCase so parameter names round-trip case-insensitively.
This is the shape to use for fleet enrollment, where each machine needs its own common name. See Choosing an issuance path — a PKI subscriber is not the right tool for this, because it pins one fixed common name.
$ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com'
$ConnectInfisicalParameters.OrganizationId = '00000000-0000-0000-0000-000000000000'
$ConnectInfisicalParameters.ClientId = 'machine-identity-client-id'
$ConnectInfisicalParameters.ClientSecret = ConvertTo-SecureString -String 'ClientSecret' -AsPlainText -Force
$ConnectInfisicalParameters.PassThru = $True
$ConnectInfisicalParameters.Verbose = $True
$Connection = Connect-Infisical @ConnectInfisicalParameters
$Project = Get-InfisicalProject -Type cert-manager | Where-Object {($_.Name -eq 'Platform')} | Select-Object -First 1
$Profile = Get-InfisicalCertificateProfile -ProjectId ($Project.Id) | Select-Object -First 1
$SanList = Get-InfisicalSANList
$RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$RequestInfisicalCertificateParameters.ProjectId = $Project.Id
$RequestInfisicalCertificateParameters.CertificateProfileId = $Profile.Id
$RequestInfisicalCertificateParameters.CommonName = $Env:ComputerName.ToUpper()
$RequestInfisicalCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$RequestInfisicalCertificateParameters.DnsName.AddRange($SanList)
$RequestInfisicalCertificateParameters.DnsName.Add('myrecord.mydomain.com')
$RequestInfisicalCertificateParameters.Ttl = '90d'
$RequestInfisicalCertificateParameters.Install = $True
$RequestInfisicalCertificateParameters.InstallChain = $True
$RequestInfisicalCertificateParameters.Verbose = $True
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
$Null = Disconnect-Infisical -Verbose
Choosing an issuance path
Request-InfisicalCertificate has three mutually exclusive issuance parameter sets. The deciding question is whether the common name varies per request:
| Parameter | Common name | Use when |
|---|---|---|
-CertificateProfileId |
Per request, constrained by policy | Fleet enrollment — many machines, each with its own CN. Works on any CA. |
-CertificateAuthorityId |
Per request, unconstrained | Fleet enrollment where no policy is wanted. Needs direct issuance on the CA. |
-PkiSubscriberSlug |
Fixed by the subscriber record | One named identity — a specific service or host, provisioned in advance. |
A PKI subscriber is a per-identity object, not a fleet template. signSubscriberCert rejects any CSR whose CN differs from the subscriber's:
Common name (CN) in the CSR does not match the subscriber's common name
It also allowlists SANs — every dNSName/email SAN in the CSR must appear in the subscriber's subjectAlternativeNames — and requires CSR key usages to be a subset of the subscriber's. (IP SANs are not covered by that check.) Enrolling N machines through subscribers therefore means creating N subscribers. Prefer a profile.
There is no -CertificateTemplateId parameter. Infisical's REST API exposes no template-based issuance route — templates are consumed internally by EST and subscribers — so when the API says "Certificate template or subscriber is required for issuance", the reachable answers are a profile, a subscriber, or direct issuance.
The cmdlet resolves and reports the issuer before generating a keypair, so -Verbose tells you exactly what will sign the request:
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Issuing via certificate profile 'a1b2c3d4-...' in project '2122628e-...'.
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Issuing directly via certificate authority 'intermediate-ca' (bf661d78-...); direct issuance is enabled.
-WhatIf names the same issuer without issuing anything:
Request-InfisicalCertificate @RequestInfisicalCertificateParameters -WhatIf
# What if: Performing the operation "Request new certificate" on target
# "certificate profile 'a1b2c3d4-...' for CN=WEB01".
Discovering profiles (recommended)
A certificate profile binds a CA to a certificate policy. The policy constrains the subject, key usages, and extended key usages with allowed/required/denied lists, so the common name still varies per request while staying inside guardrails.
Get-InfisicalCertificateProfile -ProjectId ($Project.Id) | Format-Table Id, Name, CaId
Get-InfisicalCertificatePolicy -ProjectId ($Project.Id) | Format-Table Id, Name
Profile issuance is the only path that does not consult the CA's direct-issuance flag — the service short-circuits it:
if (!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) { throw ... }
So a profile issues successfully against a CA whose EnableDirectIssuance is False. If a project has no profiles, create a policy then a profile under Certificate Management in the Infisical UI.
Discovering subscribers
Get-InfisicalPkiSubscriber -ProjectId ($Project.Id) |
Format-Table Name, CommonName, Status, Ttl, CaId
Pass the subscriber's Name to -PkiSubscriberSlug, and set -CommonName to exactly that subscriber's CommonName. Because the subscriber owns the lifetime and usage policy, -Ttl, -KeyUsage, and -ExtendedKeyUsage are not accepted on this parameter set — set them on the subscriber in Infisical instead.
An empty result means the project has no subscribers; create one under Certificate Management > Subscribers. This module is read-only for subscribers, so creation is UI or raw API (POST /api/v1/pki/subscribers).
Direct issuance on a CA
Direct issuance lets a CA sign a bare CSR with no profile, subscriber, or template in front of it. When it is off, Infisical rejects the request with 400 Certificate template or subscriber is required for issuance; this module catches that before building a CSR.
There is no UI toggle or API field for this.
enableDirectIssuanceappears in no create or update schema — the generic CA schemas accept onlynameandstatus. It is set at CA creation (the column defaults totrue) and is not editable afterwards through the public API.
A CA can therefore read False for a reason that is not obvious. Migration 20250521110635_add-external-ca-pki.ts renamed the older requireTemplateForIssuance column to enableDirectIssuance and inverted every existing value:
t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance");
...
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });
Any CA created before that migration with "require template for issuance" enabled now reads EnableDirectIssuance = False permanently. The options are to use a profile (which ignores the flag), or to create a new CA — new CAs default to true.
Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) -Kind Internal |
Format-Table Name, CommonName, Status, EnableDirectIssuance
$Ca = Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) -Kind Internal |
Where-Object {($_.EnableDirectIssuance -eq $True)} |
Select-Object -First 1
$RequestInfisicalCertificateParameters.CertificateAuthorityId = $Ca.Id
$RequestInfisicalCertificateParameters.Ttl = '90d' # required by the CA path
Subject and SAN handling
-CommonNametakes the bare value (WEB01.contoso.com), not an RDN.CN=WEB01is accepted and normalized, since the CSR builder adds theCN=prefix itself.Get-InfisicalSANListreturns DNS names and IP addresses in one list. Passing the whole list to-DnsNameis fine: IP literals are detected and emitted asiPAddressSAN entries rather than malformeddNSNameentries.-Ttl(or-NotAfter) applies to the-CertificateAuthorityIdand-CertificateProfileIdpaths. Subscriber-issued certificates take their lifetime from the subscriber definition.
Diagnostics and error handling
Every cmdlet derives from PSCmdlet, so the full set of common parameters is bound: -Verbose, -Debug, -ErrorAction, -ErrorVariable, -WarningAction, -WarningVariable, -InformationAction, -InformationVariable, -OutVariable, -PipelineVariable, and -WhatIf/-Confirm on the cmdlets that declare SupportsShouldProcess.
Output is routed by stream so those parameters mean what they say:
| Stream | Carries | Controlled by |
|---|---|---|
| Error | The failure itself, once, as a non-terminating ErrorRecord |
-ErrorAction, -ErrorVariable, 2> |
| Warning | Genuine advisories that are not failures (e.g. issuance returned no certificate) | -WarningAction, -WarningVariable |
| Verbose | Request/response trace and the diagnostic trail leading up to a failure | -Verbose |
| Debug | Low-level detail | -Debug |
A failed call surfaces exactly one error. The [Error]-tagged diagnostic lines that precede it are on the verbose stream, so they appear only under -Verbose and never compete with the ErrorRecord:
# One error, no warning noise.
Request-InfisicalCertificate @Parameters -ErrorVariable Failure -ErrorAction SilentlyContinue
# The ErrorRecord carries the API detail; no log scraping required.
$Failure[0].Exception.StatusCode # 400
$Failure[0].Exception.ApiErrorCode # BadRequest
$Failure[0].Exception.ApiErrorMessage # Certificate template or subscriber is required for issuance
$Failure[0].Exception.ApiRequestId # req-SSPFN1gc2zHvkV
-ErrorAction decides the outcome
Operation failures are reported as non-terminating errors, so -ErrorAction (or $ErrorActionPreference) governs what happens, exactly as it does for built-in cmdlets:
-ErrorAction |
Behavior |
|---|---|
Continue (default) |
Error is written; a pipeline keeps processing its remaining input |
SilentlyContinue |
Nothing is printed; the error is still in $Error and -ErrorVariable |
Ignore |
Nothing is printed and nothing is recorded in $Error |
Stop |
Promoted to a terminating error that try/catch catches |
Inquire |
Prompts |
A failing item does not abort the batch:
'web01', 'does-not-exist', 'web02' |
ForEach-Object { Get-InfisicalPkiSubscriber -ProjectId $ProjectId -Name $_ -ErrorAction SilentlyContinue }
# emits web01 and web02; the failure is available in $Error
To catch failures you must ask for it with -ErrorAction Stop or $ErrorActionPreference = 'Stop':
try {
$Certificate = Request-InfisicalCertificate @Parameters -ErrorAction Stop
} catch [PSInfisicalAPI.Errors.InfisicalApiException] {
Write-Warning "Issuance failed with HTTP $($_.Exception.StatusCode): $($_.Exception.ApiErrorMessage)"
}
Breaking change. Failures were previously terminating, so
try/catchcaught them without-ErrorAction Stop. Existingtry/catchblocks need-ErrorAction Stopadded (or$ErrorActionPreference = 'Stop'set) to keep catching.
Exception types are InfisicalApiException, InfisicalAuthenticationException, InfisicalHttpException, InfisicalSerializationException, InfisicalConfigurationException, InfisicalExportException, and InfisicalImportException, all deriving from InfisicalException.
Automatic environment-variable discovery
When Connect-Infisical is invoked with one or more parameters missing (or set to whitespace/empty), the cmdlet searches environment variables and uses the first value it finds. This makes invocation as simple as Connect-Infisical when variables are set up in advance.
Scope precedence
Scopes are searched in order; the first matching variable with a non-blank value wins:
ProcessUserMachine
Patterns
The resolver matches case-insensitively against patterns aligned with Infisical's CLI defaults plus common variants such as CLOUDINIT_INFISICAL_* and custom-prefixed names (e.g., myapp_infisical_client_id).
| Parameter | Example variable names matched |
|---|---|
BaseUri |
INFISICAL_API_URL, INFISICAL_BASE_URL, INFISICAL_HOST |
OrganizationId |
INFISICAL_ORG_ID, INFISICAL_ORGANIZATION_ID |
ProjectId |
INFISICAL_PROJECT_ID, INFISICAL_WORKSPACE_ID |
Environment |
INFISICAL_ENVIRONMENT, INFISICAL_ENV, INFISICAL_ENV_SLUG |
ClientId |
INFISICAL_CLIENT_ID, INFISICAL_UNIVERSAL_AUTH_CLIENT_ID |
ClientSecret |
INFISICAL_CLIENT_SECRET, INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET |
AccessToken |
INFISICAL_TOKEN, INFISICAL_ACCESS_TOKEN, INFISICAL_AUTH_TOKEN |
SecretPath |
INFISICAL_SECRET_PATH, INFISICAL_DEFAULT_SECRET_PATH |
ApiVersion |
INFISICAL_API_VERSION |
Sensitive values (ClientSecret, AccessToken) are read directly into a read-only SecureString and never logged.
Zero-configuration example
[Environment]::SetEnvironmentVariable('INFISICAL_API_URL', 'https://app.infisical.com', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_ORG_ID', '00000000-0000-0000-0000-000000000000', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_PROJECT_ID', '11111111-1111-1111-1111-111111111111', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_ENVIRONMENT', 'dev', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_ID', 'machine-identity-client-id', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_SECRET', 'super-secret-value', 'User')
Connect-Infisical
Get-InfisicalSecret
Mixed example (explicit values override discovery)
Explicit parameters always win over discovered values; blank/whitespace explicit values trigger discovery.
Connect-Infisical -Environment 'prod' # everything else discovered from environment
Logging
The resolver emits a single verbose line announcing the scan and one informational line per discovered variable (variable name and scope; values are never logged). Use -Verbose to see the scan announcement.
Building
pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests
The script builds the binary, runs unit tests, publishes binaries into Module/PSInfisicalAPI/bin/, regenerates the manifest, and validates that the module imports.
Extending the module
Adding a new API endpoint
All HTTP routes live in two files under src/PSInfisicalAPI/Endpoints/:
InfisicalEndpointNames.csdeclares aconst stringidentifier for each endpoint.InfisicalEndpointRegistry.csmaps each identifier to one or moreInfisicalEndpointDefinitionrecords grouped by resource (RegisterAuthentication,RegisterSecrets,RegisterPki, etc.).
To add a route:
- Add a constant in
InfisicalEndpointNames.cs(e.g.,public const string ListPkiSubscribers = "ListPkiSubscribers";). - In the matching
Register<Resource>method, callAdd(map, new InfisicalEndpointDefinition { ... })withName,Resource,Version,Method,Template, and theRequiresAuthorization/ContainsSecretMaterialInRequest/ContainsSecretMaterialInResponseflags. Use{placeholder}tokens inTemplate; they are substituted from thepathParametersdictionary passed by the caller. - If the same logical operation has more than one upstream path (legacy + current), register both definitions under the same
Name—InvokeWithCandidateFallbacktries each in order until one succeeds. - Invoke the endpoint from the appropriate client (
InfisicalPkiClient,InfisicalSecretsClient, etc.) via_invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.XYZ, "XYZ", pathParameters, query, body).
Adding a new cmdlet
Cmdlets live in src/PSInfisicalAPI/Cmdlets/ and derive from InfisicalCmdletBase, which exposes HttpClient, Logger, ResolveProjectId, and ThrowTerminatingForException. Follow the consolidated discovery pattern when the cmdlet supports both list and single-record retrieval:
[Cmdlet(VerbsCommon.Get, "InfisicalPkiSubscriber", DefaultParameterSetName = "List")]
[OutputType(typeof(InfisicalPkiSubscriber))]
public sealed class GetInfisicalPkiSubscriberCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "ByName", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("SubscriberName", "Slug")]
public string Name { get; set; }
[Parameter] public string ProjectId { get; set; }
protected override void ProcessRecord() { /* dispatch on ParameterSetName */ }
}
After adding (or removing) a cmdlet:
-
Update
build.ps1in two places — theCmdletsToExportarray inside the generated manifest block, and the$expectedCmdsarray used byTest-ModuleImports. Both must list the same cmdlets; the build fails fast if they drift. -
Add a
<command:command>entry inModule/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml. Each entry must include a non-empty<maml:description>synopsis (do not let it start with the cmdlet name — the validation gate rejects PowerShell's auto-generated fallback), a non-empty<maml:description>body, and at least one<command:example>with a non-empty<dev:code>block. -
For consolidated
List/ single-record cmdlets, ship three examples: two straight-line invocations (one per parameter set) and oneOrderedDictionarysplat. The splat must construct the dictionary withOrdinalIgnoreCaseso parameter names round-trip case-insensitively:$Params = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) $Params.ProjectId = (Get-InfisicalProject | Select-Object -First 1).Id $Result = Get-InfisicalPkiSubscriber @Params -
Add a
## Unreleasedentry toCHANGELOG.mddescribing the change (mark removals of public cmdlets or parameters as BREAKING). -
Run
./build.ps1 -RunTests. The script enforces the cmdlet list, runs the xUnit suite, and verifies that every exported cmdlet has a valid synopsis, description, and at least one non-empty example.
Committing source and build artifacts in lockstep
The embedded BuildCommitHash in Module/PSInfisicalAPI/PSInfisicalAPI.psd1 and the bundled DLL is captured from git rev-parse HEAD at build time. To keep the embedded hash truthful, commit source and build artifacts as two ordered commits:
- Stage and commit your source changes first. Suppose this produces commit
S. - Run
./build.ps1 -RunTests -CommitArtifacts. The build picks upSasHEAD, embeds it asBuildCommitHash, then stages and commits only the build outputs (Module/PSInfisicalAPI/bin/**,Module/PSInfisicalAPI/PSInfisicalAPI.psd1, and theCHANGELOG.mdbuild-stamp insertion). The commit message referencesSso the binary commit always traces back to its source. git push.
-CommitArtifacts only touches the three artifact paths above; any other dirty files in your working tree are left alone. Use the older -CommitOnSuccess switch only when you intentionally want a single commit covering everything (git add -A + git commit -m "Build <version>"); the two switches are mutually exclusive.
Continuous integration
.gitea/workflows/publish-psgallery.yml publishes the module to the PowerShell Gallery whenever a pull request is merged into main. The workflow expects a repository secret named PSGALLERY_API_KEY containing a valid Gallery API key.
License
Distributed under the GNU Affero General Public License v3.0. See LICENSE.