#!/usr/bin/python3 """ Creates Github Releases and uploads assets """ import argparse import logging import os import shutil import hashlib import requests import tarfile from github import Github, GithubException, UnknownObjectException FORMAT = "%(levelname)s - %(asctime)s: %(message)s" logging.basicConfig(format=FORMAT) CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared") GITHUB_CONFLICT_CODE = "already_exists" BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/' UPDATER_PREFIX = 'update' def get_sha256(filename): """ get the sha256 of a file """ sha256_hash = hashlib.sha256() with open(filename,"rb") as f: for byte_block in iter(lambda: f.read(4096),b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest() def send_hash(pkg_hash, name, version, account, namespace, api_token): """ send the checksum of a file to workers kv """ key = '{0}_{1}_{2}'.format(UPDATER_PREFIX, version, name) headers = { "Content-Type": "application/json", "Authorization": "Bearer " + api_token, } response = requests.put( BASE_KV_URL + account + "/storage/kv/namespaces/" + namespace + "/values/" + key, headers=headers, data=pkg_hash ) if response.status_code != 200: jsonResponse = response.json() errors = jsonResponse["errors"] if len(errors) > 0: raise Exception("failed to upload checksum: {0}", errors[0]) def assert_tag_exists(repo, version): """ Raise exception if repo does not contain a tag matching version """ tags = repo.get_tags() if not tags or tags[0].name != version: raise Exception("Tag {} not found".format(version)) def get_or_create_release(repo, version, dry_run=False): """ Get a Github Release matching the version tag or create a new one. If a conflict occurs on creation, attempt to fetch the Release on last time """ try: release = repo.get_release(version) logging.info("Release %s found", version) return release except UnknownObjectException: logging.info("Release %s not found", version) # We dont want to create a new release tag if one doesnt already exist assert_tag_exists(repo, version) if dry_run: logging.info("Skipping Release creation because of dry-run") return try: logging.info("Creating release %s", version) return repo.create_git_release(version, version, "") except GithubException as e: errors = e.data.get("errors", []) if e.status == 422 and any( [err.get("code") == GITHUB_CONFLICT_CODE for err in errors] ): logging.warning( "Conflict: Release was likely just made by a different build: %s", e.data, ) return repo.get_release(version) raise e def parse_args(): """ Parse and validate args """ parser = argparse.ArgumentParser( description="Creates Github Releases and uploads assets." ) parser.add_argument( "--api-key", default=os.environ.get("API_KEY"), help="Github API key" ) parser.add_argument( "--release-version", metavar="version", default=os.environ.get("VERSION"), help="Release version", ) parser.add_argument( "--path", default=os.environ.get("ASSET_PATH"), help="Asset path" ) parser.add_argument( "--name", default=os.environ.get("ASSET_NAME"), help="Asset Name" ) parser.add_argument( "--namespace-id", default=os.environ.get("KV_NAMESPACE"), help="workersKV namespace id" ) parser.add_argument( "--kv-account-id", default=os.environ.get("KV_ACCOUNT"), help="workersKV account id" ) parser.add_argument( "--kv-api-token", default=os.environ.get("KV_API_TOKEN"), help="workersKV API Token" ) parser.add_argument( "--dry-run", action="store_true", help="Do not create release or upload asset" ) args = parser.parse_args() is_valid = True if not args.release_version: logging.error("Missing release version") is_valid = False if not args.path: logging.error("Missing asset path") is_valid = False if not args.name: logging.error("Missing asset name") is_valid = False if not args.api_key: logging.error("Missing API key") is_valid = False if not args.namespace_id: logging.error("Missing KV namespace id") is_valid = False if not args.kv_account_id: logging.error("Missing KV account id") is_valid = False if not args.kv_api_token: logging.error("Missing KV API token") is_valid = False if is_valid: return args parser.print_usage() exit(1) def main(): """ Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """ try: args = parse_args() client = Github(args.api_key) repo = client.get_repo(CLOUDFLARED_REPO) release = get_or_create_release(repo, args.release_version, args.dry_run) if args.dry_run: logging.info("Skipping asset upload because of dry-run") return release.upload_asset(args.path, name=args.name) # check and extract if the file is a tar and gzipped file (as is the case with the macos builds) binary_path = args.path if binary_path.endswith("tgz"): try: shutil.rmtree('cfd') except OSError as e: pass zipfile = tarfile.open(binary_path, "r:gz") zipfile.extractall('cfd') # specify which folder to extract to zipfile.close() binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared') # send the sha256 (the checksum) to workers kv pkg_hash = get_sha256(binary_path) send_hash(pkg_hash, args.name, args.release_version, args.kv_account_id, args.namespace_id, args.kv_api_token) # create the artifacts directory if it doesn't exist artifact_path = os.path.join(os.getcwd(), 'artifacts') if not os.path.isdir(artifact_path): os.mkdir(artifact_path) # copy the binary to the path copy_path = os.path.join(artifact_path, args.name) try: shutil.copy(args.path, copy_path) except shutil.SameFileError: pass # the macOS release copy fails with being the same file (already in the artifacts directory). Catching to ignore. except Exception as e: logging.exception(e) exit(1) main()