Skip to content

Build, Test, and Publish PowerShell Module #72

Build, Test, and Publish PowerShell Module

Build, Test, and Publish PowerShell Module #72

Workflow file for this run

name: Build, Test, and Publish PowerShell Module
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (skip publishing, tagging, and releasing)?'
required: false
default: 'true'
permissions:
contents: write # Required for git tag and release
jobs:
#####################
# 1. Build Job
#####################
build:
runs-on: windows-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Locate module manifest and prepare
id: build_module
shell: pwsh
run: |
$manifest = Get-ChildItem -Recurse -Include *.psd1 | Where-Object { $_.Name -notlike '*Tests*' } | Select-Object -First 1
if (-not $manifest) {
Write-Error "❌ Module manifest not found!"
exit 1
}
$moduleName = [IO.Path]::GetFileNameWithoutExtension($manifest.Name)
$version = (Import-PowerShellDataFile -Path $manifest.FullName).ModuleVersion.ToString()
"MODULE_NAME=$moduleName" | Out-File -FilePath $env:GITHUB_ENV -Append
"VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append
echo "moduleName=$moduleName" >> $GITHUB_OUTPUT
echo "version=$version" >> $GITHUB_OUTPUT
Write-Host "✅ Found module '$moduleName' version $version"
#####################
# 2. Test Job
#####################
test:
needs: build
runs-on: windows-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Pester Tests with Coverage (if any)
shell: pwsh
run: |
if (Test-Path -Path ./Tests -PathType Container) {
$testFiles = Get-ChildItem -Path ./Tests -Recurse -Include '*.Tests.ps1'
if ($testFiles.Count -gt 0) {
# Install latest Pester v5
Install-Module Pester -Force -SkipPublisherCheck -Scope CurrentUser
Import-Module Pester
# Collect code files excluding Tests folder
$codeFiles = Get-ChildItem -Path ./ -Recurse -Include '*.ps1' | Where-Object { $_.FullName -notmatch '\\Tests\\' }
# Create Pester configuration
$config = New-PesterConfiguration
$config.Run.Path = './Tests'
$config.Run.PassThru = $true
$config.Output.Verbosity = 'Detailed'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = $codeFiles
# Run Pester using only the configuration
$result = Invoke-Pester -Configuration $config
# Write test + coverage results to console
$result | Out-Default
# Show coverage percentage
$coverage = [math]::Round($result.CodeCoverage.Percentage, 2)
Write-Host "Code coverage Raw Data: $($result.CodeCoverage.Percentage)"
Write-Host "Code coverage: $coverage %"
# Optional: fail workflow if coverage < threshold
$threshold = 80
if ($coverage -lt $threshold) {
Write-Error "Code coverage below threshold ($threshold%)."
exit 1
}
# Fail workflow if any tests failed
if ($result.FailedCount -gt 0) {
Write-Error "One or more Pester tests failed."
exit 1
}
}
else {
Write-Host "No Pester test files found, skipping tests."
}
}
else {
Write-Host "Tests folder not found, skipping tests."
}
#####################
# 3. Publish Job
#####################
publish:
needs: test
runs-on: windows-latest
timeout-minutes: 10
if: success()
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Show dry run mode
run: |
echo "🧪 Dry run mode:"
echo "${{ github.event.inputs.dry_run || 'true' }}"
- name: Install PowerShellGet (if needed)
shell: pwsh
run: |
if (-not (Get-Module -ListAvailable -Name PowerShellGet)) {
Install-Module -Name PowerShellGet -Force -Scope CurrentUser -AllowClobber
}
- name: Prepare Module Files
id: prepare_publish
shell: pwsh
run: |
$manifest = Get-ChildItem -Recurse -Include *.psd1 | Where-Object { $_.Name -notlike '*Tests*' } | Select-Object -First 1
if (-not $manifest) {
Write-Error "❌ Module manifest not found!"
exit 1
}
$moduleName = [IO.Path]::GetFileNameWithoutExtension($manifest.Name)
$version = (Import-PowerShellDataFile -Path $manifest.FullName).ModuleVersion.ToString()
$publishDir = Join-Path -Path $env:RUNNER_TEMP -ChildPath "publish"
$publishModuleDir = Join-Path -Path $publishDir -ChildPath $moduleName
if (Test-Path $publishDir) { Remove-Item -Recurse -Force $publishDir }
New-Item -ItemType Directory -Path $publishModuleDir -Force | Out-Null
$includeExtensions = @('*.psd1', '*.psm1', '*.ps1')
foreach ($ext in $includeExtensions) {
Copy-Item -Path "$($manifest.Directory.FullName)\$ext" -Destination $publishModuleDir -Recurse -ErrorAction SilentlyContinue
}
"MODULE_NAME=$moduleName" | Out-File -FilePath $env:GITHUB_ENV -Append
"VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append
"PUBLISH_DIR=$publishModuleDir" | Out-File -FilePath $env:GITHUB_ENV -Append
Write-Host "✅ Module prepared for publish: $publishModuleDir"
- name: Publish PowerShell Module
if: ${{ (github.event.inputs.dry_run || 'true') == 'false' }}
shell: pwsh
env:
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
run: |
Publish-Module -Path "${{ env.PUBLISH_DIR }}" -NuGetApiKey $env:PSGALLERY_API_KEY
Write-Host "✅ Module published."
- name: Delete existing Git tag (if any)
if: ${{ (github.event.inputs.dry_run || 'true') == 'false' }}
run: |
git fetch --tags
git tag -d "v${{ env.VERSION }}" 2>$null || echo "No local tag"
git push origin :refs/tags/v${{ env.VERSION }} 2>$null || echo "No remote tag"
- name: Create Git tag
if: ${{ (github.event.inputs.dry_run || 'true') == 'false' }}
run: |
git config user.name "github-actions"
git config user.email "[email protected]"
git tag "v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}"
- name: Create GitHub Release
if: ${{ (github.event.inputs.dry_run || 'true') == 'false' }}
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.VERSION }}
name: PS-NCentral-RESTAPI v${{ env.VERSION }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}