Public/Invoke-GovernanceApply.ps1
|
function Invoke-GovernanceApply { <# .SYNOPSIS Builds the resolved model then runs the compliance loop in Apply mode: every governed resource is checked, every deviation is reported, and every deviation is corrected in the same pass. Use -WhatIf to run in preview mode -- shows what would change without making any changes (equivalent to 'plan'). Use -Prune (or manifest settings.prune: true) to DELETE resources that exist in ADO but not in the resolved model. Never on by default. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$ProgramPath, [Parameter(Mandatory)][string]$ResolvedPath, [string]$Org, [switch]$WhatIf, [switch]$Prune ) # Always build first -- ensures the resolved model is current before touching live ADO. Invoke-GovernanceBuild -ProgramPath $ProgramPath -OutputPath $ResolvedPath | Out-Null $source = Import-GovernanceSource -ProgramPath $ProgramPath $manifest = $source.Manifest $teamIds = $source.TeamIds # codePath -> GUID; mutated by reconcile, written back below $targetOrg = if ($Org) { $Org } else { $manifest.org } $orgUrl = ConvertTo-AdoOrgUrl -Org $targetOrg Initialize-AdoAuth -Manifest $manifest -OrgUrl $orgUrl | Out-Null $resolved = ConvertFrom-Yaml (Get-Content $ResolvedPath -Raw) $mode = if ($WhatIf) { 'WhatIf' } else { 'Apply' } # Prune is opt-in: the -Prune switch or manifest settings.prune enables it. $pruneEnabled = $Prune.IsPresent -or ($manifest.settings -and [bool]$manifest.settings.prune) $modeLabel = if ($pruneEnabled) { "$mode + prune" } else { $mode } Write-Host "Applying '$($resolved.program)' in $orgUrl [mode: $modeLabel]" -ForegroundColor Cyan # The target project must exist before any project-scoped reconcile call. $projectDecl = $resolved.project $projectName = if ($projectDecl -and $projectDecl.name) { $projectDecl.name } else { $resolved.program } if (-not (Test-AdoProject -OrgUrl $orgUrl -Project $projectName)) { $creationSpec = "process: $($projectDecl.process), visibility: $($projectDecl.visibility), sourceControl: $($projectDecl.sourceControl)" if ($mode -eq 'Apply') { New-AdoProject -OrgUrl $orgUrl -Project $projectName -Process $projectDecl.process ` -Visibility $projectDecl.visibility -SourceControl $projectDecl.sourceControl Write-Host " [+] project: $projectName ($creationSpec) [created]" -ForegroundColor Yellow } else { # WhatIf: every governed resource lives inside the project, so there # is nothing meaningful to diff until it exists — report and stop. Write-Host " [NON-COMPLIANT] create project: $projectName ($creationSpec) (dry-run: no changes made)" -ForegroundColor Cyan Write-Host "Project '$projectName' does not exist in $orgUrl. Apply would create it and then create every resource in the resolved model." -ForegroundColor Cyan return } } Invoke-GovernanceReconcile -Resolved $resolved -OrgUrl $orgUrl -Mode $mode -TeamIds $teamIds -Prune:$pruneEnabled | Out-Null # Persist discovered/created team GUIDs back to team-ids.yaml (Apply mode only) if ($mode -eq 'Apply' -and $teamIds.Count -gt 0) { $lines = [System.Collections.Generic.List[string]]::new() $lines.Add('# Auto-maintained by governance apply. Do not edit by hand.') $lines.Add('# Maps codePath -> ADO team GUID so teams can be found after a rename.') $lines.Add('teams:') foreach ($k in ($teamIds.Keys | Sort-Object)) { $lines.Add(" ${k}: $($teamIds[$k])") } Set-Content -Path $source.TeamIdsPath -Value $lines -Encoding utf8 Write-Host "Team state written: $($source.TeamIdsPath)" -ForegroundColor DarkCyan } } |