diff --git a/Actions/TrackPRDeployment/README.md b/Actions/TrackPRDeployment/README.md new file mode 100644 index 000000000..7197d53cd --- /dev/null +++ b/Actions/TrackPRDeployment/README.md @@ -0,0 +1,26 @@ +# TrackPRDeployment + +Track PR deployments against the PR branch instead of the trigger branch (e.g. main). + +When deploying a PR build via PublishToEnvironment, the workflow runs on main but deploys artifacts built from a PR branch. GitHub's `environment:` key auto-creates a deployment record against main, which is misleading. This action deactivates that record and creates a new deployment against the actual PR branch, so the deployment shows correctly on the PR. + +## INPUT + +### ENV variables + +None + +### Parameters + +| Name | Required | Description | Default value | +| :-- | :-: | :-- | :-- | +| shell | | The shell (powershell or pwsh) in which the PowerShell script in this action should run | powershell | +| token | | The GitHub token running the action | github.token | +| environmentsMatrixJson | Yes | JSON string with the environments matrix from Initialization | | +| deployResult | Yes | The result of the Deploy job (success or failure) | | +| artifactsVersion | Yes | Artifacts version (PR\_\) | | +| sha | | The commit SHA of the workflow run, used to identify the correct auto-created deployment | github.sha | + +## OUTPUT + +None diff --git a/Actions/TrackPRDeployment/TrackPRDeployment.ps1 b/Actions/TrackPRDeployment/TrackPRDeployment.ps1 new file mode 100644 index 000000000..428d6b695 --- /dev/null +++ b/Actions/TrackPRDeployment/TrackPRDeployment.ps1 @@ -0,0 +1,163 @@ +Param( + [Parameter(Mandatory = $false)] + [string] $token = $ENV:GITHUB_TOKEN, + [Parameter(Mandatory = $true)] + [string] $environmentsMatrixJson, + [Parameter(Mandatory = $true)] + [string] $deployResult, + [Parameter(Mandatory = $true)] + [string] $artifactsVersion, + [Parameter(Mandatory = $false)] + [string] $sha = $ENV:GITHUB_SHA +) + +. (Join-Path -Path $PSScriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve) + +<# + .SYNOPSIS + Deactivate auto-created deployments and retrieve the environment URL. + .DESCRIPTION + The environment: key on the Deploy job auto-creates a deployment against the trigger ref (e.g. main). + This function finds those deployments, retrieves the environment URL from the latest status, and + deactivates them so they no longer show as active. +#> +function DeactivateAutoDeployments { + Param( + [hashtable] $headers, + [string] $repository, + [string] $environmentName, + [string] $triggerRef, + [string] $sha + ) + + $apiBase = "https://api.github.com/repos/$repository" + $encodedEnv = [System.Uri]::EscapeDataString($environmentName) + $envUrl = $null + + $logUrl = $null + + $listUri = "$apiBase/deployments?environment=$encodedEnv&ref=$triggerRef&sha=$sha&per_page=1" + OutputDebug "GET $listUri" + $existingDeps = (InvokeWebRequest -Headers $headers -Uri $listUri).Content | ConvertFrom-Json + if ($existingDeps -and @($existingDeps).Count -gt 0) { + $dep = @($existingDeps)[0] + OutputDebug "Found auto-created deployment $($dep.id) (ref: $($dep.ref), sha: $($dep.sha))" + $statusesUri = "$apiBase/deployments/$($dep.id)/statuses?per_page=1" + OutputDebug "GET $statusesUri" + $statuses = (InvokeWebRequest -Headers $headers -Uri $statusesUri).Content | ConvertFrom-Json + if ($statuses -and @($statuses).Count -gt 0) { + OutputDebug "Latest status: state=$(@($statuses)[0].state), environment_url=$(@($statuses)[0].environment_url), log_url=$(@($statuses)[0].log_url)" + if (@($statuses)[0].environment_url) { + $envUrl = @($statuses)[0].environment_url + } + if (@($statuses)[0].log_url) { + $logUrl = @($statuses)[0].log_url + } + if (@($statuses)[0].state -ne 'inactive') { + Write-Host "Deactivating auto-created deployment $($dep.id) (ref: $($dep.ref))" + $deactivateUri = "$apiBase/deployments/$($dep.id)/statuses" + OutputDebug "POST $deactivateUri (state: inactive)" + InvokeWebRequest -Headers $headers -Method 'POST' -Uri $deactivateUri -Body '{"state":"inactive"}' | Out-Null + } + else { + Write-Host "Auto-created deployment $($dep.id) is already inactive, skipping" + } + } + } + else { + OutputDebug "No auto-created deployment found for environment=$environmentName, ref=$triggerRef, sha=$sha" + } + + return @{ + EnvironmentUrl = $envUrl + LogUrl = $logUrl + } +} + +<# + .SYNOPSIS + Create a deployment record against the PR branch and set its status. +#> +function CreatePRDeployment { + Param( + [hashtable] $headers, + [string] $repository, + [string] $prRef, + [string] $prNumber, + [string] $environmentName, + [string] $environmentUrl, + [string] $logUrl, + [string] $state + ) + + $apiBase = "https://api.github.com/repos/$repository" + + $deployBody = @{ + ref = $prRef + environment = $environmentName + auto_merge = $false + required_contexts = @() + description = "Deployed via PublishToEnvironment (PR #$prNumber)" + } | ConvertTo-Json -Compress + + $createUri = "$apiBase/deployments" + OutputDebug "POST $createUri (ref: $prRef, environment: $environmentName)" + $deployment = (InvokeWebRequest -Headers $headers -Method 'POST' -Uri $createUri -Body $deployBody).Content | ConvertFrom-Json + Write-Host "Created deployment $($deployment.id) against $prRef" + + $statusBody = @{ + state = $state + environment = $environmentName + description = "Deployed PR #$prNumber to $environmentName" + } + if ($environmentUrl) { $statusBody['environment_url'] = $environmentUrl } + if ($logUrl) { $statusBody['log_url'] = $logUrl } + $statusJson = $statusBody | ConvertTo-Json -Compress + + $statusUri = "$apiBase/deployments/$($deployment.id)/statuses" + OutputDebug "POST $statusUri (state: $state)" + InvokeWebRequest -Headers $headers -Method 'POST' -Uri $statusUri -Body $statusJson | Out-Null + Write-Host "Deployment status set to $state for $environmentName" +} + +# Main +$prNumber = $artifactsVersion.Substring(3) +$repo = $ENV:GITHUB_REPOSITORY +$triggerRef = $ENV:GITHUB_REF_NAME +$state = if ($deployResult -eq 'success') { 'success' } else { 'failure' } + +OutputDebug "PR number: $prNumber, repository: $repo, triggerRef: $triggerRef, sha: $sha, deployResult: $deployResult" + +$headers = GetHeaders -token $token + +# Get PR branch ref using existing helper from Deploy.psm1 +Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "..\Deploy\Deploy.psm1" -Resolve) +$prRef = GetHeadRefFromPRId -repository $repo -prId $prNumber -token $token +if (-not $prRef) { + throw "Could not determine PR branch for PR #$prNumber" +} +Write-Host "PR #$prNumber branch: $prRef" + +# Parse environments from the matrix JSON +$matrix = $environmentsMatrixJson | ConvertFrom-Json +$environments = @($matrix.matrix.include | ForEach-Object { $_.environment }) +OutputDebug "Environments to process: $($environments -join ', ')" + +foreach ($envName in $environments) { + Write-Host "Tracking deployment for environment: $envName" + + $deploymentInfo = $null + try { + $deploymentInfo = DeactivateAutoDeployments -headers $headers -repository $repo -environmentName $envName -triggerRef $triggerRef -sha $sha + } + catch { + OutputWarning -message "Could not deactivate auto-created deployment for $envName`: $($_.Exception.Message)" + } + + try { + CreatePRDeployment -headers $headers -repository $repo -prRef $prRef -prNumber $prNumber -environmentName $envName -environmentUrl $deploymentInfo.EnvironmentUrl -logUrl $deploymentInfo.LogUrl -state $state + } + catch { + OutputWarning -message "Failed to create PR deployment for $envName`: $($_.Exception.Message)" + } +} diff --git a/Actions/TrackPRDeployment/action.yaml b/Actions/TrackPRDeployment/action.yaml new file mode 100644 index 000000000..6f5f009ed --- /dev/null +++ b/Actions/TrackPRDeployment/action.yaml @@ -0,0 +1,42 @@ +name: TrackPRDeployment +author: Microsoft Corporation +inputs: + shell: + description: Shell in which you want to run the action (powershell or pwsh) + required: false + default: powershell + token: + description: The GitHub token running the action + required: false + default: ${{ github.token }} + environmentsMatrixJson: + description: JSON string with the environments matrix from Initialization + required: true + deployResult: + description: The result of the Deploy job (success or failure) + required: true + artifactsVersion: + description: Artifacts version (PR_) + required: true + sha: + description: The commit SHA of the workflow run, used to identify the correct auto-created deployment + required: false + default: ${{ github.sha }} +runs: + using: composite + steps: + - name: run + shell: ${{ inputs.shell }} + env: + _token: ${{ inputs.token }} + _environmentsMatrixJson: ${{ inputs.environmentsMatrixJson }} + _deployResult: ${{ inputs.deployResult }} + _artifactsVersion: ${{ inputs.artifactsVersion }} + _sha: ${{ inputs.sha }} + run: | + ${{ github.action_path }}/../Invoke-AlGoAction.ps1 -ActionName "TrackPRDeployment" -Action { + ${{ github.action_path }}/TrackPRDeployment.ps1 -token $ENV:_token -environmentsMatrixJson $ENV:_environmentsMatrixJson -deployResult $ENV:_deployResult -artifactsVersion $ENV:_artifactsVersion -sha $ENV:_sha + } +branding: + icon: terminal + color: blue diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e730ac4c1..9fa3574dc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,11 @@ - Issue 2107 Publish a specific build mode to an environment - Issue 1915 CICD fails on releases/26.x branch - '26.x' cannot be recognized as a semantic version string +- Issue 2118 Deployments from "Publish To Environment" not tracked against PR branch + +### PR deployment tracking + +When deploying a PR build via "Publish To Environment", the deployment is now correctly tracked against the PR branch instead of the trigger branch (e.g. main). Previously, GitHub would show the deployment against the latest commit on main, which was misleading. A new `TrackPRDeployment` action runs after the deploy job to deactivate the auto-created deployment and create one pointing to the actual PR branch. ### The default pull request trigger is changing diff --git a/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml b/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml index e60df4fc8..409d006a8 100644 --- a/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml +++ b/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml @@ -18,6 +18,7 @@ on: permissions: actions: read contents: read + deployments: write id-token: write pull-requests: read checks: read @@ -195,6 +196,22 @@ jobs: artifactsFolder: '.artifacts' deploymentEnvironmentsJson: ${{ needs.Initialization.outputs.deploymentEnvironmentsJson }} + TrackPRDeployment: + needs: [ Initialization, Deploy ] + if: always() && (needs.Deploy.result == 'success' || needs.Deploy.result == 'failure') && startsWith(github.event.inputs.appVersion, 'PR_') + runs-on: [ windows-latest ] + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Track PR Deployment + uses: microsoft/AL-Go-Actions/TrackPRDeployment@main + with: + shell: powershell + environmentsMatrixJson: ${{ needs.Initialization.outputs.environmentsMatrixJson }} + deployResult: ${{ needs.Deploy.result }} + artifactsVersion: ${{ github.event.inputs.appVersion }} + PostProcess: needs: [ Initialization, Deploy ] if: always() diff --git a/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml b/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml index e60df4fc8..409d006a8 100644 --- a/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml @@ -18,6 +18,7 @@ on: permissions: actions: read contents: read + deployments: write id-token: write pull-requests: read checks: read @@ -195,6 +196,22 @@ jobs: artifactsFolder: '.artifacts' deploymentEnvironmentsJson: ${{ needs.Initialization.outputs.deploymentEnvironmentsJson }} + TrackPRDeployment: + needs: [ Initialization, Deploy ] + if: always() && (needs.Deploy.result == 'success' || needs.Deploy.result == 'failure') && startsWith(github.event.inputs.appVersion, 'PR_') + runs-on: [ windows-latest ] + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Track PR Deployment + uses: microsoft/AL-Go-Actions/TrackPRDeployment@main + with: + shell: powershell + environmentsMatrixJson: ${{ needs.Initialization.outputs.environmentsMatrixJson }} + deployResult: ${{ needs.Deploy.result }} + artifactsVersion: ${{ github.event.inputs.appVersion }} + PostProcess: needs: [ Initialization, Deploy ] if: always() diff --git a/Tests/TrackPRDeployment.Action.Test.ps1 b/Tests/TrackPRDeployment.Action.Test.ps1 new file mode 100644 index 000000000..721f8023b --- /dev/null +++ b/Tests/TrackPRDeployment.Action.Test.ps1 @@ -0,0 +1,28 @@ +Get-Module TestActionsHelper | Remove-Module -Force +Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1') +$errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 + +Describe "TrackPRDeployment Action Tests" { + BeforeAll { + $actionName = "TrackPRDeployment" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + $scriptName = "$actionName.ps1" + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'scriptPath', Justification = 'False positive.')] + $scriptPath = Join-Path $scriptRoot $scriptName + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] + $actionScript = GetActionScript -scriptRoot $scriptRoot -scriptName $scriptName + } + + It 'Compile Action' { + Invoke-Expression $actionScript + } + + It 'Test action.yaml matches script' { + $outputs = [ordered]@{ + } + YamlTest -scriptRoot $scriptRoot -actionName $actionName -actionScript $actionScript -outputs $outputs + } + + # Call action + +}