From bed50a4f961da33726e81efbc35b3a032ef65b4c Mon Sep 17 00:00:00 2001 From: Rainer Halanek <61878316+rahalan@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:49:10 +0200 Subject: [PATCH] feat: Add additional logic if workflow fails and issue is created (#1403) More fine grained logic, regarding commenting and assigning new failing workflow issues --- .../avm.platform.manage-workflow-issue.yml | 13 +++- ...form.set-avm-github-issue-owner-config.yml | 16 +++-- .../Set-AvmGithubIssueForWorkflow.ps1 | 54 ++++++++++++++++- .../helper/Add-GithubIssueToProject.ps1 | 57 ++++++++++++++++++ .../platform/helper/Get-AvmCsvData.ps1 | 59 +++++++++++++++++++ 5 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 avm/utilities/pipelines/platform/helper/Add-GithubIssueToProject.ps1 create mode 100644 avm/utilities/pipelines/platform/helper/Get-AvmCsvData.ps1 diff --git a/.github/workflows/avm.platform.manage-workflow-issue.yml b/.github/workflows/avm.platform.manage-workflow-issue.yml index 8d37f57d3d..9c80793d18 100644 --- a/.github/workflows/avm.platform.manage-workflow-issue.yml +++ b/.github/workflows/avm.platform.manage-workflow-issue.yml @@ -3,6 +3,7 @@ name: "avm.platform.manage-workflow-issue" on: schedule: - cron: "30 5 * * *" # Every day at 5:30 am + workflow_dispatch: jobs: manage-issues: @@ -14,16 +15,22 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - env: - GH_TOKEN: ${{ github.token }} - name: Manage issues + - uses: tibdex/github-app-token@v2 + id: generate-token + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Manage issues shell: pwsh + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} run: | # Load used functions . (Join-Path $env:GITHUB_WORKSPACE 'avm' 'utilities' 'pipelines' 'platform' 'Set-AvmGithubIssueForWorkflow.ps1') $functionInput = @{ Repo = "${{ github.repository_owner }}/${{ github.event.repository.name }}" + RepoRoot = $env:GITHUB_WORKSPACE LimitNumberOfRuns = 500 LimitInDays = 2 IgnoreWorkflows = @() diff --git a/.github/workflows/avm.platform.set-avm-github-issue-owner-config.yml b/.github/workflows/avm.platform.set-avm-github-issue-owner-config.yml index 775d83c94d..48cc265747 100644 --- a/.github/workflows/avm.platform.set-avm-github-issue-owner-config.yml +++ b/.github/workflows/avm.platform.set-avm-github-issue-owner-config.yml @@ -16,16 +16,22 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - env: - GH_TOKEN: ${{ github.token }} - name: "Run scripts" + - uses: tibdex/github-app-token@v2 + id: generate-token + with: + app_id: ${{ secrets.TEAM_LINTER_APP_ID }} + private_key: ${{ secrets.TEAM_LINTER_PRIVATE_KEY }} + - name: "Run scripts" shell: pwsh + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} run: | # Load used functions - . (Join-Path $env:GITHUB_WORKSPACE 'avm' 'utilities' 'pipelines' 'platform' 'Set-AvmGitHubIssueOwnerConfig.ps1') + . (Join-Path $env:GITHUB_WORKSPACE 'avm' 'utilities' 'pipelines' 'platform' 'Set-AvmGitHubIssueOwnerConfig.ps1') $functionInput = @{ - Repo = "${{ github.repository_owner }}/${{ github.event.repository.name }}" + Repo = "${{ github.repository_owner }}/${{ github.event.repository.name }}" + RepoRoot = $env:GITHUB_WORKSPACE IssueUrl = "${{ github.event.issue.url }}" } diff --git a/avm/utilities/pipelines/platform/Set-AvmGithubIssueForWorkflow.ps1 b/avm/utilities/pipelines/platform/Set-AvmGithubIssueForWorkflow.ps1 index 7a30bfa96d..a2c1a5f98d 100644 --- a/avm/utilities/pipelines/platform/Set-AvmGithubIssueForWorkflow.ps1 +++ b/avm/utilities/pipelines/platform/Set-AvmGithubIssueForWorkflow.ps1 @@ -6,7 +6,10 @@ Check for failing pipelines and create issues for those, that are failing. If a pipeline fails, a new issue will be created, with a link to the failed pipeline. If the issue is already existing, a comment will be added, if a new run failed (with the link for the new failed run). If a pipeline run succeeds and an issue is open for the failed run, it will be closed (and a link to the successful run is added to the issue). .PARAMETER Repo -Mandatory. The name of the respository to scan. Needs to have the structure "/" +Mandatory. The name of the respository to scan. Needs to have the structure "/", like 'Azure/bicep-registry-modules/' + +.PARAMETER RepoRoot +Optional. Path to the root of the repository. .PARAMETER LimitNumberOfRuns Optional. Number of recent runs to scan for failed runs. Default is 100. @@ -23,7 +26,7 @@ Set-AvmGithubIssueForWorkflow -Repo 'owner/repo01' -LimitNumberOfRuns 100 -Limit Check the last 100 workflow runs in the repository 'owner/repo01' that happened in the last 2 days. If the workflow name is 'Pipeline 01', then ignore the workflow run. .NOTES -The function requires GitHub CLI to be installed. +Will be triggered by the workflow avm.platform.manage-workflow-issue.yml #> function Set-AvmGithubIssueForWorkflow { [CmdletBinding(SupportsShouldProcess)] @@ -31,6 +34,9 @@ function Set-AvmGithubIssueForWorkflow { [Parameter(Mandatory = $true)] [string] $Repo, + [Parameter(Mandatory = $false)] + [string] $RepoRoot = (Get-Item -Path $PSScriptRoot).parent.parent.parent.parent.FullName, + [Parameter(Mandatory = $false)] [int] $LimitNumberOfRuns = 100, @@ -41,6 +47,10 @@ function Set-AvmGithubIssueForWorkflow { [String[]] $IgnoreWorkflows = @() ) + # Loading helper functions + . (Join-Path $RepoRoot 'avm' 'utilities' 'pipelines' 'platform' 'helper' 'Get-AvmCsvData.ps1') + . (Join-Path $RepoRoot 'avm' 'utilities' 'pipelines' 'platform' 'helper' 'Add-GithubIssueToProject.ps1') + $issues = gh issue list --state open --limit 500 --label 'Type: AVM :a: :v: :m:,Type: Bug :bug:' --json 'title,url,body,comments,labels' --repo $Repo | ConvertFrom-Json -Depth 100 $runs = gh run list --json 'url,workflowName,headBranch,startedAt' --limit $LimitNumberOfRuns --repo $Repo | ConvertFrom-Json -Depth 100 $workflowRuns = @{} @@ -81,7 +91,45 @@ function Set-AvmGithubIssueForWorkflow { if ($issues.title -notcontains $issueName) { if ($PSCmdlet.ShouldProcess("Issue [$issueName]", 'Create')) { - gh issue create --title "$issueName" --body "$failedrun" --label 'Type: AVM :a: :v: :m:,Type: Bug :bug:' --repo $Repo + $issueUrl = gh issue create --title "$issueName" --body "$failedrun" --label 'Type: AVM :a: :v: :m:,Type: Bug :bug:' --repo $Repo + $ProjectNumber = 538 # AVM - Issue Triage + $comment = @" +> [!IMPORTANT] +> @Azure/avm-core-team-technical-bicep, the workflow for the ``$moduleName`` module has failed. Please investigate the failed workflow run. +"@ + + if ($workflowRun.workflowName -match 'avm.(?:res|ptn)') { + $moduleName = $workflowRun.workflowName.Replace('.', '/') + $moduleIndex = $moduleName.StartsWith('avm/res') ? 'Bicep-Resource' : 'Bicep-Pattern' + # get CSV data + $module = Get-AvmCsvData -ModuleIndex $moduleIndex | Where-Object ModuleName -EQ $moduleName + + if (($module.ModuleStatus -ne 'Module Orphaned :eyes:') -and (-not ([string]::IsNullOrEmpty($module.PrimaryModuleOwnerGHHandle)))) { + $ProjectNumber = 566 # AVM - Module Issues + $comment = @" +> [!IMPORTANT] +> @$($module.ModuleOwnersGHTeam), the workflow for the ``$moduleName`` module has failed. Please investigate the failed workflow run. If you are not able to do so, please inform the AVM core team to take over. +"@ + # assign owner + $assign = gh issue edit $issue.url --add-assignee $module.PrimaryModuleOwnerGHHandle --repo $Repo + + if ([String]::IsNullOrEmpty($assign)) { + if ($PSCmdlet.ShouldProcess("missing user comment to issue [$($issue.title)]", 'Add')) { + $comment = @" +> [!WARNING] +> This issue couldn't be assigend due to an internal error. @$($module.PrimaryModuleOwnerGHHandle), please make sure this issue is assigned to you and please provide an initial response as soon as possible, in accordance with the [AVM Support statement](https://aka.ms/AVM/Support). +"@ + + gh issue comment $issue.url --body $reply --repo $Repo + } + } + } + } + + # add issue to project + Add-GithubIssueToProject -Repo $Repo -ProjectNumber $ProjectNumber -IssueUrl $issueUrl + # add comment + gh issue comment $issueUrl --body $comment --repo $Repo } $issuesCreated++ diff --git a/avm/utilities/pipelines/platform/helper/Add-GithubIssueToProject.ps1 b/avm/utilities/pipelines/platform/helper/Add-GithubIssueToProject.ps1 new file mode 100644 index 0000000000..cd9d0222e4 --- /dev/null +++ b/avm/utilities/pipelines/platform/helper/Add-GithubIssueToProject.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS +Adds an existing GitHub issue to an existing GitHub project (the new type, not the classic ones) + +.DESCRIPTION +Adds an existing GitHub issue to an existing GitHub project (the new type, not the classic ones) + +.PARAMETER Repo +Mandatory. The name of the respository to scan. Needs to have the structure "/", like 'Azure/bicep-registry-modules/' + +.PARAMETER ProjectNumber +Mandatory. The GitHub project number (see last part of project URL, for example 538 for https://github.com/orgs/Azure/projects/538) + +.PARAMETER IssueUrl +Mandatory. The URL of the GitHub issue, like 'https://github.com/Azure/bicep-registry-modules/issues/757' + +.EXAMPLE +Add-GithubIssueToProject -Repo 'Azure/bicep-registry-modules' -ProjectNumber 538 -IssueUrl 'https://github.com/Azure/bicep-registry-modules/issues/757' + +.NOTES +Needs to run under a context with the permissions to read/write organization projects +#> +function Add-GithubIssueToProject { + param ( + [Parameter(Mandatory = $true)] + [string] $Repo, + + [Parameter(Mandatory = $true)] + [int] $ProjectNumber, + + [Parameter(Mandatory = $true)] + [string] $IssueUrl + ) + + $Organization = $Repo.Split('/')[0] + + $Project = gh api graphql -f query=' + query($organization: String! $number: Int!){ + organization(login: $organization){ + projectV2(number: $number) { + id + } + } + }' -f organization=$Organization -F number=$ProjectNumber | ConvertFrom-Json -Depth 10 + + $ProjectId = $Project.data.organization.projectV2.id + $IssueId = (gh issue view $IssueUrl --repo $Repo --json 'id' | ConvertFrom-Json -Depth 100).id + + gh api graphql -f query=' + mutation($project:ID!, $issue:ID!) { + addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) { + item { + id + } + } + }' -f project=$ProjectId -f issue=$IssueId +} diff --git a/avm/utilities/pipelines/platform/helper/Get-AvmCsvData.ps1 b/avm/utilities/pipelines/platform/helper/Get-AvmCsvData.ps1 new file mode 100644 index 0000000000..821f476bb7 --- /dev/null +++ b/avm/utilities/pipelines/platform/helper/Get-AvmCsvData.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS +Parses AVM module CSV file + +.DESCRIPTION +Depending on the parameter, the correct CSV file will be parsed and returned a an object + +.PARAMETER ModuleIndex +Mandatory. Type of CSV file, that should be parsed ('Bicep-Resource', 'Bicep-Pattern') + +.EXAMPLE +Get-AvmCsvData -ModuleIndex 'Bicep-Resource' + +Parse the AVM Bicep modules +#> +Function Get-AvmCsvData { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [ValidateSet('Bicep-Resource', 'Bicep-Pattern')] + [string] $ModuleIndex + ) + + # CSV file URLs + $BicepResourceUrl = 'https://aka.ms/avm/index/bicep/res/csv' + $BicepPatternUrl = 'https://aka.ms/avm/index/bicep/ptn/csv' + + # Retrieve the CSV file + switch ($ModuleIndex) { + 'Bicep-Resource' { + try { + $unfilteredCSV = Invoke-WebRequest -Uri $BicepResourceUrl + } catch { + throw 'Unable to retrieve CSV file - Check network connection.' + } + } + 'Bicep-Pattern' { + try { + $unfilteredCSV = Invoke-WebRequest -Uri $BicepPatternUrl + } catch { + throw 'Unable to retrieve CSV file - Check network connection.' + } + } + } + + # Convert the CSV content to a PowerShell object + $formattedBicepFullCsv = ConvertFrom-Csv $unfilteredCSV.Content + + # Loop through each item in the filtered data + foreach ($item in $formattedBicepFullCsv) { + # Remove '@Azure/' from the ModuleOwnersGHTeam property + $item.ModuleOwnersGHTeam = $item.ModuleOwnersGHTeam -replace '@Azure\/', '' + # Remove '@Azure/' from the ModuleContributorsGHTeam property + $item.ModuleContributorsGHTeam = $item.ModuleContributorsGHTeam -replace '@Azure\/', '' + } + + # Return the modified data + return $formattedBicepFullCsv +}