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 26176498abSCleber Rosaclass CommunicationFailure(Exception): 27176498abSCleber Rosa """Failed to communicate to gitlab.com APIs.""" 28176498abSCleber Rosa 29176498abSCleber Rosa 30176498abSCleber Rosaclass NoPipelineFound(Exception): 31176498abSCleber Rosa """Communication is successfull but pipeline is not found.""" 32176498abSCleber Rosa 33176498abSCleber Rosa 34e4b937d3SAlex Bennéedef get_local_branch_commit(branch): 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 512faf56bdSCleber Rosadef get_json_http_response(url): 522faf56bdSCleber Rosa """ 532faf56bdSCleber Rosa Returns the JSON content of an HTTP GET request to gitlab.com 542faf56bdSCleber Rosa """ 552faf56bdSCleber Rosa connection = http.client.HTTPSConnection('gitlab.com') 562faf56bdSCleber Rosa connection.request('GET', url=url) 572faf56bdSCleber Rosa response = connection.getresponse() 582faf56bdSCleber Rosa if response.code != http.HTTPStatus.OK: 59*861d1d50SCleber Rosa msg = "Received unsuccessful response: %s (%s)" % (response.code, 60*861d1d50SCleber Rosa response.reason) 61*861d1d50SCleber Rosa raise CommunicationFailure(msg) 622faf56bdSCleber Rosa return json.loads(response.read()) 632faf56bdSCleber Rosa 642faf56bdSCleber Rosa 65c02b2eacSCleber Rosadef get_pipeline_status(project_id, commit_sha1): 66c02b2eacSCleber Rosa """ 67c02b2eacSCleber Rosa Returns the JSON content of the pipeline status API response 68c02b2eacSCleber Rosa """ 69c02b2eacSCleber Rosa url = '/api/v4/projects/{}/pipelines?sha={}'.format(project_id, 70c02b2eacSCleber Rosa commit_sha1) 712faf56bdSCleber Rosa json_response = get_json_http_response(url) 72c02b2eacSCleber Rosa 73c02b2eacSCleber Rosa # As far as I can tell, there should be only one pipeline for the same 74c02b2eacSCleber Rosa # project + commit. If this assumption is false, we can add further 75c02b2eacSCleber Rosa # filters to the url, such as username, and order_by. 76c02b2eacSCleber Rosa if not json_response: 77176498abSCleber Rosa raise NoPipelineFound("No pipeline found") 78c02b2eacSCleber Rosa return json_response[0] 79c02b2eacSCleber Rosa 80c02b2eacSCleber Rosa 81c02b2eacSCleber Rosadef wait_on_pipeline_success(timeout, interval, 82c02b2eacSCleber Rosa project_id, commit_sha): 83c02b2eacSCleber Rosa """ 84c02b2eacSCleber Rosa Waits for the pipeline to finish within the given timeout 85c02b2eacSCleber Rosa """ 86c02b2eacSCleber Rosa start = time.time() 87c02b2eacSCleber Rosa while True: 88c02b2eacSCleber Rosa if time.time() >= (start + timeout): 896dfcbff8SCleber Rosa msg = ("Timeout (-t/--timeout) of %i seconds reached, " 906dfcbff8SCleber Rosa "won't wait any longer for the pipeline to complete") 916dfcbff8SCleber Rosa msg %= timeout 926dfcbff8SCleber Rosa print(msg) 93c02b2eacSCleber Rosa return False 94c02b2eacSCleber Rosa 95ea8bf1e5SCleber Rosa try: 96c02b2eacSCleber Rosa status = get_pipeline_status(project_id, commit_sha) 97ea8bf1e5SCleber Rosa except NoPipelineFound: 98ea8bf1e5SCleber Rosa print('Pipeline has not been found, it may not have been created yet.') 99ea8bf1e5SCleber Rosa time.sleep(1) 100ea8bf1e5SCleber Rosa continue 101ea8bf1e5SCleber Rosa 102ea8bf1e5SCleber Rosa pipeline_status = status['status'] 103ea8bf1e5SCleber Rosa status_to_wait = ('created', 'waiting_for_resource', 'preparing', 104ea8bf1e5SCleber Rosa 'pending', 'running') 105ea8bf1e5SCleber Rosa if pipeline_status in status_to_wait: 106ea8bf1e5SCleber Rosa print('%s...' % pipeline_status) 107db5424dfSCleber Rosa time.sleep(interval) 108c02b2eacSCleber Rosa continue 109c02b2eacSCleber Rosa 110ea8bf1e5SCleber Rosa if pipeline_status == 'success': 111c02b2eacSCleber Rosa return True 112c02b2eacSCleber Rosa 113c02b2eacSCleber Rosa msg = "Pipeline failed, check: %s" % status['web_url'] 114c02b2eacSCleber Rosa print(msg) 115c02b2eacSCleber Rosa return False 116c02b2eacSCleber Rosa 117c02b2eacSCleber Rosa 11891641d55SCleber Rosadef create_parser(): 119c02b2eacSCleber Rosa parser = argparse.ArgumentParser( 120c02b2eacSCleber Rosa prog='pipeline-status', 121c02b2eacSCleber Rosa description='check or wait on a pipeline status') 122c02b2eacSCleber Rosa 123c02b2eacSCleber Rosa parser.add_argument('-t', '--timeout', type=int, default=7200, 124c02b2eacSCleber Rosa help=('Amount of time (in seconds) to wait for the ' 125c02b2eacSCleber Rosa 'pipeline to complete. Defaults to ' 126c02b2eacSCleber Rosa '%(default)s')) 127c02b2eacSCleber Rosa parser.add_argument('-i', '--interval', type=int, default=60, 128c02b2eacSCleber Rosa help=('Amount of time (in seconds) to wait between ' 129c02b2eacSCleber Rosa 'checks of the pipeline status. Defaults ' 130c02b2eacSCleber Rosa 'to %(default)s')) 131c02b2eacSCleber Rosa parser.add_argument('-w', '--wait', action='store_true', default=False, 132c02b2eacSCleber Rosa help=('Wether to wait, instead of checking only once ' 133c02b2eacSCleber Rosa 'the status of a pipeline')) 134c02b2eacSCleber Rosa parser.add_argument('-p', '--project-id', type=int, default=11167699, 135c02b2eacSCleber Rosa help=('The GitLab project ID. Defaults to the project ' 136c02b2eacSCleber Rosa 'for https://gitlab.com/qemu-project/qemu, that ' 137c02b2eacSCleber Rosa 'is, "%(default)s"')) 138e4b937d3SAlex Bennée parser.add_argument('-b', '--branch', type=str, default="staging", 139e4b937d3SAlex Bennée help=('Specify the branch to check. ' 140e4b937d3SAlex Bennée 'Use HEAD for your current branch. ' 141e4b937d3SAlex Bennée 'Otherwise looks at "%(default)s"')) 142e4b937d3SAlex Bennée parser.add_argument('-c', '--commit', 143e4b937d3SAlex Bennée default=None, 144c02b2eacSCleber Rosa help=('Look for a pipeline associated with the given ' 145c02b2eacSCleber Rosa 'commit. If one is not explicitly given, the ' 146e4b937d3SAlex Bennée 'commit associated with the default branch ' 147e4b937d3SAlex Bennée 'is used.')) 148c02b2eacSCleber Rosa parser.add_argument('--verbose', action='store_true', default=False, 149c02b2eacSCleber Rosa help=('A minimal verbosity level that prints the ' 150c02b2eacSCleber Rosa 'overall result of the check/wait')) 15191641d55SCleber Rosa return parser 152c02b2eacSCleber Rosa 15391641d55SCleber Rosadef main(): 15491641d55SCleber Rosa """ 15591641d55SCleber Rosa Script entry point 15691641d55SCleber Rosa """ 15791641d55SCleber Rosa parser = create_parser() 158c02b2eacSCleber Rosa args = parser.parse_args() 159e4b937d3SAlex Bennée 160e4b937d3SAlex Bennée if not args.commit: 161e4b937d3SAlex Bennée args.commit = get_local_branch_commit(args.branch) 162e4b937d3SAlex Bennée 16379df438eSCleber Rosa success = False 164c02b2eacSCleber Rosa try: 165c02b2eacSCleber Rosa if args.wait: 166c02b2eacSCleber Rosa success = wait_on_pipeline_success( 167c02b2eacSCleber Rosa args.timeout, 168c02b2eacSCleber Rosa args.interval, 169c02b2eacSCleber Rosa args.project_id, 170c02b2eacSCleber Rosa args.commit) 171c02b2eacSCleber Rosa else: 172c02b2eacSCleber Rosa status = get_pipeline_status(args.project_id, 173c02b2eacSCleber Rosa args.commit) 174c02b2eacSCleber Rosa success = status['status'] == 'success' 175c02b2eacSCleber Rosa except Exception as error: # pylint: disable=W0703 176c02b2eacSCleber Rosa if args.verbose: 177c02b2eacSCleber Rosa print("ERROR: %s" % error.args[0]) 17879df438eSCleber Rosa except KeyboardInterrupt: 17979df438eSCleber Rosa if args.verbose: 18079df438eSCleber Rosa print("Exiting on user's request") 181c02b2eacSCleber Rosa 182c02b2eacSCleber Rosa if success: 183c02b2eacSCleber Rosa if args.verbose: 184c02b2eacSCleber Rosa print('success') 185c02b2eacSCleber Rosa sys.exit(0) 186c02b2eacSCleber Rosa else: 187c02b2eacSCleber Rosa if args.verbose: 188c02b2eacSCleber Rosa print('failure') 189c02b2eacSCleber Rosa sys.exit(1) 190c02b2eacSCleber Rosa 191c02b2eacSCleber Rosa 192c02b2eacSCleber Rosaif __name__ == '__main__': 193c02b2eacSCleber Rosa main() 194