Integrate with the github project board

Implement a feature allowing the bot to move issues around on the
project board:
* When a change's commit message includes "WIP" or "DNM",
  the issue will be moved to the "In Progress" column.
* When a change's commit message does *not* include "WIP" or "DNM",
  assume that the change is ready for review, and move it to the
  "Submitted on Gerrit" column.
This commit is contained in:
Ian Howell 2020-04-30 14:24:26 -05:00
parent 09fa974779
commit 727e8eb44d
4 changed files with 50 additions and 12 deletions

View File

@ -1,2 +1,3 @@
Ian H Pittwood <pittwoodian@gmail.com> Ian H Pittwood <pittwoodian@gmail.com>
Ian Pittwood <pittwoodian@gmail.com> Ian Pittwood <pittwoodian@gmail.com>
Ian Howell <ian.howell0@gmail.com>

View File

@ -39,10 +39,12 @@ def main():
'2. Check associated Github Issue for a link to the change. If no such link exists, comment it.\n' '2. Check associated Github Issue for a link to the change. If no such link exists, comment it.\n'
'3. If the associated issue was closed, re-open it and comment on it describing why it was ' '3. If the associated issue was closed, re-open it and comment on it describing why it was '
're-opened and a link to the Gerrit change that was found.\n' 're-opened and a link to the Gerrit change that was found.\n'
'4. If the Gerrit change\'s commit message contains a "WIP" or "DNM" tag, add the "wip" label and ' '4. If the Gerrit change\'s commit message contains a "WIP" or "DNM" tag, add the "wip" label '
'to the issue remove other process labels such as "ready for review".\n' 'to the issue, remove other process labels (e.g. "ready for review"), and move the issue '
'to the "In Progress" column of the project board.\n'
'5. If no "WIP" or "DNM" tag is found in the change\'s commit message, add the "ready for review" ' '5. If no "WIP" or "DNM" tag is found in the change\'s commit message, add the "ready for review" '
'label to the issue and remove other process labels such as "ready for review".', 'label to the issue, remove other process labels (e.g "wip"), and move the issue '
'to the "Submitted on Gerrit" column of the project board.',
formatter_class=argparse.RawDescriptionHelpFormatter formatter_class=argparse.RawDescriptionHelpFormatter
) )
parser.add_argument('-g', '--gerrit-url', action='store', required=True, type=str, parser.add_argument('-g', '--gerrit-url', action='store', required=True, type=str,
@ -70,8 +72,9 @@ def main():
default=False, help='Enabled DEBUG level logging.') default=False, help='Enabled DEBUG level logging.')
parser.add_argument('--log-file', action='store', required=False, type=str, parser.add_argument('--log-file', action='store', required=False, type=str,
help='Specifies a file to output logs to. Defaults to `sys.stdout`.') help='Specifies a file to output logs to. Defaults to `sys.stdout`.')
parser.add_argument('gerrit_project_name', action='store', type=str, help='Target Gerrit project.') parser.add_argument('gerrit_repo_name', action='store', type=str, help='Target Gerrit repo.')
parser.add_argument('github_project_name', action='store', type=str, help='Target Github project.') parser.add_argument('github_repo_name', action='store', type=str, help='Target Github repo.')
parser.add_argument('github_project_id', action='store', type=int, help='Target Github project board ID.')
ns = parser.parse_args() ns = parser.parse_args()
args = validate(ns) args = validate(ns)
verbose = args.pop('verbose') verbose = args.pop('verbose')

View File

@ -15,6 +15,8 @@ import logging
import github import github
import pytz as pytz import pytz as pytz
from github.Repository import Repository from github.Repository import Repository
from github.Project import Project
from github.Issue import Issue
from gerrit_to_github_issues import gerrit from gerrit_to_github_issues import gerrit
from gerrit_to_github_issues import github_issues from gerrit_to_github_issues import github_issues
@ -22,16 +24,19 @@ from gerrit_to_github_issues import github_issues
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def update(gerrit_url: str, gerrit_project_name: str, github_project_name: str, github_user: str, github_password: str, def update(gerrit_url: str, gerrit_project_name: str, github_project_id: int,
github_token: str, change_age: str = None, skip_approvals: bool = False): github_repo_name: str, github_user: str, github_password: str, github_token: str,
gh, repo = github_issues.get_repo(github_project_name, github_user, github_password, github_token) change_age: str = None, skip_approvals: bool = False):
gh = github_issues.get_client(github_user, github_password, github_token)
repo = gh.get_repo(github_repo_name)
project_board = gh.get_project(github_project_id)
change_list = gerrit.get_changes(gerrit_url, gerrit_project_name, change_age=change_age) change_list = gerrit.get_changes(gerrit_url, gerrit_project_name, change_age=change_age)
for change in change_list['data']: for change in change_list['data']:
if 'commitMessage' in change: if 'commitMessage' in change:
process_change(gh, change, repo, skip_approvals) process_change(gh, change, repo, project_board, skip_approvals)
def process_change(gh: github.Github, change: dict, repo: Repository, skip_approvals: bool = False): def process_change(gh: github.Github, change: dict, repo: Repository, project_board: Project, skip_approvals: bool = False):
issue_numbers_dict = github_issues.parse_issue_number(change['commitMessage']) issue_numbers_dict = github_issues.parse_issue_number(change['commitMessage'])
issue_numbers_dict = github_issues.remove_duplicated_issue_numbers(issue_numbers_dict) issue_numbers_dict = github_issues.remove_duplicated_issue_numbers(issue_numbers_dict)
if not issue_numbers_dict: if not issue_numbers_dict:
@ -47,8 +52,13 @@ def process_change(gh: github.Github, change: dict, repo: Repository, skip_appro
bot_comment = github_issues.get_bot_comment(issue, gh.get_user().login, change['number']) bot_comment = github_issues.get_bot_comment(issue, gh.get_user().login, change['number'])
if issue.state == 'closed' and not bot_comment: if issue.state == 'closed' and not bot_comment:
LOG.debug(f'Issue #{issue_number} was closed, reopening...') LOG.debug(f'Issue #{issue_number} was closed, reopening...')
# NOTE(howell): Reopening a closed issue will move it from the
# "Done" column to the "In Progress" column on the project
# board via Github automation.
issue.edit(state='open') issue.edit(state='open')
issue.create_comment('Issue reopened due to new activity on Gerrit.\n\n') issue.create_comment('Issue reopened due to new activity on Gerrit.\n\n')
labels = [str(l.name) for l in list(issue.get_labels())] labels = [str(l.name) for l in list(issue.get_labels())]
if 'WIP' in change['commitMessage'] or 'DNM' in change['commitMessage']: if 'WIP' in change['commitMessage'] or 'DNM' in change['commitMessage']:
if 'wip' not in labels: if 'wip' not in labels:
@ -60,6 +70,7 @@ def process_change(gh: github.Github, change: dict, repo: Repository, skip_appro
issue.remove_from_labels('ready for review') issue.remove_from_labels('ready for review')
except github.GithubException: except github.GithubException:
LOG.debug(f'`ready for review` tag does not exist on issue #{issue_number}') LOG.debug(f'`ready for review` tag does not exist on issue #{issue_number}')
move_issue(project_board, issue, 'In Progress')
else: else:
if 'ready for review' not in labels: if 'ready for review' not in labels:
LOG.debug(f'add `ready for review` to #{issue_number}') LOG.debug(f'add `ready for review` to #{issue_number}')
@ -70,6 +81,7 @@ def process_change(gh: github.Github, change: dict, repo: Repository, skip_appro
issue.remove_from_labels('wip') issue.remove_from_labels('wip')
except github.GithubException: except github.GithubException:
LOG.debug(f'`wip` tag does not exist on issue #{issue_number}') LOG.debug(f'`wip` tag does not exist on issue #{issue_number}')
move_issue(project_board, issue, 'Submitted on Gerrit')
comment_msg = get_issue_comment(change, key, skip_approvals) comment_msg = get_issue_comment(change, key, skip_approvals)
if not bot_comment: if not bot_comment:
if key == 'closes': if key == 'closes':
@ -121,3 +133,26 @@ def get_issue_comment(change: dict, key: str, skip_approvals: bool = False) -> s
dt = datetime.datetime.now(pytz.timezone('America/Chicago')).strftime('%Y-%m-%d %H:%M:%S %Z').strip() dt = datetime.datetime.now(pytz.timezone('America/Chicago')).strftime('%Y-%m-%d %H:%M:%S %Z').strip()
comment_str += f'\n\n*Last Updated: {dt}*' comment_str += f'\n\n*Last Updated: {dt}*'
return comment_str return comment_str
def move_issue(project_board, issue, to_col_name):
for col in project_board.get_columns():
if col.name == to_col_name:
to_col = col
else:
for c in col.get_cards():
if c.get_content() == issue:
card = c
if not to_col:
LOG.warning(f'Column with name "{to_col_name}" could not be found for project "{project_board.name}"')
return
if not card:
LOG.warning(f'Issue with name "{issue.name}" could not be found for project "{project_board.name}"')
return
if card.move("top", to_col):
LOG.info('Moved issue "{issue.name}" to column "{to_col_name}"')
else:
LOG.warning('Failed to move issue "{issue.name}" to column "{to_col_name}"')

View File

@ -61,14 +61,13 @@ def remove_duplicated_issue_numbers(issue_dict: dict) -> dict:
return issue_dict return issue_dict
def get_repo(repo_name: str, github_user: str, github_pw: str, github_token: str) -> (github.Github, Repository): def get_client(github_user: str, github_pw: str, github_token: str) -> github.Github
if github_token: if github_token:
gh = github.Github(github_token) gh = github.Github(github_token)
elif github_user and github_pw: elif github_user and github_pw:
gh = github.Github(github_user, github_pw) gh = github.Github(github_user, github_pw)
else: else:
raise errors.GithubConfigurationError raise errors.GithubConfigurationError
return gh, gh.get_repo(repo_name)
def get_bot_comment(issue: Issue, bot_name: str, ps_number: str) -> IssueComment: def get_bot_comment(issue: Issue, bot_name: str, ps_number: str) -> IssueComment: