Microsoft.PowerPlatform.DevOps.psm1

Write-Verbose "Importing Functions"

# Import everything in these folders
foreach ($folder in @('private', 'public', 'classes')) {
    
    $root = Join-Path -Path $PSScriptRoot -ChildPath $folder
    if (Test-Path -Path $root) {
        Write-Verbose "processing folder $root"
        $files = Get-ChildItem -Path $root -Filter *.ps1

        # dot source each file
        $files | where-Object { $_.name -NotLike '*.Tests.ps1' } | 
        ForEach-Object { Write-Verbose $_.name; . $_.FullName }
    }
}

if (!(Test-Path "$env:APPDATA\Microsoft.PowerPlatform.DevOps")) {
    New-Item -Path "$env:APPDATA\Microsoft.PowerPlatform.Devops" -ItemType Directory
}
if (!(Test-Path "$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json")) {
    Copy-Item (Join-Path $PSScriptRoot "\devopsConfig.json") -Destination "$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json"
}
$global:configFile = Get-Content ("$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json") | ConvertFrom-Json
$global:version = Get-ManifestValue (Join-Path $PSScriptRoot "\Microsoft.PowerPlatform.DevOps.psd1") -PropertyName 'ModuleVersion' -Passthru
$global:projectFile = "False"
$global:gitRepo = ""

if ($global:projectFile -eq "False") {
    $global:projectTitle = "(No Project Selected)"
}
else {
    $global:projectTitle = $global:projectFile.gitRepo
}

$global:logo = @"
____ ____ _ _ __ ____ ___
| _ \ _____ _____ _ __ | _ \| | __ _| |_ / _| ___ _ __ _ __ ___ | _ \ _____ __/ _ \ _ __ ___
| |_) / _ \ \ /\ / / _ \ '__| | |_) | |/ _` | __| |_ / _ \| '__| '_ ` _ \ | | | |/ _ \ \ / / | | | '_ \/ __|
| __/ (_) \ V V / __/ | | __/| | (_| | |_| _| (_) | | | | | | | | | |_| | __/\ V /| |_| | |_) \__ \
|_| \___/ \_/\_/ \___|_| |_| |_|\__,_|\__|_| \___/|_| |_| |_| |_| |____/ \___| \_/ \___/| .__/|___/
                                                                                                  |_|
 
"@


$global:message = @"
 
Welcome to the Power Platform DevOps configuration script.
 
 ver: $global:version
 project: $global:projectTitle
 
"@


function Install-PreReqs {
    $message = "Checking Pre-requisites"
    Write-Host $message

    try {
        Invoke-InstallPreRequisites
        $global:configfile.PreReqsComplete = "True"
    }
    catch {
        $global:configfile.PreReqsComplete = "Error"
        pause
    }
    $global:configfile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json")
   
}

function Connect-AzureDevOps {
    $message = "Configuring Azure DevOps"
    Write-Host $message

    try {
        Invoke-AzureDevOps
        $global:projectFile.ADOConfigured = "True"
    }
    catch {
        $global:projectFile.ADOConfigured = "Error"
        pause
    }
    $global:projectFile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.Devops\$global:gitRepo\$global:gitRepo.json") 
   
}

function Add-Solution {
    $message = "Adding D365 / CDS Solution"
    Write-Host $message

    try {
        Invoke-AddSolution
        $global:projectFile.SolutionAdded = "True"
    }
    catch {
        $global:projectFile.SolutionAdded = "Error"
        pause
    }
    $global:projectFile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.Devops\$global:gitRepo\$global:gitRepo.json")
   
}

function Add-CICDEnvironment {
    $message = "Configuring CI/CD Environment"
    Write-Host $message

    try {
        Invoke-ConfigureCICD
    }
    catch {
        $global:projectFile.CICDEnvironmentName = "Error"
        pause
    }
    $global:projectFile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.Devops\$global:gitRepo\$global:gitRepo.json")
   
}

function Add-Project {
    $message = "Adding PowerPlatform DevOps Project"
    Write-Host $message
    if ($global:configfile.Projects.Count -gt 1 -or $global:configfile.Projects[0].ID -ne "placeholder") {
        do {
            $createOrSelect = Read-Host "Do you want to [S]elect and Existing Project or [C]reate a new one? ([Q]uit)"
            switch ($createOrSelect) {
                'C' { Add-GitRepo }
                'S' { Select-GitRepo }
            }
        } until (($createOrSelect -eq 'q') -or ($createOrSelect -eq 'c') -or ($createOrSelect -eq 's'))
    }
    else {
        Add-GitRepo
    }
    try {
    }
    catch {
        $global:projectFile.SolutionAdded = "Error"
        pause
    }
    #$global:projectFile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.Devops\$global:gitRepo\$global:gitRepo.json")
    $global:configfile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json")
   
}

function Export-Solution {
    if ($global:projectFile.CDSSolutions.Count -gt 0) {
        $options = $global:projectFile.CDSSolutions | ForEach-Object { $_.SolutionName }

        do {
            $sel = Invoke-Menu -MenuTitle "---- Please Select the Solution to Export and Unpack ------" -MenuOptions $options
            $selectedSolution = $global:projectFile.CDSSolutions[$sel].SolutionName
        } until ($selectedSolution -ne "")

        Write-Host $selectedSolution
        Write-Host $global:projectFile.ProjectLocation
        Set-Location -Path "$($global:projectFile.ProjectLocation)\$selectedSolution\Scripts"
        if ($global:projectFile.ADOConfigured -eq "True") {
            try {
                git pull origin
            }
            catch {
                pause
            }            
        }
        & .\SolutionExport.ps1
        Set-Location -Path $global:projectFile.ProjectLocation
    }
}

function Sync-Solution{
    $commitMessage = Read-Host "Enter a description for your Commit"
        git add -A
        git commit -m $commitMessage    
    if ($global:projectFile.ADOConfigured -eq "True") {
            git push origin
    }
    pause
}

function Show-SolutionChanges{
    git status
    pause
}

function Enable-AzureDeploy{
    if ($global:projectFile.ARMAdded -ne "True") {
        Copy-Item -Path (Join-Path $PSScriptRoot Snippets\AzureResources\.) -Destination $global:projectFile.ProjectLocation -Recurse -Force
        $buildYAML = Get-Content -Path "$($global:projectFile.ProjectLocation)\Build.yaml"
        $azureYAML = Get-Content -Path  (Join-Path $PSScriptRoot Snippets\AzureDeploy.yaml)
        $azureYAML = $azureYAML.Replace('replaceRepo', $global:projectFile.gitRepo)
        $buildYAML + $azureYAML | Set-Content -Path  "$($global:projectFile.ProjectLocation)\Build.yaml"

        $azureParams = Get-Content -Path "$($global:projectFile.ProjectLocation)\AzureResources\azuredeploy.parameters.json"
        $azureParams = $azureParams.Replace('AddName', $global:projectFile.gitRepo)
        $azureParams |  Set-Content -Path "$($global:projectFile.ProjectLocation)\AzureResources\azuredeploy.parameters.json"

        $adoOrg = $global:projectFile.ADOOrgName
        $adoProject = $global:projectFile.ADOProject
        $adoRepo = $global:projectFile.gitRepo.ToLower()
        $varGroupAzure = az pipelines variable-group create --organization https://dev.azure.com/$adoOrg --project $adoProject --name "$($global:projectFile.gitRepo).AzureEnvironment"  --variables AzureResourceGroup="" --authorize $true | ConvertFrom-Json
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureAppInsightsName --value "$adoRepo-staging-ai" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureLocation --value "australiaeast" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureStorageAccountName --value "$adoRepo-staging" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureFunctionAppName --value "$adoRepo-staging-fna" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureWebAppName --value "$adoRepo-staging-wba" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name WorkspaceName --value "$adoRepo-staging-log" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureKeyVaultName --value "$adoRepo-staging-kv" --group-id $varGroupAzure.id
        $global:projectFile.ARMAdded = "True"
        $global:projectFile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.Devops\$global:gitRepo\$global:gitRepo.json")
        Write-Host "Please make sure you follow the instructions for configuring a Service Connection in Azure DevOps, as detailed in $($global:projectFile.ProjectLocation)\AzureResources\Instructions.md" -ForegroundColor Yellow
        pause
    }
    else {
        Write-Host "Azure Deployment Already Enabled for this Project"
        pause
    }   
}

function Enable-FunctionApp{
    if ($global:projectFile.FunctionAppAdded -ne "True") {
        Copy-Item -Path (Join-Path $PSScriptRoot Snippets\FunctionApp\.) -Destination $global:projectFile.ProjectLocation -Recurse -Force
        dotnet sln $global:gitRepon.sln add FunctionApp\FunctionApp.csproj
        $global:projectFile.FunctionAppAdded = "True"
        $global:projectFile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.Devops\$global:gitRepo\$global:gitRepo.json")
        pause
    }
    else {
        Write-Host "Function App Already Enabled for this Project"
        pause
    }   
}

function Show-Menu {
    param (
        [string]$Title = 'Power Platform DevOps'
    )
    $devopsConfigMessage = "(ADO Org : $($global:projectFile.ADOOrgName) | ADO Project : $($global:projectFile.ADOProject) | git Repo : $($global:projectFile.gitRepo))"
    $CICDConfigMessage = "(CI/CD Environment : $($global:projectFile.CICDEnvironmentName) | CI/CD URL : $($global:projectFile.CICDEnvironmentURL))"
    if ($global:projectFile -eq "False") {
        $Title = "Power Platform DevOps (No Project Selected)"
    }
    else {
        $Title = "Power Platform DevOps ($($global:projectFile.gitRepo))"
        $global:projectTitle = $global:projectFile.gitRepo
        Set-Location $global:projectFile.ProjectLocation
    }
    [console]::ForegroundColor = "White"
    Clear-Host
    Write-Host $global:logo -ForegroundColor Magenta
    Write-Host $global:message -ForegroundColor White
    $Repeater = "=" * $Title.Length
    Write-Host "================ $Title ================" -ForegroundColor White
    
    Write-Host "1: Run Pre-requisite checks (Install / Update)." -ForegroundColor (Set-Colour $global:configfile.PreReqsComplete)
    if (($global:configfile.PreReqsComplete -eq "True")) {
        Write-Host "2: Create a New Project or Select and Existing One" -ForegroundColor (Set-Colour $global:projectFile)    
    }    
    if (!($global:projectFile -eq "False") -and ($global:configfile.PreReqsComplete -eq "True")) {
        Write-Host "3: Configure Azure DevOps" $devopsConfigMessage -ForegroundColor (Set-Colour $global:projectFile.ADOConfigured)
        Write-Host "4: Add New D365 / CDS Solution." -ForegroundColor (Set-Colour $global:projectFile.SolutionAdded)
        if ($global:projectFile.ADOConfigured -eq "True" -and $global:projectFile.SolutionAdded -eq "True") {
            Write-Host "5: Configure Continuous Deployment" $CICDConfigMessage -ForegroundColor (Set-Colour $global:projectFile.CICDEnvironmentName)        
        }
        Write-Host "A: Enable [A]zure Resource Management Deployment." -ForegroundColor (Set-Colour $global:projectFile.ARMAdded)
        Write-Host "F: Add Azure [F]unction App Project." -ForegroundColor (Set-Colour $global:projectFile.FunctionAppAdded)
        if ($global:projectFile.SolutionAdded -eq "True") {
            Write-Host "D: Add Additional [D]365 / CDS Solutions" -ForegroundColor Cyan
            Write-Host "E: [E]xport & Unpack Solution to Source Control" -ForegroundColor Cyan
            Write-Host "S: Commit and [S]ync changes to Source Control" -ForegroundColor Cyan
            Write-Host "V: [V]iew current change log for Source Control" -ForegroundColor Cyan
        }
        if ($global:projectFile.CICDEnvironmentName -ne "False") {
            Write-Host "T: Add Additional Deployment Environment [T]arget" -ForegroundColor Cyan
        }
    }
    Write-Host "Q: Press 'Q' to quit." -ForegroundColor White
    Write-Host "=================$Repeater=================" -ForegroundColor White
    Write-Host ""
    Write-Host "* (White items still need to be completed)" -ForegroundColor White
    Write-Host "* (Green items are successful)" -ForegroundColor Green
    Write-Host "* (Red items have Errors)" -ForegroundColor Red
    Write-Host "* (Purple items are optional)" -ForegroundColor Magenta
    Write-Host "* (Blue Items are used for Day-to-Day ALM)" -ForegroundColor Cyan
    Write-Host ""
}

function Invoke-PowerPlatformDevOps {
    Write-Host ""
    [console]::ForegroundColor = "White"
    do {
        $global:configFile = Get-Content ("$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json") | ConvertFrom-Json
        if ($global:projectFile -ne "False") {
            $global:gitRepo = $($global:projectFile.gitRepo)
            Set-Location $global:projectFile.ProjectLocation
        }
        Show-Menu

        $selection = Read-Host "Please make a selection"
        switch ($selection) {
            '1' {
                Install-PreReqs
            } '2' {
                Add-Project
            } '3' {
                Connect-AzureDevOps
            } '4' {
                Add-Solution
            } '5' {
                Add-CICDEnvironment
            } 'D' {
                Add-Solution
            } 'E' {
                Export-Solution
            } 'S' {
                Sync-Solution
            } 'V' {
                Show-SolutionChanges
            } 'T' {
                Add-TargetEnvironment
            } 'A' {
                Enable-AzureDeploy
            } 'F' {
                Enable-FunctionApp
            }
        }
            Write-Host ""
        }
        until ($selection -eq 'q')
    }

    Export-ModuleMember -Function 'Invoke-PowerPlatformDevOps'
    #, 'Add-CICDEnvironment', 'Add-Solution', 'Connect-AzureDevOps', 'Install-PreReqs'