This is an automated email from the ASF dual-hosted git repository.
chamikara pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new f831030 [Beam-4687]Automate JIRA for Dependency check (#6060)
f831030 is described below
commit f831030e7c92e7263b98e3ff4307aca4a324a0dd
Author: yifanzou <[email protected]>
AuthorDate: Fri Aug 3 09:30:27 2018 -0700
[Beam-4687]Automate JIRA for Dependency check (#6060)
* [BEAM-4687] create Jira client
* complete the Jira client
* [BEAM-4687] add jira manager to handle the auto jira generation
* complete the Jira manager
---
.test-infra/jenkins/dependency_check/__init__.py | 16 ++
.../dependency_check_report_generator.py | 56 +++--
.../dependency_check_report_generator_test.py | 78 +++----
.../jenkins/dependency_check/generate_report.sh | 31 +--
.../dependency_check/report_generator_config.py | 82 +++++++
.test-infra/jenkins/jira_utils/__init__.py | 16 ++
.test-infra/jenkins/jira_utils/jira_client.py | 132 +++++++++++
.test-infra/jenkins/jira_utils/jira_manager.py | 215 ++++++++++++++++++
.../jenkins/jira_utils/jira_manager_test.py | 243 +++++++++++++++++++++
.test-infra/jenkins/job_Dependency_Check.groovy | 6 +
ownership/JAVA_DEPENDENCY_OWNERS.yaml | 7 +-
ownership/PYTHON_DEPENDENCY_OWNERS.yaml | 2 +-
12 files changed, 794 insertions(+), 90 deletions(-)
diff --git a/.test-infra/jenkins/dependency_check/__init__.py
b/.test-infra/jenkins/dependency_check/__init__.py
new file mode 100644
index 0000000..cce3aca
--- /dev/null
+++ b/.test-infra/jenkins/dependency_check/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
diff --git
a/.test-infra/jenkins/dependency_check/dependency_check_report_generator.py
b/.test-infra/jenkins/dependency_check/dependency_check_report_generator.py
index 981d9eb..6171ad1 100644
--- a/.test-infra/jenkins/dependency_check/dependency_check_report_generator.py
+++ b/.test-infra/jenkins/dependency_check/dependency_check_report_generator.py
@@ -22,7 +22,9 @@ import re
import traceback
import logging
from datetime import datetime
-from bigquery_client_utils import BigQueryClientUtils
+from dependency_check.bigquery_client_utils import BigQueryClientUtils
+from jira_utils.jira_manager import JiraManager
+from dependency_check.report_generator_config import ReportGeneratorConfig
_MAX_STALE_DAYS = 360
@@ -56,7 +58,7 @@ def extract_results(file_path):
see_oudated_deps = True
raw_report.close()
return outdated_deps
- except Exception as e:
+ except:
raise
@@ -64,7 +66,7 @@ def extract_single_dep(dep):
"""
Extract a single dependency check record from Java and Python reports.
Args:
- dep: e.g "- org.assertj:assertj-core [2.5.0 -> 3.10.0]".
+ dep: e.g " - org.assertj:assertj-core [2.5.0 -> 3.10.0]".
Return:
dependency name, current version, latest version.
"""
@@ -75,7 +77,7 @@ def extract_single_dep(dep):
return match.group(1).strip(), match.group(2).strip(), match.group(3).strip()
-def prioritize_dependencies(deps, sdk_type, project_id, dataset_id, table_id):
+def prioritize_dependencies(deps, sdk_type):
"""
Extracts and analyze dependency versions and release dates.
Returns a collection of dependencies which is "high priority" in html format:
@@ -88,17 +90,26 @@ def prioritize_dependencies(deps, sdk_type, project_id,
dataset_id, table_id):
Return:
high_priority_deps: A collection of dependencies which need to be taken
care of before next release.
"""
+
+ project_id = ReportGeneratorConfig.GCLOUD_PROJECT_ID
+ dataset_id = ReportGeneratorConfig.DATASET_ID
+ table_id = ReportGeneratorConfig.get_bigquery_table_id(sdk_type)
high_priority_deps = []
bigquery_client = BigQueryClientUtils(project_id, dataset_id, table_id)
+ jira_manager = JiraManager(ReportGeneratorConfig.BEAM_JIRA_HOST,
+ ReportGeneratorConfig.BEAM_JIRA_BOT_USRENAME,
+ ReportGeneratorConfig.BEAM_JIRA_BOT_PASSWORD,
+ ReportGeneratorConfig.get_owners_file(sdk_type))
for dep in deps:
try:
- logging.info("Start processing: %s", dep)
+ logging.info("\n\nStart processing: " + dep)
dep_name, curr_ver, latest_ver = extract_single_dep(dep)
curr_release_date, latest_release_date =
query_dependency_release_dates(bigquery_client,
dep_name,
curr_ver,
latest_ver)
+ group_id = None
if sdk_type == 'Java':
# extract the groupid and artifactid
group_id, artifact_id = dep_name.split(":")
@@ -120,8 +131,10 @@ def prioritize_dependencies(deps, sdk_type, project_id,
dataset_id, table_id):
latest_release_date)
if compare_dependency_versions(curr_ver, latest_ver):
high_priority_deps.append(dep_info)
+ jira_manager.run(dep_name, latest_ver, sdk_type, group_id = group_id)
elif compare_dependency_release_dates(curr_release_date,
latest_release_date):
high_priority_deps.append(dep_info)
+ jira_manager.run(dep_name, latest_ver, sdk_type, group_id = group_id)
except:
traceback.print_exc()
continue
@@ -159,7 +172,7 @@ def compare_dependency_versions(curr_ver, latest_ver):
return True
elif int(curr_minor_ver) + _MAX_MINOR_VERSION_DIFF <=
int(latest_minor_ver):
return True
- # TODO: Comparing patch versions if needed.
+ # TODO: Comparing patch versions if needed.
return False
@@ -209,25 +222,22 @@ def compare_dependency_release_dates(curr_release_date,
latest_release_date):
Return:
boolean
"""
- if curr_release_date is None or latest_release_date is None:
- return True
+ if not curr_release_date or not latest_release_date:
+ return False
else:
if (latest_release_date - curr_release_date).days >= _MAX_STALE_DAYS:
return True
return False
-def generate_report(file_path, sdk_type, project_id, dataset_id, table_id):
+def generate_report(sdk_type):
"""
Write SDK dependency check results into a html report.
Args:
- file_path: the path that report will be write into.
sdk_type: String [Java, Python, TODO: Go]
- project_id: the gcloud project ID that is used for BigQuery API requests.
- dataset_id: the BigQuery dataset ID.
- table_id: the BigQuery table ID.
"""
- report_name = 'build/dependencyUpdates/beam-dependency-check-report.html'
+ report_name = ReportGeneratorConfig.FINAL_REPORT
+ raw_report = ReportGeneratorConfig.get_raw_report(sdk_type)
if os.path.exists(report_name):
append_write = 'a'
@@ -237,15 +247,15 @@ def generate_report(file_path, sdk_type, project_id,
dataset_id, table_id):
try:
# Extract dependency check results from build/dependencyUpdate
report = open(report_name, append_write)
- if os.path.isfile(file_path):
- outdated_deps = extract_results(file_path)
+ if os.path.isfile(raw_report):
+ outdated_deps = extract_results(raw_report)
else:
- report.write("Did not find the raw report of dependency check:
{}".format(file_path))
+ report.write("Did not find the raw report of dependency check:
{}".format(raw_report))
report.close()
return
# Prioritize dependencies by comparing versions and release dates.
- high_priority_deps = prioritize_dependencies(outdated_deps, sdk_type,
project_id, dataset_id, table_id)
+ high_priority_deps = prioritize_dependencies(outdated_deps, sdk_type)
# Write results to a report
subtitle = "<h2>High Priority Dependency Updates Of Beam {}
SDK:</h2>\n".format(sdk_type)
@@ -267,6 +277,8 @@ def generate_report(file_path, sdk_type, project_id,
dataset_id, table_id):
report.write("%s" % dep)
report.write("</table>\n")
except Exception as e:
+ traceback.print_exc()
+ logging.error("Failed generate the dependency report. " + str(e))
report.write('<p> {0} </p>'.format(str(e)))
report.close()
@@ -276,13 +288,9 @@ def main(args):
"""
Main method.
Args:
- args[0]: path of the raw report generated by Java/Python dependency check.
Typically in build/dependencyUpdates
- args[1]: type of the check [Java, Python]
- args[2]: google cloud project id
- args[3]: BQ dataset id
- args[4]: BQ table id
+ args[0]: type of the check [Java, Python]
"""
- generate_report(args[0], args[1], args[2], args[3], args[4])
+ generate_report(args[0])
if __name__ == '__main__':
diff --git
a/.test-infra/jenkins/dependency_check/dependency_check_report_generator_test.py
b/.test-infra/jenkins/dependency_check/dependency_check_report_generator_test.py
index 770527a..b899075 100644
---
a/.test-infra/jenkins/dependency_check/dependency_check_report_generator_test.py
+++
b/.test-infra/jenkins/dependency_check/dependency_check_report_generator_test.py
@@ -19,9 +19,8 @@
# This script performs testing of scenarios from
verify_performance_test_results.py
#
-from __future__ import print_function
import unittest, mock
-from mock import patch
+from mock import patch, mock_open
from datetime import datetime
from dependency_check_report_generator import prioritize_dependencies
@@ -35,27 +34,31 @@ _SDK_TYPE = 'JAVA'
_LP_CURR_VERSION_DATE = datetime.strptime('2000-01-01', '%Y-%m-%d')
_LATEST_VERSION_DATE = datetime.strptime('2000-01-02', '%Y-%m-%d')
_HP_CURR_VERSION_DATE = datetime.strptime('1999-01-01', '%Y-%m-%d')
+_MOCKED_OWNERS_FILE = "deps: "
+@patch('google.cloud.bigquery.Client')
+@patch('jira_utils.jira_manager.JiraManager')
+@patch('jira_utils.jira_manager.JiraClient')
+@patch('jira_utils.jira_manager.JiraManager.run')
class DependencyCheckReportGeneratorTest(unittest.TestCase):
"""Tests for `dependency_check_report_generator.py`."""
def setUp(self):
- print("Test name:", self._testMethodName)
+ print "\n\nTest : " + self._testMethodName
- @patch('google.cloud.bigquery.Client')
- @patch('bigquery_client_utils.BigQueryClientUtils')
+ @patch('dependency_check.bigquery_client_utils.BigQueryClientUtils')
def test_empty_dep_input(self, *args):
"""
Test on empty outdated dependencies.
- Except: empty report
+ Expect: empty report
"""
- report = prioritize_dependencies([], _SDK_TYPE, _PROJECT_ID, _DATASET_ID,
_TABLE_ID)
- self.assertEqual(len(report), 0)
+ with patch('__builtin__.open', mock_open(read_data=_MOCKED_OWNERS_FILE)):
+ report = prioritize_dependencies([], _SDK_TYPE)
+ self.assertEqual(len(report), 0)
- @patch('google.cloud.bigquery.Client')
- @patch('bigquery_client_utils.BigQueryClientUtils.query_dep_info_by_version',
+
@patch('dependency_check.bigquery_client_utils.BigQueryClientUtils.query_dep_info_by_version',
side_effect = [(_LP_CURR_VERSION_DATE, True), (_LATEST_VERSION_DATE,
False),
(_LP_CURR_VERSION_DATE, True), (_LATEST_VERSION_DATE,
False),
(_HP_CURR_VERSION_DATE, True), (_LATEST_VERSION_DATE,
False),
@@ -63,7 +66,7 @@ class DependencyCheckReportGeneratorTest(unittest.TestCase):
def test_normal_dep_input(self, *args):
"""
Test on a normal outdated dependencies set.
- Except: group1:artifact1, group2:artifact2, and group3:artifact3
+ Expect: group1:artifact1, group2:artifact2, and group3:artifact3
"""
deps = [
" - group1:artifact1 [1.0.0 -> 3.0.0]",
@@ -71,61 +74,62 @@ class DependencyCheckReportGeneratorTest(unittest.TestCase):
" - group3:artifact3 [1.0.0 -> 1.1.0]",
" - group4:artifact4 [1.0.0 -> 1.1.0]"
]
- report = prioritize_dependencies(deps, _SDK_TYPE, _PROJECT_ID,
_DATASET_ID, _TABLE_ID)
- self.assertEqual(len(report), 3)
- self.assertIn('group1:artifact1', report[0])
- self.assertIn('group2:artifact2', report[1])
- self.assertIn('group3:artifact3', report[2])
+ with patch('__builtin__.open', mock_open(read_data=_MOCKED_OWNERS_FILE)):
+ report = prioritize_dependencies(deps, _SDK_TYPE)
+ self.assertEqual(len(report), 3)
+ self.assertIn('group1:artifact1', report[0])
+ self.assertIn('group2:artifact2', report[1])
+ self.assertIn('group3:artifact3', report[2])
- @patch('google.cloud.bigquery.Client')
- @patch('bigquery_client_utils.BigQueryClientUtils.query_dep_info_by_version',
+
@patch('dependency_check.bigquery_client_utils.BigQueryClientUtils.query_dep_info_by_version',
side_effect = [(_LP_CURR_VERSION_DATE, True),
(_LATEST_VERSION_DATE, False),])
def test_dep_with_nondigit_major_versions(self, *args):
"""
Test on a outdated dependency with non-digit major number.
- Except: group1:artifact1
+ Expect: group1:artifact1
"""
deps = [" - group1:artifact1 [Release1-123 -> Release2-456]"]
- report = prioritize_dependencies(deps, _SDK_TYPE, _PROJECT_ID,
_DATASET_ID, _TABLE_ID)
- self.assertEqual(len(report), 1)
- self.assertIn('group1:artifact1', report[0])
+ with patch('__builtin__.open', mock_open(read_data=_MOCKED_OWNERS_FILE)):
+ report = prioritize_dependencies(deps, _SDK_TYPE)
+ self.assertEqual(len(report), 1)
+ self.assertIn('group1:artifact1', report[0])
- @patch('google.cloud.bigquery.Client')
- @patch('bigquery_client_utils.BigQueryClientUtils.query_dep_info_by_version',
+
@patch('dependency_check.bigquery_client_utils.BigQueryClientUtils.query_dep_info_by_version',
side_effect = [(_LP_CURR_VERSION_DATE, True),
(_LATEST_VERSION_DATE, False),])
def test_dep_with_nondigit_minor_versions(self, *args):
"""
Test on a outdated dependency with non-digit minor number.
- Except: group1:artifact1
+ Expect: group1:artifact1
"""
deps = [" - group1:artifact1 [0.rc1.0 -> 0.rc2.0]"]
- report = prioritize_dependencies(deps, _SDK_TYPE, _PROJECT_ID,
_DATASET_ID, _TABLE_ID)
- self.assertEqual(len(report), 1)
- self.assertIn('group1:artifact1', report[0])
+ with patch('__builtin__.open', mock_open(read_data=_MOCKED_OWNERS_FILE)):
+ report = prioritize_dependencies(deps, _SDK_TYPE)
+ self.assertEqual(len(report), 1)
+ self.assertIn('group1:artifact1', report[0])
- @patch('google.cloud.bigquery.Client')
- @patch('bigquery_client_utils.BigQueryClientUtils.insert_dep_to_table')
- @patch('bigquery_client_utils.BigQueryClientUtils.delete_dep_from_table')
-
@patch('bigquery_client_utils.BigQueryClientUtils.query_currently_used_dep_info_in_db',
side_effect = [(None, None)])
- @patch('bigquery_client_utils.BigQueryClientUtils.query_dep_info_by_version',
+
@patch('dependency_check.bigquery_client_utils.BigQueryClientUtils.insert_dep_to_table')
+
@patch('dependency_check.bigquery_client_utils.BigQueryClientUtils.delete_dep_from_table')
+
@patch('dependency_check.bigquery_client_utils.BigQueryClientUtils.query_currently_used_dep_info_in_db',
side_effect = [(None, None)])
+
@patch('dependency_check.bigquery_client_utils.BigQueryClientUtils.query_dep_info_by_version',
side_effect = [(_HP_CURR_VERSION_DATE, True), (_LATEST_VERSION_DATE,
False),])
def test_invalid_dep_input(self, *args):
"""
Test on a invalid outdated dependencies format.
- Except: Exception through out. And group2:artifact2 is picked.
+ Expect: Exception through out. And group2:artifact2 is picked.
"""
deps = [
"- group1:artifact1 (1.0.0, 2.0.0)",
" - group2:artifact2 [1.0.0 -> 2.0.0]"
]
- report = prioritize_dependencies(deps, _SDK_TYPE, _PROJECT_ID,
_DATASET_ID, _TABLE_ID)
- self.assertEqual(len(report), 1)
- self.assertIn('group2:artifact2', report[0])
+ with patch('__builtin__.open', mock_open(read_data=_MOCKED_OWNERS_FILE)):
+ report = prioritize_dependencies(deps, _SDK_TYPE)
+ self.assertEqual(len(report), 1)
+ self.assertIn('group2:artifact2', report[0])
if __name__ == '__main__':
diff --git a/.test-infra/jenkins/dependency_check/generate_report.sh
b/.test-infra/jenkins/dependency_check/generate_report.sh
index 7e7ff2f..644fa81 100755
--- a/.test-infra/jenkins/dependency_check/generate_report.sh
+++ b/.test-infra/jenkins/dependency_check/generate_report.sh
@@ -20,10 +20,6 @@
set -e
set -v
-PROJECT_ID='apache-beam-testing'
-DATASET_ID='beam_dependency_states'
-PYTHON_DEP_TABLE_ID='python_dependency_states'
-JAVA_DEP_TABLE_ID='java_dependency_states'
REPORT_DESCRIPTION="
<h4> A dependency update is high priority if it satisfies one of following
criteria: </h4>
<ul>
@@ -39,34 +35,25 @@ REPORT_DESCRIPTION="
In the future, issues will be filed and tracked for these automatically,
but in the meantime you can search for existing issues or open a new one.
</h4>
-<h4> For more information: <a
href=\"https://docs.google.com/document/d/15m1MziZ5TNd9rh_XN0YYBJfYkt0Oj-Ou9g0KFDPL2aA/edit#\">
Beam Dependency Update Policy </a></h4>"
+<h4> For more information: <a
href=\"https://beam.apache.org/contribute/dependencies/\"> Beam Dependency
Guide </a></h4>"
# Virtualenv for the rest of the script to run setup
/usr/bin/virtualenv dependency/check
. dependency/check/bin/activate
pip install --upgrade google-cloud-bigquery
-
-# Run the unit tests of the report generator
-pip install mock
-python
$WORKSPACE/src/.test-infra/jenkins/dependency_check/dependency_check_report_generator_test.py
\
-
rm -f build/dependencyUpdates/beam-dependency-check-report.txt
+# Insall packages and run the unit tests of the report generator and the jira
manager
+pip install mock jira pyyaml
+cd $WORKSPACE/src/.test-infra/jenkins
+python -m dependency_check.dependency_check_report_generator_test
+python -m jira_utils.jira_manager_test
+
echo "<html><body>" >
$WORKSPACE/src/build/dependencyUpdates/beam-dependency-check-report.html
-python
$WORKSPACE/src/.test-infra/jenkins/dependency_check/dependency_check_report_generator.py
\
-build/dependencyUpdates/python_dependency_report.txt \
-Python \
-$PROJECT_ID \
-$DATASET_ID \
-$PYTHON_DEP_TABLE_ID
+python -m dependency_check/dependency_check_report_generator Python
-python
$WORKSPACE/src/.test-infra/jenkins/dependency_check/dependency_check_report_generator.py
\
-build/dependencyUpdates/report.txt \
-Java \
-$PROJECT_ID \
-$DATASET_ID \
-$JAVA_DEP_TABLE_ID
+python -m dependency_check.dependency_check_report_generator Java
echo "$REPORT_DESCRIPTION </body></html>" >>
$WORKSPACE/src/build/dependencyUpdates/beam-dependency-check-report.html
diff --git a/.test-infra/jenkins/dependency_check/report_generator_config.py
b/.test-infra/jenkins/dependency_check/report_generator_config.py
new file mode 100644
index 0000000..c99c76e
--- /dev/null
+++ b/.test-infra/jenkins/dependency_check/report_generator_config.py
@@ -0,0 +1,82 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+# Defines constants and helper methods used by the dependency_report_generator
+
+import os
+
+class ReportGeneratorConfig:
+
+ # Jenkins Working Space
+ WORKING_SPACE = os.environ['WORKSPACE']
+
+ # Constants for dependency prioritization
+ GCLOUD_PROJECT_ID = 'apache-beam-testing'
+ DATASET_ID = 'beam_dependency_states'
+ PYTHON_DEP_TABLE_ID = 'python_dependency_states'
+ JAVA_DEP_TABLE_ID = 'java_dependency_states'
+ PYTHON_DEP_RAW_REPORT = WORKING_SPACE +
'/src/build/dependencyUpdates/python_dependency_report.txt'
+ JAVA_DEP_RAW_REPORT = WORKING_SPACE +
'/src/build/dependencyUpdates/report.txt'
+ FINAL_REPORT = WORKING_SPACE +
'/src/build/dependencyUpdates/beam-dependency-check-report.html'
+
+ # Constants for JIRA automation
+ BEAM_JIRA_HOST = 'https://issues.apache.org/jira/'
+ BEAM_JIRA_BOT_USRENAME= os.environ['BEAM_JIRA_BOT_USERNAME']
+ BEAM_JIRA_BOT_PASSWORD= os.environ['BEAM_JIRA_BOT_PASSWORD']
+
+ # Dependency Owners
+ JAVA_DEP_OWNERS = WORKING_SPACE +
'/src/ownership/JAVA_DEPENDENCY_OWNERS.yaml'
+ PYTHON_DEP_OWNERS = WORKING_SPACE +
'/src/ownership/PYTHON_DEPENDENCY_OWNERS.yaml'
+
+
+ @classmethod
+ def get_bigquery_table_id(cls, sdk_type):
+ if sdk_type.lower() == 'java':
+ return cls.JAVA_DEP_TABLE_ID
+ elif sdk_type.lower() == 'python':
+ return cls.PYTHON_DEP_TABLE_ID
+ else:
+ raise UndefinedSDKTypeException("""Undefined SDK Type: {0}.
+ Could not find the BigQuery table for {1}
dependencies.""".format(sdk_type, sdk_type))
+
+
+ @classmethod
+ def get_raw_report(cls, sdk_type):
+ if sdk_type.lower() == 'java':
+ return cls.JAVA_DEP_RAW_REPORT
+ elif sdk_type.lower() == 'python':
+ return cls.PYTHON_DEP_RAW_REPORT
+ else:
+ raise UndefinedSDKTypeException("""Undefined SDK Type: {0}.
+ Could not find the dependency reports for the {1}
SDK.""".format(sdk_type, sdk_type))
+
+
+ @classmethod
+ def get_owners_file(cls, sdk_type):
+ if sdk_type.lower() == 'java':
+ return cls.JAVA_DEP_OWNERS
+ elif sdk_type.lower() == 'python':
+ return cls.PYTHON_DEP_OWNERS
+ else:
+ raise UndefinedSDKTypeException("""Undefined SDK Type: {0}.
+ Could not find the Owners file for the {1} SDK.""".format(sdk_type,
sdk_type))
+
+
+class UndefinedSDKTypeException(Exception):
+ """Indicates an error has occurred in while reading constants."""
+
+ def __init__(self, msg):
+ super(UndefinedSDKTypeException, self).__init__(msg)
diff --git a/.test-infra/jenkins/jira_utils/__init__.py
b/.test-infra/jenkins/jira_utils/__init__.py
new file mode 100644
index 0000000..cce3aca
--- /dev/null
+++ b/.test-infra/jenkins/jira_utils/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
diff --git a/.test-infra/jenkins/jira_utils/jira_client.py
b/.test-infra/jenkins/jira_utils/jira_client.py
new file mode 100644
index 0000000..0c6c126
--- /dev/null
+++ b/.test-infra/jenkins/jira_utils/jira_client.py
@@ -0,0 +1,132 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+
+from jira import JIRA
+
+class JiraClient:
+
+ def __init__(self, options, basic_auth, project):
+ self.jira = JIRA(options, basic_auth=basic_auth)
+ self.project = project
+
+
+ def get_issues_by_summary(self, summary):
+ """
+ Find issues by using the summary (issue title)
+ Args:
+ summary
+ Return:
+ A list of issues
+ """
+ try:
+ issues = self.jira.search_issues("project={0} AND summary ~
'{1}'".format(self.project, summary))
+ except Exception:
+ raise
+ return issues
+
+
+ def get_issue_by_key(self, key):
+ """
+ Find issue by using the key (e.g BEAM-1234)
+ Args:
+ key
+ Return:
+ issue
+ """
+ try:
+ issue = self.jira.issue(key)
+ except Exception:
+ raise
+ return issue
+
+
+ def create_issue(self, summary, components, description, issuetype='Bug',
assignee=None, parent_key=None):
+ """
+ Create a new issue
+ Args:
+ summary - Issue title
+ components - A list of components
+ description (optional) - A string that describes the issue
+ issuetype (optional) - Bug, Improvement, New Feature, Sub-task, Task,
Wish, etc.
+ assignee (optional) - A string of JIRA user name
+ parent_key (optional) - The parent issue key is required when creating a
subtask.
+ Return:
+ Issue created
+ """
+ fields = {
+ 'project': {'key': self.project},
+ 'summary': summary,
+ 'description': description,
+ 'issuetype': {'name': issuetype},
+ 'components': [],
+ }
+ for component in components:
+ fields['components'].append({'name': component})
+ if assignee is not None:
+ fields['assignee'] = {'name': assignee}
+ if parent_key is not None:
+ fields['parent'] = {'key': parent_key}
+ fields['issuetype'] = {'name': 'Sub-task'}
+ try:
+ new_issue = self.jira.create_issue(fields = fields)
+ except Exception:
+ raise
+ return new_issue
+
+
+ def update_issue(self, issue, summary=None, components=None,
description=None, assignee=None, notify=True):
+ """
+ Create a new issue
+ Args:
+ issue - Jira issue object
+ summary (optional) - Issue title
+ components (optional) - A list of components
+ description (optional) - A string that describes the issue
+ assignee (optional) - A string of JIRA user name
+ notify - Query parameter notifyUsers. If true send the email with
notification that the issue was updated to users that watch it.
+ Admin or project admin permissions are required to disable the
notification.
+ Return:
+ Issue created
+ """
+ fields={}
+ if summary:
+ fields['summary'] = summary
+ if description:
+ fields['description'] = description
+ if assignee:
+ fields['assignee'] = {'name': assignee}
+ if components:
+ fields['components'] = []
+ for component in components:
+ fields['components'].append({'name': component})
+ try:
+ issue.update(fields=fields, notify=notify)
+ except Exception:
+ raise
+
+
+ def reopen_issue(self, issue):
+ """
+ Reopen an issue
+ Args:
+ issue - Jira issue object
+ """
+ try:
+ self.jira.transition_issue(issue.key, 3)
+ except:
+ raise
diff --git a/.test-infra/jenkins/jira_utils/jira_manager.py
b/.test-infra/jenkins/jira_utils/jira_manager.py
new file mode 100644
index 0000000..cdc4450
--- /dev/null
+++ b/.test-infra/jenkins/jira_utils/jira_manager.py
@@ -0,0 +1,215 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 logging
+import yaml
+import traceback
+from datetime import datetime
+from jira_client import JiraClient
+
+_JIRA_PROJECT_NAME = 'BEAM'
+_JIRA_COMPONENT = 'dependencies'
+_ISSUE_SUMMARY_PREFIX = 'Beam Dependency Update Request: '
+
+class JiraManager:
+
+ def __init__(self, jira_url, jira_username, jira_password, owners_file):
+ options = {
+ 'server': jira_url
+ }
+ basic_auth = (jira_username, jira_password)
+ self.jira = JiraClient(options, basic_auth, _JIRA_PROJECT_NAME)
+ with open(owners_file) as f:
+ owners = yaml.load(f)
+ self.owners_map = owners['deps']
+ logging.getLogger().setLevel(logging.INFO)
+
+
+ def run(self, dep_name, dep_latest_version, sdk_type, group_id=None):
+ """
+ Manage the jira issue for a dependency
+ Args:
+ dep_name,
+ dep_latest_version,
+ sdk_type: Java, Python
+ group_id (optional): only required for Java dependencies
+ """
+ logging.info("Start handling the JIRA issues for {0} dependency: {1}
{2}".format(
+ sdk_type, dep_name, dep_latest_version))
+ try:
+ # find the parent issue for Java deps base on the groupID
+ parent_issue = None
+ if sdk_type == 'Java':
+ summary = _ISSUE_SUMMARY_PREFIX + group_id
+ parent_issues = self._search_issues(summary)
+ for i in parent_issues:
+ if i.fields.summary == summary:
+ parent_issue = i
+ break
+ # Create a new parent issue if no existing found
+ if not parent_issue:
+ logging.info("""Did not find existing issue with name {0}. \n
+ Created a parent issue for {1}""".format(summary, group_id))
+ try:
+ parent_issue = self._create_issue(group_id, None)
+ print parent_issue.key
+ except:
+ logging.error("""Failed creating a parent issue for {0}.
+ Stop handling the JIRA issue for {1}, {2}""".format(group_id,
dep_name, dep_latest_version))
+ return
+ # Reopen the existing parent issue if it was closed
+ elif (parent_issue.fields.status.name != 'Open' and
+ parent_issue.fields.status.name != 'Reopened'):
+ logging.info("""The parent issue {0} is not opening. Attempt
reopening the issue""".format(parent_issue.key))
+ try:
+ self.jira.reopen_issue(parent_issue)
+ except:
+ traceback.print_exc()
+ logging.error("""Failed reopening the parent issue {0}.
+ Stop handling the JIRA issue for {1},
{2}""".format(parent_issue.key, dep_name, dep_latest_version))
+ return
+ logging.info("Found the parent issue {0}. Continuous to create or
update the sub-task for {1}".format(parent_issue.key, dep_name))
+ # creating a new issue/sub-task or updating on the existing issue of the
dep
+ summary = _ISSUE_SUMMARY_PREFIX + dep_name + " " + dep_latest_version
+ issues = self._search_issues(summary)
+ issue = None
+ for i in issues:
+ if i.fields.summary == summary:
+ issue = i
+ break
+ if not issue:
+ if sdk_type == 'Java':
+ issue = self._create_issue(dep_name, dep_latest_version,
is_subtask=True, parent_key=parent_issue.key)
+ else:
+ issue = self._create_issue(dep_name, dep_latest_version)
+ logging.info('Created a new issue {0} of {1} {2}'.format(issue.key,
dep_name, dep_latest_version))
+ elif issue.fields.status.name == 'Open' or issue.fields.status.name ==
'Reopened':
+ self._append_descriptions(issue, dep_name, dep_latest_version)
+ logging.info('Updated the existing issue {0} of {1}
{2}'.format(issue.key, dep_name, dep_latest_version))
+ except:
+ raise
+
+
+ def _create_issue(self, dep_name, dep_latest_version, is_subtask=False,
parent_key=None):
+ """
+ Create a new issue or subtask
+ Args:
+ dep_name,
+ dep_latest_version,
+ is_subtask,
+ parent_key: only required if the 'is_subtask'is true.
+ """
+ logging.info("Creating a new JIRA issue to track {0} upgrade
process".format(dep_name))
+ assignee, owners = self._find_owners(dep_name)
+ summary = _ISSUE_SUMMARY_PREFIX + dep_name
+ if dep_latest_version:
+ summary = summary + " " + dep_latest_version
+ description = """\n\n{0}\n
+ Please review and upgrade the {1} to the latest version {2} \n
+ cc: """.format(
+ datetime.today(),
+ dep_name,
+ dep_latest_version
+ )
+ for owner in owners:
+ description += "[~{0}], ".format(owner)
+ try:
+ if not is_subtask:
+ issue = self.jira.create_issue(summary, [_JIRA_COMPONENT],
description, assignee=assignee)
+ else:
+ issue = self.jira.create_issue(summary, [_JIRA_COMPONENT],
description, assignee=assignee, parent_key=parent_key)
+ except Exception as e:
+ logging.error("Failed creating issue: "+ str(e))
+ raise e
+ return issue
+
+
+ def _search_issues(self, summary):
+ """
+ Search issues by using issues' summary.
+ Args:
+ summary: a string
+ Return:
+ A list of issues
+ """
+ try:
+ issues = self.jira.get_issues_by_summary(summary)
+ except Exception as e:
+ logging.error("Failed searching issues: "+ str(e))
+ return []
+ return issues
+
+
+ def _append_descriptions(self, issue, dep_name, dep_latest_version):
+ """
+ Add descriptions on an existing issue.
+ Args:
+ issue: Jira issue
+ dep_name
+ dep_latest_version
+ """
+ logging.info("Updating JIRA issue {0} to track {1} upgrade process".format(
+ issue.key,
+ dep_name))
+ description = issue.fields.description + """\n\n{0}\n
+ Please review and upgrade the {1} to the latest version {2} \n
+ cc: """.format(
+ datetime.today(),
+ dep_name,
+ dep_latest_version
+ )
+ _, owners = self._find_owners(dep_name)
+ for owner in owners:
+ description += "[~{0}], ".format(owner)
+ try:
+ self.jira.update_issue(issue, description=description)
+ except Exception as e:
+ traceback.print_exc()
+ logging.error("Failed updating issue: "+ str(e))
+
+
+ def _find_owners(self, dep_name):
+ """
+ Find owners for a dependency/
+ Args:
+ dep_name
+ Return:
+ primary: The primary owner of the dep. The Jira issue will be assigned
to the primary owner.
+ others: A list of other owners of the dep. Owners will be cc'ed in the
description.
+ """
+ try:
+ dep_info = self.owners_map[dep_name]
+ owners = dep_info['owners']
+ if not owners:
+ logging.warning("Could not find owners for " + dep_name)
+ return None, []
+ except KeyError:
+ traceback.print_exc()
+ logging.warning("Could not find the dependency info of {0} in the OWNERS
configurations.".format(dep_name))
+ return None, []
+ except Exception as e:
+ traceback.print_exc()
+ logging.error("Failed finding dependency owners: "+ str(e))
+ return None, None
+
+ logging.info("Found owners of {0}: {1}".format(dep_name, owners))
+ owners = owners.split(',')
+ owners = map(str.strip, owners)
+ owners = list(filter(None, owners))
+ primary = owners[0]
+ del owners[0]
+ return primary, owners
diff --git a/.test-infra/jenkins/jira_utils/jira_manager_test.py
b/.test-infra/jenkins/jira_utils/jira_manager_test.py
new file mode 100644
index 0000000..15677d0
--- /dev/null
+++ b/.test-infra/jenkins/jira_utils/jira_manager_test.py
@@ -0,0 +1,243 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 unittest, mock
+from mock import patch, mock_open, Mock
+from jira_manager import JiraManager
+from datetime import datetime
+
+
+class MockedJiraIssue:
+ def __init__(self, key, summary, description, status):
+ self.key = key
+ self.fields = self.MockedJiraIssueFields(summary, description, status)
+
+ class MockedJiraIssueFields:
+ def __init__(self, summary, description, status):
+ self.summary = summary
+ self.description = description
+ self.status = self.MockedJiraIssueStatus(status)
+
+ class MockedJiraIssueStatus:
+ def __init__(self, status):
+ self.name = status
+
+
+@patch('jira_utils.jira_manager.JiraClient')
+class JiraManagerTest(unittest.TestCase):
+ """Tests for `jira_manager.py`."""
+
+ def setUp(self):
+ print "\n\nTest : " + self._testMethodName
+
+
+ def test_find_owners_with_single_owner(self, *args):
+ """
+ Test on _find_owners with single owner
+ Expect: the primary owner is 'owner0', an empty list of other owners.
+ """
+ owners_yaml = """
+ deps:
+ dep0:
+ owners: owner0,
+ """
+ with patch('__builtin__.open', mock_open(read_data=owners_yaml)):
+ manager = JiraManager('url', 'username', 'password', owners_yaml)
+ primary, owners = manager._find_owners('dep0')
+ self.assertEqual(primary, 'owner0')
+ self.assertEqual(len(owners), 0)
+
+
+ def test_find_owners_with_multi_owners(self, *args):
+ """
+ Test on _find_owners with multiple owners.
+ Expect: the primary owner is 'owner0', a list contains 'owner1' and
'owner2'.
+ """
+ owners_yaml = """
+ deps:
+ dep0:
+ owners: owner0, owner1 , owner2,
+ """
+ with patch('__builtin__.open', mock_open(read_data=owners_yaml)):
+ manager = JiraManager('url', 'username', 'password', owners_yaml)
+ primary, owners = manager._find_owners('dep0')
+ self.assertEqual(primary, 'owner0')
+ self.assertEqual(len(owners), 2)
+ self.assertIn('owner1', owners)
+ self.assertIn('owner2', owners)
+
+
+ def test_find_owners_with_no_owners_defined(self, *args):
+ """
+ Test on _find_owners without owner.
+ Expect: the primary owner is None, an empty list of other owners.
+ """
+ owners_yaml = """
+ deps:
+ dep0:
+ owners:
+ """
+ with patch('__builtin__.open', mock_open(read_data=owners_yaml)):
+ manager = JiraManager('url', 'username', 'password', owners_yaml)
+ primary, owners = manager._find_owners('dep0')
+ self.assertIsNone(primary)
+ self.assertEqual(len(owners), 0)
+
+
+ def test_find_owners_with_no_dep_defined(self, *args):
+ """
+ Test on _find_owners with non-defined dep.
+ Expect: through out KeyErrors. The primary owner is None, an empty list of
other owners.
+ """
+ owners_yaml = """
+ deps:
+ dep0:
+ owners:
+ """
+ with patch('__builtin__.open', mock_open(read_data=owners_yaml)):
+ manager = JiraManager('url', 'username', 'password', owners_yaml)
+ primary, owners = manager._find_owners('dep1')
+ self.assertIsNone(primary)
+ self.assertEqual(len(owners), 0)
+
+
+ @patch('jira_utils.jira_manager.datetime',
Mock(today=Mock(return_value=datetime.strptime('2000-01-01', '%Y-%m-%d'))))
+ def test_run_with_creating_new_issue(self, *args):
+ """
+ Test JiraManager.run on creating a new issue.
+ Expect: jira.create_issue is called once with certain parameters.
+ """
+ owners_yaml = """
+ deps:
+ dep0:
+ owners: owner0, owner1 , owner2,
+ """
+ with patch('__builtin__.open', mock_open(read_data=owners_yaml)):
+ manager = JiraManager('url', 'username', 'password', owners_yaml)
+ manager.run('dep0', '1.0', 'Python')
+
manager.jira.create_issue.assert_called_once_with(self._get_experct_summary('dep0',
'1.0'),
+ ['dependencies'],
+
self._get_expected_description('dep0', '1.0', ['owner1', 'owner2']),
+ assignee='owner0')
+
+
+ @patch('jira_utils.jira_manager.datetime',
Mock(today=Mock(return_value=datetime.strptime('2000-01-01', '%Y-%m-%d'))))
+ def test_run_with_updating_existing_task(self, *args):
+ """
+ Test JiraManager.run on updating an existing issue.
+ Expect: jira.update_issue is called once.
+ """
+ dep_name = 'dep0'
+ dep_latest_version = '1.0'
+ owners_yaml = """
+ deps:
+ dep0:
+ owners:
+ """
+ summary = self._get_experct_summary(dep_name, dep_latest_version)
+ description = self._get_expected_description(dep_name, dep_latest_version,
[])
+
+ with patch('__builtin__.open', mock_open(read_data=owners_yaml)):
+ with patch('jira_utils.jira_manager.JiraManager._search_issues',
+ return_value=[MockedJiraIssue('BEAM-1000', summary, description,
'Open')]):
+ manager = JiraManager('url', 'username', 'password', owners_yaml)
+ manager.run(dep_name, dep_latest_version, 'Python')
+ manager.jira.update_issue.assert_called_once()
+
+
+ @patch('jira_utils.jira_manager.datetime',
Mock(today=Mock(return_value=datetime.strptime('2000-01-01', '%Y-%m-%d'))))
+ def test_run_with_creating_new_subtask(self, *args):
+ """
+ Test JiraManager.run on creating a new sub-task.
+ Expect: jira.create_issue is called once with certain parameters.
+ """
+ dep_name = 'group0:artifact0'
+ dep_latest_version = '1.0'
+ owners_yaml = """
+ deps:
+ group0:artifact0:
+ group: group0
+ artifact: artifact0
+ owners: owner0
+ """
+ summary = self._get_experct_summary('group0', None)
+ description = self._get_expected_description(dep_name, dep_latest_version,
[])
+
+ with patch('__builtin__.open', mock_open(read_data=owners_yaml)):
+ with patch('jira_utils.jira_manager.JiraManager._search_issues',
+ side_effect = [[MockedJiraIssue('BEAM-1000', summary, description,
'Open')],
+ []]):
+ manager = JiraManager('url', 'username', 'password', owners_yaml)
+ manager.run(dep_name, dep_latest_version, 'Java', group_id='group0')
+
manager.jira.create_issue.assert_called_once_with(self._get_experct_summary(dep_name,
dep_latest_version),
+ ['dependencies'],
+
self._get_expected_description(dep_name, dep_latest_version, []),
+ assignee='owner0',
+
parent_key='BEAM-1000',
+ )
+
+
+ @patch('jira_utils.jira_manager.datetime',
Mock(today=Mock(return_value=datetime.strptime('2000-01-01', '%Y-%m-%d'))))
+ @patch('jira_utils.jira_manager.JiraClient.create_issue', side_effect =
[MockedJiraIssue('BEAM-2000', 'summary', 'description', 'Open')])
+ def test_run_with_reopening_existing_parent_issue(self, *args):
+ """
+ Test JiraManager.run on reopening a parent issue.
+ Expect: jira.reopen_issue is called once.
+ """
+ dep_name = 'group0:artifact0'
+ dep_latest_version = '1.0'
+ owners_yaml = """
+ deps:
+ group0:artifact0:
+ group: group0
+ artifact: artifact0
+ owners: owner0
+ """
+ summary = self._get_experct_summary('group0', None)
+ description = self._get_expected_description(dep_name, dep_latest_version,
[])
+ with patch('__builtin__.open', mock_open(read_data=owners_yaml)):
+ with patch('jira_utils.jira_manager.JiraManager._search_issues',
+ side_effect = [[MockedJiraIssue('BEAM-1000', summary, description,
'Closed')],
+ []]):
+ manager = JiraManager('url', 'username', 'password', owners_yaml)
+ manager.run(dep_name, dep_latest_version, sdk_type='Java',
group_id='group0')
+ manager.jira.reopen_issue.assert_called_once()
+
+
+ def _get_experct_summary(self, dep_name, dep_latest_version):
+ summary = 'Beam Dependency Update Request: ' + dep_name
+ if dep_latest_version:
+ summary = summary + " " + dep_latest_version
+ return summary
+
+
+ def _get_expected_description(self, dep_name, dep_latest_version, owners):
+ description = """\n\n{0}\n
+ Please review and upgrade the {1} to the latest version {2} \n
+ cc: """.format(
+ datetime.strptime('2000-01-01', '%Y-%m-%d'),
+ dep_name,
+ dep_latest_version
+ )
+ for owner in owners:
+ description += "[~{0}], ".format(owner)
+ return description
+
+
+if __name__ == '__main__':
+ unittest.main()
+
diff --git a/.test-infra/jenkins/job_Dependency_Check.groovy
b/.test-infra/jenkins/job_Dependency_Check.groovy
index 06041de..ac66881 100644
--- a/.test-infra/jenkins/job_Dependency_Check.groovy
+++ b/.test-infra/jenkins/job_Dependency_Check.groovy
@@ -47,6 +47,12 @@ job('beam_Dependency_Check') {
' && bash .test-infra/jenkins/dependency_check/generate_report.sh')
}
+ wrappers{
+ credentialsBinding {
+ usernamePassword('BEAM_JIRA_BOT_USERNAME', 'BEAM_JIRA_BOT_PASSWORD',
'beam-jira-bot')
+ }
+ }
+
def date = new Date().format('yyyy-MM-dd')
publishers {
extendedEmail {
diff --git a/ownership/JAVA_DEPENDENCY_OWNERS.yaml
b/ownership/JAVA_DEPENDENCY_OWNERS.yaml
index b01cc99..1c8a613 100644
--- a/ownership/JAVA_DEPENDENCY_OWNERS.yaml
+++ b/ownership/JAVA_DEPENDENCY_OWNERS.yaml
@@ -18,7 +18,7 @@
# Beam Java SDK dependency ownership
# Please update if new dependencies are introduced to the Beam.
-# Sign up with you JIRA username to take the owership of a package.
+# Sign up with you JIRA username to take the ownership of a package.
# Separate names by comma.
---
@@ -819,11 +819,6 @@ deps:
artifact: httpcore-nio
owners:
- org.apache.httpcomponents:httpcore-nio:
- group: org.apache.httpcomponents
- artifact: httpcore-nio
- owners:
-
org.apache.kafka:kafka_2.11:
group: org.apache.kafka
artifact: kafka_2.11
diff --git a/ownership/PYTHON_DEPENDENCY_OWNERS.yaml
b/ownership/PYTHON_DEPENDENCY_OWNERS.yaml
index a67a4e6..b94f1c8 100644
--- a/ownership/PYTHON_DEPENDENCY_OWNERS.yaml
+++ b/ownership/PYTHON_DEPENDENCY_OWNERS.yaml
@@ -18,7 +18,7 @@
# Beam Python SDK dependency ownership
# Please update if new dependencies are introduced to the Beam.
-# Sign up with you JIRA username to take the owership of a package.
+# Sign up with you JIRA username to take the ownership of a package.
# Separate names by comma.
---