From 3696a1905e7505123a69ed4f0f74730a8fb49fa7 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Wed, 17 Jun 2026 10:07:15 +0200 Subject: [PATCH 1/3] Add automation for monthly update info gathering. Signed-off-by: Josh Matthews --- handlers/monthly_update/__init__.py | 24 +++++++++++++++++++ .../tests/label_monthly_update.json | 21 ++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 handlers/monthly_update/__init__.py create mode 100644 handlers/monthly_update/tests/label_monthly_update.json diff --git a/handlers/monthly_update/__init__.py b/handlers/monthly_update/__init__.py new file mode 100644 index 0000000..22da891 --- /dev/null +++ b/handlers/monthly_update/__init__.py @@ -0,0 +1,24 @@ +from eventhandler import EventHandler + +MSG = ('We would like to write about this change in the monthly blog post! ' + 'We have a few questions about this PR that will make this task ' + 'easie :smile:\n\n' + '1. Who is most impacted by this change: users, ' + 'Servo developers, embedders, or some other group?\n' + '1. What observable difference does this change make?\n' + '1. What preferences (if any) need to be enabled to observe ' + 'this difference?\n' + '1. What (if any) specific URLs are affected?\n\n' + 'If this change is part of a broader feature/project please ' + 'make sure the PR description contains a `Fixes: #12345` or ' + '`Part of: #12345` issue reference.\n\n' + 'Thanks for helping us prepare the monthly blog post! :heart:') + + +class MonthlyUpdateHandler(EventHandler): + def on_issue_labeled(self, api, payload): + if payload['label']['name'].lower() == 'monthly update': + api.post_comment(MSG) + + +handler_interface = MonthlyUpdateHandler diff --git a/handlers/monthly_update/tests/label_monthly_update.json b/handlers/monthly_update/tests/label_monthly_update.json new file mode 100644 index 0000000..03e0970 --- /dev/null +++ b/handlers/monthly_update/tests/label_monthly_update.json @@ -0,0 +1,21 @@ +{ + "initial": {}, + "expected": { + "comments": 1 + }, + "payload": { + "repository": { + "owner": { + "login": "servo" + }, + "name": "highfive" + }, + "label": { + "name": "monthly update" + }, + "action": "labeled", + "issue": { + "number": 130 + } + } +} From 87ab410788013baf49cedf55d5ecf86d0594856d Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Thu, 18 Jun 2026 07:37:56 +0200 Subject: [PATCH 2/3] Change language. Signed-off-by: Josh Matthews --- handlers/monthly_update/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/handlers/monthly_update/__init__.py b/handlers/monthly_update/__init__.py index 22da891..ceb5732 100644 --- a/handlers/monthly_update/__init__.py +++ b/handlers/monthly_update/__init__.py @@ -1,8 +1,8 @@ from eventhandler import EventHandler -MSG = ('We would like to write about this change in the monthly blog post! ' - 'We have a few questions about this PR that will make this task ' - 'easie :smile:\n\n' +MSG = ('Someone thinks this change could be added to the monthly blog ' + 'post! To help with this, we need someone to answer the following ' + 'qurstions: :smile:\n\n' '1. Who is most impacted by this change: users, ' 'Servo developers, embedders, or some other group?\n' '1. What observable difference does this change make?\n' @@ -12,13 +12,16 @@ 'If this change is part of a broader feature/project please ' 'make sure the PR description contains a `Fixes: #12345` or ' '`Part of: #12345` issue reference.\n\n' + 'Please add `@%s monthly update` when answering these ' + 'questions so the bot notices your answer (or just quote this ' + 'comment).\n\n' 'Thanks for helping us prepare the monthly blog post! :heart:') class MonthlyUpdateHandler(EventHandler): def on_issue_labeled(self, api, payload): if payload['label']['name'].lower() == 'monthly update': - api.post_comment(MSG) + api.post_comment(MSG % api.user) handler_interface = MonthlyUpdateHandler From f0fdf957b86e256ccb7ee1b2088c565ef6ed7898 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Thu, 18 Jun 2026 08:07:41 +0200 Subject: [PATCH 3/3] Add automation for noticing replies. Signed-off-by: Josh Matthews --- handlers/monthly_update/__init__.py | 23 +++++++++++++++++++-- handlers/monthly_update/tests/reply.json | 26 ++++++++++++++++++++++++ newpr.py | 11 ++++++++++ test.py | 7 +++++++ 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 handlers/monthly_update/tests/reply.json diff --git a/handlers/monthly_update/__init__.py b/handlers/monthly_update/__init__.py index ceb5732..8b0d2ca 100644 --- a/handlers/monthly_update/__init__.py +++ b/handlers/monthly_update/__init__.py @@ -1,5 +1,11 @@ from eventhandler import EventHandler +import re + +REPLY = "monthly update" + +REACTION = "eyes" + MSG = ('Someone thinks this change could be added to the monthly blog ' 'post! To help with this, we need someone to answer the following ' 'qurstions: :smile:\n\n' @@ -12,7 +18,7 @@ 'If this change is part of a broader feature/project please ' 'make sure the PR description contains a `Fixes: #12345` or ' '`Part of: #12345` issue reference.\n\n' - 'Please add `@%s monthly update` when answering these ' + 'Please add `@%s %s` when answering these ' 'questions so the bot notices your answer (or just quote this ' 'comment).\n\n' 'Thanks for helping us prepare the monthly blog post! :heart:') @@ -21,7 +27,20 @@ class MonthlyUpdateHandler(EventHandler): def on_issue_labeled(self, api, payload): if payload['label']['name'].lower() == 'monthly update': - api.post_comment(MSG % api.user) + api.post_comment(MSG % (api.user, REPLY)) + + def on_new_comment(self, api, payload): + if payload['issue']['state'] != 'open': + return + + user = payload['comment']['user']['login'] + if user == api.user: # ignore comments from self + return # (since `MSG` already has `REPLY`) + + msg = payload['comment']['body'] + + if re.search(r'@%s[: ]*%s' % (api.user, REPLY), str(msg)): + api.add_reaction(payload['comment']['id'], REACTION) handler_interface = MonthlyUpdateHandler diff --git a/handlers/monthly_update/tests/reply.json b/handlers/monthly_update/tests/reply.json new file mode 100644 index 0000000..325ded4 --- /dev/null +++ b/handlers/monthly_update/tests/reply.json @@ -0,0 +1,26 @@ +{ + "initial": {}, + "expected": { + "reactions": 1 + }, + "payload": { + "repository": { + "owner": { + "login": "servo" + }, + "name": "highfive" + }, + "action": "created", + "issue": { + "number": 130, + "state": "open" + }, + "comment": { + "user": { + "login": "someone" + }, + "body": "@highfive monthly update answers!", + "id": 12 + } + } +} diff --git a/newpr.py b/newpr.py index 98ca7b1..629e093 100755 --- a/newpr.py +++ b/newpr.py @@ -86,6 +86,7 @@ class GithubAPIProvider(APIProvider): BASE_URL = "https://api.github.com/repos/" contributors_url = BASE_URL + "%s/%s/contributors?per_page=400" post_comment_url = BASE_URL + "%s/%s/issues/%s/comments" + add_reaction_url = BASE_URL + "%s/%s/issues/comments/%s/reactions" collaborators_url = BASE_URL + "%s/%s/collaborators" issue_url = BASE_URL + "%s/%s/issues/%s" get_label_url = BASE_URL + "%s/%s/issues/%s/labels" @@ -170,6 +171,16 @@ def is_new_contributor(self, username): return True url = links['next'] + def add_reaction(self, comment_id, reaction): + url = self.add_reaction_url % (self.owner, self.repo, comment_id) + try: + self.api_req("POST", url, {"content": reaction}) + except error.HTTPError as e: + if e.code == 201: + pass + else: + raise e + def post_comment(self, body): url = self.post_comment_url % (self.owner, self.repo, self.issue) try: diff --git a/test.py b/test.py index 8658aa8..478d505 100644 --- a/test.py +++ b/test.py @@ -22,6 +22,7 @@ def __init__(self, payload, user, new_contributor, labels, assignee, self.diff = diff self.pull_request = pull_request self.repo = str(self.repo) # workaround for testing + self.reactions = [] def is_new_contributor(self, username): return self.new_contributor @@ -32,6 +33,9 @@ def post_comment(self, body): def add_label(self, label): self.labels += [label] + def add_reaction(self, comment_id, reaction): + self.reactions += [reaction] + def remove_label(self, label): self.labels.remove(label) @@ -107,6 +111,9 @@ def run_tests(tests, warn=True, overwrite=False): if 'assignee' in expected: assert api.assignee == expected['assignee'], \ "%s == %s" % (api.assignee, expected['assignee']) + if 'reactions' in expected: + assert len(api.reactions) == expected['reactions'], \ + "%s == %s" % (len(api.reactions), expected['reactions']) # If this is the last test in the file, then it's time for cleanup if test['clean']: