|
| 1 | +#!/usr/bin/env python |
| 2 | +# Copyright (C) 2022 Vaticle |
| 3 | +# |
| 4 | +# This program is free software: you can redistribute it and/or modify |
| 5 | +# it under the terms of the GNU Affero General Public License as |
| 6 | +# published by the Free Software Foundation, either version 3 of the |
| 7 | +# License, or (at your option) any later version. |
| 8 | +# |
| 9 | +# This program is distributed in the hope that it will be useful, |
| 10 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | +# GNU Affero General Public License for more details. |
| 13 | +# |
| 14 | +# You should have received a copy of the GNU Affero General Public License |
| 15 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 16 | +# |
| 17 | + |
| 18 | +""" |
| 19 | +sync-dependencies.py updates bazel dependencies between @vaticle repositories |
| 20 | +
|
| 21 | +Example usage: |
| 22 | +bazel run @vaticle_dependencies//tool/sync:dependencies -- --source typedb-driver@1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4g |
| 23 | +""" |
| 24 | + |
| 25 | +import argparse |
| 26 | +import tool.common.common as tc |
| 27 | +import github |
| 28 | +import hashlib |
| 29 | +import hmac |
| 30 | +import json |
| 31 | +import os |
| 32 | +import re |
| 33 | +import subprocess as sp |
| 34 | +import sys |
| 35 | + |
| 36 | +IS_CIRCLECI = bool(os.getenv('CIRCLECI')) |
| 37 | +IS_FACTORY = bool(os.getenv('FACTORY_REPO')) |
| 38 | +IS_CI_ENV = IS_CIRCLECI or IS_FACTORY |
| 39 | + |
| 40 | +GITHUB_TOKEN = os.getenv('SYNC_DEPENDENCIES_TOKEN') |
| 41 | +if GITHUB_TOKEN is None: |
| 42 | + raise Exception("$SYNC_DEPENDENCIES_TOKEN is not set!") |
| 43 | + |
| 44 | +BOT_HOST = 'https://bot.vaticle.com' |
| 45 | +if not IS_CI_ENV: |
| 46 | + BOT_HOST = 'http://localhost:8000' |
| 47 | + |
| 48 | +BOT_SYNC_DEPS = '{0}/sync/dependencies'.format(BOT_HOST) |
| 49 | + |
| 50 | +CMDLINE_PARSER = argparse.ArgumentParser(description='Automatic updater for Vaticle inter-repository dependencies') |
| 51 | +CMDLINE_PARSER.add_argument('--dry-run', help='Do not perform any real actions') |
| 52 | +CMDLINE_PARSER.add_argument('--source', required=True) |
| 53 | + |
| 54 | +COMMIT_SUBJECT_PREFIX = "//tool/sync:dependencies" |
| 55 | +regex_git_commit = r'[0-9a-f]{40}' |
| 56 | +regex_git_tag = r'([0-9]+\.[0-9]+\.[0-9]+)' |
| 57 | + |
| 58 | +vaticle = 'vaticle' |
| 59 | +github_connection = github.Github(GITHUB_TOKEN) |
| 60 | +github_org = github_connection.get_organization(vaticle) |
| 61 | + |
| 62 | + |
| 63 | +def is_building_upstream(): |
| 64 | + """ Returns False is running in a forked repo""" |
| 65 | + if IS_CIRCLECI: |
| 66 | + return vaticle in os.getenv('CIRCLE_REPOSITORY_URL', '') |
| 67 | + elif IS_FACTORY: |
| 68 | + return vaticle == os.getenv('FACTORY_OWNER') |
| 69 | + else: |
| 70 | + return False |
| 71 | + |
| 72 | + |
| 73 | +def exception_handler(fun): |
| 74 | + """ Decorator printing additional message on CalledProcessError """ |
| 75 | + |
| 76 | + def wrapper(*args, **kwargs): |
| 77 | + # pylint: disable=missing-docstring |
| 78 | + try: |
| 79 | + fun(*args, **kwargs) |
| 80 | + except sp.CalledProcessError as ex: |
| 81 | + print(f'An error occurred when running {ex.cmd}. Process exited with code {ex.returncode} and message {ex.output}') |
| 82 | + print() |
| 83 | + raise ex |
| 84 | + |
| 85 | + return wrapper |
| 86 | + |
| 87 | + |
| 88 | +def short_commit(commit_sha): |
| 89 | + return sp.check_output(['git', 'rev-parse', '--short=7', commit_sha], cwd=os.getenv("BUILD_WORKSPACE_DIRECTORY")).decode().replace('\n', '') |
| 90 | + |
| 91 | + |
| 92 | +@exception_handler |
| 93 | +def main(): |
| 94 | + if not is_building_upstream(): |
| 95 | + print('Sync dependencies aborted: not building the upstream repo on @vaticle') |
| 96 | + exit(0) |
| 97 | + |
| 98 | + arguments = CMDLINE_PARSER.parse_args(sys.argv[1:]) |
| 99 | + source_repo, source_ref = arguments.source.split('@') |
| 100 | + |
| 101 | + if re.match(regex_git_commit, source_ref) is not None: |
| 102 | + source_ref_short = short_commit(source_ref) |
| 103 | + elif re.match(regex_git_tag, source_ref) is not None: |
| 104 | + source_ref_short = source_ref |
| 105 | + else: |
| 106 | + raise ValueError |
| 107 | + |
| 108 | + github_repo = github_org.get_repo(source_repo) |
| 109 | + github_commit = github_repo.get_commit(source_ref) |
| 110 | + source_message = github_commit.commit.message |
| 111 | + |
| 112 | + # TODO: Check that the commit author is @vaticle-bot |
| 113 | + if not source_message.startswith(COMMIT_SUBJECT_PREFIX): |
| 114 | + sync_message = '{0} {1}/{2}@{3}'.format(COMMIT_SUBJECT_PREFIX, vaticle, source_repo, source_ref_short) |
| 115 | + else: |
| 116 | + sync_message = source_message |
| 117 | + |
| 118 | + print('Requesting the synchronisation of dependency to {0}/{1}@{2}'.format(vaticle, source_repo, source_ref_short)) |
| 119 | + |
| 120 | + print('Constructing request payload:') |
| 121 | + sync_data = { |
| 122 | + 'source-repo': source_repo, |
| 123 | + 'source-ref': source_ref, |
| 124 | + 'sync-message': sync_message, |
| 125 | + } |
| 126 | + print(str(sync_data)) |
| 127 | + |
| 128 | + sync_data_json = json.dumps(sync_data) |
| 129 | + signature = hmac.new(GITHUB_TOKEN.encode(), sync_data_json.encode(), hashlib.sha1).hexdigest() |
| 130 | + |
| 131 | + if arguments.dry_run: |
| 132 | + pass |
| 133 | + else: |
| 134 | + print('Sending post request to: ' + BOT_SYNC_DEPS) |
| 135 | + tc.shell_execute([ |
| 136 | + 'curl', '-X', 'POST', '--data', sync_data_json, '-H', 'Content-Type: application/json', '-H', 'X-Hub-Signature: ' + signature, BOT_SYNC_DEPS |
| 137 | + ]) |
| 138 | + print('DONE!') |
| 139 | + |
| 140 | + |
| 141 | +if __name__ == '__main__': |
| 142 | + main() |
0 commit comments