diff --git a/.github/actions/pr_notifier/pr_notifier.py b/.github/actions/pr_notifier/pr_notifier.py new file mode 100644 index 0000000000..7efbecea70 --- /dev/null +++ b/.github/actions/pr_notifier/pr_notifier.py @@ -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', +} + +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) diff --git a/.github/actions/pr_notifier/requirements.in b/.github/actions/pr_notifier/requirements.in new file mode 100644 index 0000000000..b27ccacba2 --- /dev/null +++ b/.github/actions/pr_notifier/requirements.in @@ -0,0 +1,2 @@ +pygithub +slack_sdk diff --git a/.github/actions/pr_notifier/requirements.txt b/.github/actions/pr_notifier/requirements.txt new file mode 100644 index 0000000000..c8f43486f2 --- /dev/null +++ b/.github/actions/pr_notifier/requirements.txt @@ -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 diff --git a/.github/workflows/pr_notifier.yml b/.github/workflows/pr_notifier.yml new file mode 100644 index 0000000000..b08b0a1027 --- /dev/null +++ b/.github/workflows/pr_notifier.yml @@ -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 }}