From f5de3e2a5f9c9da62f7e47e9c86b566e3e345123 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Fri, 24 Jun 2022 07:48:48 +1000 Subject: [PATCH] Add PR review reminder DAG (#553) * [WIP] Add PR review reminder DAG * Use requests directly instead of HttpHook * Add unit tests and fix weekday calculation * Use crontab config to only run on weekdays * Move repos into a constant * Reduce calls to base_repo_name * Remove unnecessary template configuration * Refine argument types * Use session and rename var for masking * Move files and reduce necessary calls * Add unit tests and fix bugs * Move files and clean up default dry_run values * Correct Urgency types * Remove unused function * Add docs to test factory --- docker-compose.override.yml | 2 + env.template | 4 + openverse_catalog/dags/common/github.py | 52 +++ .../pr_review_reminders.py | 172 ++++++++ .../pr_review_reminders_dag.py | 60 +++ tests/dags/conftest.py | 42 ++ .../maintenance/test_pr_review_reminders.py | 191 +++++++++ tests/factories/github/__init__.py | 93 +++++ tests/factories/github/comment.json | 44 ++ tests/factories/github/pull.json | 375 ++++++++++++++++++ .../factories/github/requested_reviewer.json | 20 + 11 files changed, 1055 insertions(+) create mode 100644 openverse_catalog/dags/common/github.py create mode 100644 openverse_catalog/dags/maintenance/pr_review_reminders/pr_review_reminders.py create mode 100644 openverse_catalog/dags/maintenance/pr_review_reminders/pr_review_reminders_dag.py create mode 100644 tests/dags/maintenance/test_pr_review_reminders.py create mode 100644 tests/factories/github/__init__.py create mode 100644 tests/factories/github/comment.json create mode 100644 tests/factories/github/pull.json create mode 100644 tests/factories/github/requested_reviewer.json diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 55df2f2a40c..537e671763f 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -61,6 +61,8 @@ services: command: init volumes: - ./openverse_catalog:/usr/local/airflow/openverse_catalog + stdin_open: true + tty: true volumes: postgres: diff --git a/env.template b/env.template index 75717e79a76..33264748f28 100644 --- a/env.template +++ b/env.template @@ -34,6 +34,7 @@ AIRFLOW_VAR_API_KEY_NYPL=not_set AIRFLOW_VAR_API_KEY_THINGIVERSE=not_set AIRFLOW_VAR_API_KEY_FREESOUND=not_set + ######################################################################################## # Connection/Variable info ######################################################################################## @@ -76,6 +77,9 @@ AIRFLOW_CONN_DATA_REFRESH=http://172.17.0.1:8001 # Django Admin url. Change the following line to use the appropriate environment. DJANGO_ADMIN_URL="https://localhost:8000/admin" +# GitHub - used for maintenance +AIRFLOW_VAR_GITHUB_API_KEY="not_set" + ######################################################################################## # Other config diff --git a/openverse_catalog/dags/common/github.py b/openverse_catalog/dags/common/github.py new file mode 100644 index 00000000000..152b3d072a8 --- /dev/null +++ b/openverse_catalog/dags/common/github.py @@ -0,0 +1,52 @@ +import requests + + +class GitHubAPI: + def __init__(self, pat: str): + """ + :param pat: GitHub Personal Access Token to use to authenticate requests + """ + self.session = requests.Session() + self.session.headers["Authorization"] = f"token {pat}" + + def _make_request(self, method: str, resource: str, **kwargs) -> requests.Response: + response = getattr(self.session, method.lower())( + f"https://api.github.com/{resource}", **kwargs + ) + response.raise_for_status() + return response.json() + + def get_open_prs(self, repo: str, owner: str = "WordPress"): + return self._make_request( + "GET", + f"repos/{owner}/{repo}/pulls", + data={ + "state": "open", + "base": "main", + "sort": "updated", + # this is the default when ``sort`` is ``updated`` but + # it's helpful to specify for readers + "direction": "asc", + # we don't bother paginating because if we ever + # have more than 100 open PRs in a single repo + # then something is seriously wrong + "per_page": 100, + }, + ) + + def post_issue_comment( + self, repo: str, issue_number: int, comment_body: str, owner: str = "WordPress" + ): + return self._make_request( + "POST", + f"repos/{owner}/{repo}/issues/{issue_number}/comments", + data={"body": comment_body}, + ) + + def get_issue_comments( + self, repo: str, issue_number: int, owner: str = "WordPress" + ): + return self._make_request( + "GET", + f"repos/{owner}/{repo}/issues/{issue_number}/comments", + ) diff --git a/openverse_catalog/dags/maintenance/pr_review_reminders/pr_review_reminders.py b/openverse_catalog/dags/maintenance/pr_review_reminders/pr_review_reminders.py new file mode 100644 index 00000000000..b10bc9b1016 --- /dev/null +++ b/openverse_catalog/dags/maintenance/pr_review_reminders/pr_review_reminders.py @@ -0,0 +1,172 @@ +import datetime +import logging +from dataclasses import dataclass +from typing import Optional + +from common.github import GitHubAPI + + +logger = logging.getLogger(__name__) + + +REPOSITORIES = [ + "openverse", + "openverse-catalog", + "openverse-api", + "openverse-frontend", + "openverse-infrastructure", +] + + +class Urgency: + @dataclass + class Urgency: + label: str + days: int + + CRITICAL = Urgency("critical", 1) + HIGH = Urgency("high", 2) + MEDIUM = Urgency("medium", 4) + LOW = Urgency("low", 5) + + +@dataclass +class ReviewDelta: + urgency: Urgency.Urgency + days: int + + +def pr_urgency(pr: dict) -> Urgency.Urgency: + priority_labels = [ + label["name"] for label in pr["labels"] if "priority" in label["name"].lower() + ] + if not priority_labels: + logger.error(f"Found unabled PR ({pr['html_url']}). Skipping!") + return None + + priority_label = priority_labels[0] + + if "critical" in priority_label: + return Urgency.CRITICAL + elif "high" in priority_label: + return Urgency.HIGH + elif "medium" in priority_label: + return Urgency.MEDIUM + elif "low" in priority_label: + return Urgency.LOW + + +def days_without_weekends(today: datetime, updated_at: datetime) -> int: + """ + Adapted from: + https://stackoverflow.com/a/3615984 CC BY-SA 2.5 + """ + if today.weekday() == 0 and (today - updated_at).days < 3: + # shortcut mondays to 0 if last updated on the weekend + return 0 + + daygenerator = ( + updated_at + datetime.timedelta(x + 1) for x in range((today - updated_at).days) + ) + return sum(1 for day in daygenerator if day.weekday() < 5) + + +def get_urgency_if_urgent(pr: dict) -> Optional[ReviewDelta]: + updated_at = datetime.datetime.fromisoformat(pr["updated_at"].rstrip("Z")) + today = datetime.datetime.now() + urgency = pr_urgency(pr) + if urgency is None: + return None + + days = days_without_weekends(today, updated_at) + + return ReviewDelta(urgency, days) if days >= urgency.days else None + + +COMMENT_MARKER = ( + "This reminder is being automatically generated due to the urgency configuration." +) + + +COMMENT_TEMPLATE = ( + """ +Based on the {urgency_label} urgency of this PR, the following reviewers are being +gently reminded to review this PR: + +{user_logins} + +""" + f"{COMMENT_MARKER}" + """ +Ignoring weekend days, this PR was updated {days_since_update} day(s) ago. PRs +labelled with {urgency_label} urgency are expected to be reviewed within {urgency_days}. + +@{pr_author}, if this PR is not ready for a review, please draft it to prevent reviewers +from getting further unnecessary pings. +""" +) + + +def build_comment(review_delta: ReviewDelta, pr: dict): + user_handles = [f"@{req['login']}" for req in pr["requested_reviewers"]] + return user_handles, COMMENT_TEMPLATE.format( + urgency_label=review_delta.urgency.label, + urgency_days=review_delta.urgency.days, + user_logins="\n".join(user_handles), + days_since_update=review_delta.days, + pr_author=pr["user"]["login"], + ) + + +def base_repo_name(pr: dict): + return pr["base"]["repo"]["name"] + + +def post_reminders(github_pat: str, dry_run: bool): + gh = GitHubAPI(github_pat) + + open_prs = [] + for repo in REPOSITORIES: + open_prs += [pr for pr in gh.get_open_prs(repo) if not pr["draft"]] + + urgent_prs = [] + for pr in open_prs: + review_delta = get_urgency_if_urgent(pr) + if review_delta: + urgent_prs.append((pr, review_delta)) + + to_ping = [] + for pr, review_delta in urgent_prs: + repo = base_repo_name(pr) + comments = gh.get_issue_comments(repo, pr["number"]) + + reminder_comments = [ + comment + for comment in comments + if ( + comment["user"]["login"] == "openverse-bot" + and COMMENT_MARKER in comment["body"] + ) + ] + if reminder_comments: + # maybe in the future we re-ping in some cases? + continue + + if pr["requested_reviewers"] == []: + # no requested reviewers to ping, maybe in the future we ping + # the PR author or the whole openverse maintainers team? + continue + + to_ping.append((pr, review_delta)) + + for pr, review_delta in to_ping: + user_handles, comment_body = build_comment(review_delta, pr) + + logger.info(f"Pinging {', '.join(user_handles)} to review {pr['title']}") + if not dry_run: + gh.post_issue_comment(base_repo_name(pr), pr["number"], comment_body) + + if dry_run: + logger.info( + "This was a dry run. None of the pings listed above were actually sent." + ) diff --git a/openverse_catalog/dags/maintenance/pr_review_reminders/pr_review_reminders_dag.py b/openverse_catalog/dags/maintenance/pr_review_reminders/pr_review_reminders_dag.py new file mode 100644 index 00000000000..9768c83cb3d --- /dev/null +++ b/openverse_catalog/dags/maintenance/pr_review_reminders/pr_review_reminders_dag.py @@ -0,0 +1,60 @@ +""" +Iterates through open PRs in our repositories and pings assigned reviewers +who have not yet approved the PR or explicitly requested changes. + +This DAG runs daily and pings on the following schedule based on priority label: + +| priority | days | +| --- | --- | +| critical | 1 day | +| high | >2 days | +| medium | >4 days | +| low | >7 days | + +The DAG does not ping on Saturday and Sunday and accounts for weekend days +when determining how much time has passed since the review. + +Unfortunately the DAG does not know when someone is on vacation. It is up to the +author of the PR to re-assign review if one of the randomly selected reviewers +is unavailable for the time period during which the PR should be reviewed. +""" + +from datetime import datetime, timedelta + +from airflow.models import DAG, Variable +from airflow.operators.python import PythonOperator +from common import pr_review_reminders +from common.constants import DAG_DEFAULT_ARGS + + +DAG_ID = "pr_review_reminders" +MAX_ACTIVE_TASKS = 1 +ENVIRONMENT = Variable.get("ENVIRONMENT", default_var="dev") +DRY_RUN = Variable.get("PR_REVIEW_REMINDER_DRY_RUN", default_var=(ENVIRONMENT == "dev")) +GITHUB_PAT = Variable.get("GITHUB_API_KEY", default_var="not_set") + +dag = DAG( + dag_id=DAG_ID, + default_args={ + **DAG_DEFAULT_ARGS, + "retry_delay": timedelta(minutes=1), + }, + start_date=datetime(2022, 6, 9), + # Run every weekday + schedule_interval="0 0 * * 1-5", + max_active_tasks=MAX_ACTIVE_TASKS, + max_active_runs=MAX_ACTIVE_TASKS, + # If this was True, airflow would run this DAG in the beginning + # for each day from the start day to now + catchup=False, + # Use the docstring at the top of the file as md docs in the UI + doc_md=__doc__, + tags=["maintenance"], +) + +with dag: + PythonOperator( + task_id="pr_review_reminder_operator", + python_callable=pr_review_reminders.post_reminders, + op_kwargs={"github_pat": GITHUB_PAT, "dry_run": DRY_RUN}, + ) diff --git a/tests/dags/conftest.py b/tests/dags/conftest.py index 45685885838..db86a78c94f 100644 --- a/tests/dags/conftest.py +++ b/tests/dags/conftest.py @@ -58,3 +58,45 @@ def requests_get_mock(): with mock.patch("common.urls.requests_get", autospec=True) as mock_get: mock_get.side_effect = _make_response yield + + +@pytest.fixture +def freeze_time(monkeypatch): + """ + Now() manager patches datetime return a fixed, settable, value + (freezes time) + + https://stackoverflow.com/a/28073449 CC BY-SA 3.0 + """ + import datetime + + original = datetime.datetime + + class FreezeMeta(type): + def __instancecheck__(self, instance): + if type(instance) == original or type(instance) == Freeze: + return True + + class Freeze(datetime.datetime): + __metaclass__ = FreezeMeta + + @classmethod + def freeze(cls, val): + cls.frozen = val + + @classmethod + def now(cls): + return cls.frozen + + @classmethod + def delta(cls, timedelta=None, **kwargs): + """Moves time fwd/bwd by the delta""" + from datetime import timedelta as td + + if not timedelta: + timedelta = td(**kwargs) + cls.frozen += timedelta + + monkeypatch.setattr(datetime, "datetime", Freeze) + Freeze.freeze(original.now()) + return Freeze diff --git a/tests/dags/maintenance/test_pr_review_reminders.py b/tests/dags/maintenance/test_pr_review_reminders.py new file mode 100644 index 00000000000..9f1bb548487 --- /dev/null +++ b/tests/dags/maintenance/test_pr_review_reminders.py @@ -0,0 +1,191 @@ +from collections import defaultdict +from datetime import datetime, timedelta + +import pytest + +from openverse_catalog.dags.maintenance.pr_review_reminders.pr_review_reminders import ( + Urgency, + days_without_weekends, + post_reminders, +) +from tests.factories.github import make_pr_comment, make_pull, make_requested_reviewer + + +MONDAY = datetime(2022, 6, 13) +TUESDAY = MONDAY + timedelta(days=1) +WEDNESDAY = MONDAY + timedelta(days=2) +THURSDAY = MONDAY + timedelta(days=3) +FRIDAY = MONDAY + timedelta(days=4) +SATURDAY = MONDAY + timedelta(days=5) +SUNDAY = MONDAY + timedelta(days=6) + +NEXT_MONDAY = MONDAY + timedelta(days=7) +NEXT_TUESDAY = MONDAY + timedelta(days=8) +NEXT_WEDNESDAY = MONDAY + timedelta(days=9) + +LAST_SUNDAY = MONDAY - timedelta(days=1) +LAST_SATURDAY = MONDAY - timedelta(days=2) +LAST_FRIDAY = MONDAY - timedelta(days=3) +LAST_THURSDAY = MONDAY - timedelta(days=4) +LAST_WEDNESDAY = MONDAY - timedelta(days=5) +LAST_TUESDAY = MONDAY - timedelta(days=6) +LAST_MONDAY = MONDAY - timedelta(days=7) + + +@pytest.mark.parametrize( + "today, against, expected_days", + ( + (MONDAY, LAST_SUNDAY, 0), + (MONDAY, LAST_SATURDAY, 0), + (MONDAY, LAST_FRIDAY, 1), + (MONDAY, LAST_THURSDAY, 2), + (MONDAY, LAST_WEDNESDAY, 3), + (MONDAY, LAST_TUESDAY, 4), + (MONDAY, LAST_MONDAY, 5), + (MONDAY, MONDAY, 0), + (TUESDAY, MONDAY, 1), + (WEDNESDAY, MONDAY, 2), + (THURSDAY, MONDAY, 3), + (FRIDAY, MONDAY, 4), + (FRIDAY, THURSDAY, 1), + (THURSDAY, WEDNESDAY, 1), + (WEDNESDAY, TUESDAY, 1), + (SUNDAY, SATURDAY, 0), + (NEXT_MONDAY, LAST_MONDAY, 10), + (NEXT_TUESDAY, LAST_MONDAY, 11), + (NEXT_WEDNESDAY, LAST_MONDAY, 12), + ), +) +def test_days_without_weekends_no_weekend_days_monday(today, against, expected_days): + assert days_without_weekends(today, against) == expected_days + + +@pytest.fixture +def github(monkeypatch): + pulls = [] + pull_comments = defaultdict(list) + posted_comments = defaultdict(list) + + def get_prs(*args, **kwargs): + return pulls + + def get_comments(*args, **kwargs): + pr_number = args[2] + return pull_comments[pr_number] + + def post_comment(*args, **kwargs): + pr_number = args[2] + body = args[3] + posted_comments[pr_number].append(body) + + monkeypatch.setattr( + "openverse_catalog.dags.maintenance.pr_review_reminders.pr_review_reminders.GitHubAPI.get_open_prs", + get_prs, + ) + monkeypatch.setattr( + "openverse_catalog.dags.maintenance.pr_review_reminders.pr_review_reminders.GitHubAPI.get_issue_comments", + get_comments, + ) + monkeypatch.setattr( + "openverse_catalog.dags.maintenance.pr_review_reminders.pr_review_reminders.GitHubAPI.post_issue_comment", + post_comment, + ) + + yield { + "pulls": pulls, + "pull_comments": pull_comments, + "posted_comments": posted_comments, + } + + +@pytest.fixture(autouse=True) +def freeze_friday(freeze_time): + freeze_time.freeze(FRIDAY) + + +@pytest.mark.parametrize( + "urgency", + ( + Urgency.CRITICAL, + Urgency.HIGH, + Urgency.MEDIUM, + Urgency.LOW, + ), +) +def test_pings_past_due(github, urgency): + past_due_pull = make_pull(urgency, past_due=True) + past_due_pull["requested_reviewers"] = [ + make_requested_reviewer(f"reviewer-due-{i}") for i in range(2) + ] + not_due_pull = make_pull(urgency, past_due=False) + not_due_pull["requested_reviewers"] = [ + make_requested_reviewer(f"reviewer-not-due-{i}") for i in range(2) + ] + github["pulls"] += [past_due_pull, not_due_pull] + github["pull_comments"][past_due_pull["number"]].append( + make_pr_comment(is_reminder=False) + ) + + post_reminders("not_set", dry_run=False) + + assert past_due_pull["number"] in github["posted_comments"] + assert not_due_pull["number"] not in github["posted_comments"] + + comments = github["posted_comments"][past_due_pull["number"]] + for reviewer in past_due_pull["requested_reviewers"]: + for comment in comments: + assert f"@{reviewer['login']}" in comment + + +@pytest.mark.parametrize( + "urgency", + ( + Urgency.CRITICAL, + Urgency.HIGH, + Urgency.MEDIUM, + Urgency.LOW, + ), +) +def test_does_not_reping_past_due(github, urgency): + past_due_pull = make_pull(urgency, past_due=True) + past_due_pull["requested_reviewers"] = [ + make_requested_reviewer(f"reviewer-due-{i}") for i in range(2) + ] + not_due_pull = make_pull(urgency, past_due=False) + not_due_pull["requested_reviewers"] = [ + make_requested_reviewer(f"reviewer-not-due-{i}") for i in range(2) + ] + github["pulls"] += [past_due_pull, not_due_pull] + github["pull_comments"][past_due_pull["number"]].append( + make_pr_comment(is_reminder=True) + ) + + post_reminders("not_set", dry_run=False) + + assert past_due_pull["number"] not in github["posted_comments"] + assert not_due_pull["number"] not in github["posted_comments"] + + +@pytest.mark.parametrize( + "urgency", + ( + Urgency.CRITICAL, + Urgency.HIGH, + Urgency.MEDIUM, + Urgency.LOW, + ), +) +def test_does_not_ping_if_no_reviewers(github, urgency): + past_due_pull = make_pull(urgency, past_due=True) + past_due_pull["requested_reviewers"] = [] + not_due_pull = make_pull(urgency, past_due=False) + not_due_pull["requested_reviewers"] = [] + github["pulls"] += [past_due_pull, not_due_pull] + github["pull_comments"][past_due_pull["number"]].append( + make_pr_comment(is_reminder=False) + ) + + post_reminders("not_set", dry_run=False) + + assert past_due_pull["number"] not in github["posted_comments"] + assert not_due_pull["number"] not in github["posted_comments"] diff --git a/tests/factories/github/__init__.py b/tests/factories/github/__init__.py new file mode 100644 index 00000000000..75b7af3b592 --- /dev/null +++ b/tests/factories/github/__init__.py @@ -0,0 +1,93 @@ +import datetime +import json +from pathlib import Path + +from openverse_catalog.dags.maintenance.pr_review_reminders.pr_review_reminders import ( + COMMENT_MARKER, + Urgency, +) + + +def _read_fixture(fixture: str) -> dict: + with open(Path(__file__).parent / f"{fixture}.json") as file: + return json.loads(file.read()) + + +def _make_label(priority: Urgency) -> dict: + return {"name": f"priority: {priority.label}"} + + +def walk_backwards_in_time_until_weekday_count(today: datetime.datetime, count: int): + test_date = today + weekday_count = 0 + while weekday_count < count: + test_date = test_date - datetime.timedelta(days=1) + if test_date.weekday() < 5: + weekday_count += 1 + + return test_date + + +_pr_count = 1 + + +def make_pull(urgency: Urgency, past_due: bool) -> dict: + """ + Creates a PR object like the one returned by the GitHub API. + The PR will also be created specifically to have the priority + label associated with the passed in urgency. + + A "past due" PR is one that has an ``updated_at`` value that is + further in the past than the number of days allowed by the + urgency of the PR. + + :param urgency: The priority to apply to the PR. + :param past_due: Whether to create a PR that is "past due". + """ + global _pr_count + pull = _read_fixture("pull") + pull["number"] = pull["id"] = _pr_count + _pr_count += 1 + + for label in pull["labels"]: + if "priority" in label["name"]: + label.update(**_make_label(urgency)) + break + + if past_due: + updated_at = walk_backwards_in_time_until_weekday_count( + datetime.datetime.now(), urgency.days + ) + else: + updated_at = datetime.datetime.now() + + pull["updated_at"] = f"{updated_at.isoformat()}Z" + + return pull + + +def make_requested_reviewer(login: str) -> dict: + requested_reviewer = _read_fixture("requested_reviewer") + + requested_reviewer["login"] = login + + return requested_reviewer + + +def make_pr_comment(is_reminder: bool) -> dict: + comment = _read_fixture("comment") + + if is_reminder: + comment["user"]["login"] = "openverse-bot" + + comment["body"] = ( + ("This is a comment\n" f"{COMMENT_MARKER}\n\n" "Please review me :)") + if is_reminder + else ( + "This looks great! Amazing work :tada: " + "You're lovely and valued as a contributor " + "and as a whole person." + ) + ) + + return comment diff --git a/tests/factories/github/comment.json b/tests/factories/github/comment.json new file mode 100644 index 00000000000..7c1ea0b02a5 --- /dev/null +++ b/tests/factories/github/comment.json @@ -0,0 +1,44 @@ +{ + "author_association": "NONE", + "body": "**API Developer Docs Preview**: _Ready_\n\n\n\nPlease note that GitHub pages takes a little time to deploy newly pushed code, if the links above don't work or you see old versions, wait 5 minutes and try again.\n\nYou can check [the GitHub pages deployment action list](https://github.com/WordPress/openverse-api/actions/workflows/pages/pages-build-deployment) to see the current status of the deployments.", + "created_at": "2022-06-20T23:10:10Z", + "html_url": "https://github.com/WordPress/openverse-api/pull/763#issuecomment-1160936954", + "id": 1160936954, + "issue_url": "https://api.github.com/repos/WordPress/openverse-api/issues/763", + "node_id": "IC_kwDOFSZ4Yc5FMn36", + "performed_via_github_app": null, + "reactions": { + "+1": 0, + "-1": 0, + "confused": 0, + "eyes": 0, + "heart": 0, + "hooray": 0, + "laugh": 0, + "rocket": 0, + "total_count": 0, + "url": "https://api.github.com/repos/WordPress/openverse-api/issues/comments/1160936954/reactions" + }, + "updated_at": "2022-06-20T23:11:40Z", + "url": "https://api.github.com/repos/WordPress/openverse-api/issues/comments/1160936954", + "user": { + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/apps/github-actions", + "id": 41898282, + "login": "github-actions[bot]", + "node_id": "MDM6Qm90NDE4OTgyODI=", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "type": "Bot", + "url": "https://api.github.com/users/github-actions%5Bbot%5D" + } +} diff --git a/tests/factories/github/pull.json b/tests/factories/github/pull.json new file mode 100644 index 00000000000..17d21918ef0 --- /dev/null +++ b/tests/factories/github/pull.json @@ -0,0 +1,375 @@ +{ + "_links": { + "comments": { + "href": "https://api.github.com/repos/WordPress/openverse-frontend/issues/1234598237459283745/comments" + }, + "commits": { + "href": "https://api.github.com/repos/WordPress/openverse-frontend/pulls/1234598237459283745/commits" + }, + "html": { + "href": "https://github.com/WordPress/openverse-frontend/pull/1234598237459283745" + }, + "issue": { + "href": "https://api.github.com/repos/WordPress/openverse-frontend/issues/1234598237459283745" + }, + "review_comment": { + "href": "https://api.github.com/repos/WordPress/openverse-frontend/pulls/comments{/number}" + }, + "review_comments": { + "href": "https://api.github.com/repos/WordPress/openverse-frontend/pulls/1234598237459283745/comments" + }, + "self": { + "href": "https://api.github.com/repos/WordPress/openverse-frontend/pulls/1234598237459283745" + }, + "statuses": { + "href": "https://api.github.com/repos/WordPress/openverse-frontend/statuses/asdfasdfasdfasdfasdfasdfasdf" + } + }, + "active_lock_reason": null, + "assignee": null, + "assignees": [], + "author_association": "NONE", + "auto_merge": null, + "base": { + "label": "WordPress:main", + "ref": "main", + "repo": { + "allow_forking": true, + "archive_url": "https://api.github.com/repos/WordPress/openverse-frontend/{archive_format}{/ref}", + "archived": false, + "assignees_url": "https://api.github.com/repos/WordPress/openverse-frontend/assignees{/user}", + "blobs_url": "https://api.github.com/repos/WordPress/openverse-frontend/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/WordPress/openverse-frontend/branches{/branch}", + "clone_url": "https://github.com/WordPress/openverse-frontend.git", + "collaborators_url": "https://api.github.com/repos/WordPress/openverse-frontend/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/WordPress/openverse-frontend/comments{/number}", + "commits_url": "https://api.github.com/repos/WordPress/openverse-frontend/commits{/sha}", + "compare_url": "https://api.github.com/repos/WordPress/openverse-frontend/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/WordPress/openverse-frontend/contents/{+path}", + "contributors_url": "https://api.github.com/repos/WordPress/openverse-frontend/contributors", + "created_at": "2021-04-05T13:24:08Z", + "default_branch": "main", + "deployments_url": "https://api.github.com/repos/WordPress/openverse-frontend/deployments", + "description": "The gateway to the Openverse. Openverse is a search tool for CC-licensed and public domain content across the internet.", + "disabled": false, + "downloads_url": "https://api.github.com/repos/WordPress/openverse-frontend/downloads", + "events_url": "https://api.github.com/repos/WordPress/openverse-frontend/events", + "fork": false, + "forks": 44, + "forks_count": 44, + "forks_url": "https://api.github.com/repos/WordPress/openverse-frontend/forks", + "full_name": "WordPress/openverse-frontend", + "git_commits_url": "https://api.github.com/repos/WordPress/openverse-frontend/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/WordPress/openverse-frontend/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/WordPress/openverse-frontend/git/tags{/sha}", + "git_url": "git://github.com/WordPress/openverse-frontend.git", + "has_downloads": true, + "has_issues": true, + "has_pages": true, + "has_projects": false, + "has_wiki": false, + "homepage": "https://wordpress.org/openverse", + "hooks_url": "https://api.github.com/repos/WordPress/openverse-frontend/hooks", + "html_url": "https://github.com/WordPress/openverse-frontend", + "id": 354842893, + "is_template": false, + "issue_comment_url": "https://api.github.com/repos/WordPress/openverse-frontend/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/WordPress/openverse-frontend/issues/events{/number}", + "issues_url": "https://api.github.com/repos/WordPress/openverse-frontend/issues{/number}", + "keys_url": "https://api.github.com/repos/WordPress/openverse-frontend/keys{/key_id}", + "labels_url": "https://api.github.com/repos/WordPress/openverse-frontend/labels{/name}", + "language": "Vue", + "languages_url": "https://api.github.com/repos/WordPress/openverse-frontend/languages", + "license": { + "key": "mit", + "name": "MIT License", + "node_id": "MDc6TGljZW5zZTEz", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit" + }, + "merges_url": "https://api.github.com/repos/WordPress/openverse-frontend/merges", + "milestones_url": "https://api.github.com/repos/WordPress/openverse-frontend/milestones{/number}", + "mirror_url": null, + "name": "openverse-frontend", + "node_id": "MDEwOlJlcG9zaXRvcnkzNTQ4NDI4OTM=", + "notifications_url": "https://api.github.com/repos/WordPress/openverse-frontend/notifications{?since,all,participating}", + "open_issues": 206, + "open_issues_count": 206, + "owner": { + "avatar_url": "https://avatars.githubusercontent.com/u/276006?v=4", + "events_url": "https://api.github.com/users/WordPress/events{/privacy}", + "followers_url": "https://api.github.com/users/WordPress/followers", + "following_url": "https://api.github.com/users/WordPress/following{/other_user}", + "gists_url": "https://api.github.com/users/WordPress/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/WordPress", + "id": 276006, + "login": "WordPress", + "node_id": "MDEyOk9yZ2FuaXphdGlvbjI3NjAwNg==", + "organizations_url": "https://api.github.com/users/WordPress/orgs", + "received_events_url": "https://api.github.com/users/WordPress/received_events", + "repos_url": "https://api.github.com/users/WordPress/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/WordPress/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/WordPress/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/WordPress" + }, + "private": false, + "pulls_url": "https://api.github.com/repos/WordPress/openverse-frontend/pulls{/number}", + "pushed_at": "2022-06-20T17:06:59Z", + "releases_url": "https://api.github.com/repos/WordPress/openverse-frontend/releases{/id}", + "size": 311971, + "ssh_url": "git@github.com:WordPress/openverse-frontend.git", + "stargazers_count": 46, + "stargazers_url": "https://api.github.com/repos/WordPress/openverse-frontend/stargazers", + "statuses_url": "https://api.github.com/repos/WordPress/openverse-frontend/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/WordPress/openverse-frontend/subscribers", + "subscription_url": "https://api.github.com/repos/WordPress/openverse-frontend/subscription", + "svn_url": "https://github.com/WordPress/openverse-frontend", + "tags_url": "https://api.github.com/repos/WordPress/openverse-frontend/tags", + "teams_url": "https://api.github.com/repos/WordPress/openverse-frontend/teams", + "topics": [ + "creative-commons", + "hacktoberfest", + "javascript", + "open-source", + "openverse", + "search-engine", + "vue", + "wordpress" + ], + "trees_url": "https://api.github.com/repos/WordPress/openverse-frontend/git/trees{/sha}", + "updated_at": "2022-06-17T08:21:08Z", + "url": "https://api.github.com/repos/WordPress/openverse-frontend", + "visibility": "public", + "watchers": 46, + "watchers_count": 46 + }, + "sha": "11bf9bd7af8d6d37d2e5639fb5d30017901f2bee", + "user": { + "avatar_url": "https://avatars.githubusercontent.com/u/276006?v=4", + "events_url": "https://api.github.com/users/WordPress/events{/privacy}", + "followers_url": "https://api.github.com/users/WordPress/followers", + "following_url": "https://api.github.com/users/WordPress/following{/other_user}", + "gists_url": "https://api.github.com/users/WordPress/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/WordPress", + "id": 276006, + "login": "WordPress", + "node_id": "MDEyOk9yZ2FuaXphdGlvbjI3NjAwNg==", + "organizations_url": "https://api.github.com/users/WordPress/orgs", + "received_events_url": "https://api.github.com/users/WordPress/received_events", + "repos_url": "https://api.github.com/users/WordPress/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/WordPress/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/WordPress/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/WordPress" + } + }, + "body": "## Fixes\r\nFixes #437498349234 by @reporter", + "closed_at": null, + "comments_url": "https://api.github.com/repos/WordPress/openverse-frontend/issues/1234598237459283745/comments", + "commits_url": "https://api.github.com/repos/WordPress/openverse-frontend/pulls/1234598237459283745/commits", + "created_at": "2022-03-30T05:41:16Z", + "diff_url": "https://github.com/WordPress/openverse-frontend/pull/1234598237459283745.diff", + "draft": false, + "head": { + "label": "helpful-contributor:super-sweet-feature-branch", + "ref": "super-sweet-feature-branch", + "repo": { + "allow_forking": true, + "archive_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/{archive_format}{/ref}", + "archived": false, + "assignees_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/assignees{/user}", + "blobs_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/branches{/branch}", + "clone_url": "https://github.com/helpful-contributor/openverse-frontend.git", + "collaborators_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/comments{/number}", + "commits_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/commits{/sha}", + "compare_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/contents/{+path}", + "contributors_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/contributors", + "created_at": "2022-03-15T04:43:48Z", + "default_branch": "main", + "deployments_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/deployments", + "description": "The gateway to the Openverse. Openverse is a search tool for CC-licensed and public domain content across the internet.", + "disabled": false, + "downloads_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/downloads", + "events_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/events", + "fork": true, + "forks": 0, + "forks_count": 0, + "forks_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/forks", + "full_name": "helpful-contributor/openverse-frontend", + "git_commits_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/git/tags{/sha}", + "git_url": "git://github.com/helpful-contributor/openverse-frontend.git", + "has_downloads": true, + "has_issues": false, + "has_pages": false, + "has_projects": true, + "has_wiki": false, + "homepage": "https://wordpress.org/openverse", + "hooks_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/hooks", + "html_url": "https://github.com/helpful-contributor/openverse-frontend", + "id": 470008661, + "is_template": false, + "issue_comment_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/issues/events{/number}", + "issues_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/issues{/number}", + "keys_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/keys{/key_id}", + "labels_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/labels{/name}", + "language": "Vue", + "languages_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/languages", + "license": { + "key": "mit", + "name": "MIT License", + "node_id": "MDc6TGljZW5zZTEz", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit" + }, + "merges_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/merges", + "milestones_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/milestones{/number}", + "mirror_url": null, + "name": "openverse-frontend", + "node_id": "R_kgDOHAPDVQ", + "notifications_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/notifications{?since,all,participating}", + "open_issues": 0, + "open_issues_count": 0, + "owner": { + "avatar_url": "https://avatars.githubusercontent.com/u/0?v=4", + "events_url": "https://api.github.com/users/helpful-contributor/events{/privacy}", + "followers_url": "https://api.github.com/users/helpful-contributor/followers", + "following_url": "https://api.github.com/users/helpful-contributor/following{/other_user}", + "gists_url": "https://api.github.com/users/helpful-contributor/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/helpful-contributor", + "id": 0, + "login": "helpful-contributor", + "node_id": "0", + "organizations_url": "https://api.github.com/users/helpful-contributor/orgs", + "received_events_url": "https://api.github.com/users/helpful-contributor/received_events", + "repos_url": "https://api.github.com/users/helpful-contributor/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/helpful-contributor/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/helpful-contributor/subscriptions", + "type": "User", + "url": "https://api.github.com/users/helpful-contributor" + }, + "private": false, + "pulls_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/pulls{/number}", + "pushed_at": "2022-06-16T11:44:59Z", + "releases_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/releases{/id}", + "size": 342938, + "ssh_url": "git@github.com:helpful-contributor/openverse-frontend.git", + "stargazers_count": 0, + "stargazers_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/stargazers", + "statuses_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/subscribers", + "subscription_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/subscription", + "svn_url": "https://github.com/helpful-contributor/openverse-frontend", + "tags_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/tags", + "teams_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/teams", + "topics": [], + "trees_url": "https://api.github.com/repos/helpful-contributor/openverse-frontend/git/trees{/sha}", + "updated_at": "2022-03-28T08:10:33Z", + "url": "https://api.github.com/repos/helpful-contributor/openverse-frontend", + "visibility": "public", + "watchers": 0, + "watchers_count": 0 + }, + "sha": "asdfasdfasdfasdfasdfasdfasdf", + "user": { + "avatar_url": "https://avatars.githubusercontent.com/u/0?v=4", + "events_url": "https://api.github.com/users/helpful-contributor/events{/privacy}", + "followers_url": "https://api.github.com/users/helpful-contributor/followers", + "following_url": "https://api.github.com/users/helpful-contributor/following{/other_user}", + "gists_url": "https://api.github.com/users/helpful-contributor/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/helpful-contributor", + "id": 0, + "login": "helpful-contributor", + "node_id": "0", + "organizations_url": "https://api.github.com/users/helpful-contributor/orgs", + "received_events_url": "https://api.github.com/users/helpful-contributor/received_events", + "repos_url": "https://api.github.com/users/helpful-contributor/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/helpful-contributor/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/helpful-contributor/subscriptions", + "type": "User", + "url": "https://api.github.com/users/helpful-contributor" + } + }, + "html_url": "https://github.com/WordPress/openverse-frontend/pull/1234598237459283745", + "id": 123412341234, + "issue_url": "https://api.github.com/repos/WordPress/openverse-frontend/issues/1234598237459283745", + "labels": [ + { + "color": "ffcc00", + "default": false, + "description": "Not blocking but should be addressed soon", + "id": 3029513057, + "name": "\ud83d\udfe8 priority: medium", + "node_id": "MDU6TGFiZWwzMDI5NTEzMDU3", + "url": "https://api.github.com/repos/WordPress/openverse-frontend/labels/%F0%9F%9F%A8%20priority:%20medium" + }, + { + "color": "ffffff", + "default": false, + "description": "Bug fix", + "id": 3029513140, + "name": "\ud83d\udee0 goal: fix", + "node_id": "MDU6TGFiZWwzMDI5NTEzMTQw", + "url": "https://api.github.com/repos/WordPress/openverse-frontend/labels/%F0%9F%9B%A0%20goal:%20fix" + }, + { + "color": "04338c", + "default": false, + "description": "Concerns the textual material in the repository", + "id": 3029513145, + "name": "\ud83d\udcc4 aspect: text", + "node_id": "MDU6TGFiZWwzMDI5NTEzMTQ1", + "url": "https://api.github.com/repos/WordPress/openverse-frontend/labels/%F0%9F%93%84%20aspect:%20text" + } + ], + "locked": false, + "merge_commit_sha": "a9s8d6f09as7g690a86sdf098a", + "merged_at": null, + "milestone": null, + "node_id": "PR_asdfasdfasdfasdfasdfasdf", + "number": 1234598237459283745, + "patch_url": "https://github.com/WordPress/openverse-frontend/pull/1234598237459283745.patch", + "requested_reviewers": [], + "requested_teams": [], + "review_comment_url": "https://api.github.com/repos/WordPress/openverse-frontend/pulls/comments{/number}", + "review_comments_url": "https://api.github.com/repos/WordPress/openverse-frontend/pulls/1234598237459283745/comments", + "state": "open", + "statuses_url": "https://api.github.com/repos/WordPress/openverse-frontend/statuses/asdfasdfasdfasdfasdfasdfasdf", + "title": "Super cool PR to add amazing new features.", + "updated_at": "2022-06-16T11:57:28Z", + "url": "https://api.github.com/repos/WordPress/openverse-frontend/pulls/1234598237459283745", + "user": { + "avatar_url": "https://avatars.githubusercontent.com/u/0?v=4", + "events_url": "https://api.github.com/users/helpful-contributor/events{/privacy}", + "followers_url": "https://api.github.com/users/helpful-contributor/followers", + "following_url": "https://api.github.com/users/helpful-contributor/following{/other_user}", + "gists_url": "https://api.github.com/users/helpful-contributor/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/helpful-contributor", + "id": 0, + "login": "helpful-contributor", + "node_id": "0", + "organizations_url": "https://api.github.com/users/helpful-contributor/orgs", + "received_events_url": "https://api.github.com/users/helpful-contributor/received_events", + "repos_url": "https://api.github.com/users/helpful-contributor/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/helpful-contributor/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/helpful-contributor/subscriptions", + "type": "User", + "url": "https://api.github.com/users/helpful-contributor" + } +} diff --git a/tests/factories/github/requested_reviewer.json b/tests/factories/github/requested_reviewer.json new file mode 100644 index 00000000000..b8e1b59498b --- /dev/null +++ b/tests/factories/github/requested_reviewer.json @@ -0,0 +1,20 @@ +{ + "avatar_url": "https://avatars.githubusercontent.com/u/redacted", + "events_url": "https://api.github.com/users/example-user/events{/privacy}", + "followers_url": "https://api.github.com/users/example-user/followers", + "following_url": "https://api.github.com/users/example-user/following{/other_user}", + "gists_url": "https://api.github.com/users/example-user/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/example-user", + "id": 0, + "login": "example-user", + "node_id": "0", + "organizations_url": "https://api.github.com/users/example-user/orgs", + "received_events_url": "https://api.github.com/users/example-user/received_events", + "repos_url": "https://api.github.com/users/example-user/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/example-user/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/example-user/subscriptions", + "type": "User", + "url": "https://api.github.com/users/example-user" +}