Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,37 @@ docker:
password: password
```

if Jira is configured for the repository we create a new issue (story) for the PR and assign it to the owner.
On new commit create closed sub-task under the PR story with the commiter as assignee
On reviewed PR create closed sub-task under the PR story with the reviewer as assignee

- `server`: FQDN Jira server url
- `project`: project key to open the issue
Comment thread
rnetser marked this conversation as resolved.
- `token`: Jira token
- `epic`: epic name, if provided a new issue will be created under the epic
- `user-mapping`: mapping from github username to jira username if different

`jira` setting can be placed as global (for all repositories) or per repository.
`jira` in repository setting will override `jira` in global setting for the repository

```yaml
jira:
server: jira server url
project: project key to open the issue
token: jira token
epic: epic name # Optional
user-mapping:
github username: jira username
```

To enable jira for a repository, set `jira-tracking: true` in repository settings

```yaml
repositories:
my-repository:
jira-tracking: true
```

## Supported actions

Following actions are done automatically:
Expand Down
17 changes: 17 additions & 0 deletions example.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ auto-verified-and-merged-users:
- "renovate[bot]"
- "pre-commit-ci[bot]"

jira:
server: <JIRA URL>
project: <PROJECT KEY>
tokan: <JIRA TOKEN>
user-mapping:
<GITHUB USER>: <JIRA USER> # if github user is not the same as jira

repositories:
my-repository:
name: my-org/my-repository
Expand Down Expand Up @@ -66,3 +73,13 @@ repositories:
can-be-merged-required-labels: # check for extra labels to set PR as can be merged
- my-label1
- my-label2

jira-tracking: true

jira: # override Jira global settings
server: <JIRA URL>
project: <PROJECT KEY>
token: <JIRA TOKEN>
epic: <EPIC KEY> # Optional
user-mapping:
<GITHUB USER>: <JIRA USER> # if github user is not the same as jira
339 changes: 335 additions & 4 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ colorama = "^0.4.6"
ruff = "^0.4.0"
timeout-sampler = "^0.0.25"
requests = "^2.31.0"
jira = "^3.8.0"
pyhelper-utils = "^0.0.13"

[tool.poetry.group.dev.dependencies]
ipdb = "^0.13.13"
Expand Down
2 changes: 1 addition & 1 deletion webhook_server_container/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
set_all_in_progress_check_runs_to_queued,
set_repositories_settings,
)
from pyhelper_utils.general import ignore_exceptions
from webhook_server_container.utils.helpers import (
get_api_with_highest_rate_limit,
ignore_exceptions,
)
from webhook_server_container.utils.webhook import create_webhook

Expand Down
184 changes: 166 additions & 18 deletions webhook_server_container/libs/github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from timeout_sampler import TimeoutSampler, TimeoutExpiredError

from webhook_server_container.libs.config import Config
from webhook_server_container.libs.jira_api import JiraApi
from webhook_server_container.utils.constants import (
ADD_STR,
APPROVED_BY_LABEL_PREFIX,
Expand All @@ -33,6 +34,7 @@
HAS_CONFLICTS_LABEL_STR,
HOLD_LABEL_STR,
IN_PROGRESS_STR,
JIRA_STR,
LGTM_STR,
NEEDS_REBASE_LABEL_STR,
PYTHON_MODULE_INSTALL_STR,
Expand All @@ -48,12 +50,13 @@
PRE_COMMIT_STR,
OTHER_MAIN_BRANCH,
)
from pyhelper_utils.general import ignore_exceptions
from webhook_server_container.utils.dockerhub_rate_limit import DockerHub
from webhook_server_container.utils.helpers import (
get_api_with_highest_rate_limit,
extract_key_from_dict,
get_github_repo_api,
ignore_exceptions,
get_value_from_dicts,
run_command,
get_apis_and_tokes_from_config,
)
Expand All @@ -75,6 +78,9 @@ def __init__(self, hook_data, repositories_app_api, missing_app_repositories):
self.parent_committer = None
self.log_uuid = shortuuid.uuid()[:5]
self.container_repo_dir = "/tmp/repository"
self.jira_conn = None
self.jira_track_pr = False
self.issue_title = None

# filled by self._repo_data_from_config()
self.dockerhub_username = None
Expand All @@ -90,11 +96,15 @@ def __init__(self, hook_data, repositories_app_api, missing_app_repositories):
self.github_app_id = None
self.container_release = None
self.can_be_merged_required_labels = []
self.jira = None
self.jira_tracking = False
self.jira_enabled_repository = False
# End of filled by self._repo_data_from_config()

self.config = Config()
self._repo_data_from_config()
self._set_log_prefix_color()
# self.log_repository_features()
Comment thread
myakove marked this conversation as resolved.

self.github_app_api = self.get_github_app_api()

Expand All @@ -115,11 +125,24 @@ def __init__(self, hook_data, repositories_app_api, missing_app_repositories):
self.dockerhub = DockerHub(username=self.dockerhub_username, password=self.dockerhub_password)

self.pull_request = self._get_pull_request()
self.owners_content = self.get_owners_content()

if self.pull_request:
self.last_commit = self._get_last_commit()
self.parent_committer = self.pull_request.user.login

self.owners_content = self.get_owners_content()
if self.jira_enabled_repository:
reviewers_and_approvers = self.reviewers + self.approvers
if self.parent_committer in reviewers_and_approvers:
self.jira_assignee = self.jira_user_mapping.get(self.parent_committer, self.parent_committer)
self.jira_track_pr = True
self.issue_title = f"[AUTO:FROM:GITHUB] PR [{self.pull_request.number}]: {self.pull_request.title}"
self.app.logger.info(f"{self.log_prefix} Jira tracking is enabled for the current pull request.")
else:
self.app.logger.info(
f"{self.log_prefix} Jira tracking is disabled for the current pull request. "
f"Committer {self.parent_committer} is not in {reviewers_and_approvers}"
)

self.supported_user_labels_str = "".join([f" * {label}\n" for label in USER_LABELS_DICT.keys()])
self.welcome_msg = f"""
Expand Down Expand Up @@ -266,17 +289,44 @@ def _repo_data_from_config(self):
if not repo_data:
raise RepositoryNotFoundError(f"Repository {self.repository_name} not found in config file")

self.github_app_id = config_data["github-app-id"]
self.webhook_url = config_data.get("webhook_ip")
self.github_app_id = get_value_from_dicts(
primary_dict=repo_data, secondary_dict=config_data, key="github-app-id"
)
self.repository_full_name = repo_data["name"]
self.pypi = repo_data.get("pypi")
self.verified_job = repo_data.get("verified_job", True)
self.tox_enabled = repo_data.get("tox")
self.tox_python_version = repo_data.get("tox_python_version", "python")
self.slack_webhook_url = repo_data.get("slack_webhook_url")
self.pypi = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="pypi")
self.verified_job = get_value_from_dicts(
primary_dict=repo_data, secondary_dict=config_data, key="verified-job", return_on_none=True
)
self.tox_enabled = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="tox")
self.tox_python_version = get_value_from_dicts(
primary_dict=repo_data, secondary_dict=config_data, key="tox-python-version", return_on_none="python"
)
self.slack_webhook_url = get_value_from_dicts(
primary_dict=repo_data, secondary_dict=config_data, key="slack_webhook_url"
)
self.build_and_push_container = repo_data.get("container")
self.dockerhub = repo_data.get("docker")
self.pre_commit = repo_data.get("pre-commit")
self.dockerhub = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="docker")
self.pre_commit = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="pre-commit")
self.jira = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="jira")

if self.jira:
self.jira_server = self.jira.get("server")
self.jira_project = self.jira.get("project")
self.jira_token = self.jira.get("token")
self.jira_epic = self.jira.get("epic")
self.jira_user_mapping = self.jira.get("user-mapping", {})

# Check if repository is enabled for jira
self.jira_tracking = get_value_from_dicts(
primary_dict=repo_data, secondary_dict=config_data, key="jira-tracking"
)
if self.jira_tracking:
self.jira_enabled_repository = all([self.jira_server, self.jira_project, self.jira_token])
if not self.jira_enabled_repository:
Comment thread
rnetser marked this conversation as resolved.
# if not (self.jira_enabled_repository := all([self.jira_server, self.jira_project, self.jira_token])):
self.app.logger.error(
f"{self.log_prefix} Jira configuration is not valid. Server: {self.jira_server}, Project: {self.jira_project}, Token: {self.jira_token}"
)

if self.dockerhub:
self.dockerhub_username = self.dockerhub["username"]
Expand All @@ -292,11 +342,12 @@ def _repo_data_from_config(self):
self.container_command_args = self.build_and_push_container.get("args")
self.container_release = self.build_and_push_container.get("release")

self.auto_verified_and_merged_users = config_data.get(
"auto-verified-and-merged-users",
repo_data.get("auto-verified-and-merged-users", []),
self.auto_verified_and_merged_users = get_value_from_dicts(
primary_dict=repo_data, secondary_dict=config_data, key="auto-verified-and-merged-users", return_on_none=[]
)
self.can_be_merged_required_labels = get_value_from_dicts(
primary_dict=repo_data, secondary_dict=config_data, key="can-be-merged-required-labels", return_on_none=[]
)
self.can_be_merged_required_labels = config_data.get("can-be-merged-required-labels", [])

def _get_pull_request(self, number=None):
if number:
Expand Down Expand Up @@ -746,6 +797,22 @@ def process_pull_request_webhook_data(self):
self.app.logger.info(f"{self.log_prefix} Creating welcome comment")
self.pull_request.create_issue_comment(self.welcome_msg)
self.create_issue_for_new_pull_request()

if self.jira_track_pr:
self.get_jira_conn()
if not self.jira_conn:
self.app.logger.error(f"{self.log_prefix} Jira connection not found")
return

self.app.logger.info(f"{self.log_prefix} Creating Jira story")
jira_story_key = self.jira_conn.create_story(
title=self.issue_title,
body=self.pull_request.html_url,
epic_key=self.jira_epic,
assignee=self.jira_assignee,
)
self._add_label(label=f"{JIRA_STR}:{jira_story_key}")

self.process_opened_or_synchronize_pull_request(pull_request_branch=pull_request_branch)

if hook_action == "synchronize":
Expand All @@ -758,12 +825,29 @@ def process_pull_request_webhook_data(self):
):
self._remove_label(label=_label_name)

if self.jira_track_pr:
if _story_key := self.get_story_key_with_jira_connection():
self.app.logger.info(f"{self.log_prefix} Creating sub-task for Jira story {_story_key}")
self.jira_conn.create_closed_subtask(
title=f"{self.issue_title}: New commit from {self.parent_committer}",
parent_key=_story_key,
assignee=self.jira_assignee,
body=f"PR: {self.pull_request.title}, new commit pushed by {self.parent_committer}",
)

self.process_opened_or_synchronize_pull_request(pull_request_branch=pull_request_branch)

if hook_action == "closed":
self.close_issue_for_merged_or_closed_pr(hook_action=hook_action)

is_merged = pull_request_data.get("merged")

if self.jira_track_pr:
if _story_key := self.get_story_key_with_jira_connection():
self.app.logger.info(f"{self.log_prefix} Closing Jira story")
self.jira_conn.close_issue(
key=_story_key, comment=f"PR: {self.pull_request.title} is closed. Megred: {is_merged}"
)

if is_merged:
self.app.logger.info(f"{self.log_prefix} PR is merged")

Expand Down Expand Up @@ -822,12 +906,38 @@ def process_pull_request_review_webhook_data(self):
approved
changes_requested
"""
reviewed_user = self.hook_data["review"]["user"]["login"]

review_state = self.hook_data["review"]["state"]
self.manage_reviewed_by_label(
review_state=self.hook_data["review"]["state"],
review_state=review_state,
action=ADD_STR,
reviewed_user=self.hook_data["review"]["user"]["login"],
reviewed_user=reviewed_user,
)

if self.jira_track_pr:
_story_label = [_label for _label in self.pull_request.labels if _label.name.startswith(JIRA_STR)]
if _story_label:
if reviewed_user == self.parent_committer:
self.app.logger.info(
f"{self.log_prefix} Skipping Jira review sub-task creation for review by {reviewed_user} which is parent committer"
)
return

_story_key = _story_label[0].name.split(":")[-1]
self.get_jira_conn()
if not self.jira_conn:
self.app.logger.error(f"{self.log_prefix} Jira connection not found")
return
Comment thread
myakove marked this conversation as resolved.

self.app.logger.info(f"{self.log_prefix} Creating sub-task for Jira story {_story_key}")
self.jira_conn.create_closed_subtask(
title=f"{self.issue_title}: reviewed by: {reviewed_user} - {review_state}",
parent_key=_story_key,
assignee=self.jira_user_mapping.get(reviewed_user, self.parent_committer),
body=f"PR: {self.pull_request.title}, reviewed by: {reviewed_user}",
)

def manage_reviewed_by_label(self, review_state, action, reviewed_user):
self.app.logger.info(
f"{self.log_prefix} "
Expand Down Expand Up @@ -1539,3 +1649,41 @@ def get_checkrun_text(self, err, out):
return f"```\n{err}\n\n{out}\n```"[:65534]
else:
return f"```\n{err}\n\n{out}\n```"

@ignore_exceptions(logger=FLASK_APP.logger)
def get_jira_conn(self):
self.jira_conn = JiraApi(
server=self.jira_server,
project=self.jira_project,
token=self.jira_token,
)

def log_repository_features(self):
repository_features = f"""
auto-verified-and-merged-users: {self.auto_verified_and_merged_users}
can-be-merged-required-labels: {self.can_be_merged_required_labels}
pypi: {self.pypi}
verified-job: {self.verified_job}
tox-enabled: {self.tox_enabled}
tox-python-version: {self.tox_python_version}
docker: {self.dockerhub}
pre-commit: {self.pre_commit}
slack-webhook-url: {self.slack_webhook_url}
container: {self.build_and_push_container}
jira-tracking: {self.jira_tracking}
jira-server: {self.jira_server}
jira-project: {self.jira_project}
jira-token: {self.jira_token}
jira-enabled-repository: {self.jira_enabled_repository}
jira-user-mapping: {self.jira_user_mapping}
"""
self.app.logger.info(f"{self.log_prefix} Repository features: {repository_features}")

def get_story_key_with_jira_connection(self):
_story_label = [_label for _label in self.pull_request.labels if _label.name.startswith(JIRA_STR)]
if _story_key := _story_label[0].name.split(":")[-1]:
self.get_jira_conn()
if not self.jira_conn:
self.app.logger.error(f"{self.log_prefix} Jira connection not found")
return None
return _story_key
Loading