1c02b2eacSCleber Rosa#!/usr/bin/env python3 2c02b2eacSCleber Rosa# 3c02b2eacSCleber Rosa# Copyright (c) 2019-2020 Red Hat, Inc. 4c02b2eacSCleber Rosa# 5c02b2eacSCleber Rosa# Author: 6c02b2eacSCleber Rosa# Cleber Rosa <crosa@redhat.com> 7c02b2eacSCleber Rosa# 8c02b2eacSCleber Rosa# This work is licensed under the terms of the GNU GPL, version 2 or 9c02b2eacSCleber Rosa# later. See the COPYING file in the top-level directory. 10c02b2eacSCleber Rosa 11c02b2eacSCleber Rosa""" 12c02b2eacSCleber RosaChecks the GitLab pipeline status for a given commit ID 13c02b2eacSCleber Rosa""" 14c02b2eacSCleber Rosa 15c02b2eacSCleber Rosa# pylint: disable=C0103 16c02b2eacSCleber Rosa 17c02b2eacSCleber Rosaimport argparse 18c02b2eacSCleber Rosaimport http.client 19c02b2eacSCleber Rosaimport json 20c02b2eacSCleber Rosaimport os 21c02b2eacSCleber Rosaimport subprocess 22c02b2eacSCleber Rosaimport time 23c02b2eacSCleber Rosaimport sys 24c02b2eacSCleber Rosa 25c02b2eacSCleber Rosa 26*176498abSCleber Rosaclass CommunicationFailure(Exception): 27*176498abSCleber Rosa """Failed to communicate to gitlab.com APIs.""" 28*176498abSCleber Rosa 29*176498abSCleber Rosa 30*176498abSCleber Rosaclass NoPipelineFound(Exception): 31*176498abSCleber Rosa """Communication is successfull but pipeline is not found.""" 32*176498abSCleber Rosa 33*176498abSCleber Rosa 34d9143750SCleber Rosadef get_local_branch_commit(branch='staging'): 35c02b2eacSCleber Rosa """ 36c02b2eacSCleber Rosa Returns the commit sha1 for the *local* branch named "staging" 37c02b2eacSCleber Rosa """ 38d9143750SCleber Rosa result = subprocess.run(['git', 'rev-parse', branch], 39c02b2eacSCleber Rosa stdin=subprocess.DEVNULL, 40c02b2eacSCleber Rosa stdout=subprocess.PIPE, 41c02b2eacSCleber Rosa stderr=subprocess.DEVNULL, 42c02b2eacSCleber Rosa cwd=os.path.dirname(__file__), 43c02b2eacSCleber Rosa universal_newlines=True).stdout.strip() 44d9143750SCleber Rosa if result == branch: 45d9143750SCleber Rosa raise ValueError("There's no local branch named '%s'" % branch) 46c02b2eacSCleber Rosa if len(result) != 40: 47d9143750SCleber Rosa raise ValueError("Branch '%s' HEAD doesn't look like a sha1" % branch) 48c02b2eacSCleber Rosa return result 49c02b2eacSCleber Rosa 50c02b2eacSCleber Rosa 51c02b2eacSCleber Rosadef get_pipeline_status(project_id, commit_sha1): 52c02b2eacSCleber Rosa """ 53c02b2eacSCleber Rosa Returns the JSON content of the pipeline status API response 54c02b2eacSCleber Rosa """ 55c02b2eacSCleber Rosa url = '/api/v4/projects/{}/pipelines?sha={}'.format(project_id, 56c02b2eacSCleber Rosa commit_sha1) 57c02b2eacSCleber Rosa connection = http.client.HTTPSConnection('gitlab.com') 58c02b2eacSCleber Rosa connection.request('GET', url=url) 59c02b2eacSCleber Rosa response = connection.getresponse() 60c02b2eacSCleber Rosa if response.code != http.HTTPStatus.OK: 61*176498abSCleber Rosa raise CommunicationFailure("Failed to receive a successful response") 62c02b2eacSCleber Rosa json_response = json.loads(response.read()) 63c02b2eacSCleber Rosa 64c02b2eacSCleber Rosa # As far as I can tell, there should be only one pipeline for the same 65c02b2eacSCleber Rosa # project + commit. If this assumption is false, we can add further 66c02b2eacSCleber Rosa # filters to the url, such as username, and order_by. 67c02b2eacSCleber Rosa if not json_response: 68*176498abSCleber Rosa raise NoPipelineFound("No pipeline found") 69c02b2eacSCleber Rosa return json_response[0] 70c02b2eacSCleber Rosa 71c02b2eacSCleber Rosa 72c02b2eacSCleber Rosadef wait_on_pipeline_success(timeout, interval, 73c02b2eacSCleber Rosa project_id, commit_sha): 74c02b2eacSCleber Rosa """ 75c02b2eacSCleber Rosa Waits for the pipeline to finish within the given timeout 76c02b2eacSCleber Rosa """ 77c02b2eacSCleber Rosa start = time.time() 78c02b2eacSCleber Rosa while True: 79c02b2eacSCleber Rosa if time.time() >= (start + timeout): 806dfcbff8SCleber Rosa msg = ("Timeout (-t/--timeout) of %i seconds reached, " 816dfcbff8SCleber Rosa "won't wait any longer for the pipeline to complete") 826dfcbff8SCleber Rosa msg %= timeout 836dfcbff8SCleber Rosa print(msg) 84c02b2eacSCleber Rosa return False 85c02b2eacSCleber Rosa 86c02b2eacSCleber Rosa status = get_pipeline_status(project_id, commit_sha) 87c02b2eacSCleber Rosa if status['status'] == 'running': 88c02b2eacSCleber Rosa print('running...') 89db5424dfSCleber Rosa time.sleep(interval) 90c02b2eacSCleber Rosa continue 91c02b2eacSCleber Rosa 92c02b2eacSCleber Rosa if status['status'] == 'success': 93c02b2eacSCleber Rosa return True 94c02b2eacSCleber Rosa 95c02b2eacSCleber Rosa msg = "Pipeline failed, check: %s" % status['web_url'] 96c02b2eacSCleber Rosa print(msg) 97c02b2eacSCleber Rosa return False 98c02b2eacSCleber Rosa 99c02b2eacSCleber Rosa 10091641d55SCleber Rosadef create_parser(): 101c02b2eacSCleber Rosa parser = argparse.ArgumentParser( 102c02b2eacSCleber Rosa prog='pipeline-status', 103c02b2eacSCleber Rosa description='check or wait on a pipeline status') 104c02b2eacSCleber Rosa 105c02b2eacSCleber Rosa parser.add_argument('-t', '--timeout', type=int, default=7200, 106c02b2eacSCleber Rosa help=('Amount of time (in seconds) to wait for the ' 107c02b2eacSCleber Rosa 'pipeline to complete. Defaults to ' 108c02b2eacSCleber Rosa '%(default)s')) 109c02b2eacSCleber Rosa parser.add_argument('-i', '--interval', type=int, default=60, 110c02b2eacSCleber Rosa help=('Amount of time (in seconds) to wait between ' 111c02b2eacSCleber Rosa 'checks of the pipeline status. Defaults ' 112c02b2eacSCleber Rosa 'to %(default)s')) 113c02b2eacSCleber Rosa parser.add_argument('-w', '--wait', action='store_true', default=False, 114c02b2eacSCleber Rosa help=('Wether to wait, instead of checking only once ' 115c02b2eacSCleber Rosa 'the status of a pipeline')) 116c02b2eacSCleber Rosa parser.add_argument('-p', '--project-id', type=int, default=11167699, 117c02b2eacSCleber Rosa help=('The GitLab project ID. Defaults to the project ' 118c02b2eacSCleber Rosa 'for https://gitlab.com/qemu-project/qemu, that ' 119c02b2eacSCleber Rosa 'is, "%(default)s"')) 120c02b2eacSCleber Rosa try: 121d9143750SCleber Rosa default_commit = get_local_branch_commit() 122c02b2eacSCleber Rosa commit_required = False 123c02b2eacSCleber Rosa except ValueError: 124c02b2eacSCleber Rosa default_commit = '' 125c02b2eacSCleber Rosa commit_required = True 126c02b2eacSCleber Rosa parser.add_argument('-c', '--commit', required=commit_required, 127c02b2eacSCleber Rosa default=default_commit, 128c02b2eacSCleber Rosa help=('Look for a pipeline associated with the given ' 129c02b2eacSCleber Rosa 'commit. If one is not explicitly given, the ' 130c02b2eacSCleber Rosa 'commit associated with the local branch named ' 131c02b2eacSCleber Rosa '"staging" is used. Default: %(default)s')) 132c02b2eacSCleber Rosa parser.add_argument('--verbose', action='store_true', default=False, 133c02b2eacSCleber Rosa help=('A minimal verbosity level that prints the ' 134c02b2eacSCleber Rosa 'overall result of the check/wait')) 13591641d55SCleber Rosa return parser 136c02b2eacSCleber Rosa 13791641d55SCleber Rosadef main(): 13891641d55SCleber Rosa """ 13991641d55SCleber Rosa Script entry point 14091641d55SCleber Rosa """ 14191641d55SCleber Rosa parser = create_parser() 142c02b2eacSCleber Rosa args = parser.parse_args() 14379df438eSCleber Rosa success = False 144c02b2eacSCleber Rosa try: 145c02b2eacSCleber Rosa if args.wait: 146c02b2eacSCleber Rosa success = wait_on_pipeline_success( 147c02b2eacSCleber Rosa args.timeout, 148c02b2eacSCleber Rosa args.interval, 149c02b2eacSCleber Rosa args.project_id, 150c02b2eacSCleber Rosa args.commit) 151c02b2eacSCleber Rosa else: 152c02b2eacSCleber Rosa status = get_pipeline_status(args.project_id, 153c02b2eacSCleber Rosa args.commit) 154c02b2eacSCleber Rosa success = status['status'] == 'success' 155c02b2eacSCleber Rosa except Exception as error: # pylint: disable=W0703 156c02b2eacSCleber Rosa if args.verbose: 157c02b2eacSCleber Rosa print("ERROR: %s" % error.args[0]) 15879df438eSCleber Rosa except KeyboardInterrupt: 15979df438eSCleber Rosa if args.verbose: 16079df438eSCleber Rosa print("Exiting on user's request") 161c02b2eacSCleber Rosa 162c02b2eacSCleber Rosa if success: 163c02b2eacSCleber Rosa if args.verbose: 164c02b2eacSCleber Rosa print('success') 165c02b2eacSCleber Rosa sys.exit(0) 166c02b2eacSCleber Rosa else: 167c02b2eacSCleber Rosa if args.verbose: 168c02b2eacSCleber Rosa print('failure') 169c02b2eacSCleber Rosa sys.exit(1) 170c02b2eacSCleber Rosa 171c02b2eacSCleber Rosa 172c02b2eacSCleber Rosaif __name__ == '__main__': 173c02b2eacSCleber Rosa main() 174