public/Invoke-AzBootstrap.ps1
function Invoke-AzBootstrap { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param( # # required parameters # [Parameter(Mandatory = $true)] [string]$TemplateRepoUrl, [Parameter(Mandatory = $true)] [string]$TargetRepoName, [Parameter(Mandatory = $true)] [string]$Location, # # optional parameters # [string]$TargetDirectory, # if not specified, the repo will be cloned to ./$TargetRepoName [string]$InitialEnvironmentName = "dev", # github repo parameters [string]$GitHubOwner, # if not specified, the current user will be used [ValidateSet('private', 'public', 'internal')] [string]$GitHubVisibility, # azure parameters [string]$ResourceGroupName, [string]$PlanManagedIdentityName, [string]$ApplyManagedIdentityName, # github branch protection [string]$ProtectedBranchName = 'default-branch-protection', [int]$BranchRequiredApprovals = 1, [bool]$BranchDismissStaleReviews = $true, [bool]$BranchRequireCodeOwnerReview = $false, [bool]$BranchRequireLastPushApproval = $false, [bool]$BranchRequireThreadResolution = $true, [string[]]$BranchAllowedMergeMethods = @("squash"), [bool]$BranchEnableCopilotReview = $true, # deployment reviewers [string[]]$ApplyEnvironmentUserReviewers, [string[]]$ApplyEnvironmentTeamReviewers, [bool]$AddOwnerAsReviewer = $true ) #region: check parameters if (-not $TemplateRepoUrl -or [string]::IsNullOrWhiteSpace($TemplateRepoUrl)) { throw "Template repository URL is required." } if (-not $TargetRepoName -or [string]::IsNullOrWhiteSpace($TargetRepoName)) { throw "Target repository name is required." } if (-not $Location -or [string]::IsNullOrWhiteSpace($Location)) { throw "Location is required." } #endregion #region: check target directory if (-not $TargetDirectory -or [string]::IsNullOrWhiteSpace($TargetDirectory)) { $TargetDirectory = Join-Path -Path (Get-Location) -ChildPath $TargetRepoName } if (Test-Path $TargetDirectory) { throw "Target directory '$TargetDirectory' already exists. Please specify a new directory." } #endregion #region: check CLI tools Write-Host "[az-bootstrap] Checking GitHub CLI authentication status..." if (-not (Test-GitHubCLI)) { throw "GitHub CLI is not authenticated. Please run 'gh auth login' to authenticate." } Write-Verbose "[az-bootstrap] Retrieving current Azure subscription and tenant details..." $azContext = Get-AzCliContext $currentArmSubscriptionId = $azContext.SubscriptionId $currentArmTenantId = $azContext.TenantId #endregion if (-not $currentArmSubscriptionId -or -not $currentArmTenantId) { throw "Failed to retrieve current Azure Subscription ID and Tenant ID. Ensure you are logged in with 'az login' and a default subscription is set." } Write-Host "[az-bootstrap] Using Azure Tenant: $currentArmTenantId, authenticated as: $($azContext.UserName | Out-String -NoNewline)" Write-Host "[az-bootstrap] Using Azure Subscription: $($azContext.SubscriptionName | Out-String -NoNewline), id: $currentArmSubscriptionId" #endregion # GitHub repo $ownerArg = if ($GitHubOwner) { "--owner $GitHubOwner" } else { "" } $visibilityArg = switch ($GitHubVisibility) { "private" { "--private" } "internal" { "--internal" } Default { "--public" } } Write-Host "[az-bootstrap] Creating new GitHub repo '$TargetRepoName' from template: $TemplateRepoUrl" $cmd = "gh repo create $TargetRepoName --template $TemplateRepoUrl $visibilityArg $ownerArg" Invoke-Expression $cmd if ($LASTEXITCODE -ne 0) { throw "Failed to create new GitHub repository from template." } $actualOwner = if ($GitHubOwner) { $GitHubOwner } else { # Try to get the current user/org from gh CLI $user = gh auth status --show-token 2>$null | Select-String 'Logged in to github.com account (.*) \(' | ForEach-Object { $_.Matches.Groups[1].Value } if ($user) { $user } else { throw "Could not determine GitHub owner. Please specify -Owner." } } $repoUrl = "https://github.com/$actualOwner/$TargetRepoName.git" Write-Host "[az-bootstrap] Cloning new repo '$actualOwner/$TargetRepoName' to $TargetDirectory" git clone $repoUrl $TargetDirectory if ($LASTEXITCODE -ne 0) { throw "Failed to clone new repository from $repoUrl." } Push-Location $TargetDirectory try { $RepoInfo = Get-GitHubRepositoryInfo -OverrideOwner $actualOwner -OverrideRepo $TargetRepoName if (-not $RepoInfo) { throw "Could not determine GitHub repository information. Ensure you are in a git repository or provide the -Owner parameter." } Write-Host "[az-bootstrap] Setting up branch protection for '$($RepoInfo.Owner)/$($RepoInfo.Repo)' on branch '$ProtectedBranchName'..." New-GitHubBranchRuleset -Owner $RepoInfo.Owner ` -Repo $RepoInfo.Repo ` -RulesetName "default-branch-protection" ` -TargetPattern $ProtectedBranchName ` -RequiredApprovals $BranchRequiredApprovals ` -DismissStaleReviews $BranchDismissStaleReviews ` -RequireCodeOwnerReview $BranchRequireCodeOwnerReview ` -RequireLastPushApproval $BranchRequireLastPushApproval ` -RequireThreadResolution $BranchRequireThreadResolution ` -AllowedMergeMethods $BranchAllowedMergeMethods ` -EnableCopilotReview $BranchEnableCopilotReview # Construct names for the initial environment $initialRgName = if (-not [string]::IsNullOrWhiteSpace($ResourceGroupName)) { $ResourceGroupName } else { "rg-$($RepoInfo.Repo)-$InitialEnvironmentName" } $planMiName = if (-not [string]::IsNullOrWhiteSpace($PlanManagedIdentityName)) { $PlanManagedIdentityName } else { "mi-$($RepoInfo.Repo)-$InitialEnvironmentName-plan" } $applyMiName = if (-not [string]::IsNullOrWhiteSpace($ApplyManagedIdentityName)) { $ApplyManagedIdentityName } else { $planMiName.Replace("-plan", "-apply") } $initialPlanEnvName = "${InitialEnvironmentName}-iac-plan" $initialApplyEnvName = "${InitialEnvironmentName}-iac-apply" Write-Host "[az-bootstrap] Creating initial environment '$InitialEnvironmentName'..." $addEnvParams = @{ EnvironmentName = $InitialEnvironmentName ResourceGroupName = $initialRgName Location = $Location PlanManagedIdentityName = $planMiName ApplyManagedIdentityName = $applyMiName ArmTenantId = $currentArmTenantId ArmSubscriptionId = $currentArmSubscriptionId GitHubOwner = $RepoInfo.Owner GitHubRepo = $RepoInfo.Repo PlanEnvName = $initialPlanEnvName ApplyEnvName = $initialApplyEnvName ApplyEnvironmentUserReviewers = $ApplyEnvironmentUserReviewers ApplyEnvironmentTeamReviewers = $ApplyEnvironmentTeamReviewers AddOwnerAsReviewer = $AddOwnerAsReviewer } $DeploymentEnv = Add-AzBootstrapEnvironment @addEnvParams } catch { Write-Error "Failed to add initial environment '$InitialEnvironmentName': $_" throw } finally { Pop-Location } Write-Host "[az-bootstrap] Repository : '$($RepoInfo.Owner)/$($RepoInfo.Repo)'." Write-Host "[az-bootstrap] ...cloned to: '$($TargetDirectory)'." Write-Host "[az-bootstrap] Azure Bootstrap complete. 🎉" } Export-ModuleMember -Function Invoke-AzBootstrap |