github_release.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. #!/usr/bin/python3
  2. """
  3. Creates Github Releases and uploads assets
  4. """
  5. import argparse
  6. import logging
  7. import os
  8. import shutil
  9. import hashlib
  10. import requests
  11. import tarfile
  12. from github import Github, GithubException, UnknownObjectException
  13. FORMAT = "%(levelname)s - %(asctime)s: %(message)s"
  14. logging.basicConfig(format=FORMAT)
  15. CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
  16. GITHUB_CONFLICT_CODE = "already_exists"
  17. BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/'
  18. UPDATER_PREFIX = 'update'
  19. def get_sha256(filename):
  20. """ get the sha256 of a file """
  21. sha256_hash = hashlib.sha256()
  22. with open(filename,"rb") as f:
  23. for byte_block in iter(lambda: f.read(4096),b""):
  24. sha256_hash.update(byte_block)
  25. return sha256_hash.hexdigest()
  26. def send_hash(pkg_hash, name, version, account, namespace, api_token):
  27. """ send the checksum of a file to workers kv """
  28. key = '{0}_{1}_{2}'.format(UPDATER_PREFIX, version, name)
  29. headers = {
  30. "Content-Type": "application/json",
  31. "Authorization": "Bearer " + api_token,
  32. }
  33. response = requests.put(
  34. BASE_KV_URL + account + "/storage/kv/namespaces/" + namespace + "/values/" + key,
  35. headers=headers,
  36. data=pkg_hash
  37. )
  38. if response.status_code != 200:
  39. jsonResponse = response.json()
  40. errors = jsonResponse["errors"]
  41. if len(errors) > 0:
  42. raise Exception("failed to upload checksum: {0}", errors[0])
  43. def assert_tag_exists(repo, version):
  44. """ Raise exception if repo does not contain a tag matching version """
  45. tags = repo.get_tags()
  46. if not tags or tags[0].name != version:
  47. raise Exception("Tag {} not found".format(version))
  48. def get_or_create_release(repo, version, dry_run=False):
  49. """
  50. Get a Github Release matching the version tag or create a new one.
  51. If a conflict occurs on creation, attempt to fetch the Release on last time
  52. """
  53. try:
  54. release = repo.get_release(version)
  55. logging.info("Release %s found", version)
  56. return release
  57. except UnknownObjectException:
  58. logging.info("Release %s not found", version)
  59. # We dont want to create a new release tag if one doesnt already exist
  60. assert_tag_exists(repo, version)
  61. if dry_run:
  62. logging.info("Skipping Release creation because of dry-run")
  63. return
  64. try:
  65. logging.info("Creating release %s", version)
  66. return repo.create_git_release(version, version, "")
  67. except GithubException as e:
  68. errors = e.data.get("errors", [])
  69. if e.status == 422 and any(
  70. [err.get("code") == GITHUB_CONFLICT_CODE for err in errors]
  71. ):
  72. logging.warning(
  73. "Conflict: Release was likely just made by a different build: %s",
  74. e.data,
  75. )
  76. return repo.get_release(version)
  77. raise e
  78. def parse_args():
  79. """ Parse and validate args """
  80. parser = argparse.ArgumentParser(
  81. description="Creates Github Releases and uploads assets."
  82. )
  83. parser.add_argument(
  84. "--api-key", default=os.environ.get("API_KEY"), help="Github API key"
  85. )
  86. parser.add_argument(
  87. "--release-version",
  88. metavar="version",
  89. default=os.environ.get("VERSION"),
  90. help="Release version",
  91. )
  92. parser.add_argument(
  93. "--path", default=os.environ.get("ASSET_PATH"), help="Asset path"
  94. )
  95. parser.add_argument(
  96. "--name", default=os.environ.get("ASSET_NAME"), help="Asset Name"
  97. )
  98. parser.add_argument(
  99. "--namespace-id", default=os.environ.get("KV_NAMESPACE"), help="workersKV namespace id"
  100. )
  101. parser.add_argument(
  102. "--kv-account-id", default=os.environ.get("KV_ACCOUNT"), help="workersKV account id"
  103. )
  104. parser.add_argument(
  105. "--kv-api-token", default=os.environ.get("KV_API_TOKEN"), help="workersKV API Token"
  106. )
  107. parser.add_argument(
  108. "--dry-run", action="store_true", help="Do not create release or upload asset"
  109. )
  110. args = parser.parse_args()
  111. is_valid = True
  112. if not args.release_version:
  113. logging.error("Missing release version")
  114. is_valid = False
  115. if not args.path:
  116. logging.error("Missing asset path")
  117. is_valid = False
  118. if not args.name:
  119. logging.error("Missing asset name")
  120. is_valid = False
  121. if not args.api_key:
  122. logging.error("Missing API key")
  123. is_valid = False
  124. if not args.namespace_id:
  125. logging.error("Missing KV namespace id")
  126. is_valid = False
  127. if not args.kv_account_id:
  128. logging.error("Missing KV account id")
  129. is_valid = False
  130. if not args.kv_api_token:
  131. logging.error("Missing KV API token")
  132. is_valid = False
  133. if is_valid:
  134. return args
  135. parser.print_usage()
  136. exit(1)
  137. def main():
  138. """ Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
  139. try:
  140. args = parse_args()
  141. client = Github(args.api_key)
  142. repo = client.get_repo(CLOUDFLARED_REPO)
  143. release = get_or_create_release(repo, args.release_version, args.dry_run)
  144. if args.dry_run:
  145. logging.info("Skipping asset upload because of dry-run")
  146. return
  147. release.upload_asset(args.path, name=args.name)
  148. # check and extract if the file is a tar and gzipped file (as is the case with the macos builds)
  149. binary_path = args.path
  150. if binary_path.endswith("tgz"):
  151. try:
  152. shutil.rmtree('cfd')
  153. except OSError as e:
  154. pass
  155. zipfile = tarfile.open(binary_path, "r:gz")
  156. zipfile.extractall('cfd') # specify which folder to extract to
  157. zipfile.close()
  158. binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared')
  159. # send the sha256 (the checksum) to workers kv
  160. pkg_hash = get_sha256(binary_path)
  161. send_hash(pkg_hash, args.name, args.release_version, args.kv_account_id, args.namespace_id, args.kv_api_token)
  162. # create the artifacts directory if it doesn't exist
  163. artifact_path = os.path.join(os.getcwd(), 'artifacts')
  164. if not os.path.isdir(artifact_path):
  165. os.mkdir(artifact_path)
  166. # copy the binary to the path
  167. copy_path = os.path.join(artifact_path, args.name)
  168. try:
  169. shutil.copy(args.path, copy_path)
  170. except shutil.SameFileError:
  171. pass # the macOS release copy fails with being the same file (already in the artifacts directory). Catching to ignore.
  172. except Exception as e:
  173. logging.exception(e)
  174. exit(1)
  175. main()