Private/New-AvmPullRequestBranch.ps1
|
function New-AvmPullRequestBranch { <# .SYNOPSIS Creates or refreshes a branch with approved AVM updates and opens (or updates) a PR via the GitHub or Azure DevOps CLI. .DESCRIPTION Shared helper used by Approve-AvmUpdate's 'github' and 'azuredevops' modes. Assumes the caller has already changed to the target working directory (Push-Location) and is currently positioned on the base branch/ref that the new branch should be created from. Idempotent by design (Renovate/Dependabot pattern): the branch is always rebuilt fresh from the current base ref and force-pushed, so re-running with the same pending updates (same deterministic -BranchName) converges on the same branch/PR instead of accumulating new ones. This also transparently handles a branch that exists on the remote with no matching open PR — it is simply overwritten. When -ExistingPr is supplied, no new PR is created — the existing PR's title/body are updated in place ('gh pr edit' / 'az repos pr update') and its URL is returned. .PARAMETER ApprovalMode 'github' or 'azuredevops' — selects which CLI is used to open/update the PR. .PARAMETER BranchName Deterministic name of the branch to create/refresh (see Get-AvmUpdateBranchName). .PARAMETER Items Approved plan items to apply on this branch. .PARAMETER Title PR title. .PARAMETER BodyFilePath Path to a Markdown file containing the PR body/description. Using a file avoids OS argument-length limits and shell-quoting issues with large reports (github mode passes this straight through to '--body-file'; azuredevops mode reads and truncates the content, since 'az repos pr create/update' has no file-based description option). .PARAMETER ExistingPr Optional PSCustomObject (number, url, branchName) for an already-open PR on this exact branch. When supplied, the PR is updated in place instead of a new one being created. .PARAMETER CommitMessage Commit message used for the update commit. .PARAMETER AzureDevOpsConfig Optional hashtable with organization/project/repository, passed to 'az repos pr create'/'update' when set (see Resolve-AvmAzureDevOpsContext). .OUTPUTS String PR URL, or $null if PR creation/update was skipped/failed (the push may still have succeeded even when the PR URL is $null). #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([string])] param( [Parameter(Mandatory)] [ValidateSet('github', 'azuredevops')] [string]$ApprovalMode, [Parameter(Mandatory)] [string]$BranchName, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Items, [Parameter(Mandatory)] [string]$Title, [Parameter()] [string]$BodyFilePath, [Parameter()] [PSCustomObject]$ExistingPr, [Parameter()] [string]$CommitMessage = "chore(avm): update AVM module versions [$(Get-Date -Format 'yyyy-MM-dd')]", [Parameter()] [hashtable]$AzureDevOpsConfig ) Write-Verbose "Creating/refreshing branch: $BranchName" if (-not $PSCmdlet.ShouldProcess($BranchName, 'Create/refresh branch, force-push, and create/update PR')) { return $null } # Always rebuild the branch fresh from the current ref (idempotent) rather than trying # to incrementally patch whatever is already on the remote. git branch -D $BranchName 2>&1 | Write-Verbose git checkout -b $BranchName 2>&1 | Write-Verbose $null = Update-AvmModuleVersion -ApprovedPlan ([PSCustomObject]@{ approvedItems = $Items }) git add -A 2>&1 | Write-Verbose git diff --cached --quiet if ($LASTEXITCODE -ne 0) { git commit -m $CommitMessage 2>&1 | Write-Verbose } else { Write-Verbose "No file changes to commit on '$BranchName'." } $pushOutput = git push --force -u origin $BranchName 2>&1 Write-Verbose ($pushOutput -join "`n") if ($LASTEXITCODE -ne 0) { throw "git push failed: $($pushOutput -join ' ')" } $bodyText = if ($BodyFilePath -and (Test-Path $BodyFilePath)) { Get-Content $BodyFilePath -Raw } else { 'AVM update plan — see artifacts.' } $prUrl = $null switch ($ApprovalMode) { 'github' { if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { Write-Warning "gh CLI not found; PR creation skipped. Push to '$BranchName' succeeded." break } if ($ExistingPr) { $editArgs = @('pr', 'edit', "$($ExistingPr.number)", '--title', $Title) if ($BodyFilePath -and (Test-Path $BodyFilePath)) { $editArgs += @('--body-file', $BodyFilePath) } else { $editArgs += @('--body', $bodyText) } $editOutput = & gh @editArgs 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "gh pr edit failed: $($editOutput -join ' ')" } else { Write-Host " Updated existing PR in place: $($ExistingPr.url)" -ForegroundColor DarkGray } $prUrl = $ExistingPr.url } else { $createArgs = @('pr', 'create', '--title', $Title) if ($BodyFilePath -and (Test-Path $BodyFilePath)) { $createArgs += @('--body-file', $BodyFilePath) } else { $createArgs += @('--body', $bodyText) } $createOutput = & gh @createArgs 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "gh pr create failed: $($createOutput -join ' ')" } else { $prUrl = ($createOutput | Select-Object -Last 1).Trim() } } } 'azuredevops' { if (-not (Get-Command az -ErrorAction SilentlyContinue)) { Write-Warning "az CLI not found; PR creation skipped. Push to '$BranchName' succeeded." break } $description = ConvertTo-AvmAzureDevOpsDescription -Body $bodyText $adoArgs = @() if ($AzureDevOpsConfig) { if ($AzureDevOpsConfig.organization) { $adoArgs += @('--organization', $AzureDevOpsConfig.organization) } if ($AzureDevOpsConfig.project) { $adoArgs += @('--project', $AzureDevOpsConfig.project) } if ($AzureDevOpsConfig.repository) { $adoArgs += @('--repository', $AzureDevOpsConfig.repository) } } if ($ExistingPr) { $updateArgs = @('repos', 'pr', 'update', '--id', "$($ExistingPr.number)", '--title', $Title, '--description', $description) + $adoArgs $updateOutput = & az @updateArgs 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "az repos pr update failed: $($updateOutput -join ' ')" } else { Write-Host " Updated existing PR in place: $($ExistingPr.url)" -ForegroundColor DarkGray } $prUrl = $ExistingPr.url } else { $createArgs = @('repos', 'pr', 'create', '--title', $Title, '--description', $description, '--source-branch', $BranchName, '-o', 'json') + $adoArgs $createOutput = & az @createArgs 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "az repos pr create failed: $($createOutput -join ' ')" } else { $parsed = $createOutput | ConvertFrom-Json -ErrorAction SilentlyContinue $prUrl = if ($parsed -and $parsed.PSObject.Properties['url']) { $parsed.url } else { ($createOutput -join ' ') } } } } } return $prUrl } |