diff --git a/doc/source/reference/using.rst b/doc/source/reference/using.rst index b2aaf24314..5f43dfa893 100644 --- a/doc/source/reference/using.rst +++ b/doc/source/reference/using.rst @@ -608,6 +608,18 @@ To set the pre-release group membership: tox -e aclmanager -- groups pre_release ttx +tools/check_approval.py +----------------------- + +A script to test that release requests have been approved by a team +liaison. + +Example: + +:: + + tox -e check_approval -- 695375 + tools/membership_freeze_test.py -------------------------------- diff --git a/tools/check_approval.py b/tools/check_approval.py new file mode 100755 index 0000000000..39d66a475c --- /dev/null +++ b/tools/check_approval.py @@ -0,0 +1,145 @@ +#!/usr/bin/python3 +# +# Check PTL/liaison has approved release +# +# Copyright 2019 Thierry Carrez +# All Rights Reserved. +# +# 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 argparse +import json +import logging +import os +import sys + +from openstack_governance import governance +import requests +from requests.packages import urllib3 +import yaml + + +GERRIT_URL = 'https://review.opendev.org/' +LOG = logging.getLogger(__name__) + +# Turn of warnings about bad SSL config. +# https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings +urllib3.disable_warnings() + + +def get_team(deliverablefile): + with open(deliverablefile, 'r') as dfile: + team = yaml.safe_load(dfile)['team'] + return team + + +def get_liaisons(team): + with open('data/release_liaisons.yaml', 'r') as lfile: + liaisons = yaml.safe_load(lfile) + if team in liaisons: + return [i['email'] for i in liaisons[team]] + else: + print('WARNING: %s team does not exist in liaisons file' % team) + return [] + + +class GerritChange(object): + + def __init__(self, gov_data, changeid): + # Load governance data + self.gov_data = governance.Governance.from_remote_repo() + + # Grab changeid details from Gerrit + call = 'changes/%s' % changeid + \ + '?o=CURRENT_REVISION&o=CURRENT_FILES&o=DETAILED_LABELS' + \ + '&o=DETAILED_ACCOUNTS' + raw = requests.get(GERRIT_URL + call) + + # Gerrit's REST API prepends a JSON-breaker to avoid XSS + if raw.text.startswith(")]}'"): + trimmed = raw.text[4:] + else: + trimmed = raw.text + + # Try to decode and bail with much detail if it fails + try: + decoded = json.loads(trimmed) + except Exception: + LOG.error( + '\nrequest returned %s error to query:\n\n %s\n' + '\nwith detail:\n\n %s\n', + raw, raw.url, trimmed) + raise + + # Instantiate object with retrieved data + self.raw_data = decoded + self.approvers = [i['email'] + for i in decoded['labels']['Code-Review']['all'] + if i['value'] > 0] + self.approvers.append(decoded['owner']['email']) + currev = decoded['current_revision'] + self.deliv_files = list(decoded['revisions'][currev]['files'].keys()) + + def is_approved(self): + LOG.debug('Approvals: %s' % self.approvers) + approved = True + for deliv_file in self.deliv_files: + team = get_team(deliv_file) + try: + govteam = self.gov_data.get_team(team) + except ValueError: + print('✕ %s mentions unknown team %s' % (deliv_file, team)) + approved = False + break + + # Check that deliverable is indeed defined in governance team + delivname, _ = os.path.splitext(os.path.basename(deliv_file)) + if delivname not in govteam.deliverables: + print('✕ %s not in %s governance' % (deliv_file, team)) + approved = False + break + + # Fetch PTL and release liaisons + liaisons = get_liaisons(team) + if 'email' in govteam.ptl: + liaisons.append(govteam.ptl['email']) + LOG.debug('%s needs %s' % (deliv_file, liaisons)) + + for approver in self.approvers: + if approver in liaisons: + print('✓ %s validated by %s' % (deliv_file, approver)) + break + else: + print('✕ %s missing PTL/liaison approval' % deliv_file) + approved = False + return approved + + +def main(args=sys.argv[1:]): + parser = argparse.ArgumentParser() + parser.add_argument('changeid') + parser.add_argument("--debug", action='store_true') + args = parser.parse_args(args) + + if (args.debug): + logging.basicConfig(level=logging.DEBUG) + + gov_data = governance.Governance.from_remote_repo() + change = GerritChange(gov_data, args.changeid) + + if not change.is_approved(): + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/tox.ini b/tox.ini index ad841bb6ab..53a6566393 100644 --- a/tox.ini +++ b/tox.ini @@ -58,6 +58,9 @@ commands = bash -c "find {toxinidir} \ [testenv:aclmanager] commands = python {toxinidir}/tools/aclmanager.py {posargs} +[testenv:check_approval] +commands = python {toxinidir}/tools/check_approval.py {posargs} + [testenv:membership_freeze_test] commands = python {toxinidir}/tools/membership_freeze_test.py {posargs}