176 lines
6.3 KiB
YAML
176 lines
6.3 KiB
YAML
|
|
# Persona Engine - validation stage
|
||
|
|
#
|
||
|
|
# Everything here runs with no tenant, no credentials, and no network (SC-008). That
|
||
|
|
# is the point: a pipeline that needs a directory connection to tell you a rule file
|
||
|
|
# is wrong cannot run on every pull request, and the check that only runs sometimes is
|
||
|
|
# the one that stops catching things.
|
||
|
|
#
|
||
|
|
# Gate order is deliberate, cheapest and most categorical first. Sanitization runs
|
||
|
|
# before anything else because a leaked identifier in a branch is a problem whether or
|
||
|
|
# not the code compiles, and every later stage prints file contents into build logs.
|
||
|
|
|
||
|
|
trigger:
|
||
|
|
branches:
|
||
|
|
include:
|
||
|
|
- main
|
||
|
|
paths:
|
||
|
|
include:
|
||
|
|
- src/*
|
||
|
|
- tests/*
|
||
|
|
- config/*
|
||
|
|
- pipelines/*
|
||
|
|
- Invoke-PersonaEngine.ps1
|
||
|
|
- Edit-PersonaEngineConfig.ps1
|
||
|
|
- PersonaEngine.psd1
|
||
|
|
- PersonaEngine.psm1
|
||
|
|
|
||
|
|
pr:
|
||
|
|
branches:
|
||
|
|
include:
|
||
|
|
- main
|
||
|
|
|
||
|
|
pool:
|
||
|
|
vmImage: windows-latest
|
||
|
|
|
||
|
|
variables:
|
||
|
|
# Pinned rather than latest. A validation stage that changes behaviour when an
|
||
|
|
# upstream module publishes is not a gate, it is a coin flip.
|
||
|
|
pesterVersion: '5.6.1'
|
||
|
|
analyzerVersion: '1.22.0'
|
||
|
|
|
||
|
|
steps:
|
||
|
|
- checkout: self
|
||
|
|
fetchDepth: 0
|
||
|
|
|
||
|
|
- task: PowerShell@2
|
||
|
|
displayName: 'Gate 1 - Sanitization (SC-013)'
|
||
|
|
inputs:
|
||
|
|
pwsh: true
|
||
|
|
filePath: tests/Test-Sanitization.ps1
|
||
|
|
failOnStderr: false
|
||
|
|
|
||
|
|
- task: PowerShell@2
|
||
|
|
displayName: 'Install pinned analysis modules'
|
||
|
|
inputs:
|
||
|
|
pwsh: true
|
||
|
|
targetType: inline
|
||
|
|
script: |
|
||
|
|
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
|
||
|
|
Install-Module Pester -RequiredVersion $(pesterVersion) -Force -SkipPublisherCheck -Scope CurrentUser
|
||
|
|
Install-Module PSScriptAnalyzer -RequiredVersion $(analyzerVersion) -Force -Scope CurrentUser
|
||
|
|
|
||
|
|
- task: PowerShell@2
|
||
|
|
displayName: 'Gate 2 - PSScriptAnalyzer'
|
||
|
|
inputs:
|
||
|
|
pwsh: true
|
||
|
|
targetType: inline
|
||
|
|
script: |
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
$findings = Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1
|
||
|
|
|
||
|
|
if ($findings) {
|
||
|
|
$findings | Format-Table -AutoSize | Out-String | Write-Host
|
||
|
|
}
|
||
|
|
|
||
|
|
$blocking = @($findings | Where-Object Severity -in 'Error', 'Warning')
|
||
|
|
if ($blocking.Count -gt 0) {
|
||
|
|
throw "PSScriptAnalyzer reported $($blocking.Count) blocking finding(s)."
|
||
|
|
}
|
||
|
|
|
||
|
|
- task: PowerShell@2
|
||
|
|
displayName: 'Gate 3 - Engine purity (Principle IV)'
|
||
|
|
inputs:
|
||
|
|
pwsh: true
|
||
|
|
filePath: tests/Test-EnginePurity.ps1
|
||
|
|
|
||
|
|
- task: PowerShell@2
|
||
|
|
displayName: 'Gate 4 - Shipped schema is valid JSON Schema'
|
||
|
|
inputs:
|
||
|
|
pwsh: true
|
||
|
|
targetType: inline
|
||
|
|
script: |
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
|
||
|
|
# V-5a: Test-Json returns $true when the schema itself cannot be parsed, so a
|
||
|
|
# broken schema would let every later check pass while validating nothing.
|
||
|
|
# This stage exists solely to catch that.
|
||
|
|
$errors = $null
|
||
|
|
$null = '{}' | Test-Json -SchemaFile ./config/persona-engine.schema.json -ErrorAction SilentlyContinue -ErrorVariable errors
|
||
|
|
|
||
|
|
$unusable = @($errors | Where-Object { $_.Exception.Message -match 'Cannot parse the JSON schema' })
|
||
|
|
if ($unusable.Count -gt 0) {
|
||
|
|
throw 'config/persona-engine.schema.json is not valid JSON Schema. No configuration can be schema-validated until it is repaired.'
|
||
|
|
}
|
||
|
|
|
||
|
|
# The contract copy and the shipped copy must stay identical, or a rule author
|
||
|
|
# reading the contract validates against a different schema than the engine.
|
||
|
|
$shipped = (Get-FileHash ./config/persona-engine.schema.json -Algorithm SHA256).Hash
|
||
|
|
$contract = (Get-FileHash ./specs/001-persona-engine/contracts/persona-engine.schema.json -Algorithm SHA256).Hash
|
||
|
|
|
||
|
|
if ($shipped -ne $contract) {
|
||
|
|
throw 'config/persona-engine.schema.json and the contract copy have diverged.'
|
||
|
|
}
|
||
|
|
|
||
|
|
- task: PowerShell@2
|
||
|
|
displayName: 'Gate 5 - Example configuration passes all four layers'
|
||
|
|
inputs:
|
||
|
|
pwsh: true
|
||
|
|
targetType: inline
|
||
|
|
script: |
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive
|
||
|
|
if ($LASTEXITCODE -ne 0) { throw "The shipped example configuration failed validation with exit code $LASTEXITCODE." }
|
||
|
|
|
||
|
|
- task: PowerShell@2
|
||
|
|
displayName: 'Gate 6 - Offline Pester suite (SC-008)'
|
||
|
|
inputs:
|
||
|
|
pwsh: true
|
||
|
|
targetType: inline
|
||
|
|
script: |
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
|
||
|
|
$config = & ./tests/PesterConfiguration.ps1 -Suite Offline
|
||
|
|
$config.Run.Exit = $false
|
||
|
|
$config.Run.PassThru = $true
|
||
|
|
$config.TestResult.Enabled = $true
|
||
|
|
$config.TestResult.OutputPath = './testResults.offline.xml'
|
||
|
|
$config.Output.Verbosity = 'Normal'
|
||
|
|
|
||
|
|
$result = Invoke-Pester -Configuration $config
|
||
|
|
|
||
|
|
# Asserted, not assumed. A configuration change that silently filtered every
|
||
|
|
# test out would otherwise report a green pipeline over zero coverage.
|
||
|
|
if ($result.TotalCount -eq 0) { throw 'The offline suite ran no tests.' }
|
||
|
|
if ($result.FailedCount -gt 0) { throw "$($result.FailedCount) offline test(s) failed." }
|
||
|
|
|
||
|
|
Write-Host "Offline suite: $($result.PassedCount) passed, $($result.FailedCount) failed."
|
||
|
|
|
||
|
|
- task: PublishTestResults@2
|
||
|
|
displayName: 'Publish offline test results'
|
||
|
|
condition: succeededOrFailed()
|
||
|
|
inputs:
|
||
|
|
testResultsFormat: NUnit
|
||
|
|
testResultsFiles: './testResults.offline.xml'
|
||
|
|
testRunTitle: 'Persona Engine - offline suite'
|
||
|
|
|
||
|
|
- task: PowerShell@2
|
||
|
|
displayName: 'Gate 7 - No Graph module was loaded (SC-008)'
|
||
|
|
inputs:
|
||
|
|
pwsh: true
|
||
|
|
targetType: inline
|
||
|
|
script: |
|
||
|
|
# The proof that the offline suite is genuinely offline. If a test ever
|
||
|
|
# imports the Graph SDK, the "runs with no tenant" claim quietly stops being
|
||
|
|
# true and nobody notices until an air-gapped build fails.
|
||
|
|
$config = & ./tests/PesterConfiguration.ps1 -Suite Offline
|
||
|
|
$config.Run.PassThru = $true
|
||
|
|
$config.Output.Verbosity = 'None'
|
||
|
|
$null = Invoke-Pester -Configuration $config
|
||
|
|
|
||
|
|
$graph = Get-Module | Where-Object Name -like 'Microsoft.Graph*'
|
||
|
|
if ($graph) {
|
||
|
|
throw "A Graph module was loaded during the offline suite: $($graph.Name -join ', ')"
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host 'No Graph module was loaded. SC-008 holds.'
|