From 927396537de0648aa5df8f941a9dbae96d59376e Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 9 Feb 2022 09:39:30 -0800 Subject: [PATCH] Send Slack post when proposing issues (#30872) --- .../workflows/iteration-stats-slackbot.yml | 12 +- .github/workflows/label-move.yml | 27 ----- .github/workflows/post-iteration-stats.ps1 | 15 ++- .github/workflows/project-automation.yml | 20 ++-- .../update-project-item-statuses.ps1 | 82 ------------- .github/workflows/update-project-items.ps1 | 108 ++++++++++++++++++ 6 files changed, 133 insertions(+), 131 deletions(-) delete mode 100644 .github/workflows/update-project-item-statuses.ps1 create mode 100644 .github/workflows/update-project-items.ps1 diff --git a/.github/workflows/iteration-stats-slackbot.yml b/.github/workflows/iteration-stats-slackbot.yml index 6e3fb4c0518..562be0da785 100644 --- a/.github/workflows/iteration-stats-slackbot.yml +++ b/.github/workflows/iteration-stats-slackbot.yml @@ -3,7 +3,6 @@ name: Code Insights iteration stats Slack bot on: schedule: # Every Friday 5pm PST (Saturday 1am UTC) - # Note: script will exit if there is no iteration ending that day (every 2 weeks, but cron doesn't support that) - cron: 0 1 * * 6 jobs: @@ -18,12 +17,13 @@ jobs: SLACK_WEBHOOK_URI: ${{ secrets.INSIGHTS_ITERATION_SLACKBOT_WEBHOOK_URI }} with: script: | - $global:InformationPreference = 'Continue' - $global:ProgressPreference = 'SilentlyContinue' + $InformationPreference = 'Continue' + $ProgressPreference = 'SilentlyContinue' + $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3.0 - Install-Module PSGitHub -Force -ErrorAction Stop - Install-Module PSSlack -Force -ErrorAction Stop + Install-Module PSGitHub -Force + Install-Module PSSlack -Force if (!$env:GITHUB_TOKEN) { throw "No GITHUB_TOKEN env var provided" @@ -31,4 +31,4 @@ jobs: $PSDefaultParameterValues['*GitHub*:Token'] = ConvertTo-SecureString -String $env:GITHUB_TOKEN -AsPlainText -Force - ./.github/workflows/post-iteration-stats.ps1 -ProjectNodeId 'MDExOlByb2plY3ROZXh0MzI3Ng==' -SlackChannel '#code-insights-internal' -SlackWebhookUri $env:SLACK_WEBHOOK_URI + ./.github/workflows/post-iteration-stats.ps1 -ProjectNodeId 'MDExOlByb2plY3ROZXh0MzI3Ng==' -SlackChannel '#code-insights-planning' -SlackWebhookUri $env:SLACK_WEBHOOK_URI diff --git a/.github/workflows/label-move.yml b/.github/workflows/label-move.yml index 2f73da26e53..4048f9f367c 100644 --- a/.github/workflows/label-move.yml +++ b/.github/workflows/label-move.yml @@ -14,33 +14,6 @@ jobs: # } # } # }' -F project=212 - code-insights-board: - runs-on: ubuntu-latest - env: - PROJECT_ID: MDExOlByb2plY3ROZXh0MzI3Ng== # https://github.com/orgs/sourcegraph/projects/200 - GITHUB_TOKEN: ${{ secrets.GH_PROJECTS_ACTION_TOKEN }} - steps: - - name: Get issue if relevant - if: ${{ contains(github.event.issue.labels.*.name, 'team/code-insights') }} - env: - NODE_ID: ${{ github.event.issue.node_id }} - run: echo 'NODE_ID='$NODE_ID >> $GITHUB_ENV - - name: Get pull request if relevant - if: ${{ contains(github.event.pull_request.labels.*.name, 'team/code-insights') }} - env: - NODE_ID: ${{ github.event.pull_request.node_id }} - run: echo 'NODE_ID='$NODE_ID >> $GITHUB_ENV - - name: Add to Code Insights board - if: ${{ env.NODE_ID != '' }} - run: | - gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query=' - mutation($project:ID!, $node_id:ID!) { - addProjectNextItem(input: {projectId: $project, contentId: $node_id}) { - projectNextItem { - id - } - } - }' -f project=$PROJECT_ID -f node_id=$NODE_ID code-intel-board: runs-on: ubuntu-latest env: diff --git a/.github/workflows/post-iteration-stats.ps1 b/.github/workflows/post-iteration-stats.ps1 index 410664f3dfa..01946edb8ba 100644 --- a/.github/workflows/post-iteration-stats.ps1 +++ b/.github/workflows/post-iteration-stats.ps1 @@ -20,20 +20,19 @@ param( $todayInPST = [TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTime]::Today, 'Pacific Standard Time').Date $currentMilestone = Get-GitHubMilestone -Owner sourcegraph -RepositoryName sourcegraph -State open | - Where-Object { $_.Title -like 'Insights iteration*' -and $_.DueOn.Date -eq $todayInPST } + Where-Object { $_.Title -like 'Insights iteration*' -and $todayInPST -le $_.DueOn.Date -and $todayInPST -ge $_.DueOn.Date.AddDays(-11) } if (!$currentMilestone) { - Write-Warning "No milestone found that ends today ($($todayInPST.ToLongDateString()))" + Write-Warning "No current milestone found for today ($($todayInPST.ToLongDateString()))" return } -$items = Get-GitHubBetaProjectItem -ProjectNodeId $ProjectNodeId | Where-Object { $_.Content -and $_.Content.Milestone } +Write-Information "Current milestone for today: $($currentMilestone.Title)" -Write-Information "$($items.Count) items in project" +$currentIterationItems = Find-GitHubIssue "org:sourcegraph is:issue milestone:`"$($currentMilestone.Title)`"" | + Get-GitHubBetaProjectItem | + Where-Object { $_.project.id -eq $ProjectNodeId } -$byIteration = $items | Group-Object -Property { $_.Content.Milestone.Title } -AsHashTable - -$currentIterationItems = $byIteration[$currentMilestone.Title] $finishedItems = $currentIterationItems | Where-Object { $_.Fields['Status'] -eq 'Done' } $notSized = $currentIterationItems | Where-Object { !$_.Fields['Size 🔵'] } @@ -61,7 +60,7 @@ $notFinished = $currentIterationItems | Where-Object { $_.Fields['Status'] -ne ' $message = " Beep bop, this is your friendly iteration bot, with some fresh stats to help with our next iteration planning! :spiral_calendar_pad: -*$($currentMilestone.Title) (ending today)* +*$($currentMilestone.Title) (current)* Sum of finished issues: :large_blue_circle: *$($stats.Sum)* | :desktop_computer: Frontend: $($frontendStats.Sum) | :database: Backend: $($backendStats.Sum) _$($stats.Count) issues, average size $($stats.Average.ToString('#.##')), smallest $($stats.Minimum), largest $($stats.Maximum)_ diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml index 65bc679dc39..f7a061cde6f 100644 --- a/.github/workflows/project-automation.yml +++ b/.github/workflows/project-automation.yml @@ -1,27 +1,31 @@ -name: Code Insights GitHub project automation +name: "[OPTIONAL/DOESN'T BLOCK MERGING] Code Insights GitHub project automation" on: issues: - types: [closed, reopened] + types: [opened, closed, reopened, milestoned, labeled] pull_request: types: [opened, edited, synchronize, ready_for_review, converted_to_draft] jobs: - update-status: + update-project-items: + name: Update project items runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Update status + - name: Update project items uses: Amadevus/pwsh-script@v2 env: GITHUB_TOKEN: ${{ secrets.GH_PROJECTS_ACTION_TOKEN }} + SLACK_WEBHOOK_URI: ${{ secrets.INSIGHTS_ITERATION_SLACKBOT_WEBHOOK_URI }} with: script: | - $global:InformationPreference = 'Continue' - $global:ProgressPreference = 'SilentlyContinue' + $InformationPreference = 'Continue' + $ProgressPreference = 'SilentlyContinue' + $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3.0 - Install-Module PSGitHub -Force -ErrorAction Stop + Install-Module PSGitHub -Force + Install-Module PSSlack -Force if (!$env:GITHUB_TOKEN) { throw "No GITHUB_TOKEN env var provided" @@ -29,4 +33,4 @@ jobs: $PSDefaultParameterValues['*GitHub*:Token'] = ConvertTo-SecureString -String $env:GITHUB_TOKEN -AsPlainText -Force - ./.github/workflows/update-project-item-statuses.ps1 -ProjectNodeId 'MDExOlByb2plY3ROZXh0MzI3Ng==' -TeamLabel 'team/code-insights' + ./.github/workflows/update-project-items.ps1 -ProjectNodeId 'MDExOlByb2plY3ROZXh0MzI3Ng==' -TeamLabel 'team/code-insights' -SlackChannel '#code-insights-planning' -SlackWebhookUri $env:SLACK_WEBHOOK_URI diff --git a/.github/workflows/update-project-item-statuses.ps1 b/.github/workflows/update-project-item-statuses.ps1 deleted file mode 100644 index 33cd58df2d7..00000000000 --- a/.github/workflows/update-project-item-statuses.ps1 +++ /dev/null @@ -1,82 +0,0 @@ -# Script to run as a GitHub action on issue and PR updates that will update the associated GitHub Beta project items. - -[CmdletBinding()] -param( - # GitHub GraphQL Node ID of the GitHub Beta project - [Parameter(Mandatory)] - [string] $ProjectNodeId, - - # The team/* label to filter issues/PRs by. All issues/PRs that don't have this label will be ignored. - [Parameter(Mandatory)] - [string] $TeamLabel -) - -# Regex for extracting the "Closes #1234" pattern in GitHub PR descriptions -$fixIssuePattern = "(?:close|fixe?|resolve)(?:[sd])? (?:#|(?[\w_-]+)/(?[\w_-]+)#|https://github\.com/(?[\w_-]+)/(?[\w_-]+)/issues/)(?\d+)" - -switch ($github.event_name) { - - 'issues' { - # Find project item for the issue - # THIS DOES NOT SCALE AS THE PROJECT GETS LARGE, but it's the only way possible afaict. - # One way this is mitigated is that this request is streamed/paginated in order of most-recent-first, - # which means we can hope the issue is usually found in the first page(s). - - if (-not ($github.event.issue.labels | Where-Object { $_.name -eq $TeamLabel })) { - Write-Information "Issue does not have $TeamLabel label, exiting." - return - } - - $status = if ($github.event.action -eq 'closed') { 'Done' } else { 'In Progress' } - - Get-GitHubBetaProjectItem -ProjectNodeId $ProjectNodeId | - Where-Object { $_.content -and $_.content.number -eq $github.event.issue.number } | - Select-Object -First 1 | - Set-GitHubBetaProjectItemField -FieldName 'Status' -Value $status | - ForEach-Object { Write-Information "Updated `"Status`" field of project item for $($_.content.url) to `"$status`"" } - } - - 'pull_request' { - $pr = $github.event.pull_request - - # Ignore merged and closed PRs - if ($pr.state -ne 'open') { - return - } - - $status = if ($pr.draft) { 'In Progress' } else { 'In Review' } - - # Get fixed issues from the PR description - $fixedIssues = [regex]::Matches($pr.body, $fixIssuePattern, [Text.RegularExpressions.RegexOptions]::IgnoreCase) | - ForEach-Object { - $owner = if ($_.Groups['owner'].Success) { $_.Groups['owner'].Value } else { $github.event.repository.owner.login } - $repo = if ($_.Groups['repo'].Success) { $_.Groups['repo'].Value } else { $github.event.repository.name } - $number = $_.Groups['number'].Value - Get-GitHubIssue -Owner $owner -Repository $repo -Number $number - } | - Where-Object { $_.labels | Where-Object { $_.name -eq $TeamLabel } } - - if (!$fixedIssues) { - Write-Information "No fixed issues with $TeamLabel label referenced from PR description, exiting." - return - } - - Write-Information "Fixed issues:" - $fixedIssues | ForEach-Object HtmlUrl | Write-Information - - # Find project items for the issues the PR references - Get-GitHubBetaProjectItem -ProjectNodeId $ProjectNodeId | - Where-Object { - $item = $_ - $fixedIssues | Where-Object { - $item.content -and - $_.Number -eq $item.content.number -and - $_.Owner -eq $item.content.repository.owner.login -and - $_.RepositoryName -eq $item.content.repository.name - } - } | - Select-Object -First $fixedIssues.Count | - Set-GitHubBetaProjectItemField -FieldName 'Status' -Value $status | - ForEach-Object { Write-Information "Updated `"Status`" field of project item for $($_.content.url) to `"$status`"" } - } -} diff --git a/.github/workflows/update-project-items.ps1 b/.github/workflows/update-project-items.ps1 new file mode 100644 index 00000000000..fc4e8adc9f1 --- /dev/null +++ b/.github/workflows/update-project-items.ps1 @@ -0,0 +1,108 @@ +# Script to run as a GitHub action on issue and PR updates that will update the associated GitHub Beta project items. + +[CmdletBinding()] +param( + # GitHub GraphQL Node ID of the GitHub Beta project + [Parameter(Mandatory)] + [string] $ProjectNodeId, + + # The team/* label to filter issues/PRs by. All issues/PRs that don't have this label will be ignored. + [Parameter(Mandatory)] + [string] $TeamLabel, + + # Previously set up webhook URI from https://sourcegraph.slack.com/apps/A0F7XDUAZ + [Parameter(Mandatory)] + [string] $SlackWebhookUri, + + # Slack channel to post to + [Parameter(Mandatory)] + [string] $SlackChannel +) + +# Regex for extracting the "Closes #1234" pattern in GitHub PR descriptions +$fixIssuePattern = "(?:close|fixe?|resolve)(?:[sd])? (?:#|(?[\w_-]+)/(?[\w_-]+)#|https://github\.com/(?[\w_-]+)/(?[\w_-]+)/issues/)(?\d+)" + +switch ($github.event_name) { + + 'issues' { + if (-not ($github.event.issue.labels | Where-Object { $_.name -eq $TeamLabel })) { + Write-Information "Issue does not have $TeamLabel label, exiting." + return + } + + Write-Information "Issue was $($github.event.action)" + + if ($github.event.action -in 'opened', 'labeled', 'milestoned') { + # If team label was added or issue was just opened, add to project board + # If added to an iteration, update status and set "proposed by" to the event actor + # Idempotent, will return the item if already exists in the board (this is fine because we checked for the team label) + $item = [pscustomobject]$github.event.issue | Add-GitHubBetaProjectItem -ProjectNodeId $ProjectNodeId + + if ($item.content.milestone) { + $proposer = $github.event.sender.login + Write-Information "Updating issue as 'Proposed for iteration' by @$proposer" + + $item | + Set-GitHubBetaProjectItemField -Name 'Status' -Value 'Proposed for iteration' | + Set-GitHubBetaProjectItemField -Name 'Proposed by' -Value $proposer + + + # Post Slack message + + $size = $item.Fields['Size 🔵'] + $iterationTitle = $item.content.milestone.title + $issueUrl = $item.content.url + + $stats = Find-GitHubIssue "org:sourcegraph is:issue milestone:`"$($item.content.milestone.title)`"" | + Get-GitHubBetaProjectItem | + Where-Object { $_.project.id -eq $ProjectNodeId -and $_.Fields['Status'] -ne 'Done' } | + ForEach-Object { $_.Fields['Size 🔵'] ?? 1 } | + Measure-Object -AllStats + + $message = "*$proposer* proposed a new issue $($size ? "of *size $size*" : "without a size") for iteration *$($iterationTitle)*:`n" + + "$issueUrl`n" + + "`n" + + "There are now $($stats.Sum) points of open issues in the iteration." + + Write-Information "Sending Slack message:`n$message" + + Send-SlackMessage -Text $message -Username 'Iteration Bot' -IconEmoji ':robot:' -Channel $SlackChannel -Uri $SlackWebhookUri -UnfurlLinks $true + } + } else { + # If issue was closed or reopened, update Status column + $status = if ($github.event.action -eq 'closed') { 'Done' } else { 'In Progress' } + + [pscustomobject]$github.event.issue | + # Idempotent, will return the item if already exists + Add-GitHubBetaProjectItem -ProjectNodeId $ProjectNodeId | + Set-GitHubBetaProjectItemField -FieldName 'Status' -Value $status | + ForEach-Object { Write-Information "Updated `"Status`" field of project item for $($_.content.url) to `"$status`"" } + } + } + + 'pull_request' { + $pr = $github.event.pull_request + + # Ignore merged and closed PRs + if ($pr.state -ne 'open') { + return + } + + $status = if ($pr.draft) { 'In Progress' } else { 'In Review' } + + # Get fixed issues from the PR description + [regex]::Matches($pr.body, $fixIssuePattern, [Text.RegularExpressions.RegexOptions]::IgnoreCase) | + ForEach-Object { + $owner = if ($_.Groups['owner'].Success) { $_.Groups['owner'].Value } else { $github.event.repository.owner.login } + $repo = if ($_.Groups['repo'].Success) { $_.Groups['repo'].Value } else { $github.event.repository.name } + $number = $_.Groups['number'].Value + Write-Information "Found fixed issue $owner/$repo#$number" + Get-GitHubIssue -Owner $owner -Repository $repo -Number $number + } | + Where-Object { $_.labels | Where-Object { $_.name -eq $TeamLabel } } | + # Idempotent, will return the item if already exists + Add-GitHubBetaProjectItem -ProjectNodeId $ProjectNodeId | + Set-GitHubBetaProjectItemField -FieldName 'Status' -Value $status | + ForEach-Object { Write-Information "Updated `"Status`" field of project item for $($_.content.url) to `"$status`"" } + } +}