[CmdletBinding()] param( [string]$StacksRoot = (Join-Path $PSScriptRoot "..\stacks"), [string]$ExemptionsPath = (Join-Path $PSScriptRoot "portainer-stack-env-exemptions.json") ) $ErrorActionPreference = "Stop" $failures = New-Object System.Collections.Generic.List[string] $exemptions = @{} if (Test-Path $ExemptionsPath) { $raw = Get-Content -Raw -LiteralPath $ExemptionsPath | ConvertFrom-Json foreach ($property in $raw.PSObject.Properties) { $exemptions[$property.Name] = @($property.Value) } } function Get-ServiceBlocks { param([string[]]$Lines) $inServices = $false $currentName = $null $currentLines = New-Object System.Collections.Generic.List[string] $blocks = New-Object System.Collections.Generic.List[object] foreach ($line in $Lines) { if ($line -match '^services:\s*$') { $inServices = $true continue } if (-not $inServices) { continue } if ($line -match '^[A-Za-z0-9_-]+:\s*$') { break } if ($line -match '^ ([A-Za-z0-9_-]+):\s*$') { if ($null -ne $currentName) { $blocks.Add([pscustomobject]@{ Name = $currentName Lines = @($currentLines) }) } $currentName = $Matches[1] $currentLines = New-Object System.Collections.Generic.List[string] continue } if ($null -ne $currentName) { $currentLines.Add($line) } } if ($null -ne $currentName) { $blocks.Add([pscustomobject]@{ Name = $currentName Lines = @($currentLines) }) } return $blocks } Get-ChildItem -LiteralPath $StacksRoot -Directory | Sort-Object Name | ForEach-Object { $stackName = $_.Name $composePath = Join-Path $_.FullName "docker-compose.yml" $stackEnvPath = Join-Path $_.FullName "stack.env" if (-not (Test-Path $composePath)) { return } if (-not (Test-Path $stackEnvPath)) { $failures.Add("${stackName}: missing stack.env") } $lines = Get-Content -LiteralPath $composePath $interpolations = Select-String -LiteralPath $composePath -Pattern '\$\{' | Where-Object { $_.Line -notmatch '^\s*#' } foreach ($match in $interpolations) { $failures.Add("${stackName}: compose interpolation at $($match.Path):$($match.LineNumber): $($match.Line.Trim())") } $allowedMissing = @() if ($exemptions.ContainsKey($stackName)) { $allowedMissing = @($exemptions[$stackName]) } foreach ($service in Get-ServiceBlocks -Lines $lines) { $hasEnvFile = $service.Lines | Where-Object { $_ -match '^\s+env_file:\s*$' -or $_ -match '^\s+- stack\.env\s*$' } if (-not $hasEnvFile -and $allowedMissing -notcontains $service.Name) { $failures.Add("${stackName}/${service.Name}: missing env_file: stack.env") } } } if ($failures.Count -gt 0) { $failures | ForEach-Object { Write-Error $_ } exit 1 } Write-Host "Portainer stack env checks passed."