Build, Test, and Publish PowerShell Module #72
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |