When publishing a PowerShell module to the PowerShell Gallery via GitHub Actions, or cutting a module release, apply this domain knowledge.
PowerShell Gallery Publishing — Domain Knowledge
Tag-triggered publish workflow
Drive publishing off a release tag rather than every push, so a release is an explicit, auditable act. A workflow that triggers on a versioned tag (and optionally workflow_dispatch for manual re-runs):
on:
push:
tags:
- '<Module>-v*' # e.g. MyModule-v1.2.3
workflow_dispatch:
inputs:
module: { description: Module to publish, required: true, type: choice, options: [ ... ] }
jobs:
publish:
runs-on: windows-latest
environment: powershell-gallery # gate the secret behind a named environment
steps:
- uses: actions/checkout@v5
- name: Validate
shell: pwsh
run: ./build/Test-Modules.ps1
- name: Resolve module + verify tag matches manifest
shell: pwsh
env: { REF_NAME: "${{ github.ref_name }}" }
run: |
if ($env:REF_NAME -notmatch '^(?<module>[\w.]+)-v(?<version>\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$') { throw "Invalid tag: $env:REF_NAME" }
$manifest = Test-ModuleManifest "./modules/$($Matches.module)/$($Matches.module).psd1"
if ("$($manifest.Version)" -ne $Matches.version) { throw "Tag version $($Matches.version) != manifest $($manifest.Version)." }
- name: Publish
shell: pwsh
env: { PSGALLERY_API_KEY: "${{ secrets.PSGALLERY_API_KEY }}" }
run: Publish-PSResource -Path ./out/<Module> -Repository PSGallery -ApiKey $env:PSGALLERY_API_KEY
- Encode the module name AND version in the tag (
<Module>-vX.Y.Z) so one workflow serves a multi-module repo — parse the tag to pick which module to publish. - Fail the run if the tag version does not equal the manifest
ModuleVersion. This catches the classic "tagged v1.2.3 but forgot to bump the.psd1" mistake before a wrong-versioned package reaches the Gallery. - Gate the API key behind a GitHub Actions environment (
environment: powershell-gallery), not a bare repo secret — you get environment protection rules and a clear audit surface.
PowerShell Gallery API key
- Sign in at the Gallery, then avatar, then API Keys, then Create.
- Scope: Push new packages and package versions.
- Glob pattern: restrict the key to just your module family (e.g.
MyModule.*) so a leaked key cannot push arbitrary package names. - Store it as the environment secret the workflow reads (e.g.
PSGALLERY_API_KEY). With the GitHub CLI:gh secret set PSGALLERY_API_KEY --env powershell-gallery.
Cutting a release
- Bump the version in the module manifest (
.psd1ModuleVersion), the README version line, and the changelog. - Promote
## [Unreleased]notes into a dated/versioned section and leave an empty[Unreleased]behind (keeps a CI "changelog updated" gate happy). - Commit, then create and push the tag:
The tag push triggers the publish workflow, then validation, then the Gallery publish.git tag MyModule-v1.2.3 git push origin MyModule-v1.2.3
Gotcha — GitHub does NOT fire tag workflows when you push more than three tags at once
If a single git push delivers more than three tags, GitHub dispatches no push events for them, so tag-triggered workflows never run. This bites a multi-module repo that tags several modules and pushes them together (e.g. git push --tags after creating four *-vX.Y.Z tags) — the publish pipeline silently does nothing.
Fixes:
- Push tags one at a time (or in batches of three or fewer):
git push origin MyModule-v1.2.3per tag. - Or trigger the missed publishes manually via
workflow_dispatch. - Never rely on a bulk
git push --tagsto fire per-tag release automation.
Reserving a module name
- PowerShell Gallery names are first-come-first-served, and your first successful publish reserves the name to your account.
- Check availability before the first release with the Gallery OData API or
Find-PSResource -Name MyModule -Repository PSGallery(no result means available). - If a name matters, publish an initial
0.1.0to claim it rather than risk someone else taking it.