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):
31d30b5bc9SMichael Tokarev    """Communication is successful 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:
59861d1d50SCleber Rosa        msg = "Received unsuccessful response: %s (%s)" % (response.code,
60861d1d50SCleber Rosa                                                           response.reason)
61861d1d50SCleber 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:
776179f32eSCleber Rosa        msg = "No pipeline found for project %s and commit %s" % (project_id,
786179f32eSCleber Rosa                                                                  commit_sha1)
796179f32eSCleber Rosa        raise NoPipelineFound(msg)
80c02b2eacSCleber Rosa    return json_response[0]
81c02b2eacSCleber Rosa
82c02b2eacSCleber Rosa
83c02b2eacSCleber Rosadef wait_on_pipeline_success(timeout, interval,
84c02b2eacSCleber Rosa                             project_id, commit_sha):
85c02b2eacSCleber Rosa    """
86c02b2eacSCleber Rosa    Waits for the pipeline to finish within the given timeout
87c02b2eacSCleber Rosa    """
88c02b2eacSCleber Rosa    start = time.time()
89c02b2eacSCleber Rosa    while True:
90c02b2eacSCleber Rosa        if time.time() >= (start + timeout):
916dfcbff8SCleber Rosa            msg = ("Timeout (-t/--timeout) of %i seconds reached, "
926dfcbff8SCleber Rosa                   "won't wait any longer for the pipeline to complete")
936dfcbff8SCleber Rosa            msg %= timeout
946dfcbff8SCleber Rosa            print(msg)
95c02b2eacSCleber Rosa            return False
96c02b2eacSCleber Rosa
97ea8bf1e5SCleber Rosa        try:
98c02b2eacSCleber Rosa            status = get_pipeline_status(project_id, commit_sha)
99ea8bf1e5SCleber Rosa        except NoPipelineFound:
100ea8bf1e5SCleber Rosa            print('Pipeline has not been found, it may not have been created yet.')
101ea8bf1e5SCleber Rosa            time.sleep(1)
102ea8bf1e5SCleber Rosa            continue
103ea8bf1e5SCleber Rosa
104ea8bf1e5SCleber Rosa        pipeline_status = status['status']
105ea8bf1e5SCleber Rosa        status_to_wait = ('created', 'waiting_for_resource', 'preparing',
106ea8bf1e5SCleber Rosa                          'pending', 'running')
107ea8bf1e5SCleber Rosa        if pipeline_status in status_to_wait:
108ea8bf1e5SCleber Rosa            print('%s...' % pipeline_status)
109db5424dfSCleber Rosa            time.sleep(interval)
110c02b2eacSCleber Rosa            continue
111c02b2eacSCleber Rosa
112ea8bf1e5SCleber Rosa        if pipeline_status == 'success':
113c02b2eacSCleber Rosa            return True
114c02b2eacSCleber Rosa
115c02b2eacSCleber Rosa        msg = "Pipeline failed, check: %s" % status['web_url']
116c02b2eacSCleber Rosa        print(msg)
117c02b2eacSCleber Rosa        return False
118c02b2eacSCleber Rosa
119c02b2eacSCleber Rosa
12091641d55SCleber Rosadef create_parser():
121c02b2eacSCleber Rosa    parser = argparse.ArgumentParser(
122c02b2eacSCleber Rosa        prog='pipeline-status',
123c02b2eacSCleber Rosa        description='check or wait on a pipeline status')
124c02b2eacSCleber Rosa
125c02b2eacSCleber Rosa    parser.add_argument('-t', '--timeout', type=int, default=7200,
126c02b2eacSCleber Rosa                        help=('Amount of time (in seconds) to wait for the '
127c02b2eacSCleber Rosa                              'pipeline to complete.  Defaults to '
128c02b2eacSCleber Rosa                              '%(default)s'))
129c02b2eacSCleber Rosa    parser.add_argument('-i', '--interval', type=int, default=60,
130c02b2eacSCleber Rosa                        help=('Amount of time (in seconds) to wait between '
131c02b2eacSCleber Rosa                              'checks of the pipeline status.  Defaults '
132c02b2eacSCleber Rosa                              'to %(default)s'))
133c02b2eacSCleber Rosa    parser.add_argument('-w', '--wait', action='store_true', default=False,
134*a062f7f9SManos Pitsidianakis                        help=('Whether to wait, instead of checking only once '
135c02b2eacSCleber Rosa                              'the status of a pipeline'))
136c02b2eacSCleber Rosa    parser.add_argument('-p', '--project-id', type=int, default=11167699,
137c02b2eacSCleber Rosa                        help=('The GitLab project ID. Defaults to the project '
138c02b2eacSCleber Rosa                              'for https://gitlab.com/qemu-project/qemu, that '
139c02b2eacSCleber Rosa                              'is, "%(default)s"'))
140e4b937d3SAlex Bennée    parser.add_argument('-b', '--branch', type=str, default="staging",
141e4b937d3SAlex Bennée                        help=('Specify the branch to check. '
142e4b937d3SAlex Bennée                              'Use HEAD for your current branch. '
143e4b937d3SAlex Bennée                              'Otherwise looks at "%(default)s"'))
144e4b937d3SAlex Bennée    parser.add_argument('-c', '--commit',
145e4b937d3SAlex Bennée                        default=None,
146c02b2eacSCleber Rosa                        help=('Look for a pipeline associated with the given '
147c02b2eacSCleber Rosa                              'commit.  If one is not explicitly given, the '
148e4b937d3SAlex Bennée                              'commit associated with the default branch '
149e4b937d3SAlex Bennée                              'is used.'))
150c02b2eacSCleber Rosa    parser.add_argument('--verbose', action='store_true', default=False,
151c02b2eacSCleber Rosa                        help=('A minimal verbosity level that prints the '
152c02b2eacSCleber Rosa                              'overall result of the check/wait'))
15391641d55SCleber Rosa    return parser
154c02b2eacSCleber Rosa
15591641d55SCleber Rosadef main():
15691641d55SCleber Rosa    """
15791641d55SCleber Rosa    Script entry point
15891641d55SCleber Rosa    """
15991641d55SCleber Rosa    parser = create_parser()
160c02b2eacSCleber Rosa    args = parser.parse_args()
161e4b937d3SAlex Bennée
162e4b937d3SAlex Bennée    if not args.commit:
163e4b937d3SAlex Bennée        args.commit = get_local_branch_commit(args.branch)
164e4b937d3SAlex Bennée
16579df438eSCleber Rosa    success = False
166c02b2eacSCleber Rosa    try:
167c02b2eacSCleber Rosa        if args.wait:
168c02b2eacSCleber Rosa            success = wait_on_pipeline_success(
169c02b2eacSCleber Rosa                args.timeout,
170c02b2eacSCleber Rosa                args.interval,
171c02b2eacSCleber Rosa                args.project_id,
172c02b2eacSCleber Rosa                args.commit)
173c02b2eacSCleber Rosa        else:
174c02b2eacSCleber Rosa            status = get_pipeline_status(args.project_id,
175c02b2eacSCleber Rosa                                         args.commit)
176c02b2eacSCleber Rosa            success = status['status'] == 'success'
177c02b2eacSCleber Rosa    except Exception as error:      # pylint: disable=W0703
178c02b2eacSCleber Rosa        if args.verbose:
179c02b2eacSCleber Rosa            print("ERROR: %s" % error.args[0])
18079df438eSCleber Rosa    except KeyboardInterrupt:
18179df438eSCleber Rosa        if args.verbose:
18279df438eSCleber Rosa            print("Exiting on user's request")
183c02b2eacSCleber Rosa
184c02b2eacSCleber Rosa    if success:
185c02b2eacSCleber Rosa        if args.verbose:
186c02b2eacSCleber Rosa            print('success')
187c02b2eacSCleber Rosa        sys.exit(0)
188c02b2eacSCleber Rosa    else:
189c02b2eacSCleber Rosa        if args.verbose:
190c02b2eacSCleber Rosa            print('failure')
191c02b2eacSCleber Rosa        sys.exit(1)
192c02b2eacSCleber Rosa
193c02b2eacSCleber Rosa
194c02b2eacSCleber Rosaif __name__ == '__main__':
195c02b2eacSCleber Rosa    main()
196