Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 2 additions & 49 deletions .github/workflows/powershell-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,11 @@ jobs:
with:
persist-credentials: false

- name: Install PSScriptAnalyzer
shell: pwsh
run: |
Install-Module -Name PSScriptAnalyzer -RequiredVersion '1.22.0' -Force -Scope CurrentUser

- name: Run PSScriptAnalyzer
id: lint
shell: pwsh
run: |
$settings = './PSScriptAnalyzerSettings.psd1'
$files = Get-ChildItem -Recurse -Include '*.ps1','*.psm1','*.psd1' |
Where-Object { $_.FullName -notmatch 'node_modules|\.copilot-tracking' }

$results = $files | ForEach-Object { Invoke-ScriptAnalyzer -Path $_.FullName -Settings $settings }

New-Item -ItemType Directory -Force -Path lint-results | Out-Null

if ($results) {
$output = $results | Format-Table -AutoSize | Out-String
$output | Set-Content -Path lint-results/psscriptanalyzer-results.txt
Write-Host $output
"PSSCRIPTANALYZER_FAILED=true" | Out-File -FilePath $env:GITHUB_ENV -Append
} else {
"No PSScriptAnalyzer violations found." | Set-Content -Path lint-results/psscriptanalyzer-results.txt
Write-Host "No PSScriptAnalyzer violations found."
}
./scripts/linting/Invoke-PSScriptAnalyzer.ps1 -SoftFail:$${{ inputs.soft-fail == 'true' }}

- name: Upload Lint Results
if: always()
Expand All @@ -76,30 +56,3 @@ jobs:
name: powershell-lint-results
path: lint-results/
retention-days: 30

- name: Job Summary
if: always()
shell: bash
run: |
{
echo "## PowerShell Lint Results"
echo ""
if [ "${PSSCRIPTANALYZER_FAILED:-}" = "true" ]; then
echo "- :x: PSScriptAnalyzer: violations found"
else
echo "- :white_check_mark: PSScriptAnalyzer: passed"
fi
} >> "$GITHUB_STEP_SUMMARY"

- name: Fail on Lint Violations
if: always() && env.PSSCRIPTANALYZER_FAILED == 'true'
shell: bash
env:
SOFT_FAIL: ${{ inputs.soft-fail }}
run: |
if [ "$SOFT_FAIL" = "true" ]; then
echo "::warning::PowerShell lint violations found (soft-fail enabled)"
else
echo "::error::PowerShell lint violations found"
exit 1
fi
32 changes: 32 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ on: # yamllint disable-line rule:truthy
default: false
type: boolean

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
# CodeQL Analysis job for PR code security scanning
codeql-analysis:
Expand Down Expand Up @@ -235,6 +239,7 @@ jobs:
with:
working-directory: 'src/azure-resource-providers'
test-results-output: 'PWSH-TEST-RESULTS.xml'
secrets: inherit

# Terraform variable compliance check for PRs
terraform-var-compliance:
Expand Down Expand Up @@ -291,3 +296,30 @@ jobs:
"securityThreshold": "high"
}
secrets: inherit

# Pester tests for PowerShell scripts
pester-tests:
name: Pester Tests
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
steps:
- name: Checkout
uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.2.2
with:
fetch-depth: 0

- name: Run Pester Tests
id: pester
shell: pwsh
run: |
./scripts/Invoke-Pester.ps1 -CI -ChangedOnly

- name: Upload Test Results
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v4
with:
name: pester-test-results
path: test-results/
retention-days: 30
19 changes: 8 additions & 11 deletions .github/workflows/resource-provider-pwsh-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ on: # yamllint disable-line rule:truthy
type: string
default: 'src/azure-resource-providers'
test-results-output:
description: 'Path to output PowerShell test results'
description: 'Directory path for PowerShell test result output files'
required: false
type: string
default: 'PWSH-TEST-RESULTS.xml'
default: 'test-results'

permissions:
contents: read
Expand All @@ -71,30 +71,27 @@ jobs:

# Run Pester tests for the resource provider scripts
- name: Run Pester Tests
id: pester
shell: pwsh
working-directory: ${{ inputs.working-directory }}
run: |
./scripts/Invoke-Pester.ps1 -Path ./${{ inputs.working-directory }} -OutputFile ${{ inputs.test-results-output }}
if ($LASTEXITCODE -ne 0) {
Write-Error "Pester tests failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
working-directory: ${{ github.workspace }}
../../scripts/Invoke-Pester.ps1 -CI -Path '.' -OutputPath '${{ inputs.test-results-output }}'

# Publish the Pester test results
- name: Publish Test Results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: pester-test-results
path: ${{ inputs.test-results-output }}
name: rp-pester-test-results
path: ${{ inputs.working-directory }}/${{ inputs.test-results-output }}/

- name: Publish test summary to GitHub
if: ${{ !cancelled() }}
uses: actions/github-script@450193c5abd4cdb17ba9f3ffcfe8f635c4bb6c2a # v7.0.1
with:
script: |
const fs = require('fs');
const resultsFile = '${{ inputs.test-results-output }}';
const resultsFile = '${{ inputs.working-directory }}/${{ inputs.test-results-output }}/test-results.xml';

if (!fs.existsSync(resultsFile)) {
core.warning('Test results file not found: ' + resultsFile);
Expand Down
132 changes: 93 additions & 39 deletions scripts/Invoke-Pester.ps1
Original file line number Diff line number Diff line change
@@ -1,55 +1,109 @@
# Used for Azure DevOps unit test results
[CmdletBinding()]
param(
[switch]$CI,
[switch]$ChangedOnly,
[switch]$CodeCoverage,
[string]$ConfigPath = (Join-Path $PSScriptRoot 'tests/pester.config.ps1'),
[string]$OutputPath = './test-results',
[string[]]$Path
)

# To run locally, simply just run Invoke-Pester, no need to run this script
$ErrorActionPreference = 'Stop'

param (
[Parameter(Mandatory = $true)]
[string]$Path,
Import-Module (Join-Path $PSScriptRoot 'ci/Modules/CIHelpers.psm1') -Force

[Parameter(Mandatory = $true)]
[string]$OutputFile
)
$pesterModule = Get-Module -ListAvailable -Name Pester |
Where-Object { $_.Version -eq [version]'5.7.1' } |
Select-Object -First 1

function Find-PesterModule {
param (
[string]$ModuleName = "Pester",
[string]$MinimumVersion = "5.0.0"
)
if (-not $pesterModule) {
Install-Module -Name Pester -RequiredVersion '5.7.1' -Force -Scope CurrentUser -SkipPublisherCheck
}

# Check if the module is installed
$module = Get-Module -ListAvailable -Name $ModuleName | Sort-Object -Property Version -Descending | Select-Object -First 1
Import-Module Pester -RequiredVersion '5.7.1' -Force

if ($null -eq $module -or $module.Version -lt [version]$MinimumVersion) {
Write-Host "Installing or updating $ModuleName to version $MinimumVersion or higher..."
Install-Module -Name $ModuleName -MinimumVersion $MinimumVersion -Force -AllowClobber
}
else {
Write-Host "$ModuleName version $($module.Version) is already installed."
$configParams = @{}
if ($CI) { $configParams['CI'] = $true }
if ($CodeCoverage) { $configParams['CodeCoverage'] = $true }
if ($Path) { $configParams['Path'] = $Path }
$configParams['OutputPath'] = $OutputPath

$config = & $ConfigPath @configParams

if ($ChangedOnly) {
$changedTests = & (Join-Path $PSScriptRoot 'tests/Get-ChangedTestFiles.ps1')
if ($changedTests.Count -eq 0) {
Write-Host 'No changed test files found.'
Write-CIStepSummary "## Pester Test Results`n`nNo changed test files to run."
Set-CIOutput -Name 'test-result' -Value 'passed'
Set-CIOutput -Name 'test-count' -Value '0'
Set-CIOutput -Name 'fail-count' -Value '0'
exit 0
}
$config.Run.Path = $changedTests
}

# Ensure Pester module is installed and at least version 5
Find-PesterModule -ModuleName "Pester" -MinimumVersion "5.0.0"

if (-not (Test-Path $OutputPath)) {
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
}

Import-Module Pester
$result = Invoke-Pester -Configuration $config

$configuration = [PesterConfiguration]@{
Run = @{
Path = $Path
}
Output = @{
Verbosity = 'Detailed'
function Get-FailedTest {
param([object]$Container)
$failures = @()
foreach ($block in $Container.Blocks) {
foreach ($test in $block.Tests) {
if ($test.Result -eq 'Failed') {
$failures += @{
Name = $test.ExpandedName
Error = $test.ErrorRecord.Exception.Message
File = $test.ScriptBlock.File
Line = $test.ScriptBlock.StartPosition.StartLine
}
}
}
if ($block.Blocks.Count -gt 0) {
$failures += Get-FailedTest -Container $block
}
}
TestResult = @{
Enabled = $true
OutputFormat = "NUnitXml"
OutputPath = $OutputFile
return $failures
}

$allFailures = @()
foreach ($container in $result.Containers) {
$allFailures += Get-FailedTest -Container $container
}

$result | Select-Object TotalCount, PassedCount, FailedCount, SkippedCount, Duration |
ConvertTo-Json | Set-Content (Join-Path $OutputPath 'test-summary.json')

if ($allFailures.Count -gt 0) {
$allFailures | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $OutputPath 'test-failures.json')
foreach ($failure in $allFailures) {
Write-CIAnnotation -Level 'Error' -Message "Test failed: $($failure.Name) — $($failure.Error)" `
-File $failure.File -Line $failure.Line
}
}

# Print out the Path and OutputFile variables
Write-Host "Test file path: $Path"
Write-Host "Output test file path: $OutputFile"
$summary = @"
## Pester Test Results

Invoke-Pester -Configuration $configuration
| Metric | Value |
|--------|-------|
| Total | $($result.TotalCount) |
| Passed | $($result.PassedCount) |
| Failed | $($result.FailedCount) |
| Skipped | $($result.SkippedCount) |
| Duration | $($result.Duration) |
"@

Write-CIStepSummary $summary

Set-CIOutput -Name 'test-result' -Value $(if ($result.FailedCount -eq 0) { 'passed' } else { 'failed' })
Set-CIOutput -Name 'test-count' -Value $result.TotalCount.ToString()
Set-CIOutput -Name 'fail-count' -Value $result.FailedCount.ToString()

if ($CI -and $result.FailedCount -gt 0) {
exit 1
}
Loading
Loading