08 · Publishing Modules (PowerShell Gallery)¶
A module that only ever lives in one repo's Import-Module -Force cycle
hasn't really shipped. Publishing to the PowerShell Gallery — the
Install-Module/Find-Module ecosystem every PowerShell user already
has access to — means anyone can Install-Module YourModule the same way
they'd install Pester or Az. This module covers getting a module
ready for that, and what actually happens on publish (without needing a
real Gallery API key to follow along).
A complete manifest, generated properly¶
New-ModuleManifest -Path './GreetingKit/GreetingKit.psd1' `
-RootModule 'GreetingKit.psm1' `
-ModuleVersion '1.0.0' `
-Author 'Mastery Path' `
-Description 'A tiny demo module for the PowerShell Gallery publishing lesson.' `
-FunctionsToExport 'Get-Greeting' `
-Tags 'demo', 'greeting' `
-ProjectUri 'https://example.com/greetingkit' `
-LicenseUri 'https://example.com/license'
New-ModuleManifest is worth using over hand-writing a .psd1 from
scratch — it fills in every field the Gallery actually reads
(Tags, ProjectUri, LicenseUri, ReleaseNotes) with correct syntax,
and Gallery search/discoverability depends on those being present and
accurate. Tags in particular is how Find-Module -Tag 'demo' finds your
module at all.
ModuleType Version PreRelease Name ExportedCommands
---------- ------- ---------- ---- ----------------
Script 1.0.0 GreetingKit Get-Greeting
Test-ModuleManifest parses the file the same way Import-Module and
Publish-Module will — running it before publishing catches a malformed
manifest immediately instead of discovering the problem after a failed
(or worse, partially succeeded) publish.
Linting before publishing: PSScriptAnalyzer¶
RuleName Severity ScriptName Line Message
-------- -------- ---------- ---- -------
PSUseToExportFieldsInManifest Warning GreetingKit.psd1 75 Do not use wildcard
or $null in this
field. Explicitly
specify a list for
CmdletsToExport.
PSUseToExportFieldsInManifest Warning GreetingKit.psd1 81 ...for AliasesToExport.
PSProvideCommentHelp Information GreetingKit.psm1 1 The cmdlet
'Get-Greeting' does
not have a help
comment.
Real findings against a genuinely minimal module: New-ModuleManifest's
own defaults leave CmdletsToExport/AliasesToExport as '*', which
PSScriptAnalyzer correctly flags — an explicit empty array (@()) or a
real list is faster for Import-Module to resolve and clearer about
intent. The PSProvideCommentHelp finding is a nudge, not an error:
adding <# .SYNOPSIS ... #> comment-based help (module 01, Level 3)
means Get-Help Get-Greeting works for anyone who installs it, which
matters far more once "anyone" is a real Gallery audience rather than
just your own team. Run Invoke-ScriptAnalyzer -Recurse as a required
step before every publish — most published-module quality problems are
things this catches automatically.
What Publish-Module actually does¶
This isn't run here — it needs a real PowerShell Gallery account and API
key, and would genuinely publish a package to a public registry, which
this course won't do without you deciding to. What it does, so the step
isn't a mystery: it packages the module folder as a NuGet package,
uploads it using your API key (obtained from your Gallery account
settings, never hardcoded — the same SecureString/environment-variable
discipline from Level 3's security module applies directly here), and the
version in .psd1's ModuleVersion becomes the published version. A
version number can never be republished or overwritten — every publish
needs to bump ModuleVersion first, even for a one-line fix.
Versioning: semantic versioning matters here specifically¶
Format is Major.Minor.Patch. Increment:
- Patch (1.2.3 → 1.2.4) for a bug fix, no behavior change to
callers
- Minor (1.2.3 → 1.3.0) for a new function/parameter that doesn't
break existing callers
- Major (1.2.3 → 2.0.0) for anything that could break someone
already depending on the current behavior — a renamed parameter,
changed return type, removed function
Anyone who ran Install-Module YourModule is trusting that a minor/patch
bump won't break their script; violating that (a breaking change without
a major version bump) is the fastest way to lose a Gallery module's
users' trust.
#Requires and dependency declarations¶
At the top of a script or module file, #Requires makes PowerShell
refuse to even attempt to run the file if the version/dependency isn't
met, with a clear error naming exactly what's missing — far better than
letting the script run partway and fail confusingly on the first call
that needed the missing piece. For a module with real dependencies,
declare them in the manifest's RequiredModules too, so
Install-Module YourModule pulls them in automatically.
The trap: publishing with local/test artifacts still in the folder¶
A module folder that also contains ./Tests/, a ./bin/ build output
directory, or a stray .vscode/ config gets packaged and published
alongside the actual module — harmless usually, but it bloats the
package and can occasionally leak something not meant to ship (a local
test fixture with fake-but-realistic-looking credentials, for instance).
Publish-Module -Path publishes everything under that path; keep the
publish path scoped to exactly the module's real content, or use a
.gitignore-style exclude/staging step that copies only what should ship
into a clean folder before calling Publish-Module on that.
Cheat sheet¶
| Step | Tool |
|---|---|
| Generate a correct manifest | New-ModuleManifest |
| Validate it parses correctly | Test-ModuleManifest |
| Lint before publishing | Invoke-ScriptAnalyzer -Recurse |
| Publish | Publish-Module -Path ... -NuGetApiKey ... |
| Bump version before every publish | ModuleVersion in .psd1, semantic versioning |
| Declare PowerShell/module dependencies | #Requires, RequiredModules in manifest |
| Keep the publish folder clean | scope -Path to only real module content |
How It Actually Works¶
Publish-Module doesn't upload your working directory as-is — it first
runs the same manifest validation Test-ModuleManifest performs (parsing
the .psd1 in restricted language mode, as covered in Module 08's
manifest mechanics), then packages the module folder into a NuGet
package (.nupkg, a renamed ZIP with a NuGet-specific manifest
generated from your .psd1 fields), because the PowerShell Gallery is
built on NuGet's hosting/versioning infrastructure under the hood —
Find-Module/Install-Module are PowerShell-shaped clients over what is
functionally a NuGet feed, which is why module versions follow strict
semantic-version parsing rules (NuGet's own NuGetVersion type) and why
a malformed version string in your manifest fails publishing with a NuGet-
level error rather than a PowerShell-specific one.
Because the entire module folder becomes the package payload, "local/test
artifacts still in the folder" (a bin/Debug build output, a stray
.git directory, local test-data fixtures) get packaged and published
verbatim — there's no build step that prunes non-essential files unless
you explicitly maintain a separate build/staging folder and publish
that, which is exactly why real-world module projects keep source
layout separate from the folder actually passed to Publish-Module.
Version immutability on the Gallery (you can't overwrite 1.2.0 once
published, only publish a new version) mirrors NuGet's own immutable-
package-version guarantee — this exists specifically so that anyone who
already resolved a dependency against 1.2.0 keeps getting the exact
bytes they tested against, which is the same reasoning behind
RequiredVersion/ModuleVersion semantics in RequiredModules from
Module 08: the whole dependency-resolution model assumes a given
version string always refers to identical, unchanging content.
Exercise¶
Take the AdminToolkit module from Level 3's project. Generate its
manifest properly with New-ModuleManifest (rather than the hand-written
one from that lesson), run Invoke-ScriptAnalyzer -Recurse against it and
fix every Warning-or-higher finding, add comment-based help to each
public function to resolve the PSProvideCommentHelp findings, and bump
its ModuleVersion to 1.1.0 to reflect the new documentation as a
minor, non-breaking addition.