2
0

gitlab-pipeline-status 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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 successfull but pipeline is not found."""
  25. def get_local_branch_commit(branch='staging'):
  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_pipeline_status(project_id, commit_sha1):
  41. """
  42. Returns the JSON content of the pipeline status API response
  43. """
  44. url = '/api/v4/projects/{}/pipelines?sha={}'.format(project_id,
  45. commit_sha1)
  46. connection = http.client.HTTPSConnection('gitlab.com')
  47. connection.request('GET', url=url)
  48. response = connection.getresponse()
  49. if response.code != http.HTTPStatus.OK:
  50. raise CommunicationFailure("Failed to receive a successful response")
  51. json_response = json.loads(response.read())
  52. # As far as I can tell, there should be only one pipeline for the same
  53. # project + commit. If this assumption is false, we can add further
  54. # filters to the url, such as username, and order_by.
  55. if not json_response:
  56. raise NoPipelineFound("No pipeline found")
  57. return json_response[0]
  58. def wait_on_pipeline_success(timeout, interval,
  59. project_id, commit_sha):
  60. """
  61. Waits for the pipeline to finish within the given timeout
  62. """
  63. start = time.time()
  64. while True:
  65. if time.time() >= (start + timeout):
  66. msg = ("Timeout (-t/--timeout) of %i seconds reached, "
  67. "won't wait any longer for the pipeline to complete")
  68. msg %= timeout
  69. print(msg)
  70. return False
  71. try:
  72. status = get_pipeline_status(project_id, commit_sha)
  73. except NoPipelineFound:
  74. print('Pipeline has not been found, it may not have been created yet.')
  75. time.sleep(1)
  76. continue
  77. pipeline_status = status['status']
  78. status_to_wait = ('created', 'waiting_for_resource', 'preparing',
  79. 'pending', 'running')
  80. if pipeline_status in status_to_wait:
  81. print('%s...' % pipeline_status)
  82. time.sleep(interval)
  83. continue
  84. if pipeline_status == 'success':
  85. return True
  86. msg = "Pipeline failed, check: %s" % status['web_url']
  87. print(msg)
  88. return False
  89. def create_parser():
  90. parser = argparse.ArgumentParser(
  91. prog='pipeline-status',
  92. description='check or wait on a pipeline status')
  93. parser.add_argument('-t', '--timeout', type=int, default=7200,
  94. help=('Amount of time (in seconds) to wait for the '
  95. 'pipeline to complete. Defaults to '
  96. '%(default)s'))
  97. parser.add_argument('-i', '--interval', type=int, default=60,
  98. help=('Amount of time (in seconds) to wait between '
  99. 'checks of the pipeline status. Defaults '
  100. 'to %(default)s'))
  101. parser.add_argument('-w', '--wait', action='store_true', default=False,
  102. help=('Wether to wait, instead of checking only once '
  103. 'the status of a pipeline'))
  104. parser.add_argument('-p', '--project-id', type=int, default=11167699,
  105. help=('The GitLab project ID. Defaults to the project '
  106. 'for https://gitlab.com/qemu-project/qemu, that '
  107. 'is, "%(default)s"'))
  108. try:
  109. default_commit = get_local_branch_commit()
  110. commit_required = False
  111. except ValueError:
  112. default_commit = ''
  113. commit_required = True
  114. parser.add_argument('-c', '--commit', required=commit_required,
  115. default=default_commit,
  116. help=('Look for a pipeline associated with the given '
  117. 'commit. If one is not explicitly given, the '
  118. 'commit associated with the local branch named '
  119. '"staging" is used. Default: %(default)s'))
  120. parser.add_argument('--verbose', action='store_true', default=False,
  121. help=('A minimal verbosity level that prints the '
  122. 'overall result of the check/wait'))
  123. return parser
  124. def main():
  125. """
  126. Script entry point
  127. """
  128. parser = create_parser()
  129. args = parser.parse_args()
  130. success = False
  131. try:
  132. if args.wait:
  133. success = wait_on_pipeline_success(
  134. args.timeout,
  135. args.interval,
  136. args.project_id,
  137. args.commit)
  138. else:
  139. status = get_pipeline_status(args.project_id,
  140. args.commit)
  141. success = status['status'] == 'success'
  142. except Exception as error: # pylint: disable=W0703
  143. if args.verbose:
  144. print("ERROR: %s" % error.args[0])
  145. except KeyboardInterrupt:
  146. if args.verbose:
  147. print("Exiting on user's request")
  148. if success:
  149. if args.verbose:
  150. print('success')
  151. sys.exit(0)
  152. else:
  153. if args.verbose:
  154. print('failure')
  155. sys.exit(1)
  156. if __name__ == '__main__':
  157. main()