Templates/migrations/github-repos/Run-Export-RepoInventory.ps1

<#
.SYNOPSIS
    Builds or refreshes this migration's repository inventory/approval CSV from
    the source Azure DevOps organisation named in github-repos-config.json.
 
.DESCRIPTION
    Enumerates every project and every git repository in the source organisation
    and merges the result into the inventory CSV next to this script (see
    Export-GitRepoInventory for the merge rules: customer-edited TargetName,
    Approved and Notes columns are always preserved; fact columns are
    refreshed; vanished repositories are marked MissingFromSource, never
    deleted).
 
    The CSV is the engagement's approval record: commit it, have the customer
    mark the repositories to migrate with Approved = yes (editing TargetName
    where the pre-filled name is not wanted), and commit their edits. Then run
    Run-Migrate-ReposToGitHub.ps1 (or Sync.ps1) to migrate the approved rows.
 
.PARAMETER ConfigPath
    Optional path to the configuration file. Defaults to
    github-repos-config.json alongside this script.
 
.PARAMETER IncludeDisabled
    Also add rows for disabled source repositories (existing rows are always
    refreshed regardless).
#>

[CmdletBinding(SupportsShouldProcess = $true)]
param(
    [string]$ConfigPath = (Join-Path $PSScriptRoot 'github-repos-config.json'),
    [switch]$IncludeDisabled
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

if (-not (Get-Module -Name 'NKDAgility.AzureDevOps.AutomationTools')) {
    . "$PSScriptRoot\..\..\init.ps1"
}

if (-not (Test-Path -LiteralPath $ConfigPath)) {
    throw "Configuration file not found: $ConfigPath"
}

Write-Host "==> Loading config: $ConfigPath" -ForegroundColor Cyan
$config = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json

# Expands "$ENV:NAME" / "${ENV:NAME}" placeholders in a config string using the
# process environment. JSON is not expanded by PowerShell, so we resolve any
# environment-variable references (tokens loaded by Set-AutomationSecrets)
# ourselves before use. Tokens are fallbacks behind ambient identity, so
# -AllowMissing returns $null for an unset variable instead of throwing.
function Expand-EnvPlaceholder {
    param([string]$Value, [switch]$AllowMissing)
    try {
        [regex]::Replace($Value, '\$(?:ENV:(?<n>\w+)|\{ENV:(?<n>\w+)\})', {
            param($m)
            $name = $m.Groups['n'].Value
            $resolved = [Environment]::GetEnvironmentVariable($name)
            if ([string]::IsNullOrEmpty($resolved)) {
                throw "Environment variable '$name' referenced in config is not set. Run Set-AutomationSecrets first (Sync.ps1 does this automatically)."
            }
            $resolved
        }, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
    }
    catch {
        if ($AllowMissing) { return $null }
        throw
    }
}

$inventoryPath = $config.InventoryCsv
if (-not [System.IO.Path]::IsPathRooted($inventoryPath)) {
    $inventoryPath = Join-Path $PSScriptRoot $inventoryPath
}

$sourceOrg = Expand-EnvPlaceholder -Value $config.SourceOrg
$params = @{
    Collection = $sourceOrg
    Path       = $inventoryPath
}

# Ambient identity first: probe Entra (the module caches the token, so the inventory
# call reuses it) and only fall back to the configured PAT when Entra is unavailable.
try {
    $null = Get-AzureDevOpsAccessToken -Collection $sourceOrg
}
catch {
    $sourcePat = Expand-EnvPlaceholder -Value $config.SourcePat -AllowMissing
    if (-not $sourcePat) {
        throw ("Neither Entra sign-in nor a source PAT is available for {0}: {1}" -f $sourceOrg, $_.Exception.Message)
    }
    Write-Warning ("Entra sign-in unavailable ({0}); using the configured source PAT." -f $_.Exception.Message)
    $params.Pat = $sourcePat
}

# The GitHub side is optional here: with it, pre-filled TargetNames are also
# collision-checked against repositories that already exist in the target org.
# Same policy: the gh CLI / GITHUB_TOKEN resolve inside the module; the config
# token is only passed when that ambient resolution fails. With no GitHub
# credential at all the inventory still runs - the collision check is skipped
# with a warning, because the inventory itself only needs the source.
if ($config.PSObject.Properties['GitHubOrg'] -and $config.GitHubOrg) {
    try {
        $null = Get-GitHubAccessToken -Org (Expand-EnvPlaceholder -Value $config.GitHubOrg)
        $params.GitHubOrg = Expand-EnvPlaceholder -Value $config.GitHubOrg
    }
    catch {
        $githubToken = Expand-EnvPlaceholder -Value $config.GitHubToken -AllowMissing
        if ($githubToken) {
            $params.GitHubOrg = Expand-EnvPlaceholder -Value $config.GitHubOrg
            $params.GitHubToken = $githubToken
        }
        else {
            Write-Warning ("No GitHub credential available; skipping the collision check against '{0}'. ({1})" -f $config.GitHubOrg, $_.Exception.Message)
        }
    }
}
if ($IncludeDisabled) { $params.IncludeDisabled = $true }

Export-GitRepoInventory @params

Write-Host ''
Write-Host 'Next steps:' -ForegroundColor Cyan
Write-Host " 1. Commit $(Split-Path -Leaf $inventoryPath) - it is the engagement's approval record." -ForegroundColor Gray
Write-Host ' 2. Have the customer mark rows Approved = yes (and adjust TargetName where wanted).' -ForegroundColor Gray
Write-Host ' 3. Commit their edits, then run .\Sync.ps1 -WhatIf to preview the migration.' -ForegroundColor Gray