Add task to add PRs to review to todoist
This commit is contained in:
parent
7cd7c4685c
commit
d2d0f6e291
7 changed files with 127 additions and 63 deletions
|
@ -9,3 +9,4 @@ Do things
|
||||||
|
|
||||||
- Healthcheck endpoint
|
- Healthcheck endpoint
|
||||||
- Add assigned issues from certain GitHub repos to Todoist
|
- Add assigned issues from certain GitHub repos to Todoist
|
||||||
|
- Add all unreviewed PRs for certain GitHub repos to Todoist
|
||||||
|
|
|
@ -3,11 +3,13 @@ import asyncio
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
from .todoist_assigned_issues import todoist_assigned_issues
|
from .todoist_assigned_issues import todoist_assigned_issues
|
||||||
|
from .todoist_repo_prs import todoist_repo_prs
|
||||||
|
|
||||||
|
|
||||||
def create_scheduler():
|
def create_scheduler():
|
||||||
scheduler = AsyncIOScheduler()
|
scheduler = AsyncIOScheduler()
|
||||||
scheduler.add_job(todoist_assigned_issues, 'interval', minutes=15)
|
scheduler.add_job(todoist_assigned_issues, 'interval', minutes=15)
|
||||||
|
scheduler.add_job(todoist_repo_prs, 'interval', minutes=15)
|
||||||
return scheduler
|
return scheduler
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from github import Issue
|
from github import Issue
|
||||||
|
|
||||||
from actioner.clients import github, todoist
|
from actioner.clients import github, todoist
|
||||||
from actioner.utils import get_todoist_project_from_repo
|
from actioner.utils import get_todoist_project_from_repo
|
||||||
|
from actioner.utils.github import get_existing_task, get_issue_link
|
||||||
|
|
||||||
REPOS = frozenset([
|
REPOS = frozenset([
|
||||||
'srobo/tasks',
|
'srobo/tasks',
|
||||||
|
@ -28,25 +28,10 @@ def get_status_for_issue(issue: Issue) -> int:
|
||||||
return max(priorities, default=1)
|
return max(priorities, default=1)
|
||||||
|
|
||||||
|
|
||||||
def get_issue_link(issue: Issue) -> str:
|
|
||||||
return "[#{id}]({url})".format(
|
|
||||||
id=issue.number,
|
|
||||||
url=issue.html_url
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def issue_to_task_name(issue: Issue) -> str:
|
def issue_to_task_name(issue: Issue) -> str:
|
||||||
return get_issue_link(issue) + ": " + issue.title
|
return get_issue_link(issue) + ": " + issue.title
|
||||||
|
|
||||||
|
|
||||||
def get_existing_task(tasks: Dict[int, str], issue: Issue):
|
|
||||||
issue_link = get_issue_link(issue)
|
|
||||||
for task_id, task_title in tasks.items():
|
|
||||||
if task_title.startswith(issue_link):
|
|
||||||
return task_id
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def todoist_assigned_issues():
|
def todoist_assigned_issues():
|
||||||
me = github.get_user()
|
me = github.get_user()
|
||||||
todoist.projects.sync()
|
todoist.projects.sync()
|
||||||
|
|
70
actioner/scheduler/todoist_repo_prs.py
Normal file
70
actioner/scheduler/todoist_repo_prs.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from github import PullRequest
|
||||||
|
|
||||||
|
from actioner.clients import github, todoist
|
||||||
|
from actioner.utils import get_todoist_project_from_repo
|
||||||
|
from actioner.utils.github import get_existing_task, get_issue_link
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
REPOS = frozenset([
|
||||||
|
'srobo/core-team-minutes'
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def pr_to_task_name(pr: PullRequest) -> str:
|
||||||
|
return "Review " + get_issue_link(pr) + ": " + pr.title
|
||||||
|
|
||||||
|
|
||||||
|
def get_my_review(me, pr: PullRequest):
|
||||||
|
for review in pr.get_reviews():
|
||||||
|
if review.user.login == me.login:
|
||||||
|
return review
|
||||||
|
|
||||||
|
|
||||||
|
def todoist_repo_prs():
|
||||||
|
me = github.get_user()
|
||||||
|
todoist.projects.sync()
|
||||||
|
todoist.items.sync()
|
||||||
|
for repo_name in REPOS:
|
||||||
|
project_id = get_todoist_project_from_repo(repo_name)
|
||||||
|
existing_tasks = {item['id']: item['content'] for item in todoist.state['items'] if item['project_id'] == project_id}
|
||||||
|
repo = github.get_repo(repo_name)
|
||||||
|
for pr in repo.get_pulls(state='all'):
|
||||||
|
existing_task_id = get_existing_task(existing_tasks, pr)
|
||||||
|
|
||||||
|
if pr.state == 'closed' and existing_task_id:
|
||||||
|
my_review = get_my_review(me, pr)
|
||||||
|
if pr.merged and my_review and my_review.state == 'APPROVED':
|
||||||
|
logger.info("Completing task to review '{}'".format(pr.title))
|
||||||
|
todoist.items.complete([existing_task_id])
|
||||||
|
else:
|
||||||
|
logger.info("Deleting task to review '{}'".format(pr.title))
|
||||||
|
todoist.items.delete([existing_task_id])
|
||||||
|
|
||||||
|
elif pr.state == 'open':
|
||||||
|
if existing_task_id is None:
|
||||||
|
logger.info("Creating task to review '{}'".format(pr.title))
|
||||||
|
existing_task_id = todoist.items.add(
|
||||||
|
pr_to_task_name(pr),
|
||||||
|
project_id
|
||||||
|
)['id']
|
||||||
|
|
||||||
|
existing_task = todoist.items.get_by_id(existing_task_id)
|
||||||
|
my_review = get_my_review(me, pr)
|
||||||
|
if existing_task_id and my_review:
|
||||||
|
if my_review.commit_id == pr.head.sha and not existing_task['checked']:
|
||||||
|
logger.info("Completing task to review '{}'".format(pr.title))
|
||||||
|
todoist.items.complete([existing_task_id])
|
||||||
|
elif existing_task['checked']:
|
||||||
|
logger.info("Re-opening task to review '{}'".format(pr.title))
|
||||||
|
todoist.items.uncomplete([existing_task_id])
|
||||||
|
existing_task.update(
|
||||||
|
content=pr_to_task_name(pr)
|
||||||
|
)
|
||||||
|
if pr.milestone and pr.milestone.due_on:
|
||||||
|
existing_task.update(date_string=pr.milestone.due_on.strftime("%d/%m/%Y"))
|
||||||
|
|
||||||
|
todoist.commit()
|
16
actioner/utils/github.py
Normal file
16
actioner/utils/github.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_issue_link(issue_or_pr) -> str:
|
||||||
|
return "[#{id}]({url})".format(
|
||||||
|
id=issue_or_pr.number,
|
||||||
|
url=issue_or_pr.html_url
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_task(tasks: Dict[int, str], issue_or_pr):
|
||||||
|
issue_link = get_issue_link(issue_or_pr)
|
||||||
|
for task_id, task_title in tasks.items():
|
||||||
|
if issue_link in task_title:
|
||||||
|
return task_id
|
||||||
|
return None
|
|
@ -1,55 +1,9 @@
|
||||||
from collections import namedtuple
|
from actioner.scheduler.todoist_assigned_issues import REPOS
|
||||||
|
|
||||||
from actioner.scheduler.todoist_assigned_issues import (
|
|
||||||
REPOS,
|
|
||||||
get_existing_task,
|
|
||||||
get_issue_link,
|
|
||||||
issue_to_task_name,
|
|
||||||
)
|
|
||||||
from actioner.utils import get_todoist_project_from_repo
|
from actioner.utils import get_todoist_project_from_repo
|
||||||
from tests import BaseTestCase
|
from tests import BaseTestCase
|
||||||
|
|
||||||
FakeIssue = namedtuple('FakeIssue', ['number', 'html_url', 'title'])
|
|
||||||
|
|
||||||
|
|
||||||
class IssueTaskNameTestCase(BaseTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.issue = FakeIssue(123, 'https://github.com/repo/thing', 'issue title')
|
|
||||||
|
|
||||||
def test_creates_link(self):
|
|
||||||
self.assertEqual(get_issue_link(self.issue), "[#123](https://github.com/repo/thing)")
|
|
||||||
self.assertIn(self.issue.html_url, get_issue_link(self.issue))
|
|
||||||
|
|
||||||
def test_task_name_contains_title(self):
|
|
||||||
self.assertIn(self.issue.title, issue_to_task_name(self.issue))
|
|
||||||
|
|
||||||
def test_task_name_contains_link(self):
|
|
||||||
self.assertIn(get_issue_link(self.issue), issue_to_task_name(self.issue))
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationTestCase(BaseTestCase):
|
class ConfigurationTestCase(BaseTestCase):
|
||||||
def test_repo_is_known(self):
|
def test_repo_is_known(self):
|
||||||
for repo in REPOS:
|
for repo in REPOS:
|
||||||
self.assertIsNotNone(get_todoist_project_from_repo(repo))
|
self.assertIsNotNone(get_todoist_project_from_repo(repo))
|
||||||
|
|
||||||
|
|
||||||
class ExistingTaskTestCase(BaseTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.tasks = {
|
|
||||||
123: '[#1](url): title',
|
|
||||||
456: '[#2](url/2): title 2',
|
|
||||||
789: '[#3](url/3): title 3',
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_finds_existing_repos(self):
|
|
||||||
self.assertEqual(
|
|
||||||
get_existing_task(self.tasks, FakeIssue(1, 'url', 'title')),
|
|
||||||
123
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_not_existing_repo(self):
|
|
||||||
self.assertIsNone(
|
|
||||||
get_existing_task(self.tasks, FakeIssue(123, 'url', 'title'))
|
|
||||||
)
|
|
||||||
|
|
36
tests/test_utils/test_github.py
Normal file
36
tests/test_utils/test_github.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from actioner.utils.github import get_existing_task, get_issue_link
|
||||||
|
from tests import BaseTestCase
|
||||||
|
|
||||||
|
FakeIssue = namedtuple('FakeIssue', ['number', 'html_url', 'title'])
|
||||||
|
|
||||||
|
|
||||||
|
class IssueLinkTestCase(BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.issue = FakeIssue(123, 'https://github.com/repo/thing', 'issue title')
|
||||||
|
|
||||||
|
def test_creates_link(self):
|
||||||
|
self.assertEqual(get_issue_link(self.issue), "[#123](https://github.com/repo/thing)")
|
||||||
|
|
||||||
|
|
||||||
|
class ExistingTaskTestCase(BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.tasks = {
|
||||||
|
123: '[#1](url): title',
|
||||||
|
456: '[#2](url/2): title 2',
|
||||||
|
789: '[#3](url/3): title 3',
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_finds_existing_repos(self):
|
||||||
|
self.assertEqual(
|
||||||
|
get_existing_task(self.tasks, FakeIssue(1, 'url', 'title')),
|
||||||
|
123
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_not_existing_repo(self):
|
||||||
|
self.assertIsNone(
|
||||||
|
get_existing_task(self.tasks, FakeIssue(123, 'url', 'title'))
|
||||||
|
)
|
Reference in a new issue