Public/Common/New-Migration.ps1

function New-Migration {
    <#
    .SYNOPSIS
    Scaffolds a new numbered migration folder in the customer workspace from a template.
 
    .DESCRIPTION
    Creates migrations\NN-<Name>\ (NN = next free number) in the workspace and copies the
    matching template from the module's own Templates\migrations\<type>\ folder:
 
      DataImport - Microsoft Data Import Tool (Migrator.exe) collection lift-and-shift:
                          DataImport-Scratchbook.ps1 + DataImport-Cleanup.ps1
      MigrationTools - Azure DevOps Migration Tools engagement: Sync.ps1 orchestrator,
                          Run-Migrate-Repos/Artifacts binders, configuration-*.json
      MigrationPlatform - Azure DevOps Migration Platform engagement: Sync.ps1 + platform-config.json
      GitHubRepos - Azure DevOps repos to GitHub engagement: inventory/approval CSV
                          workflow, Sync.ps1 orchestrator, github-repos-config.json
 
    .PARAMETER Name
    Migration name; becomes the folder suffix (e.g. 'CollectionImport' -> '01-CollectionImport').
 
    .PARAMETER Type
    Which template to scaffold: DataImport, MigrationTools, MigrationPlatform or GitHubRepos.
 
    .PARAMETER Path
    Workspace root. Defaults to the initialised workspace.
 
    .EXAMPLE
    New-Migration -Name 'CollectionImport' -Type DataImport
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateSet('DataImport', 'MigrationTools', 'MigrationPlatform', 'GitHubRepos')]
        [string]$Type,

        [string]$Path
    )

    if (-not $Path) {
        $Path = (Get-AutomationWorkspace).Root
    }

    $templateFolderNames = @{
        DataImport        = 'data-import'
        MigrationTools    = 'migration-tools'
        MigrationPlatform = 'migration-platform'
        GitHubRepos       = 'github-repos'
    }
    # Templates ship inside the module so they travel with it when it is copied into a
    # client workspace, and stay locked to the engine version that consumes them. Never
    # resolve them by walking up from the module root - above it is the client's repo.
    $templatePath = Join-Path $script:ModuleRoot (Join-Path 'Templates\migrations' $templateFolderNames[$Type])
    if (-not (Test-Path -LiteralPath $templatePath)) {
        throw "Migration template not found: $templatePath"
    }

    $migrationsFolder = Join-Path $Path 'migrations'
    New-Item -Path $migrationsFolder -ItemType Directory -Force | Out-Null

    # Next free NN- prefix across existing migration folders.
    $existingNumbers = Get-ChildItem -Path $migrationsFolder -Directory |
        Where-Object { $_.Name -match '^(\d+)-' } |
        ForEach-Object { [int]$Matches[1] }
    $next = if ($existingNumbers) { [int]($existingNumbers | Measure-Object -Maximum).Maximum + 1 } else { 1 }
    $migrationFolder = Join-Path $migrationsFolder ('{0:D2}-{1}' -f $next, $Name)
    if (Test-Path -LiteralPath $migrationFolder) {
        throw "Migration folder already exists: $migrationFolder"
    }

    Copy-Item -Path $templatePath -Destination $migrationFolder -Recurse

    # In a workspace the module lives in .system\, whose files init.ps1 marks read-only
    # (and Copy-Item preserves the attribute). Scaffolded files are seeds the engagement
    # owns and edits, so clear it.
    Get-ChildItem -Path $migrationFolder -Recurse -File | ForEach-Object { $_.IsReadOnly = $false }

    # Seed provenance. Engagement folders are copied once and then owned and edited by
    # the engagement, so template improvements never reach them. Recording what produced
    # this one is what lets you tell, years later, why it does not match today's template.
    @{
        type          = $Type
        template      = $templateFolderNames[$Type]
        moduleVersion = [string]$MyInvocation.MyCommand.Module.Version
        scaffoldedAt  = (Get-Date).ToString('o')
    } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $migrationFolder '.template.json')

    Write-FixStep "Created $migrationFolder ($Type)"
    Get-ChildItem -Path $migrationFolder -Recurse -File -Name | ForEach-Object {
        Write-Host " $_" -ForegroundColor DarkGray
    }

    return Get-Item -LiteralPath $migrationFolder
}