29aa318e9f
- 's/depenency/dependency/' - 's/ambigiuous/ambiguous/' Change-Id: I20be1d3ff0152d999e16da5c23bb64626db2de9a Signed-off-by: Gael Chamoulaud <gchamoul@redhat.com>
225 lines
8.2 KiB
Python
225 lines
8.2 KiB
Python
#!/usr/bin/env 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.
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: jenkins_deps
|
|
version_added: "2.0"
|
|
short_description: Parses the Gerrit commit message and identifies cross repo dependency changes
|
|
description:
|
|
- Parses the Gerrit commit message and identifies cross repo dependency changes.
|
|
The expected format in the commit message is:
|
|
Depends-On: <change-id>[@<gerrit-instance-shorthand>]
|
|
Where <change-id> is the gerrit Change-Id of the dependent change,
|
|
<gerrit-instance> should be a part of a hostname in ALLOWED_HOSTS.
|
|
options:
|
|
host:
|
|
description:
|
|
- The hostname of the Gerrit server.
|
|
required: True
|
|
change_id:
|
|
description:
|
|
- The change-id of the Gerrit change, starting with I...
|
|
required: True
|
|
branch:
|
|
description:
|
|
- The branch of the change.
|
|
required: True
|
|
patchset_rev:
|
|
description:
|
|
- The sha hash of the patchset to be tested. Latest will be used if omitted.
|
|
required: False
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
- jenkins-deps:
|
|
host: review.openstack.org
|
|
change_id: I387b6bfd763d2d86cad68a3119b0edd0caa237b0
|
|
patchset_rev: d18f21853e2f3be7382a20d0f42232ff3a78b348
|
|
'''
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
import requests
|
|
|
|
# we ignore any other host reference
|
|
ALLOWED_HOSTS = ['review.openstack.org',
|
|
'review.gerrithub.io',
|
|
'review.rdoproject.org']
|
|
|
|
|
|
def parse_commit_msg(current_host, msg):
|
|
'''Look for dependency links in the commit message.'''
|
|
tags = []
|
|
for line in iter(msg.splitlines()):
|
|
# note: this regexp takes care of sanitizing the input
|
|
tag = re.search(r'Depends-On: *(I[0-9a-f]+)@?([0-9a-z\.\-:]*)',
|
|
line, re.IGNORECASE)
|
|
if tag:
|
|
change_id = tag.group(1)
|
|
target = tag.group(2)
|
|
if target == '':
|
|
host = current_host
|
|
else:
|
|
# match a shorthand hostname for our allowed list
|
|
for hostname in ALLOWED_HOSTS:
|
|
if target in hostname:
|
|
host = hostname
|
|
break
|
|
else:
|
|
logging.warning('Cannot resolve "%s" to a host from the '
|
|
'ALLOWED HOSTS list', target)
|
|
continue
|
|
tags.append({'host': host,
|
|
'change_id': change_id,
|
|
'branch': None,
|
|
'revision': None})
|
|
return tags
|
|
|
|
|
|
def get_details(host, change_id, branch, revision):
|
|
'''Get the details of a specific change'''
|
|
url = ''.join(['https://', host, '/changes/?q=change:', change_id])
|
|
try:
|
|
req = requests.get(url)
|
|
req.raise_for_status()
|
|
except requests.exceptions.HTTPError:
|
|
return {'fail_msg': ''.join(['warning: failed to query change '
|
|
'details from ', url])}
|
|
# strip XSSI attack prevention prefix
|
|
data = json.loads(req.text[4:])
|
|
if len(data) == 0:
|
|
return {'fail_msg': ''.join(['warning: no change found with id ',
|
|
change_id, ' at ', url])}
|
|
elif len(data) == 1:
|
|
# not filtering by branch if not necessary
|
|
full_id = data[0]['id']
|
|
else:
|
|
# there are more than one change with the same ID
|
|
try:
|
|
full_id = [change['id'] for change
|
|
in data if change['branch'] == branch][0]
|
|
except IndexError:
|
|
return {'fail_msg': ''.join(['warning: no change found with id ',
|
|
change_id, ' for branch ', branch,
|
|
' at ', url])}
|
|
url = ''.join(['https://', host, '/changes/', full_id,
|
|
'?o=ALL_REVISIONS&o=ALL_COMMITS'])
|
|
try:
|
|
req = requests.get(url)
|
|
req.raise_for_status()
|
|
except requests.exceptions.HTTPError:
|
|
return {'fail_msg': ''.join(['warning: failed to fetch details of ',
|
|
change_id, ' from ', url])}
|
|
# strip XSSI attack prevention prefix
|
|
data = json.loads(req.text[4:])
|
|
if revision is None:
|
|
revision = data['current_revision']
|
|
if revision not in data['revisions']:
|
|
return {'fail_msg': ''.join(['warning: cannot find revision ',
|
|
revision, ' of change ', change_id,
|
|
' at ', url])}
|
|
return {'host': host,
|
|
'change_id': str(data['change_id']),
|
|
'project': str(data['project']),
|
|
'branch': str(data['branch']),
|
|
'refspec': str(data['revisions'][revision]['ref']),
|
|
'commit_msg':
|
|
str(data['revisions'][revision]['commit']['message'])}
|
|
|
|
|
|
def resolve_dep(host, change_id, branch, revision):
|
|
'''Dependency resolution.
|
|
|
|
Resolve the dependencies in the target commits until there are no more
|
|
dependent changes. If the branch or revision is None, it can still resolve
|
|
the dependencies. It only uses the branch when the change_id is ambiguous
|
|
and by default uses the latest patchset's revision.
|
|
|
|
The function avoids circular dependencies and only allows one change per
|
|
project to be added to the output list.
|
|
|
|
Returns a list of dictionaries with the dependent changes.
|
|
'''
|
|
|
|
resolved_ids = []
|
|
deps = []
|
|
to_resolve = [{'host': host,
|
|
'change_id': change_id,
|
|
'branch': branch,
|
|
'revision': revision}]
|
|
output_msg = []
|
|
while len(to_resolve) > 0:
|
|
change = to_resolve.pop()
|
|
# use the original branch as default
|
|
if change['branch'] is None:
|
|
change['branch'] = branch
|
|
|
|
# avoid circular dependencies
|
|
if change['change_id'] in resolved_ids:
|
|
continue
|
|
|
|
details = get_details(**change)
|
|
if 'fail_msg' in details:
|
|
output_msg.append(details['fail_msg'])
|
|
continue
|
|
resolved_ids.append(details['change_id'])
|
|
|
|
# allow only one of each project as a dependency
|
|
if details['project'] not in (d['project'] for d in deps):
|
|
deps.append({'host': change['host'],
|
|
'project': details['project'],
|
|
'branch': details['branch'],
|
|
'refspec': details['refspec']})
|
|
else:
|
|
output_msg.append(
|
|
''.join(['warning: skipping ', change['change_id'], ' on ',
|
|
change['host'], ' because project "',
|
|
details['project'], '" is already a dependency']))
|
|
continue
|
|
new_deps = parse_commit_msg(change['host'], details['commit_msg'])
|
|
to_resolve.extend(new_deps)
|
|
if len(deps) == 0:
|
|
output_msg.append('error: failed to resolve the target change')
|
|
return {'failed': True,
|
|
'msg': ', '.join(output_msg)}
|
|
else:
|
|
return {'changed': True,
|
|
'ansible_facts': {'artg_change_list': deps},
|
|
'msg': ', '.join(output_msg)}
|
|
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec=dict(
|
|
host=dict(required=True, type='str'),
|
|
change_id=dict(required=True, type='str'),
|
|
branch=dict(required=False, default=None, type='str'),
|
|
patchset_rev=dict(required=False, default=None, type='str')
|
|
)
|
|
)
|
|
result = resolve_dep(module.params['host'],
|
|
module.params['change_id'],
|
|
module.params['branch'],
|
|
module.params['patchset_rev'])
|
|
module.exit_json(**result)
|
|
|
|
|
|
# see http://docs.ansible.com/developing_modules.html#common-module-boilerplate
|
|
from ansible.module_utils.basic import *
|
|
|
|
if __name__ == "__main__":
|
|
main()
|