
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.
159 lines
7.3 KiB
Python
159 lines
7.3 KiB
Python
# Licensed under the Apache License, Version 2.0 (the 'License');
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an 'AS IS' BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
import datetime
|
|
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
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
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, project_board, skip_approvals)
|
|
|
|
|
|
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:
|
|
LOG.warning(f'No issue tag found for change #{change["number"]}')
|
|
return
|
|
for key, issues_list in issue_numbers_dict.items():
|
|
for issue_number in issues_list:
|
|
try:
|
|
issue = repo.get_issue(issue_number)
|
|
except github.GithubException:
|
|
LOG.warning(f'Issue #{issue_number} not found for project')
|
|
return
|
|
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:
|
|
LOG.debug(f'add `wip` to #{issue_number}')
|
|
issue.add_to_labels('wip')
|
|
if 'ready for review' in labels:
|
|
try:
|
|
LOG.debug(f'rm `ready for review` to #{issue_number}')
|
|
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}')
|
|
issue.add_to_labels('ready for review')
|
|
if 'wip' in labels:
|
|
try:
|
|
LOG.debug(f'rm `wip` to #{issue_number}')
|
|
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':
|
|
comment_msg += '\n\nThis change will close this issue when merged.'
|
|
LOG.debug(f'Comment to post on #{issue_number}: {comment_msg}')
|
|
issue.create_comment(comment_msg)
|
|
LOG.info(f'Comment posted to issue #{issue_number}')
|
|
else:
|
|
LOG.debug(f'Comment to edit on #{issue_number}: {comment_msg}')
|
|
comment = github_issues.get_bot_comment(issue, gh.get_user().login, change['number'])
|
|
comment.edit(comment_msg)
|
|
LOG.info(f'Comment edited to issue #{issue_number}')
|
|
|
|
|
|
def get_issue_comment(change: dict, key: str, skip_approvals: bool = False) -> str:
|
|
comment_str = f'## Related Change [#{change["number"]}]({change["url"]})\n\n' \
|
|
f'**Subject:** {change["subject"]}\n' \
|
|
f'**Link:** {change["url"]}\n' \
|
|
f'**Status:** {change["status"]}\n' \
|
|
f'**Owner:** {change["owner"]["name"]} ({change["owner"]["email"]})\n\n'
|
|
if key == 'closes':
|
|
comment_str += 'This change will close this issue when merged.\n\n'
|
|
if not skip_approvals:
|
|
comment_str += '### Approvals\n' \
|
|
'```diff\n'
|
|
|
|
approval_dict = {
|
|
'Code-Review': [],
|
|
'Verified': [],
|
|
'Workflow': []
|
|
}
|
|
if 'approvals' in change['currentPatchSet']:
|
|
for approval in change['currentPatchSet']['approvals']:
|
|
if approval['type'] in approval_dict:
|
|
approval_dict[approval['type']].append((approval['by']['name'], approval['value']))
|
|
else:
|
|
LOG.warning(f'Approval type "{approval["type"]}" is not a known approval type')
|
|
|
|
for key in ['Code-Review', 'Verified', 'Workflow']:
|
|
comment_str += f'{key}\n'
|
|
if approval_dict[key]:
|
|
for approval in approval_dict[key]:
|
|
if int(approval[1]) > 0:
|
|
comment_str += '+'
|
|
comment_str += f'{approval[1]} {approval[0]}\n'
|
|
else:
|
|
comment_str += '! None\n'
|
|
comment_str += '```'
|
|
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}"')
|