Private/Compile/Test-GovernanceSources.ps1
|
function Test-GovernanceSources { <# .SYNOPSIS Validates a program's sources.yaml (pre-migration source locations, consumed by preflight) against the resolved model. Returns an issue string per problem; empty = valid. Offline — no live calls. Entry shape, keyed by codePath: sources: PTL-FND: org: legacy-org # source organisation (name or URL) project: LegacyProject # source team project areaPath: LegacyProject\Node # project-rooted, no leading '\' teams: ['Old Team Name'] # optional: source teams whose # membership is "works there today" repos: # optional: which source repos are include: ['Node*'] # this team's (wildcard globs) accessToken: $Env:NAME # optional PAT fallback reference — # an env-var reference, NEVER a token Optional top-level `labels:` (ADR-008) attaches engagement vocabulary to preflight findings, keyed by check id — e.g. the rule number, owner lane and task id from the team-facing standard the findings feed: labels: area.orphan: { rule: A2, lane: PM, task: 2 } tag.unsanctioned: { rule: B4, lane: Eng, task: 6 } Keys must be known check ids ($script:GovernancePreflightChecks); values must be flat maps of scalars. Optional top-level `scope:` (ADR-010) is the migration query: complete flat WIQL naming which work items are actually moving, so validation is not run across archive nobody will migrate. A node may override it with its own `scope:`. scope: label: "2026.1 and onwards" # what to call it in the report query: >- SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = 'Proj' AND [System.IterationPath] NOT UNDER 'Proj\ARCHIVE' Optional top-level `reporting:` (ADR-009) frames the rendered markdown fix report — all fields optional: reporting: standard: "the team-facing standard these labels refer to" audience: "who the report is written for" candidateTagMinUses: 20 # tags used on more than this many work items # are listed as vocabulary candidates iterationTop: 7 # iteration paths shown for migration context title: "Pre-migration readiness check" #> [CmdletBinding()] param( [object]$Sources, [Parameter(Mandatory)][object]$Resolved, [object]$Labels = $null, [object]$Reporting = $null, [object]$Scope = $null ) $issues = @() # scope: the migration query (ADR-010). The query owns project and area # selection; the engine adds ID paging when it runs the query. $testScope = { param($s, $at) $found = @() if ($s -isnot [System.Collections.IDictionary]) { return @("$at must be a map with a 'query' (a flat WIQL query)") } foreach ($k in @($s.Keys)) { if ($k -notin 'query', 'label') { $found += "$at '$k' is not a scope field. Known: query, label" } } $q = [string]$s['query'] if ([string]::IsNullOrWhiteSpace($q)) { $found += "$at is missing 'query' - a flat WIQL SELECT [System.Id] FROM WorkItems WHERE query" } else { try { $null = Get-AdoWiqlWhereClause -Query $q } catch { $found += "$at $($_.Exception.Message)" } } if ($s.Contains('label') -and $s['label'] -isnot [string]) { $found += "$at label must be a string" } return $found } if ($Scope) { $issues += & $testScope $Scope 'sources.yaml scope' } if ($Reporting) { if ($Reporting -isnot [System.Collections.IDictionary]) { $issues += "sources.yaml reporting must be a map" } else { $known = @('standard', 'audience', 'candidateTagMinUses', 'iterationTop', 'title') foreach ($k in @($Reporting.Keys)) { if ($k -notin $known) { $issues += "sources.yaml reporting '$k' is not a reporting field. Known: $($known -join ', ')" } } foreach ($k in 'candidateTagMinUses', 'iterationTop') { if ($Reporting.Contains($k)) { $v = $Reporting[$k] if (-not ($v -is [int] -or $v -is [long]) -or [long]$v -lt 1) { $issues += "sources.yaml reporting '$k' must be a positive integer, got '$v'" } } } foreach ($k in 'standard', 'audience', 'title') { if ($Reporting.Contains($k) -and $Reporting[$k] -isnot [string]) { $issues += "sources.yaml reporting '$k' must be a string" } } } } if ($Labels) { if ($Labels -isnot [System.Collections.IDictionary]) { $issues += "sources.yaml labels must be a map keyed by preflight check id" } else { foreach ($check in @($Labels.Keys)) { if ($check -notin $script:GovernancePreflightChecks) { $issues += "sources.yaml labels '$check' is not a preflight check id. Known: $($script:GovernancePreflightChecks -join ', ')" continue } $fields = $Labels[$check] if ($fields -isnot [System.Collections.IDictionary]) { $issues += "sources.yaml labels '$check' must be a map of fields to attach (e.g. { rule: A2, lane: PM })" continue } foreach ($k in @($fields.Keys)) { if ($k -in 'class', 'check', 'subject', 'message') { $issues += "sources.yaml labels '$check' may not override the engine field '$k'" } if ($fields[$k] -is [System.Collections.IDictionary] -or ($fields[$k] -is [array])) { $issues += "sources.yaml labels '$check'.'$k' must be a scalar" } } } } } if (-not $Sources) { return $issues } $teamCodes = @($Resolved.teams | Where-Object { $_.codePath } | ForEach-Object { $_.codePath }) $futureCodes = @($Resolved.teams | Where-Object { $_.scope -eq 'future' } | ForEach-Object { $_.codePath }) foreach ($code in @($Sources.Keys)) { $src = $Sources[$code] $at = "sources.yaml '$code'" if ($code -notin $teamCodes) { $issues += "$at does not resolve to a team codePath in the resolved model" continue } if ($code -in $futureCodes) { $issues += "$at is scope:future — invisible to compliance, remove the flag before preflighting it" } if (-not $src.org) { $issues += "$at is missing 'org' (the source organisation)" } if (-not $src.project) { $issues += "$at is missing 'project' (the source team project)" } $area = [string]$src.areaPath if (-not $area) { $issues += "$at is missing 'areaPath' (the source area subtree, project-rooted)" } else { if ($area.StartsWith('\')) { $issues += "$at areaPath must be project-rooted without a leading '\' (e.g. 'Project\Node'), got '$area'" } elseif ($src.project -and (($area -split '\\')[0] -ne [string]$src.project)) { $issues += "$at areaPath must start with the source project segment '$($src.project)', got '$area'" } } if ($src.teams -and @($src.teams | Where-Object { $_ -isnot [string] -or [string]::IsNullOrWhiteSpace($_) }).Count -gt 0) { $issues += "$at teams must be a list of source team names" } if ($src.repos -and -not $src.repos.include) { $issues += "$at repos must carry an 'include' list of wildcard patterns (omit 'repos' entirely to skip repo checks)" } if ($src.scope) { $issues += & $testScope $src.scope "$at scope" } # The same never-a-literal-token rule as manifest accessToken: a value # that is not an $Env: reference would end up committed. $tokenRef = [string]$src.accessToken if ($tokenRef -and $tokenRef -notmatch '^\$\{?Env:') { $issues += "$at accessToken must be an environment-variable reference ('`$Env:NAME'), never a literal token" } } return $issues } |