2
0

gitlab-pipeline-status 6.7 KB

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