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 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'
'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'
'4. If the Gerrit change\'s commit message contains a "WIP" or "DNM" tag, add the "wip" label and '
'to the issue remove other process labels such as "ready for review".\n'
'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 (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" '
'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
)
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.')
parser.add_argument('--log-file', action='store', required=False, type=str,
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('github_project_name', action='store', type=str, help='Target Github project.')
parser.add_argument('gerrit_repo_name', action='store', type=str, help='Target Gerrit repo.')
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()
args = validate(ns)
verbose = args.pop('verbose')

View File

@ -15,6 +15,8 @@ import logging
import github
import pytz as pytz
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 github_issues
@ -22,16 +24,19 @@ from gerrit_to_github_issues import github_issues
LOG = logging.getLogger(__name__)
def update(gerrit_url: str, gerrit_project_name: str, github_project_name: str, github_user: str, github_password: str,
github_token: str, change_age: str = None, skip_approvals: bool = False):
gh, repo = github_issues.get_repo(github_project_name, github_user, github_password, github_token)
def update(gerrit_url: str, gerrit_project_name: str, github_project_id: int,
github_repo_name: str, github_user: str, github_password: str, github_token: str,
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)
for change in change_list['data']:
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.remove_duplicated_issue_numbers(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'])
if issue.state == 'closed' and not bot_comment:
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.create_comment('Issue reopened due to new activity on Gerrit.\n\n')
labels = [str(l.name) for l in list(issue.get_labels())]
if 'WIP' in change['commitMessage'] or 'DNM' in change['commitMessage']:
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')
except github.GithubException:
LOG.debug(f'`ready for review` tag does not exist on issue #{issue_number}')
move_issue(project_board, issue, 'In Progress')
else:
if 'ready for review' not in labels:
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')
except github.GithubException:
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)
if not bot_comment:
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()
comment_str += f'\n\n*Last Updated: {dt}*'
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
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:
gh = github.Github(github_token)
elif github_user and github_pw:
gh = github.Github(github_user, github_pw)
else:
raise errors.GithubConfigurationError
return gh, gh.get_repo(repo_name)
def get_bot_comment(issue: Issue, bot_name: str, ps_number: str) -> IssueComment: