Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tooling: first pass at oncall tooling #2014

Merged
merged 2 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions .github/actions/pr_notifier/pr_notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Script for collecting PRs in need of review, and informing reviewers via
# slack.
#
# By default this runs in "developer mode" which means that it collects PRs
# associated with reviewers and API reviewers, and spits them out (badly
# formatted) to the command line.
#
# .github/workflows/pr_notifier.yml runs the script with --cron_job
# which instead sends the collected PRs to the various slack channels.
#
# NOTE: Slack IDs can be found in the user's full profile from within Slack.

from __future__ import print_function

import argparse
import datetime
import os
import sys

import github
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

REVIEWERS = {
'alyssawilk': 'U78RP48V9',
'Augustyniak': 'U017R1YHXGQ',
'buildbreaker': 'UEUEP1QP4',
'jpsim': 'U02KAPRELKA',
'junr03': 'U79K0Q431',
'RyanTheOptimist': 'U01SW3JC8GP',
'goaway': 'U7TDPD3L2',
'snowp': 'U93KTPQP6',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to add buildbreaker, jpsim, and Augustyniak here - this is just for review reminders, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @buildbreaker @jpsim @Augustyniak so when you eventually get slack pings from the "PR reminders" app, you know where they came from :-)

}

def get_slo_hours():
# on Monday, allow for 24h + 48h
if datetime.date.today().weekday() == 0:
return 72
return 24


# Return true if the PR has a waiting tag, false otherwise.
def is_waiting(labels):
for label in labels:
if label.name == 'waiting' or label.name == 'waiting:any':
return True
return False


# Generate a pr message, bolding the time if it's out-SLO
def pr_message(pr_age, pr_url, pr_title, delta_days, delta_hours):
if pr_age < datetime.timedelta(hours=get_slo_hours()):
return "<%s|%s> has been waiting %s days %s hours\n" % (
pr_url, pr_title, delta_days, delta_hours)
else:
return "<%s|%s> has been waiting *%s days %s hours*\n" % (
pr_url, pr_title, delta_days, delta_hours)


# Adds reminder lines to the appropriate assignee to review the assigned PRs
# Returns true if one of the assignees is in the primary_assignee_map, false otherwise.
def add_reminders(
assignees, assignees_and_prs, message, primary_assignee_map):
has_primary_assignee = False
for assignee_info in assignees:
assignee = assignee_info.login
if assignee in primary_assignee_map:
has_primary_assignee = True
if assignee not in assignees_and_prs.keys():
assignees_and_prs[
assignee] = "Hello, %s, here are your PR reminders for the day \n" % assignee
assignees_and_prs[assignee] = assignees_and_prs[assignee] + message
return has_primary_assignee


def track_prs():
git = github.Github()
repo = git.get_repo('envoyproxy/envoy-mobile')

# A dict of maintainer : outstanding_pr_string to be sent to slack
reviewers_and_prs = {}
# Out-SLO PRs to be sent to #envoy-maintainer-oncall
stalled_prs = ""

# Snag all PRs, including drafts
for pr_info in repo.get_pulls("open", "updated", "desc"):
labels = pr_info.labels
assignees = pr_info.assignees
# If the PR is waiting, continue.
if is_waiting(labels):
continue
# Drafts are not covered by our SLO (repokitteh warns of this)
if pr_info.draft:
continue
# envoy-mobile currently doesn't triage unassigned PRs.
if not(pr_info.assignees):
continue

# Update the time based on the time zone delta from github's
pr_age = pr_info.updated_at - datetime.timedelta(hours=4)
delta = datetime.datetime.now() - pr_age
delta_days = delta.days
delta_hours = delta.seconds // 3600

# If we get to this point, the review may be in SLO - nudge if it's in
# SLO, nudge in bold if not.
message = pr_message(delta, pr_info.html_url, pr_info.title, delta_days, delta_hours)

# If the PR has been out-SLO for over a day, inform maintainers.
if delta > datetime.timedelta(hours=get_slo_hours() + 36):
stalled_prs = stalled_prs + message

# Add a reminder to each maintainer-assigner on the PR.
add_reminders(pr_info.assignees, reviewers_and_prs, message, REVIEWERS)

# Return the dict of {reviewers : PR notifications},
# and stalled PRs
return reviewers_and_prs, stalled_prs


def post_to_assignee(client, assignees_and_messages, assignees_map):
# Post updates to individual assignees
for key in assignees_and_messages:
message = assignees_and_messages[key]

# Only send messages if we have the slack UID
if key not in assignees_map:
continue
uid = assignees_map[key]

# Ship messages off to slack.
try:
print(assignees_and_messages[key])
response = client.conversations_open(users=uid, text="hello")
channel_id = response["channel"]["id"]
response = client.chat_postMessage(channel=channel_id, text=message)
except SlackApiError as e:
print("Unexpected error %s", e.response["error"])


def post_to_oncall(client, out_slo_prs):
try:
response = client.chat_postMessage(
channel='#envoy-mobile-oncall', text=("*Stalled PRs*\n\n%s" % out_slo_prs))
except SlackApiError as e:
print("Unexpected error %s", e.response["error"])


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'--cron_job',
action="store_true",
help="true if this is run by the daily cron job, false if run manually by a developer")
args = parser.parse_args()

reviewers_and_messages, stalled_prs = track_prs()

if not args.cron_job:
print(reviewers_and_messages)
print("\n\n\n")
print(stalled_prs)
exit(0)

SLACK_BOT_TOKEN = os.getenv('SLACK_BOT_TOKEN')
if not SLACK_BOT_TOKEN:
print(
'Missing SLACK_BOT_TOKEN: please export token from https://api.slack.com/apps/A023NPQQ33K/oauth?'
)
sys.exit(1)

client = WebClient(token=SLACK_BOT_TOKEN)
post_to_oncall(client, reviewers_and_messages['unassigned'], stalled_prs)
post_to_assignee(client, reviewers_and_messages, REVIEWERS)
2 changes: 2 additions & 0 deletions .github/actions/pr_notifier/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pygithub
slack_sdk
124 changes: 124 additions & 0 deletions .github/actions/pr_notifier/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --generate-hashes .github/actions/pr_notifier/requirements.txt
#
certifi==2021.5.30 \
--hash=sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee \
--hash=sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8
# via requests
cffi==1.14.5 \
--hash=sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813 \
--hash=sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373 \
--hash=sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69 \
--hash=sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f \
--hash=sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06 \
--hash=sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05 \
--hash=sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea \
--hash=sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee \
--hash=sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0 \
--hash=sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396 \
--hash=sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7 \
--hash=sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f \
--hash=sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73 \
--hash=sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315 \
--hash=sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76 \
--hash=sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1 \
--hash=sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49 \
--hash=sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed \
--hash=sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892 \
--hash=sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482 \
--hash=sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058 \
--hash=sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5 \
--hash=sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53 \
--hash=sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045 \
--hash=sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3 \
--hash=sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55 \
--hash=sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5 \
--hash=sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e \
--hash=sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c \
--hash=sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369 \
--hash=sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827 \
--hash=sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053 \
--hash=sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa \
--hash=sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4 \
--hash=sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322 \
--hash=sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132 \
--hash=sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62 \
--hash=sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa \
--hash=sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0 \
--hash=sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396 \
--hash=sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e \
--hash=sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991 \
--hash=sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6 \
--hash=sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc \
--hash=sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1 \
--hash=sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406 \
--hash=sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333 \
--hash=sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d \
--hash=sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c
# via pynacl
chardet==4.0.0 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5
# via requests
deprecated==1.2.13 \
--hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \
--hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d
# via pygithub
idna==2.10 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0
# via requests
pycparser==2.20 \
--hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705
# via cffi
pygithub==1.55 \
--hash=sha256:1bbfff9372047ff3f21d5cd8e07720f3dbfdaf6462fcaed9d815f528f1ba7283 \
--hash=sha256:2caf0054ea079b71e539741ae56c5a95e073b81fa472ce222e81667381b9601b
# via -r requirements.in
pyjwt==2.1.0 \
--hash=sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1 \
--hash=sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130
# via pygithub
pynacl==1.4.0 \
--hash=sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4 \
--hash=sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4 \
--hash=sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574 \
--hash=sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d \
--hash=sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634 \
--hash=sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25 \
--hash=sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f \
--hash=sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505 \
--hash=sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122 \
--hash=sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7 \
--hash=sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420 \
--hash=sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f \
--hash=sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96 \
--hash=sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6 \
--hash=sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6 \
--hash=sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514 \
--hash=sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff \
--hash=sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80
# via pygithub
requests==2.25.1 \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e
# via pygithub
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via pynacl
slack_sdk==3.13.0 \
--hash=sha256:54f2a5f7419f1ab932af9e3200f7f2f93db96e0f0eb8ad7d3b4214aa9f124641 \
--hash=sha256:aae6ce057e286a5e7fe7a9f256e85b886eee556def8e04b82b08f699e64d7f67
# via -r requirements.in
urllib3==1.26.6 \
--hash=sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4 \
--hash=sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f
# via requests
wrapt==1.12.1 \
--hash=sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7
# via deprecated
26 changes: 26 additions & 0 deletions .github/workflows/pr_notifier.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
on:
workflow_dispatch:
schedule:
- cron: '0 5 * * 1,2,3,4,5'

jobs:
pr_notifier:
name: PR Notifier
runs-on: ubuntu-latest
if: github.repository_owner == 'envoyproxy'

steps:
- uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: '3.8'
architecture: 'x64'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ./.github/actions/pr_notifier/requirements.txt
- name: Notify about PRs
run: python ./.github/actions/pr_notifier/pr_notifier.py --cron_job
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}