Skip to content

feat(ci): Enhanced pre-release validation v2 #2

feat(ci): Enhanced pre-release validation v2

feat(ci): Enhanced pre-release validation v2 #2

# Pre-Release Validation v2
# =========================
# Comprehensive test suite for release candidates
#
# This workflow runs EVERYTHING before a release:
# - Unit tests on all platforms (Ubuntu, Windows, macOS)
# - Unit tests on all PowerShell versions (5.1, 7.x)
# - Code coverage analysis with threshold enforcement
# - Integration tests against all supported Netbox versions
# - PSScriptAnalyzer code quality checks
# - Documentation completeness validation
# - Breaking change detection vs PSGallery release
# - Module import verification
# - Generates a comprehensive release readiness report
#
# Trigger: Manual only (workflow_dispatch)
# Duration: ~12-15 minutes
# Use: Before merging to main / creating a release
name: Pre-Release Validation
on:
workflow_dispatch:
inputs:
version:
description: 'Version being validated (e.g., 4.5.0)'
required: true
type: string
skip_integration:
description: 'Skip integration tests (faster, unit tests only)'
required: false
default: false
type: boolean
coverage_threshold:
description: 'Minimum code coverage percentage (default: 70)'
required: false
default: '70'
type: string
env:
NETBOX_TOKEN: "0123456789abcdef0123456789abcdef01234567"
COMPOSE_PROJECT_NAME: "powernetbox"
jobs:
# ============================================================
# Stage 1: Unit Tests - Full Platform Matrix
# ============================================================
unit-tests:
name: Unit [${{ matrix.os }}, ${{ matrix.pwsh && 'PS7' || 'PS5.1' }}]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
pwsh: [true]
include:
- os: windows-latest
pwsh: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies (pwsh)
if: matrix.pwsh
shell: pwsh
run: |
if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) {
Register-PSRepository -Default
}
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -Force -Scope CurrentUser -MinimumVersion 5.0.0
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
- name: Install dependencies (powershell 5.1)
if: '!matrix.pwsh'
shell: powershell
run: |
if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) {
Register-PSRepository -Default
}
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -Force -Scope CurrentUser -MinimumVersion 5.0.0
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
- name: Build module (pwsh)
if: matrix.pwsh
shell: pwsh
run: ./deploy.ps1 -Environment dev -SkipVersion
- name: Build module (powershell 5.1)
if: '!matrix.pwsh'
shell: powershell
run: ./deploy.ps1 -Environment dev -SkipVersion
- name: Run unit tests (pwsh)
if: matrix.pwsh
shell: pwsh
run: |
$config = New-PesterConfiguration
$config.Run.Path = './Tests/*.Tests.ps1'
$config.Run.Exit = $true
$config.Filter.ExcludeTag = @('Integration', 'Live')
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = 'TestResults.xml'
$config.TestResult.OutputFormat = 'NUnitXml'
$config.Output.Verbosity = 'Normal'
Invoke-Pester -Configuration $config
- name: Run unit tests (powershell 5.1)
if: '!matrix.pwsh'
shell: powershell
run: |
$config = New-PesterConfiguration
$config.Run.Path = './Tests/*.Tests.ps1'
$config.Run.Exit = $true
$config.Filter.ExcludeTag = @('Integration', 'Live')
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = 'TestResults.xml'
$config.TestResult.OutputFormat = 'NUnitXml'
$config.Output.Verbosity = 'Normal'
Invoke-Pester -Configuration $config
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: unit-tests-${{ matrix.os }}-${{ matrix.pwsh && 'ps7' || 'ps51' }}
path: TestResults.xml
retention-days: 30
# ============================================================
# Stage 2: Code Coverage Analysis
# ============================================================
code-coverage:
name: Code Coverage
runs-on: ubuntu-latest
outputs:
coverage_percent: ${{ steps.coverage.outputs.coverage_percent }}
covered_commands: ${{ steps.coverage.outputs.covered_commands }}
total_commands: ${{ steps.coverage.outputs.total_commands }}
missed_functions: ${{ steps.coverage.outputs.missed_functions }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -Force -Scope CurrentUser -MinimumVersion 5.0.0
- name: Build module
shell: pwsh
run: ./deploy.ps1 -Environment dev -SkipVersion
- name: Run tests with code coverage
id: coverage
shell: pwsh
run: |
Write-Host "=== Running Tests with Code Coverage ===" -ForegroundColor Cyan
Write-Host ""
# Get all function files for coverage
$functionFiles = Get-ChildItem -Path ./Functions -Filter *.ps1 -Recurse |
Where-Object { $_.Name -notmatch '\.Tests\.ps1$' } |
Select-Object -ExpandProperty FullName
Write-Host "Analyzing coverage for $($functionFiles.Count) function files..." -ForegroundColor Yellow
$config = New-PesterConfiguration
$config.Run.Path = './Tests/*.Tests.ps1'
$config.Run.Exit = $false
$config.Filter.ExcludeTag = @('Integration', 'Live')
$config.Output.Verbosity = 'Normal'
# Enable code coverage
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = $functionFiles
$config.CodeCoverage.OutputPath = 'coverage.xml'
$config.CodeCoverage.OutputFormat = 'JaCoCo'
$result = Invoke-Pester -Configuration $config
# Calculate coverage
$coverage = $result.CodeCoverage
$coveredCommands = $coverage.CommandsExecutedCount
$totalCommands = $coverage.CommandsAnalyzedCount
$missedCommands = $coverage.CommandsMissedCount
if ($totalCommands -gt 0) {
$coveragePercent = [math]::Round(($coveredCommands / $totalCommands) * 100, 2)
} else {
$coveragePercent = 0
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " CODE COVERAGE RESULTS" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Coverage: $coveragePercent%" -ForegroundColor $(if ($coveragePercent -ge 70) { 'Green' } elseif ($coveragePercent -ge 50) { 'Yellow' } else { 'Red' })
Write-Host "Executed: $coveredCommands commands"
Write-Host "Analyzed: $totalCommands commands"
Write-Host "Missed: $missedCommands commands"
Write-Host ""
# Find functions with 0% coverage
$missedFiles = $coverage.CommandsMissed |
Group-Object File |
ForEach-Object {
$file = $_.Name
$fileName = Split-Path $file -Leaf
$totalInFile = ($coverage.CommandsAnalyzed | Where-Object { $_.File -eq $file }).Count
$missedInFile = $_.Count
if ($totalInFile -eq $missedInFile -and $totalInFile -gt 0) {
$fileName -replace '\.ps1$', ''
}
} | Where-Object { $_ } | Select-Object -First 20
$missedFunctionsStr = if ($missedFiles.Count -gt 0) {
($missedFiles | Select-Object -First 10) -join ', '
} else {
"None"
}
if ($missedFiles.Count -gt 0) {
Write-Host "Functions with 0% coverage (first 10):" -ForegroundColor Yellow
$missedFiles | Select-Object -First 10 | ForEach-Object {
Write-Host " - $_" -ForegroundColor Yellow
}
if ($missedFiles.Count -gt 10) {
Write-Host " ... and $($missedFiles.Count - 10) more" -ForegroundColor Yellow
}
}
# Set outputs
"coverage_percent=$coveragePercent" >> $env:GITHUB_OUTPUT
"covered_commands=$coveredCommands" >> $env:GITHUB_OUTPUT
"total_commands=$totalCommands" >> $env:GITHUB_OUTPUT
"missed_functions=$missedFunctionsStr" >> $env:GITHUB_OUTPUT
# Export detailed report
@{
CoveragePercent = $coveragePercent
CoveredCommands = $coveredCommands
TotalCommands = $totalCommands
MissedCommands = $missedCommands
Threshold = ${{ inputs.coverage_threshold }}
MissedFunctions = $missedFiles
} | ConvertTo-Json -Depth 5 | Out-File "coverage-report.json"
# Check threshold
$threshold = [int]"${{ inputs.coverage_threshold }}"
if ($coveragePercent -lt $threshold) {
Write-Host ""
Write-Host "::warning::Code coverage ($coveragePercent%) is below threshold ($threshold%)"
}
- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: code-coverage
path: |
coverage.xml
coverage-report.json
retention-days: 30
# ============================================================
# Stage 3: Code Quality - PSScriptAnalyzer
# ============================================================
code-quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install PSScriptAnalyzer
shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
- name: Run PSScriptAnalyzer
id: pssa
shell: pwsh
run: |
Write-Host "Running PSScriptAnalyzer on Functions/..." -ForegroundColor Cyan
$results = Invoke-ScriptAnalyzer -Path ./Functions -Recurse -Settings PSGallery
$errors = $results | Where-Object { $_.Severity -eq 'Error' }
$warnings = $results | Where-Object { $_.Severity -eq 'Warning' }
$info = $results | Where-Object { $_.Severity -eq 'Information' }
Write-Host ""
Write-Host "=== PSScriptAnalyzer Results ===" -ForegroundColor Cyan
Write-Host "Errors: $($errors.Count)" -ForegroundColor $(if ($errors.Count -gt 0) { 'Red' } else { 'Green' })
Write-Host "Warnings: $($warnings.Count)" -ForegroundColor $(if ($warnings.Count -gt 0) { 'Yellow' } else { 'Green' })
Write-Host "Info: $($info.Count)" -ForegroundColor Gray
if ($errors.Count -gt 0) {
Write-Host ""
Write-Host "=== Errors ===" -ForegroundColor Red
$errors | ForEach-Object {
Write-Host " $($_.ScriptName):$($_.Line) - $($_.RuleName): $($_.Message)" -ForegroundColor Red
}
}
$results | ConvertTo-Json | Out-File "pssa-results.json"
if ($errors.Count -gt 0) {
Write-Host ""
Write-Host "::error::PSScriptAnalyzer found $($errors.Count) error(s)"
exit 1
}
- name: Upload PSSA results
uses: actions/upload-artifact@v4
if: always()
with:
name: code-quality-pssa
path: pssa-results.json
retention-days: 30
# ============================================================
# Stage 4: Documentation Validation
# ============================================================
documentation-validation:
name: Documentation Validation
runs-on: ubuntu-latest
outputs:
total_functions: ${{ steps.docs.outputs.total_functions }}
documented_functions: ${{ steps.docs.outputs.documented_functions }}
documentation_percent: ${{ steps.docs.outputs.documentation_percent }}
missing_docs: ${{ steps.docs.outputs.missing_docs }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate documentation
id: docs
shell: pwsh
run: |
Write-Host "=== Validating Function Documentation ===" -ForegroundColor Cyan
Write-Host ""
# Build module first
./deploy.ps1 -Environment prod -SkipVersion
Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force
# Get all public functions
$publicFunctions = Get-Command -Module PowerNetbox | Where-Object { $_.Name -match '-' }
Write-Host "Checking documentation for $($publicFunctions.Count) public functions..." -ForegroundColor Yellow
Write-Host ""
$results = @()
$missingDocs = @()
foreach ($func in $publicFunctions) {
$help = Get-Help $func.Name -ErrorAction SilentlyContinue
$hasSynopsis = $help.Synopsis -and
$help.Synopsis -ne $func.Name -and
$help.Synopsis -notmatch '^[\s\r\n]*$'
$hasDescription = $help.Description -and
$help.Description.Text -and
$help.Description.Text.Trim() -ne ''
$hasExamples = $help.Examples -and
$help.Examples.Example -and
$help.Examples.Example.Count -gt 0
$hasParameters = $true # Less strict on parameters
$isDocumented = $hasSynopsis -and $hasDescription -and $hasExamples
$results += [PSCustomObject]@{
Function = $func.Name
Synopsis = $hasSynopsis
Description = $hasDescription
Examples = $hasExamples
Documented = $isDocumented
}
if (-not $isDocumented) {
$missing = @()
if (-not $hasSynopsis) { $missing += "Synopsis" }
if (-not $hasDescription) { $missing += "Description" }
if (-not $hasExamples) { $missing += "Examples" }
$missingDocs += "$($func.Name) (missing: $($missing -join ', '))"
}
}
$totalFunctions = $results.Count
$documentedFunctions = ($results | Where-Object { $_.Documented }).Count
$documentationPercent = if ($totalFunctions -gt 0) {
[math]::Round(($documentedFunctions / $totalFunctions) * 100, 1)
} else { 0 }
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " DOCUMENTATION VALIDATION RESULTS" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Total functions: $totalFunctions"
Write-Host "Fully documented: $documentedFunctions"
Write-Host "Documentation: $documentationPercent%" -ForegroundColor $(if ($documentationPercent -ge 90) { 'Green' } elseif ($documentationPercent -ge 70) { 'Yellow' } else { 'Red' })
Write-Host ""
if ($missingDocs.Count -gt 0 -and $missingDocs.Count -le 20) {
Write-Host "Functions with incomplete documentation:" -ForegroundColor Yellow
$missingDocs | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
} elseif ($missingDocs.Count -gt 20) {
Write-Host "Functions with incomplete documentation: $($missingDocs.Count) functions" -ForegroundColor Yellow
Write-Host "(First 10 shown)" -ForegroundColor Gray
$missingDocs | Select-Object -First 10 | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
}
# Set outputs
$missingDocsStr = if ($missingDocs.Count -gt 0) {
($missingDocs | Select-Object -First 5) -join '; '
} else { "None" }
"total_functions=$totalFunctions" >> $env:GITHUB_OUTPUT
"documented_functions=$documentedFunctions" >> $env:GITHUB_OUTPUT
"documentation_percent=$documentationPercent" >> $env:GITHUB_OUTPUT
"missing_docs=$missingDocsStr" >> $env:GITHUB_OUTPUT
# Export full report
@{
TotalFunctions = $totalFunctions
DocumentedFunctions = $documentedFunctions
DocumentationPercent = $documentationPercent
MissingDocumentation = $missingDocs
Details = $results
} | ConvertTo-Json -Depth 5 | Out-File "documentation-report.json"
- name: Upload documentation report
uses: actions/upload-artifact@v4
with:
name: documentation-validation
path: documentation-report.json
retention-days: 30
# ============================================================
# Stage 5: Breaking Change Detection
# ============================================================
breaking-changes:
name: Breaking Change Detection
runs-on: ubuntu-latest
outputs:
removed_commands: ${{ steps.compare.outputs.removed_commands }}
added_commands: ${{ steps.compare.outputs.added_commands }}
removed_count: ${{ steps.compare.outputs.removed_count }}
added_count: ${{ steps.compare.outputs.added_count }}
has_breaking_changes: ${{ steps.compare.outputs.has_breaking_changes }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Compare with PSGallery release
id: compare
shell: pwsh
run: |
Write-Host "=== Breaking Change Detection ===" -ForegroundColor Cyan
Write-Host ""
# Build current module
Write-Host "Building current module..." -ForegroundColor Yellow
./deploy.ps1 -Environment prod -SkipVersion
Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force -DisableNameChecking
$currentCommands = Get-Command -Module PowerNetbox |
Where-Object { $_.Name -match '-' } |
Select-Object -ExpandProperty Name |
Sort-Object
Write-Host "Current module has $($currentCommands.Count) commands"
# Try to get PSGallery version
Write-Host ""
Write-Host "Fetching latest PSGallery release..." -ForegroundColor Yellow
$galleryCommands = @()
try {
# Save to temp location
$tempPath = Join-Path $env:RUNNER_TEMP 'PSGalleryModule'
New-Item -ItemType Directory -Path $tempPath -Force | Out-Null
Save-Module -Name PowerNetbox -Path $tempPath -Repository PSGallery -ErrorAction Stop
# Find and import
$galleryManifest = Get-ChildItem -Path $tempPath -Filter 'PowerNetbox.psd1' -Recurse | Select-Object -First 1
if ($galleryManifest) {
Import-Module $galleryManifest.FullName -Force -DisableNameChecking -Prefix 'Gallery'
$galleryCommands = Get-Command -Module PowerNetbox |
Where-Object { $_.Name -match '-Gallery' } |
ForEach-Object { $_.Name -replace '-Gallery', '-' } |
Sort-Object
# Actually get commands without prefix trick
Remove-Module PowerNetbox -Force -ErrorAction SilentlyContinue
Import-Module $galleryManifest.FullName -Force -DisableNameChecking
$galleryCommands = Get-Command -Module PowerNetbox |
Where-Object { $_.Name -match '-' } |
Select-Object -ExpandProperty Name |
Sort-Object
$galleryVersion = (Get-Module PowerNetbox).Version
Write-Host "PSGallery version: $galleryVersion with $($galleryCommands.Count) commands" -ForegroundColor Green
# Reimport current
Remove-Module PowerNetbox -Force
Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force -DisableNameChecking
}
} catch {
Write-Host "Could not fetch PSGallery module: $_" -ForegroundColor Yellow
Write-Host "Skipping breaking change detection (no baseline)" -ForegroundColor Yellow
}
# Compare
$removedCommands = @()
$addedCommands = @()
if ($galleryCommands.Count -gt 0) {
$removedCommands = $galleryCommands | Where-Object { $_ -notin $currentCommands }
$addedCommands = $currentCommands | Where-Object { $_ -notin $galleryCommands }
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " BREAKING CHANGE ANALYSIS" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
if ($removedCommands.Count -gt 0) {
Write-Host "⚠️ REMOVED COMMANDS (Breaking Changes): $($removedCommands.Count)" -ForegroundColor Red
$removedCommands | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
} else {
Write-Host "✅ No removed commands" -ForegroundColor Green
}
Write-Host ""
if ($addedCommands.Count -gt 0) {
Write-Host "➕ NEW COMMANDS: $($addedCommands.Count)" -ForegroundColor Green
if ($addedCommands.Count -le 20) {
$addedCommands | ForEach-Object { Write-Host " + $_" -ForegroundColor Green }
} else {
$addedCommands | Select-Object -First 10 | ForEach-Object { Write-Host " + $_" -ForegroundColor Green }
Write-Host " ... and $($addedCommands.Count - 10) more" -ForegroundColor Gray
}
} else {
Write-Host "No new commands" -ForegroundColor Gray
}
}
# Set outputs
$removedStr = if ($removedCommands.Count -gt 0) { ($removedCommands | Select-Object -First 5) -join ', ' } else { "None" }
$addedStr = if ($addedCommands.Count -gt 0) { ($addedCommands | Select-Object -First 5) -join ', ' } else { "None" }
$hasBreaking = if ($removedCommands.Count -gt 0) { "true" } else { "false" }
"removed_commands=$removedStr" >> $env:GITHUB_OUTPUT
"added_commands=$addedStr" >> $env:GITHUB_OUTPUT
"removed_count=$($removedCommands.Count)" >> $env:GITHUB_OUTPUT
"added_count=$($addedCommands.Count)" >> $env:GITHUB_OUTPUT
"has_breaking_changes=$hasBreaking" >> $env:GITHUB_OUTPUT
# Export report
@{
CurrentCommandCount = $currentCommands.Count
GalleryCommandCount = $galleryCommands.Count
RemovedCommands = $removedCommands
AddedCommands = $addedCommands
HasBreakingChanges = ($removedCommands.Count -gt 0)
} | ConvertTo-Json -Depth 5 | Out-File "breaking-changes-report.json"
- name: Upload breaking changes report
uses: actions/upload-artifact@v4
with:
name: breaking-changes
path: breaking-changes-report.json
retention-days: 30
# ============================================================
# Stage 6: Integration Tests - All Netbox Versions
# ============================================================
integration-tests:
name: Integration [Netbox ${{ matrix.netbox_short }}]
runs-on: ubuntu-latest
if: ${{ !inputs.skip_integration }}
needs: [unit-tests]
strategy:
fail-fast: false
matrix:
include:
- netbox: "v4.1.11-3.0.2"
netbox_short: "4.1.11"
- netbox: "v4.2.9-3.2.1"
netbox_short: "4.2.9"
- netbox: "v4.3.7-3.3.0"
netbox_short: "4.3.7"
- netbox: "v4.4.9-3.4.2"
netbox_short: "4.4.9"
env:
NETBOX_VERSION: ${{ matrix.netbox }}
NETBOX_HOST: "localhost:8000"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Start Netbox ${{ matrix.netbox_short }}
run: |
echo "Starting Netbox ${{ matrix.netbox_short }}..."
docker compose -f docker-compose.ci.yml pull
docker compose -f docker-compose.ci.yml up -d
- name: Wait for Netbox
run: |
echo "Waiting for Netbox to be ready..."
MAX_WAIT=300
ELAPSED=0
while [ $ELAPSED -lt $MAX_WAIT ]; do
HEALTH=$(docker inspect --format='{{.State.Health.Status}}' powernetbox-netbox-1 2>/dev/null || echo "starting")
echo "[${ELAPSED}s] Status: $HEALTH"
if [ "$HEALTH" = "healthy" ]; then
curl -sf http://localhost:8000/login/ > /dev/null 2>&1 && exit 0
fi
[ "$HEALTH" = "unhealthy" ] && docker compose -f docker-compose.ci.yml logs netbox --tail 50 && exit 1
sleep 10
ELAPSED=$((ELAPSED + 10))
done
exit 1
- name: Install PowerShell dependencies
shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -Force -Scope CurrentUser -MinimumVersion 5.0.0
- name: Build module
shell: pwsh
run: |
./deploy.ps1 -Environment dev -SkipVersion
Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force
- name: Run integration tests
shell: pwsh
env:
NETBOX_HOST: ${{ env.NETBOX_HOST }}
NETBOX_TOKEN: ${{ env.NETBOX_TOKEN }}
run: |
$config = New-PesterConfiguration
$config.Run.Path = './Tests/Integration.Tests.ps1'
$config.Run.Exit = $true
$config.Filter.Tag = @('Integration')
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = 'IntegrationResults.xml'
$config.TestResult.OutputFormat = 'NUnitXml'
$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration $config
- name: Upload integration results
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-${{ matrix.netbox_short }}
path: IntegrationResults.xml
retention-days: 30
- name: Cleanup
if: always()
run: docker compose -f docker-compose.ci.yml down -v --remove-orphans
# ============================================================
# Stage 7: Module Verification
# ============================================================
module-verification:
name: Module Verification
runs-on: ubuntu-latest
outputs:
command_count: ${{ steps.verify.outputs.command_count }}
module_version: ${{ steps.verify.outputs.module_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build and verify module
id: verify
shell: pwsh
run: |
Write-Host "=== Building Module ===" -ForegroundColor Cyan
./deploy.ps1 -Environment prod -SkipVersion
Write-Host ""
Write-Host "=== Importing Module ===" -ForegroundColor Cyan
Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force -ErrorAction Stop
$module = Get-Module PowerNetbox
Write-Host "Name: $($module.Name)"
Write-Host "Version: $($module.Version)"
$commands = Get-Command -Module PowerNetbox
$publicCommands = $commands | Where-Object { $_.Name -match '-' }
Write-Host ""
Write-Host "=== Command Statistics ===" -ForegroundColor Cyan
Write-Host "Total commands: $($commands.Count)"
Write-Host "Public commands: $($publicCommands.Count)"
$expectedMinimum = 400
if ($publicCommands.Count -lt $expectedMinimum) {
Write-Host "::error::Expected at least $expectedMinimum public commands, found $($publicCommands.Count)"
exit 1
}
Write-Host ""
Write-Host "=== Command Breakdown ===" -ForegroundColor Cyan
$breakdown = $publicCommands | Group-Object { ($_.Name -split '-')[0] } | Sort-Object Count -Descending
foreach ($group in $breakdown) {
Write-Host " $($group.Name): $($group.Count)"
}
# Set outputs
"command_count=$($publicCommands.Count)" >> $env:GITHUB_OUTPUT
"module_version=$($module.Version)" >> $env:GITHUB_OUTPUT
$publicCommands | Select-Object Name | ConvertTo-Json | Out-File "command-list.json"
- name: Upload command list
uses: actions/upload-artifact@v4
with:
name: module-verification
path: command-list.json
retention-days: 30
# ============================================================
# Final: Enhanced Release Readiness Report
# ============================================================
release-report:
name: Release Readiness Report
runs-on: ubuntu-latest
needs: [unit-tests, code-coverage, code-quality, documentation-validation, breaking-changes, integration-tests, module-verification]
if: always()
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Generate enhanced release report
shell: pwsh
run: |
$version = "${{ inputs.version }}"
$skipIntegration = "${{ inputs.skip_integration }}" -eq 'true'
$coverageThreshold = [int]"${{ inputs.coverage_threshold }}"
# Collect job results
$unitTests = "${{ needs.unit-tests.result }}"
$codeCoverage = "${{ needs.code-coverage.result }}"
$codeQuality = "${{ needs.code-quality.result }}"
$docsValidation = "${{ needs.documentation-validation.result }}"
$breakingChanges = "${{ needs.breaking-changes.result }}"
$integration = if ($skipIntegration) { "skipped" } else { "${{ needs.integration-tests.result }}" }
$moduleVerify = "${{ needs.module-verification.result }}"
# Get metrics from job outputs
$coveragePercent = "${{ needs.code-coverage.outputs.coverage_percent }}"
$coveredCommands = "${{ needs.code-coverage.outputs.covered_commands }}"
$totalCommands = "${{ needs.code-coverage.outputs.total_commands }}"
$missedFunctions = "${{ needs.code-coverage.outputs.missed_functions }}"
$totalFunctions = "${{ needs.documentation-validation.outputs.total_functions }}"
$documentedFunctions = "${{ needs.documentation-validation.outputs.documented_functions }}"
$docsPercent = "${{ needs.documentation-validation.outputs.documentation_percent }}"
$missingDocs = "${{ needs.documentation-validation.outputs.missing_docs }}"
$removedCommands = "${{ needs.breaking-changes.outputs.removed_commands }}"
$addedCommands = "${{ needs.breaking-changes.outputs.added_commands }}"
$removedCount = "${{ needs.breaking-changes.outputs.removed_count }}"
$addedCount = "${{ needs.breaking-changes.outputs.added_count }}"
$hasBreaking = "${{ needs.breaking-changes.outputs.has_breaking_changes }}"
$commandCount = "${{ needs.module-verification.outputs.command_count }}"
$moduleVersion = "${{ needs.module-verification.outputs.module_version }}"
# Determine overall status
$coverageOk = [double]$coveragePercent -ge $coverageThreshold
$allPassed = ($unitTests -eq 'success') -and
($codeCoverage -eq 'success') -and
($codeQuality -eq 'success') -and
($docsValidation -eq 'success') -and
($moduleVerify -eq 'success') -and
($skipIntegration -or $integration -eq 'success')
$overallStatus = if ($allPassed) { "READY" } else { "NOT READY" }
$statusEmoji = if ($allPassed) { "✅" } else { "❌" }
# Coverage status
$coverageStatus = if ([double]$coveragePercent -ge $coverageThreshold) { "✅" } else { "⚠️" }
$docsStatus = if ([double]$docsPercent -ge 80) { "✅" } elseif ([double]$docsPercent -ge 60) { "⚠️" } else { "❌" }
$breakingStatus = if ($hasBreaking -eq 'true') { "⚠️ BREAKING" } else { "✅ None" }
$report = @"
# $statusEmoji Pre-Release Validation Report
**Version:** $version

Check failure on line 827 in .github/workflows/pre-release-validation.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/pre-release-validation.yml

Invalid workflow file

You have an error in your yaml syntax on line 827
**Module Version:** $moduleVersion
**Date:** $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC')
**Status:** **$overallStatus**
---
## 📊 Quality Metrics
| Metric | Value | Status |
|--------|-------|--------|
| **Code Coverage** | $coveragePercent% ($coveredCommands/$totalCommands commands) | $coverageStatus (threshold: $coverageThreshold%) |
| **Documentation** | $docsPercent% ($documentedFunctions/$totalFunctions functions) | $docsStatus |
| **Breaking Changes** | $removedCount removed, $addedCount added | $breakingStatus |
| **Public Commands** | $commandCount | ✅ |
---
## 🧪 Test Results
| Stage | Result | Details |
|-------|--------|---------|
| Unit Tests (4 platforms) | $(if ($unitTests -eq 'success') { '✅ PASS' } else { '❌ FAIL' }) | Ubuntu, Windows, macOS × PS 5.1/7.x |
| Code Coverage | $(if ($codeCoverage -eq 'success') { '✅ PASS' } else { '❌ FAIL' }) | $coveragePercent% coverage |
| Code Quality (PSSA) | $(if ($codeQuality -eq 'success') { '✅ PASS' } else { '❌ FAIL' }) | PSScriptAnalyzer |
| Documentation | $(if ($docsValidation -eq 'success') { '✅ PASS' } else { '❌ FAIL' }) | $docsPercent% documented |
| Breaking Changes | $(if ($breakingChanges -eq 'success') { '✅ PASS' } else { '❌ FAIL' }) | $removedCount breaking, $addedCount new |
| Integration Tests | $(if ($skipIntegration) { '⏭️ SKIPPED' } elseif ($integration -eq 'success') { '✅ PASS' } else { '❌ FAIL' }) | Netbox 4.1, 4.2, 4.3, 4.4 |
| Module Verification | $(if ($moduleVerify -eq 'success') { '✅ PASS' } else { '❌ FAIL' }) | Import, command count |
---
## 🖥️ Platform Matrix
| Platform | PowerShell | Status |
|----------|------------|--------|
| Ubuntu | 7.x | $(if ($unitTests -eq 'success') { '✅' } else { '❓' }) |
| Windows | 7.x | $(if ($unitTests -eq 'success') { '✅' } else { '❓' }) |
| Windows | 5.1 | $(if ($unitTests -eq 'success') { '✅' } else { '❓' }) |
| macOS | 7.x | $(if ($unitTests -eq 'success') { '✅' } else { '❓' }) |
---
## 🔌 Netbox Compatibility
| Version | Status |
|---------|--------|
| 4.1.11 | $(if ($skipIntegration) { '⏭️' } elseif ($integration -eq 'success') { '✅' } else { '❓' }) |
| 4.2.9 | $(if ($skipIntegration) { '⏭️' } elseif ($integration -eq 'success') { '✅' } else { '❓' }) |
| 4.3.7 | $(if ($skipIntegration) { '⏭️' } elseif ($integration -eq 'success') { '✅' } else { '❓' }) |
| 4.4.9 | $(if ($skipIntegration) { '⏭️' } elseif ($integration -eq 'success') { '✅' } else { '❓' }) |
---
## 📋 Coverage Details
**Functions with 0% coverage (sample):** $missedFunctions
**Functions missing documentation (sample):** $missingDocs
$(if ($hasBreaking -eq 'true') {
@"
---
## ⚠️ Breaking Changes Detected
**Removed commands:** $removedCommands
These commands exist in the PSGallery release but are missing from the current build.
Consider if this is intentional and document in release notes.
"@
})
---
## ✅ Release Checklist
- [$(if ($unitTests -eq 'success') { 'x' } else { ' ' })] All unit tests pass (4 platforms)
- [$(if ($coverageOk) { 'x' } else { ' ' })] Code coverage ≥ $coverageThreshold%
- [$(if ($codeQuality -eq 'success') { 'x' } else { ' ' })] No PSScriptAnalyzer errors
- [$(if ($docsValidation -eq 'success') { 'x' } else { ' ' })] Documentation validated
- [$(if ($hasBreaking -ne 'true') { 'x' } else { ' ' })] No breaking changes (or documented)
- [$(if ($skipIntegration -or $integration -eq 'success') { 'x' } else { ' ' })] Integration tests pass
- [$(if ($moduleVerify -eq 'success') { 'x' } else { ' ' })] Module loads correctly
- [ ] Version updated in PowerNetbox.psd1
- [ ] CHANGELOG updated
- [ ] PR approved
---
## 🚀 Next Steps
$(if ($allPassed) {
@"
1. Merge to main branch
2. Create release tag: ``v$version``
3. GitHub Actions will publish to PSGallery
"@
} else {
@"
1. Review failed checks above
2. Fix issues and push updates
3. Re-run this validation workflow
"@
})
---
*Generated by Pre-Release Validation v2*
"@
Write-Host $report
$report | Out-File "release-report.md"
$report | Out-File -FilePath $env:GITHUB_STEP_SUMMARY
if (-not $allPassed) {
Write-Host ""
Write-Host "::error::Release validation FAILED - see report above"
exit 1
}
- name: Upload release report
uses: actions/upload-artifact@v4
if: always()
with:
name: release-report
path: release-report.md
retention-days: 90