Public/Core/Update-EntraOps.ps1

<#
.SYNOPSIS
    Update of EntraOps PowerShell module and GitHub workflow files.

.DESCRIPTION
    Cmdlet is used to update the EntraOps PowerShell module and GitHub workflow files.
    If the config file's "AutomatedEntraOpsUpdate" section defines "Repository", "Branch" and/or
    "TargetUpdateFolders", those values are used instead of the built-in defaults, unless the
    corresponding -Repository, -Branch or -TargetUpdateFolders parameter is explicitly passed to
    this cmdlet. GitHub workflow definitions are excluded from the default targets. To update them
    interactively, explicitly select ./.github/workflows together with ./.github/actions and
    ./.github/scripts, then publish the reviewed local changes with an authorized user credential.

    The supported distribution repositories are declared once in the local EntraOpsUpdateContract.json:
    the public "EntraOps" release repository (default, no credentials) and the private
    "EntraOps-Insiders" repository, which requires -PersonalAccessToken or the ENTRAOPS_PAT
    environment variable (the EntraOpsUpdatePat secret in the workflow).

.PARAMETER Branch
    Upstream ref to update from: a branch, release tag or full commit SHA. Default is "main".
    Pin a release tag or commit SHA for unattended updates. Overridden by AutomatedEntraOpsUpdate.Branch
    in the config file unless passed explicitly.

.PARAMETER Repository
    Distribution repository to update from, resolved through the local EntraOpsUpdateContract.json
    (e.g. "EntraOps" or "EntraOps-Insiders"). Default is "EntraOps". Overridden by
    AutomatedEntraOpsUpdate.Repository in the config file unless passed explicitly. Ignored when -UpstreamUrl is set.

.PARAMETER UpstreamUrl
    Git URL of a custom upstream repository to clone instead of the contract-resolved repository.
    A Personal Access Token passed with -PersonalAccessToken is used for this URL. The ENTRAOPS_PAT environment
    variable is only used for github.com URLs. Credentials embedded in the URL are not logged or recorded.

.PARAMETER PersonalAccessToken
    Personal Access Token for private repositories (required for "EntraOps-Insiders"). Defaults to the
    ENTRAOPS_PAT environment variable. Passed to git as an HTTP header via environment variables and never
    sent to the public release repository.

.PARAMETER ConfigFile
    Path to the EntraOps configuration file. Default is "./EntraOpsConfig.json". Relative paths are
    resolved against the EntraOps repository root. Its AutomatedEntraOpsUpdate section can override
    -Repository, -Branch and -TargetUpdateFolders, and it is used to re-apply workflow parameters and
    Azure DevOps schedules after the update.

.PARAMETER TemporaryUpdateFolder
    Folder for the temporary clone of the update candidate, relative to the current PowerShell location
    unless absolute. Default is "TmpUpdate". It must not exist yet and is removed after the update.

.PARAMETER RunBrowserTests
    Also run the documentation and reporting browser tests when validating the update candidate.

.PARAMETER SkipCandidateValidation
    Skip validation of the update candidate before local files are replaced. Not allowed when
    ./.github/workflows is updated. Use only for fully trusted sources.

.PARAMETER PreparedCandidatePath
    Path to an already downloaded update candidate (git checkout) to apply instead of cloning, relative
    to the current PowerShell location unless absolute. The folder is left in place after the update.

.PARAMETER ValidatedSourceCommit
    Full commit SHA of a candidate that was already validated in a separate, credential-free job.
    Requires -PreparedCandidatePath. The update fails if the prepared candidate is at another commit,
    and validation is not repeated in this process.

.PARAMETER BrowserTestsValidated
    Records in .EntraOpsUpdateManifest.json that browser tests ran during the separate validation of
    -ValidatedSourceCommit.

.PARAMETER TargetUpdateFolders
    Repository-relative targets to replace from the configured upstream ref. Workflow definitions
    are supported but excluded by default. Select ./.github/workflows together with
    ./.github/actions and ./.github/scripts for an interactive workflow refresh. Automated workflow
    publication additionally requires the GitHub App credentials documented in Docs/content/core.md.
    Targets are resolved against the EntraOps repository root, independent of the current location.

.EXAMPLE
    This example updates EntraOps with default values of the main branch and the default target folders.
    Update-EntraOps

.EXAMPLE
    This example explicitly refreshes GitHub workflow definitions and their required dependencies.
    Update-EntraOps -ConfigFile ./EntraOpsConfig.json -RunBrowserTests `
        -TargetUpdateFolders @('./.github/actions', './.github/scripts', './.github/workflows')
#>


function Update-EntraOps {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $False)]
        [System.String]$Branch = "main"
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$Repository = "EntraOps"
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$UpstreamUrl
        ,        
        [Parameter(Mandatory = $False)]
        [System.String]$PersonalAccessToken
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$ConfigFile = "./EntraOpsConfig.json"
        ,
        [Parameter(Mandatory = $False)]
        [ValidateSet("./.azure-pipelines", "./.github/actions", "./.github/agents", "./.github/scripts", "./.github/workflows", "./Docs", "./EntraOps", "./Parsers", "./Queries", "./Reports", "./Samples", "./Tests", "./Workbooks", "./package.json", "./package-lock.json", "./playwright.config.mjs", "./CHANGELOG.md", "./EntraOpsUpdateContract.json")]
        [Object]$TargetUpdateFolders = @("./.github/actions", "./.github/agents", "./.github/scripts", "./Docs", "./EntraOps", "./Parsers", "./Queries", "./Reports", "./Samples", "./Tests", "./Workbooks", "./package.json", "./package-lock.json", "./playwright.config.mjs", "./CHANGELOG.md", "./EntraOpsUpdateContract.json")
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$TemporaryUpdateFolder = "TmpUpdate"
        ,
        [Parameter(Mandatory = $False)]
        [switch]$RunBrowserTests
        ,
        [Parameter(Mandatory = $False)]
        [switch]$SkipCandidateValidation
        ,
        [Parameter(Mandatory = $False)]
        [string]$PreparedCandidatePath
        ,
        [Parameter(Mandatory = $False)]
        [ValidatePattern('^[0-9a-fA-F]{40}$')]
        [string]$ValidatedSourceCommit
        ,
        [Parameter(Mandatory = $False)]
        [switch]$BrowserTestsValidated
    )

    $ErrorActionPreference = "Stop"

    if ([string]::IsNullOrWhiteSpace((Get-EntraOpsRepositoryRoot))) {
        throw "Update-EntraOps updates a repository checkout of EntraOps (GitHub, Azure DevOps or local clone). This module is installed without a repository (e.g. from the PowerShell Gallery): use Update-PSResource EntraOps (or Update-Module EntraOps if installed with Install-Module), then Initialize-EntraOpsWorkspace -Force to refresh classification templates and reporting apps."
    }
    # Allow the config file's "AutomatedEntraOpsUpdate" section to override the default
    # Repository/Branch/TargetUpdateFolders used for self-update, unless the caller explicitly
    # passed -Repository, -Branch or -TargetUpdateFolders.
    # Repository-relative paths (config file and update targets) resolve against the EntraOps
    # repository root rather than the caller's current location.
    $ConfigFilePath = if ([System.IO.Path]::IsPathRooted($ConfigFile)) { $ConfigFile } else { Join-Path $EntraOpsBaseFolder $ConfigFile }
    if (Test-Path -LiteralPath $ConfigFilePath) {
        try {
            $UpdateConfig = (Get-Content -LiteralPath $ConfigFilePath | ConvertFrom-Json).AutomatedEntraOpsUpdate
            if (-not $PSBoundParameters.ContainsKey('Repository') -and -not [string]::IsNullOrWhiteSpace($UpdateConfig.Repository)) {
                Write-Verbose "Using Repository '$($UpdateConfig.Repository)' from config file '$ConfigFile'."
                $Repository = $UpdateConfig.Repository
            }
            if (-not $PSBoundParameters.ContainsKey('Branch') -and -not [string]::IsNullOrWhiteSpace($UpdateConfig.Branch)) {
                Write-Verbose "Using Branch '$($UpdateConfig.Branch)' from config file '$ConfigFile'."
                $Branch = $UpdateConfig.Branch
            }
            if (-not $PSBoundParameters.ContainsKey('TargetUpdateFolders') -and $UpdateConfig.TargetUpdateFolders) {
                Write-Verbose "Using TargetUpdateFolders '$($UpdateConfig.TargetUpdateFolders -join ', ')' from config file '$ConfigFile'."
                $TargetUpdateFolders = @($UpdateConfig.TargetUpdateFolders)
            }
        } catch {
            Write-Warning "Failed to read Repository/Branch/TargetUpdateFolders override from config file '$ConfigFile'. Falling back to defaults. Error: $_"
        }
    }

    # Validate config-derived targets explicitly. ValidateSet protects command-line arguments, but
    # values read from JSON bypass parameter binding and must not escape the intended repository
    # update boundary.
    $AllowedUpdateTargets = @('./.azure-pipelines', './.github/actions', './.github/agents', './.github/scripts', './.github/workflows', './Docs', './EntraOps', './Parsers', './Queries', './Reports', './Samples', './Tests', './Workbooks', './package.json', './package-lock.json', './playwright.config.mjs', './CHANGELOG.md', './EntraOpsUpdateContract.json')
    $TargetUpdateFolders = @($TargetUpdateFolders | ForEach-Object { [string]$_ } | Select-Object -Unique)
    if ($TargetUpdateFolders.Count -eq 0) {
        throw 'At least one TargetUpdateFolders entry is required.'
    }
    $InvalidUpdateTargets = @($TargetUpdateFolders | Where-Object { $AllowedUpdateTargets -notcontains $_ })
    if ($InvalidUpdateTargets.Count -gt 0) {
        throw "Unsupported TargetUpdateFolders value(s) in '$ConfigFile': $($InvalidUpdateTargets -join ', '). Use only repository targets exposed by Update-EntraOps."
    }
    if ($TargetUpdateFolders -contains './.github/workflows') {
        $MissingWorkflowDependencies = @(@('./.github/actions', './.github/scripts') | Where-Object { $TargetUpdateFolders -notcontains $_ })
        if ($MissingWorkflowDependencies.Count -gt 0) {
            throw "Updating './.github/workflows' also requires target(s): $($MissingWorkflowDependencies -join ', ')."
        }
        if ($SkipCandidateValidation) {
            throw "Candidate validation cannot be skipped when './.github/workflows' is updated."
        }
    }
    if ([string]::IsNullOrWhiteSpace($Branch) -or $Branch.StartsWith('-') -or $Branch -match '\s') {
        throw "Unsupported automated-update ref '$Branch'. Use a branch, release tag, or full commit SHA without whitespace or a leading dash."
    }
    if ($ValidatedSourceCommit -and -not $PreparedCandidatePath) {
        throw '-ValidatedSourceCommit requires -PreparedCandidatePath. Resolve and validate the candidate in a separate, credential-free job before applying it.'
    }

    $PersonalAccessTokenFromEnvironment = $false
    if ([string]::IsNullOrWhiteSpace($PersonalAccessToken) -and -not [string]::IsNullOrWhiteSpace($env:ENTRAOPS_PAT)) {
        $PersonalAccessToken = $env:ENTRAOPS_PAT
        $PersonalAccessTokenFromEnvironment = $true
    }
    $UsePersonalAccessToken = $false

    if (-not [string]::IsNullOrWhiteSpace($UpstreamUrl)) {
        $UpstreamUri = $null
        if (-not [System.Uri]::TryCreate($UpstreamUrl, [System.UriKind]::Absolute, [ref]$UpstreamUri)) {
            $UpstreamUri = $null
        }
        # Credentials embedded in the URL must not reach the console, logs or .EntraOpsUpdateManifest.json
        $SourceRepository = $UpstreamUrl
        if ($null -ne $UpstreamUri -and -not [string]::IsNullOrEmpty($UpstreamUri.UserInfo)) {
            $SanitizedUpstreamUri = [System.UriBuilder]::new($UpstreamUri)
            $SanitizedUpstreamUri.UserName = ''
            $SanitizedUpstreamUri.Password = ''
            $SourceRepository = $SanitizedUpstreamUri.Uri.AbsoluteUri
        }
        if (-not [string]::IsNullOrWhiteSpace($PersonalAccessToken)) {
            # ENTRAOPS_PAT is a GitHub token: another host only gets an explicit -PersonalAccessToken
            if ($PersonalAccessTokenFromEnvironment -and ($null -eq $UpstreamUri -or $UpstreamUri.Host -ne 'github.com')) {
                Write-Warning "ENTRAOPS_PAT is not sent to the custom upstream host '$(if ($null -ne $UpstreamUri) { $UpstreamUri.Host } else { $SourceRepository })'. Pass -PersonalAccessToken to authenticate against it."
            } elseif ($null -eq $UpstreamUri) {
                throw "-PersonalAccessToken requires an absolute https URL as -UpstreamUrl. No local folder was changed."
            } else {
                $UsePersonalAccessToken = $true
            }
        }
    } else {
        # The local contract is the single source of truth for which Cloud-Architekt repositories
        # distribute EntraOps and whether they need a Personal Access Token. Resolving it here, before
        # any clone, keeps the credential decision independent of the downloaded candidate.
        $Source = Resolve-EntraOpsUpdateSource -Repository $Repository -ContractPath (Join-Path $EntraOpsBaseFolder 'EntraOpsUpdateContract.json')
        $SourceRepository = $Source.Repository
        if ($Source.RequiresPersonalAccessToken) {
            if ([string]::IsNullOrWhiteSpace($PersonalAccessToken) -and [string]::IsNullOrWhiteSpace($PreparedCandidatePath)) {
                throw "Update source '$SourceRepository' is a private distribution repository and requires a Personal Access Token. Pass -PersonalAccessToken, set ENTRAOPS_PAT (the EntraOpsUpdatePat secret), or use the public 'EntraOps' release repository. No local folder was changed."
            }
            $UsePersonalAccessToken = -not [string]::IsNullOrWhiteSpace($PersonalAccessToken)
        } elseif (-not [string]::IsNullOrWhiteSpace($PersonalAccessToken)) {
            if ($Source.IsKnownDistributionRepository) {
                Write-Verbose "Update source '$SourceRepository' is public. The provided Personal Access Token is not used."
            } else {
                $UsePersonalAccessToken = $true
            }
        }
    }

    # A branch name is mutable: whatever is on it at run time is installed and executed. Release tags
    # and full commit SHAs are immutable and are the supported choice for scheduled, unattended updates.
    if ($Branch -notmatch '^[0-9a-fA-F]{40}$' -and $Branch -notmatch '^v?\d+\.\d+') {
        Write-Warning "EntraOps is updated from the mutable ref '$Branch'. Any upstream commit is installed and executed on the next run without review. Pin 'AutomatedEntraOpsUpdate.Branch' in $ConfigFile to a release tag or a full commit SHA for unattended updates."
    }

    $CloneUpdateCandidate = {
        param([string]$RepositoryUrl)
        & git clone --filter=blob:none --no-checkout -- $RepositoryUrl $TemporaryUpdateFolder
        if ($LASTEXITCODE -ne 0) {
            throw "git clone failed with exit code $LASTEXITCODE. Verify the repository name and whether a Personal Access Token (secret: EntraOpsUpdatePat) is required."
        }
        & git -C $TemporaryUpdateFolder fetch --depth 1 origin -- $Branch
        if ($LASTEXITCODE -ne 0) {
            throw "git fetch failed for requested ref '$Branch' with exit code $LASTEXITCODE. Use an advertised branch, release tag, or reachable full commit SHA."
        }
        & git -C $TemporaryUpdateFolder checkout --detach FETCH_HEAD
        if ($LASTEXITCODE -ne 0) {
            throw "git checkout failed for requested ref '$Branch' with exit code $LASTEXITCODE."
        }
    }
    if (-not [string]::IsNullOrWhiteSpace($UpstreamUrl)) {
        $RepositoryUrl = $UpstreamUrl
    } else {
        $RepositoryUrl = $Source.RepositoryUrl
    }

    # Resolve relative paths against the PowerShell location. Set-Location does not move
    # [Environment]::CurrentDirectory, so [System.IO.Path]::GetFullPath would otherwise point at the
    # directory pwsh was started in while git clones into $PWD.
    $OwnCandidateCheckout = [string]::IsNullOrWhiteSpace($PreparedCandidatePath)
    if ($OwnCandidateCheckout) {
        $TemporaryUpdateFolder = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($TemporaryUpdateFolder)
        # The folder is removed after the update: never clone into (and later delete) an existing folder.
        if (Test-Path -LiteralPath $TemporaryUpdateFolder) {
            throw "Temporary update folder '$TemporaryUpdateFolder' already exists. Remove it or pass another -TemporaryUpdateFolder. No local folder was changed."
        }
    } else {
        $TemporaryUpdateFolder = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PreparedCandidatePath)
    }

    try {
        if (-not $OwnCandidateCheckout) {
            if (-not (Test-Path -LiteralPath $TemporaryUpdateFolder -PathType Container)) {
                throw "Prepared update candidate '$TemporaryUpdateFolder' does not exist. No local file was changed."
            }
            Write-Output "Using prepared update candidate at '$TemporaryUpdateFolder'."
        } elseif ($UsePersonalAccessToken) {
            $ChannelLabel = if ($null -ne $Source) { "$($Source.Channel) channel" } else { "custom upstream" }
            Write-Output "Cloning repository '$SourceRepository' ($ChannelLabel, ref: $Branch) using Personal Access Token..."
            # Pass credentials via environment-based HTTP header to avoid exposing the PAT in process listings, logs, or error messages
            $PreviousConfigCount = $env:GIT_CONFIG_COUNT
            $PreviousConfigKey0 = $env:GIT_CONFIG_KEY_0
            $PreviousConfigValue0 = $env:GIT_CONFIG_VALUE_0
            $UpstreamHost = if (-not [string]::IsNullOrWhiteSpace($UpstreamUrl)) { ([System.Uri]$UpstreamUrl).Host } else { "github.com" }
            try {
                $env:GIT_CONFIG_COUNT = "1"
                $env:GIT_CONFIG_KEY_0 = "http.https://$UpstreamHost/.extraheader"
                $env:GIT_CONFIG_VALUE_0 = "AUTHORIZATION: basic $([Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("x-access-token:$PersonalAccessToken")))"
                & $CloneUpdateCandidate $RepositoryUrl
            } finally {
                # Restore previous env state
                if ($null -eq $PreviousConfigCount) { Remove-Item env:GIT_CONFIG_COUNT -ErrorAction SilentlyContinue } else { $env:GIT_CONFIG_COUNT = $PreviousConfigCount }
                if ($null -eq $PreviousConfigKey0) { Remove-Item env:GIT_CONFIG_KEY_0 -ErrorAction SilentlyContinue } else { $env:GIT_CONFIG_KEY_0 = $PreviousConfigKey0 }
                if ($null -eq $PreviousConfigValue0) { Remove-Item env:GIT_CONFIG_VALUE_0 -ErrorAction SilentlyContinue } else { $env:GIT_CONFIG_VALUE_0 = $PreviousConfigValue0 }
            }
        } else {
            $ChannelLabel = if ($null -ne $Source) { "$($Source.Channel) channel" } else { "custom upstream" }
            Write-Output "Cloning repository '$SourceRepository' ($ChannelLabel, ref: $Branch) without authentication..."
            & $CloneUpdateCandidate $RepositoryUrl
        }

        $SourceCommit = (& git -C $TemporaryUpdateFolder rev-parse HEAD).Trim()
        if ($LASTEXITCODE -ne 0 -or $SourceCommit -notmatch '^[0-9a-fA-F]{40}$') {
            throw 'Could not resolve the immutable commit SHA of the downloaded update candidate. No local folder was changed.'
        }

        if ($ValidatedSourceCommit -and $SourceCommit -ne $ValidatedSourceCommit) {
            throw "Prepared candidate commit '$SourceCommit' does not match separately validated commit '$ValidatedSourceCommit'. No local folder was changed."
        }

        Test-EntraOpsUpdateContract -CandidateRoot $TemporaryUpdateFolder -Repository $SourceRepository -TargetUpdateFolders $TargetUpdateFolders | Out-Null

        # The contract validator confirms every requested target exists before anything is removed,
        # so a stale or dishonest source contract cannot leave the deployment partially updated.

        # List the files to install before candidate code runs (validation): git in the candidate afterwards could
        # run its hooks (e.g. core.fsmonitor) with this process' credentials, and .git or the index could be changed
        $CandidateIsGitCheckout = Test-Path -LiteralPath (Join-Path $TemporaryUpdateFolder '.git')
        $TargetSources = [ordered]@{}
        foreach ($TargetUpdateFolder in $TargetUpdateFolders) {
            if ($CandidateIsGitCheckout) {
                $TrackedFiles = @(Get-EntraOpsGitTrackedFile -Root $TemporaryUpdateFolder -Pathspec @($TargetUpdateFolder -replace '^\./', '') | ForEach-Object { $_.Path })
                if ($TrackedFiles.Count -eq 0) {
                    throw "Update candidate tracks no file in '$TargetUpdateFolder'. No local folder was changed."
                }
                $TargetSources[$TargetUpdateFolder] = $TrackedFiles
            } else {
                $TargetSources[$TargetUpdateFolder] = $null
            }
        }

        if ($ValidatedSourceCommit) {
            Write-Output "Applying candidate $SourceRepository@$SourceCommit after validation completed in a separate credential-free job."
        } elseif (-not $SkipCandidateValidation) {
            $CandidateValidator = Join-Path $EntraOpsBaseFolder '.github/scripts/Test-EntraOpsUpdateCandidate.ps1'
            if (-not (Test-Path -LiteralPath $CandidateValidator -PathType Leaf)) {
                throw "Trusted update candidate validator not found at '$CandidateValidator'. No local folder was changed."
            }
            Write-Output "Validating update candidate $SourceRepository@$SourceCommit in a child process with a sanitized environment before changing local files..."
            # Allowlist (not denylist) so any credential variable is excluded by construction. The OS/profile
            # entries carry no secrets and are required for pwsh, git and npm to start on Windows.
            $AllowedEnvironmentNames = @(
                'PATH', 'PSModulePath', 'LANG', 'LC_ALL', 'TMPDIR', 'TEMP', 'TMP', 'CI',
                'HOME', 'USER', 'USERNAME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
                'ProgramData', 'ProgramFiles', 'ProgramFiles(x86)', 'SystemRoot', 'SystemDrive', 'windir',
                'COMSPEC', 'PATHEXT', 'DOTNET_ROOT', 'DOTNET_CLI_TELEMETRY_OPTOUT', 'POWERSHELL_TELEMETRY_OPTOUT'
            )
            $SavedEnvironment = @{}
            foreach ($Entry in Get-ChildItem Env:) { $SavedEnvironment[$Entry.Name] = $Entry.Value }
            try {
                foreach ($Entry in Get-ChildItem Env:) { Remove-Item -LiteralPath "Env:$($Entry.Name)" -ErrorAction SilentlyContinue }
                foreach ($Name in $AllowedEnvironmentNames) {
                    if ($SavedEnvironment.ContainsKey($Name)) { Set-Item -LiteralPath "Env:$Name" -Value $SavedEnvironment[$Name] }
                }
                $ValidatorArguments = @('-NoLogo', '-NoProfile', '-NonInteractive', '-File', $CandidateValidator, '-CandidateRoot', $TemporaryUpdateFolder)
                if ($RunBrowserTests) { $ValidatorArguments += '-RunBrowserTests' }
                & (Get-Process -Id $PID).Path @ValidatorArguments
                if ($LASTEXITCODE -ne 0) { throw "Candidate validation child process failed with exit code $LASTEXITCODE." }
            } finally {
                foreach ($Entry in Get-ChildItem Env:) { Remove-Item -LiteralPath "Env:$($Entry.Name)" -ErrorAction SilentlyContinue }
                foreach ($Name in $SavedEnvironment.Keys) { Set-Item -LiteralPath "Env:$Name" -Value $SavedEnvironment[$Name] }
            }
        } else {
            Write-Warning 'Candidate validation was explicitly skipped. Apply only candidates from a fully trusted source.'
        }

        # Checked without git before any local folder is removed: a link would install the content of its target,
        # and a missing tracked file would leave the target incomplete
        foreach ($TargetUpdateFolder in $TargetUpdateFolders) {
            $RelativeTarget = $TargetUpdateFolder -replace '^\./', ''
            $SourcePath = Join-Path $TemporaryUpdateFolder $RelativeTarget
            # The target itself and every parent folder inside the candidate
            $Segments = $RelativeTarget -split '/'
            $LinkedItems = @(for ($Index = 1; $Index -le $Segments.Count; $Index++) {
                    $SegmentPath = Join-Path $TemporaryUpdateFolder ($Segments[0..($Index - 1)] -join '/')
                    $SegmentItem = Get-Item -LiteralPath $SegmentPath -Force -ErrorAction SilentlyContinue
                    if ($null -ne $SegmentItem -and $SegmentItem.LinkType) { $SegmentPath }
                })
            if ((Test-Path -LiteralPath $SourcePath -PathType Container) -and $LinkedItems.Count -eq 0) {
                $LinkedItems += @(Get-ChildItem -LiteralPath $SourcePath -Recurse -Force | Where-Object { $_.LinkType } | ForEach-Object { $_.FullName })
            }
            if ($LinkedItems.Count -gt 0) {
                throw "Update candidate contains symbolic link(s) or junction(s) in '$TargetUpdateFolder': $($LinkedItems -join ', '). No local folder was changed."
            }

            if ($null -ne $TargetSources[$TargetUpdateFolder]) {
                $MissingFiles = @($TargetSources[$TargetUpdateFolder] | Where-Object {
                        $SourceItem = Get-Item -LiteralPath (Join-Path $TemporaryUpdateFolder $_) -Force -ErrorAction SilentlyContinue
                        $null -eq $SourceItem -or $SourceItem.PSIsContainer -or $SourceItem.LinkType
                    })
                if ($MissingFiles.Count -gt 0) {
                    throw "Update candidate is missing tracked file(s) in '$TargetUpdateFolder' or they are no regular files: $(@($MissingFiles | Select-Object -First 10) -join ', '). No local folder was changed."
                }
            } elseif (-not (Test-Path -LiteralPath $SourcePath)) {
                throw "Update candidate no longer contains '$TargetUpdateFolder'. No local folder was changed."
            }
        }

        foreach ($TargetUpdateFolder in $TargetUpdateFolders) {
            $TargetPath = Join-Path $EntraOpsBaseFolder $TargetUpdateFolder

            if (Test-Path -LiteralPath $TargetPath) {
                Write-Output "Removing folder $TargetUpdateFolder..."
                try {
                    Remove-Item -LiteralPath $TargetPath -Force -Recurse
                } catch {
                    throw "Failed to remove folder $($TargetUpdateFolder). Error: $_"
                }
            }

            Write-Output "Updating folder $TargetUpdateFolder..."
            try {
                if ($null -ne $TargetSources[$TargetUpdateFolder]) {
                    # Only the tracked (validated) files of the checkout
                    foreach ($TrackedFile in $TargetSources[$TargetUpdateFolder]) {
                        $Destination = Join-Path $EntraOpsBaseFolder $TrackedFile
                        # .NET instead of New-Item: tracked paths can contain wildcard characters such as [ ]
                        [void][System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($Destination))
                        Copy-Item -LiteralPath (Join-Path $TemporaryUpdateFolder $TrackedFile) -Destination $Destination -Force
                    }
                } else {
                    Copy-Item -LiteralPath (Join-Path $TemporaryUpdateFolder $TargetUpdateFolder) -Destination $TargetPath -Force -Recurse
                }
            } catch {
                throw "Failed to copy folder $($TemporaryUpdateFolder)/$($TargetUpdateFolder) to $($TargetUpdateFolder). The local folder is now incomplete and must be restored from source control. Error: $_"
            }
        }

        # Do not execute newly downloaded module code in the updater process. In automated runs a
        # later step receives a repository token; in interactive runs the caller can still have a PAT,
        # Azure context or other credentials in its environment. Load the module in a subsequent clean
        # process/session after reviewing the applied diff.
        Write-Output 'Updated module import deferred until a subsequent clean process or session.'

        # Reapply the configuration with the helpers of the loaded (trusted) module. Candidate code must not
        # run here, not even in a child process: the token-bearing publish step runs next on the same agent.
        # The installed helpers must therefore understand the new templates; the GitHub helper rewrites the
        # whole workflow YAML and fails if a release renames a node it sets. That failure stays fatal, so
        # workflows with template defaults are never published.
        $RefreshGitHubWorkflows = $TargetUpdateFolders -contains './.github/workflows'
        $RefreshAzureDevOpsPipelines = $TargetUpdateFolders -contains './.azure-pipelines'
        if ($RefreshGitHubWorkflows -or $RefreshAzureDevOpsPipelines) {
            # The helpers resolve the configuration and template paths relative to the location
            Push-Location -LiteralPath $EntraOpsBaseFolder
            try {
                if ($RefreshGitHubWorkflows) {
                    Write-Host "Re-adding workflow parameters in GitHub workflows after update..."
                    try {
                        Update-EntraOpsRequiredWorkflowParameters -ConfigFile $ConfigFile
                    } catch {
                        throw "Failed to update required workflow parameters. Error: $_ The updated files were copied, but the configuration was not re-applied to them. The installed EntraOps module may not support the updated workflow templates: update the module in a clean session (Update-PSResource EntraOps or Update-Module EntraOps), then run Update-EntraOpsRequiredWorkflowParameters -ConfigFile '$ConfigFile' from the repository root and commit the result, or rerun the update."
                    }
                }
                # The upstream pipeline templates ship with default schedules; re-apply the ones from the configuration
                if ($RefreshAzureDevOpsPipelines) {
                    Write-Host "Re-applying the configured schedules and service connection in Azure DevOps pipelines after update..."
                    try {
                        Update-EntraOpsAzureDevOpsSchedules -ConfigFile $ConfigFile -PipelineFolder './.azure-pipelines'
                    } catch {
                        throw "Failed to update the Azure DevOps pipeline schedules. Error: $_ The updated files were copied, but the configuration was not re-applied to them. Update the EntraOps module in a clean session, then run Update-EntraOpsAzureDevOpsSchedules -ConfigFile '$ConfigFile' -PipelineFolder './.azure-pipelines' from the repository root and commit the result, or rerun the update."
                    }
                }
            } finally {
                Pop-Location
            }
        }

        $UpdateManifest = [ordered]@{
            SchemaVersion        = 1
            Repository           = $SourceRepository
            Channel              = $Source.Channel
            RequestedRef         = $Branch
            SourceCommit         = $SourceCommit
            ValidatedBeforeApply = [bool]($ValidatedSourceCommit -or -not $SkipCandidateValidation)
            ValidationIsolation  = if ($ValidatedSourceCommit) { 'SeparateJob' } elseif ($SkipCandidateValidation) { 'Skipped' } else { 'SanitizedChildProcess' }
            BrowserTestsRun      = [bool]($RunBrowserTests -or $BrowserTestsValidated)
            AppliedDateTime      = (Get-Date).ToUniversalTime().ToString('o')
            TargetUpdateFolders  = @($TargetUpdateFolders)
        }
        $UpdateManifestPath = Join-Path $EntraOpsBaseFolder '.EntraOpsUpdateManifest.json'
        $UpdateManifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $UpdateManifestPath -Encoding utf8
    } finally {
        # A caller-supplied -PreparedCandidatePath is left in place for inspection.
        if ($OwnCandidateCheckout -and (Test-Path -LiteralPath $TemporaryUpdateFolder)) {
            Write-Output "Cleaning up temporary folder $TemporaryUpdateFolder."
            Remove-Item -LiteralPath $TemporaryUpdateFolder -Force -Recurse -ErrorAction SilentlyContinue
        }
    }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA1koqCuk5R/IGf
# vf5RMyLpFp8qzjXZD6i/dZP8hQVztaCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
# ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
# dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l
# dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG
# A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl
# BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb
# Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC
# 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va
# qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ
# 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet
# /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe
# GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV
# WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl
# s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb
# MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc
# noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl
# jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA
# AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8
# +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud
# DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w
# bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v
# c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np
# dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG
# CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM
# BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1
# BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR
# mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl
# lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs
# NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv
# n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggZrMIIEU6ADAgECAhBX5gfP8LabvnxC
# XoItEBa/MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB
# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgMjAyMSBDQTAeFw0yNjEwMDYwODI4MjNaFw0yNzEwMDYwODI4MjJaMIGQ
# MQswCQYDVQQGEwJERTEdMBsGA1UECAwUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQ
# BgNVBAcMCUxhaG5zdGVpbjEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVy
# MS4wLAYDVQQDDCVPcGVuIFNvdXJjZSBEZXZlbG9wZXIgVGhvbWFzIE5hdW5oZWlt
# MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAphp2ljSmVsRB6gvkEq7A
# DUMENpKZiDFoAfF1ktD1nCwaVyI+bJ0ROjHwAn4iJgS+vlBDPOUIw2GLHkiqUhOg
# HSwzg6gj45GNmt6zxsKP0zPlhMB/fb0zmz7/F7e3yfKCXLfhbHuk0Es9gJ+8Wmet
# 9StVwwSE/K0MpW06GKA9nPWyE4N/ADwigDIj9rsTKJR4cve9w8G1b3zohpUc/qMr
# onab+HVw5r9b3sOBnnaXC6zvrX4fovRz1ztwfWjRjILLoFG6mW/TuK5F2ICD3Rhl
# 9R2rJWWWhzTDMsQlzvX+/SaM74q3OFpIVNdhvtmWU9pKUBRAyYMijcDqKEFGSzrc
# 8JFmGMpP5TuroWF+N21d3px2eARvW1mV0DCfFAH+Og20SH/gVW6/ltQQEkH/z/Zt
# 49ARyJrgESLOQtlMCIYSoKISdkk4ml4VUS1E/PcbzXEYj3a7m6DOxcd3WuY03UT9
# cnk3p0LMN6LeLYAZgVY+HRloZlH+kW2SkOHNuWgtXvZhAgMBAAGjggF4MIIBdDAM
# BgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3NjYTIwMjEu
# Y3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcwZTAsBggr
# BgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20wNQYIKwYB
# BQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIwMjEuY2Vy
# MB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQWBBTBvGyH
# MXj1dMbVnxuof9oP4PY7cDBLBgNVHSAERDBCMAgGBmeBDAEEATA2BgsqhGgBhvZ3
# AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwvQ1BTMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF
# AAOCAgEAkC165c7/r6y3VeEEpNXas9bQs6ZpKPdJffHkf9xoPi4kWvAIWGc4ocG1
# DJLb18RXiDYShDmT5sb/6I08FeipKi2DNJkuOq6yOTJ3S7+hNFIZCyd6i7WKO8ea
# P5I+FAbyJP4+5coZoMg45bUa9JoocaJCm+5n7WiCfN7/EQpClW5Trm4+ZMbp7EMN
# F1uLJgGCMnN0SsoUarp7PHlqupMxMC4Uhhe+lAP2ytcSIU81L97/qjrADbLWqICk
# 9il5iGM+aZndguq/cmfH9rJj4GRZHWnG6WwnzYD9+KgYlRI6em+/9dq+IRiXe2Bs
# thUuIaFW7U2gWw8tBTEYm1UBLHdJx+L1gjJCLs5KT/wy5wU32wZxNDqPUQejUC1x
# GvU3cKqmLgRq+yNf3gFVfzqIoggyM3U9wQvnk7nBtCkpjXS2hdx4yxzcmrlWsDHr
# z6+6/jeyQySzbYUlFmKnc4n9Mw2soHOajEua5poFARVRS5KgUBuiiQ21YFNYzWBo
# qGuECaaAI/C5rGxapMJ3dAOBnHw6NLqBet+ZXBJEccX5y89MLlOQu+jDmJP8HieR
# qDLlwfhVZnw4RY8Rus43NZBwhNlVuOUXqocHKmJhKNlekda3qsJj93eQBQOxKJcc
# w5O08q9jukpH2+yzFAj+7ll33DuNUgWptogc4Awtbx3glzcEvJQwggaCMIIEaqAD
# AgECAhAo8HfBHDa9/l90MkdwJy4DMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYT
# AlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMT
# G0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQTAeFw0yNjAzMTEwNzM0NTRaFw0z
# NjAyMjcwNzM0NTRaMFAxCzAJBgNVBAYTAlBMMSEwHwYDVQQKDBhBc3NlY28gRGF0
# YSBTeXN0ZW1zIFMuQS4xHjAcBgNVBAMMFUNlcnR1bSBUaW1lc3RhbXAgMjAyNjCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALi9zfBw+xI12hi2F9uNQBNO
# STkTIi13fuh3DiWybI5392bBfJw6tO7zDs7UqP/rejVf4xFTU3XNOPsuOFkP5SPF
# snWwn3VJL+NhNhsuujvYsQtaQnvNo1wk46reTgws1L8OFEdKaxgyqT4zOQx81mLw
# NZSqXNU9Rrb9oAJWJF4Mydz+C1ebu0D0+vgM3tqAQ8TU203fqgKCPqIdHloaFqH9
# eyR+8tnpmy2hnnN59ZOtmsuHeRwf5iTOhlhf0d/qqquLOSIGPgGVL1Hj5XBUox2O
# E7tQRr+l/xxNbL6OuwBO4Aa2EbuxYg37cpXv5i2/lTL4NASB/duf9W4fjA3Ro+GP
# +bC54n2OYiM3Cq8z92ejx8F5HWO2e4/75eC95B9tMw/eq7109GDyW2+2ut5Bn0o8
# MXJTERkyystWbqcnUkomA97qfiDCwvpWuOMdfTOksXe9S+xDxOJaCbiyTivgPulx
# cT+tfQTRXBr5UwtPvc+INnafOEilzVHA11tv+pHcylryxBc08cUihf/ttqkZS0bm
# x3VxQay8uohaTp0//ECA9vYGW51SKdJMJhXvKyIdBLy8WYNchTt8yRITv9Adxg9J
# qGujh+TnP1bW3aQjFtlEl7SzgNeI8OVA/CJAWWUuRT3Au9ToOpHy/flrrknhN4he
# Ew7/6ILMv4HSsj0YZ4otAgMBAAGjggFQMIIBTDB1BggrBgEFBQcBAQRpMGcwOwYI
# KwYBBQUHMAKGL2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdHNj
# YTIwMjEuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMB8GA1UdIwQYMBaAFL5UAi+/QGxzQ86sCSVOnkNEGu7gMAwGA1UdEwEB/wQC
# MAAwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0ucGwv
# Y3RzY2EyMDIxLmNybDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E
# BAMCB4AwIgYDVR0gBBswGTAIBgZngQwBBAIwDQYLKoRoAYb2dwIFAQswHQYDVR0O
# BBYEFCM5aiirmhKnpKfyH/lFmr+N/oIDMA0GCSqGSIb3DQEBDAUAA4ICAQBm/pOb
# rAveTqYFPCSthxNVwBHTW4TfuBQY2KGCdMlH6ALjVPYf2XARNGMTXScR0JQ2/c2L
# xLpoHHhRwxFf3d933DhFS165rYj6iPSQKm41rc9AMpTCjMdmai1aK9o870pOzTzL
# RhWFVIxm+5Qf+t4V5z14tPoUsAWdfRKD1Zzvzfe5AHlE9rxOZcGROiULxs3Edf7Z
# J8RZI0jzD9juj2oEX2XS7BVKuNCK1W0h1mjapv028yqo8UtZh5/1Ib48Yyp88BDY
# Shh5PSGUUuS5tYXVfcduIPBe3PdNTYRYdT5VNxkwLgjMzIcr/6SH2fejFJDjnCxj
# vXLuarErJg/IdI+hXdGIBDVOGv1zrkF4ktoQvFZIp5DIAYh0GwyUhc87qqn5UaYT
# j2+iyiBDSNh7aGCrkuAnqg7Qh7frhX+QhMGeJ9SpLpnG76qN85hLyjvYR6gRYiLT
# j8G4fxjqV400/buPw0rBo9yJZyq/8cGdCOLPOYf9qpefTcrZKeO9pg+UJHx8UABb
# aTqizRxm8sUSEycccCCFhIit3AI/3wGuSOADeiA676NcQ7+dsP04YvzvdN7pTZNy
# g672WpBDJdg0eItHl4yomvNeIOWh1qs4QqOW6TI8Ocsi4uSDTLorxLUDnBTwKQzz
# pxd1YUctEgRh32R0J9FkFkeEruJU94RljFr1uDCCBrkwggShoAMCAQICEQCZo4AK
# JlU7ZavcboSms+o5MA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAG
# A1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVt
# IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3Rl
# ZCBOZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjE4WhcNMzYwNTE4MDUzMjE4WjBW
# MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu
# MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQCdI88EMCM7wUYs5zNzPmNdenW6vlxNur3r
# Lfi+5OZ+U3iZIB+AspO+CC/bj+taJUbMbFP1gQBJUzDUCPx7BNLgid1TyztVLn52
# NKgxxu8gpyTr6EjWyGzKU/gnIu+bHAse1LCitX3CaOE13rbuHbtrxF2tPU8f253Q
# gX6eO8yTbGps1Mg+yda3DcTsOYOhSYNCJiL+5wnjZ9weoGRtvFgMHtJg6i671OPX
# IciiHO4Lwo2p9xh/tnj+JmCQEn5QU0NxzrOiRna4kjFaA9ZcwSaG7WAxeC/xoZSx
# F1oK1UPZtKVt+yrsGKqWONoK6f5EmBOAVEK2y4ATDSkb34UD7JA32f+Rm0wsr5aj
# zftDhA5mBipVZDjHpwzv8bTKzCDUSUuUmPo1govD0RwFcTtMXcfJtm1i+P2UNXad
# PyYVKRxKQATHN3imsfBiNRdN5kiVVeqP55piqgxOkyt+HkwIA4gbmSc3hD8ke66t
# 9MjlcNg73rZZlrLHsAIV/nJ0mmgSjBI/TthoGJDydekOQ2tQD2Dup/+sKQptalDl
# ui59SerVSJg8gAeV7N/ia4mrGoiez+SqV3olVfxyLFt3o/OQOnBmjhKUANoKLYlK
# mUpKEFI0PfoT8Q1W/y6s9LTI6ekbi0igEbFUIBE8KDUGfIwnisEkBw5KcBZ3XwnH
# mfznwlKo8QIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
# 3XRdTADbe5+gdMqxbvc8wDLAcM0wHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61Pgc
# ptE6Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDAGA1Ud
# HwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYI
# KwYBBQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0
# dW0uY29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
# Y3RuY2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0
# cDovL3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQB1iFgP5Y9Q
# KJpTnxDsQ/z0O23JmoZifZdEOEmQvo/79PQg9nLF/GJe6ZiUBEyDBHMtFRK0mXj3
# Qv3gL0sYXe+PPMfwmreJHvgFGWQ7XwnfMh2YIpBrkvJnjwh8gIlNlUl4KENTK5DL
# qsYPEtRQCw7R6p4s2EtWyDDr/M58iY2UBEqfUU/ujR9NuPyKk0bEcEi62JGxauFY
# zZ/yld13fHaZskIoq2XazjaD0pQkcQiIueL0HKiohS6XgZuUtCKA7S6CHttZEsOb
# QJ1j2s0urIDdqF7xaXFVaTHKtAuMfwi0jXtF3JJphrJfc+FFILgCbX/uYBPBlbBI
# P4Ht4xxk2GmfzMn7oxPITpigQFJFWuzTMUUgdRHTxaTSKRJ/6Uh7ki/pFjf9sUAS
# WgxT69QF9Ki4JF5nBIujxZ2sOU9e1HSCJwOfK07t5nnzbs1LbHuAIGJsRJiQ6HX/
# DW1XFOlXY1rc9HufFhWU+7Uk+hFkJsfzqBz3pRO+5aI6u5abI4Qws4YaeJH7H7M8
# X/YNoaArZbV4Ql+jarKsE0+8XvC4DJB+IVcvC9Ydqahi09mjQse4fxfef0L7E3hh
# o2O3bLDM6v60rIRUCi2fJT2/IRU5ohgyTch4GuYWefSBsp5NPJh4QRTP9DC3gc5Q
# EKtbrTY0Ka87Web7/zScvLmvQBm8JDFpDjCCBrkwggShoAMCAQICEQDn/2nHOzXO
# S5Em2HR8aKWHMA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UE
# ChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl
# cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBO
# ZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjA3WhcNMzYwNTE4MDUzMjA3WjBWMQsw
# CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQw
# IgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0EwggIiMA0GCSqGSIb3
# DQEBAQUAA4ICDwAwggIKAoICAQDpEh8ENe25XXrFppVBvoplf0530W0lddNmjtv4
# YSh/f7eDQKFaIqc7tHj7ox+u8vIsJZlroakUeMS3i3T8aJRC+eQs4FF0GqvkM6+W
# ZO8kmzZfxmZaBYmMLs8FktgFYCzywmXeQ1fEExflee2OpbHVk665eXRHjH7MYZIz
# Nnjl2m8Hy8ulB9mR8wL/W0v0pjKNT6G0sfrx1kk+3OGosFUb7yWNnVkWKU4qSxLv
# 16kJ6oVJ4BSbZ4xMak6JLeB8szrK9vwGDpvGDnKCUMYL3NuviwH1x4gZG0JAXU3x
# 2pOAz91JWKJSAmRy/l0s0l5bEYKolg+DMqVhlOANd8Yh5mkQWaMEvBRE/kAGzIqg
# WhwzN2OsKIVtO8mf5sPWSrvyplSABAYa13rMYnzwfg08nljZHghquCJYCa/xHK9a
# cev9UD7Y+usr15d7mrszzxhF1JOr1Mpup2chNSBlyOObhlSO16rwrffVrg/SzaKf
# SndS5swRhr8bnDqNJY9TNyEYvBYpgF95K7p0g4LguR4A++Z1nFIHWVY5v0fNVZmg
# zxD9uVo/gta3onGOQj3JCxgYx0KrCXu4yc9QiVwTFLWbNdHFSjBCt5/8Q9pLuRhV
# ocdCunhcHudMS1CGQ/Rn0+7P+fzMgWdRKfEOh/hjLrnQ8BdJiYrZNxvIOhM2aa3z
# EDHNwwIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvlQC
# L79AbHNDzqwJJU6eQ0Qa7uAwHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61PgcptE6
# Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMDAGA1UdHwQp
# MCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYIKwYB
# BQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ru
# Y2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0cDov
# L3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQC4k1l3yUwV/ZQH
# CKCneqAs8EGTnwEUJLdDpokN/dMhKjK0rR5qX8nIIHzxpQR3TAw2IRw1Uxsr2Pli
# G3bCFqSdQTUbfaTq6V3vBzEebDru9QFjqlKnxCF2h1jhLNFFplbPJiW+JSnJTh1f
# KEqEdKdxgl9rVTvlxfEJ7exOn25MGbd/wGPwuSmMxRJVO0wnqgS7kmoJjNF9zqee
# hFSDDP8ZVkWg4EZ2tIS0M3uZmByRr+1Lkwjjt8AtW83mVnZTyTsOb+FNfwJY7DS4
# FmWhkRbgcHRetreoTirPOr/ozyDKhT8MTSTf6Lttg6s6T/u08mDWw6HK04ZRDfQ9
# sb77QV8mKgO44WGP31vXnVKoWVJpFBjPvjL8/Zck/5wXX2iqjOaLStFOR/IQki+E
# hn4zlcgVm22ZVCBPF+l8nAwUUShCtKuSU7GmZLKCmmxQMkSiWILTm8EtVD6AxnJh
# oq8EnhjEEyUoflkeRF2WhFiVQOmWTwZRr44IxWGkNJC6tTorW5rl2Zl+2e9JLPYf
# 3pStAPMDoPKIjVXd6NW2+fZrNUBeDo2eOa5Fn7Brs/HLQff5Xgris5MeUbdVgDrF
# 8uxO6cLPvZPo63j62SsNg55pTWk9fUIF9iPoRbb4QurjoY/woI1RAOKtYtTic6aA
# Jq3u83RIPpGXBSJKwx4KJAOZnCDCtTGCBqIwggaeAgEBMGowVjELMAkGA1UEBhMC
# UEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMb
# Q2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBX5gfP8LabvnxCXoItEBa/MA0G
# CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI
# hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC
# ARUwLwYJKoZIhvcNAQkEMSIEIDYPmWBoJizPsbwNezw9Q4t26IB/FeYWzikY6I8Q
# CoHlMA0GCSqGSIb3DQEBAQUABIIBgF//i4M/c0Rd8eHhjRzsvDQx30hOISD6eMEt
# h10UbCwJ04K2E8cB1PI3VJgTESxJfFuqBT/zBaCRvGYBhj2wSrQe9pC4kepFKyoN
# 5K0HIazV3vq/Vwzk1o8eOjk8rXhX3SsfvQAgPAt+fORHRCrH/A3UnMlrWCpFaXSN
# A02ybKv/bJwxBwg4OYCAysHRs/7uNNUeU5T8er1vRYITwccbf2VAP8T4dq4VzRT5
# 93RAw3To957NTSsFZGol+NlMyaqFv0zeYD7jePEyAwlrz4cHj8MPcgQsrRyXazdD
# +a3h4t6e92TXAQq37vHfKRr3D8y22wW000P1qxBUwA0X9zfzz8xvAfLw4hMXhJkQ
# TKYAD1867GIGFauayaEgaxzwgI/2HgWHfDWgZxKA2+fCGq7qbA0/xQd7vzB6oEG6
# JvlkRjBPZcW3f7GUtfvmn51F+4RLKLfeJKszzG8rE7FmTqWzQ/RizLx7GJS1kaHf
# VyN71BHV8PWhXBWkDxlVVz6a2DbTJaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTM5MTlaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCcfT5gNMviZdQcIulVyQr2TgbczJAebeZX
# 8vbHnWZW4NLIQkm319ohO81ZK+13bK4wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIANxPXTX2Yp+VZ3gjNM6Hq+D9cOBPZtWOAGRzxu1OSJbf1GchJ
# Sa0eQunTrToYquMdEbDkYqiaopt3z8QjFPHW/7NFAeQ1B/N7NTbM6OWb6wTrsLgS
# 3a800uzQkF3nohO+lTdr6kKwhPHWaP62DWbccc6UKMW8ryLtR5YXNZjWYPmHMsxf
# L8133VKdlFqh8d/DqhXyuo4bXC7JhkxJCd5MVmkRPvhAhL/E7Gf2TNQFhrKCD2i3
# y/vUVOiq0Yh1vpgBJXJIGzKiCZo1r4tS7r2PNglALVK7aqq0qCaSkhMr9zc2QB7P
# mX8mo4xTqzgbpH+K/ipZbsLSW5ti+bPQ6Z5MZVqiWB7FJ7e+bIL8oEyUI1tlL4ek
# Jat3l4IvXEKCcE66A8k4bZFuRgVCFRwhVzsQQYpocI29kUhpp4lezyiqGfMvoILz
# VHs5NDI4QPA1LffLBILGDqrXFVwNebOjkBho851UInxcJE/OPZ5etwkwdP6CGQju
# LH9DFP23btq4cAn99mzCthXNz0/avNoe9wBpmtYfQKVCziVrC7K/r3VTKGMY74uB
# FfEdNsRgcgXxDOWah54l4Q9WjGfnKX3I1Lm+yMRPPwUSuxmvbLne2OaDWlMRmxZc
# qx9OUoXqQuy7UAPzbkia27PMGqUXa5JnrbXs1mBw9sY+O6T4M8H7hjETi9o=
# SIG # End signature block