def from_github(repo: GithubRepository, pull_id: int) -> 'PullRequestDetails': """Retrieves a single pull request. References: https://developer.github.com/v3/pulls/#get-a-single-pull-request Args: repo: The github repo to get the pull request from. pull_id: The id of the pull request. Raises: RuntimeError: If the request does not return status 200 (success). """ url = "https://api.github.com/repos/{}/{}/pulls/{}".format( repo.organization, repo.name, pull_id) response = repo.get(url) if response.status_code != 200: raise RuntimeError( 'Pull check failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) payload = json.JSONDecoder().decode(response.content.decode()) return PullRequestDetails(payload, repo)
def main(): access_token = os.getenv(ACCESS_TOKEN_ENV_VARIABLE) if not access_token: project_id = 'cirq-infra' print('{} not set. Trying secret manager.'.format( ACCESS_TOKEN_ENV_VARIABLE), file=sys.stderr) client = secretmanager_v1beta1.SecretManagerServiceClient() secret_name = (f'projects/{project_id}/' f'secrets/cirq-bot-api-key/versions/1') response = client.access_secret_version(name=secret_name) access_token = response.payload.data.decode('UTF-8') repo = GithubRepository( organization=GITHUB_REPO_ORGANIZATION, name=GITHUB_REPO_NAME, access_token=access_token) log('Watching for automergeable PRs.') problem_seen_times = {} # type: Dict[int, datetime.datetime] while True: try: duty_cycle(repo, problem_seen_times) except Exception: # Anything but a keyboard interrupt / system exit. traceback.print_exc() wait_for_polling_period()
def check_collaborator_has_write( repo: GithubRepository, username: str) -> Optional[CannotAutomergeError]: """Checks whether the given user is a collaborator (admin and write access). References: https://developer.github.com/v3/issues/events/#list-events-for-an-issue Args: repo: The github repo to check. username: The github username to check whether the user is a collaborator. Returns: CannotAutomergeError if the user does not have admin and write permissions and so cannot use automerge, None otherwise. Raises: RuntimeError: If the request does not return status 200 (success). """ url = "https://api.github.com/repos/{}/{}/collaborators/{}/permission" "".format( repo.organization, repo.name, username) response = repo.get(url) if response.status_code != 200: raise RuntimeError( 'Collaborator check failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) payload = json.JSONDecoder().decode(response.content.decode()) if payload['permission'] not in ['admin', 'write']: return CannotAutomergeError( 'Only collaborators with write permission can use automerge.') return None
def remote_repo(self) -> GithubRepository: """Return the GithubRepository corresponding to this pull request.""" return GithubRepository( organization=self.payload['head']['repo']['owner']['login'], name=self.payload['head']['repo']['name'], access_token=self.repo.access_token, )
def edit_comment(repo: GithubRepository, text: str, comment_id: int) -> None: """Edits an existing github comment. References: https://developer.github.com/v3/issues/comments/#edit-a-comment Args: repo: The github repo that contains the comment. text: The new comment text. comment_id: The id of the comment to edit. Raises: RuntimeError: If the request does not return status 200 (success). """ url = "https://api.github.com/repos/{}/{}/issues/comments/{}".format( repo.organization, repo.name, comment_id ) data = {'body': text} response = repo.patch(url, json=data) if response.status_code != 200: raise RuntimeError( 'Edit comment failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content ) )
def list_open_pull_requests(repo: GithubRepository, base_branch: Optional[str] = None, per_page: int = 100) -> List[PullRequestDetails]: url = ( f"https://api.github.com/repos/{repo.organization}/{repo.name}/pulls" f"?per_page={per_page}") data = { 'state': 'open', } if base_branch is not None: data['base'] = base_branch response = repo.get(url, json=data) if response.status_code != 200: raise RuntimeError( 'List pulls failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) pulls = json.JSONDecoder().decode(response.content.decode()) results = [PullRequestDetails(pull, repo) for pull in pulls] # Filtering via the API doesn't seem to work, so we do it ourselves. if base_branch is not None: results = [ result for result in results if result.base_branch_name == base_branch ] return results
def add_labels_to_pr(repo: GithubRepository, pull_id: int, *labels: str) -> None: """Add lables to a pull request. References: https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue Args: repo: The github repo where the pull request lives. pull_id: The id of the pull request. *labels: The labels to add to the pull request. Raises: RuntimeError: If the request to add labels returned anything other than success. """ url = "https://api.github.com/repos/{}/{}/issues/{}/labels".format( repo.organization, repo.name, pull_id ) response = repo.post(url, json=list(labels)) if response.status_code != 200: raise RuntimeError( 'Add labels failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content ) )
def get_repo_ref(repo: GithubRepository, ref: str) -> Dict[str, Any]: """Get a given github reference. References: https://developer.github.com/v3/git/refs/#get-a-reference Args: repo: The github repo to get the reference from. ref: The id of the reference. Returns: The raw response of the request for the reference.. Raises: RuntimeError: If the request does not return status 200 (success). """ url = f"https://api.github.com/repos/{repo.organization}/{repo.name}/git/refs/{ref}" response = repo.get(url) if response.status_code != 200: raise RuntimeError( 'Refs get failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content ) ) payload = json.JSONDecoder().decode(response.content.decode()) return payload
def get_branch_details(repo: GithubRepository, branch: str) -> Any: """Get details about a github branch. References: https://developer.github.com/v3/repos/branches/#get-branch Args: repo: The github repo that has the branch. branch: The name of the branch. Returns: The raw response to the query to get details. Raises: RuntimeError: If the request does not return status 200 (success). """ url = "https://api.github.com/repos/{}/{}/branches/{}".format( repo.organization, repo.name, branch) response = repo.get(url) if response.status_code != 200: raise RuntimeError( 'Failed to get branch details. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) return json.JSONDecoder().decode(response.content.decode())
def get_all(repo: GithubRepository, url_func: Callable[[int], str]) -> List[Any]: """Get all results, accounting for pagination. Args: repo: The github repo to call GET on. url_func: A function from an integer page number to the url to get the result for that page. Returns: A list of the results by page. Raises: RuntimeError: If the request does not return status 200 (success). """ results: List[Any] = [] page = 0 has_next = True while has_next: url = url_func(page) response = repo.get(url) if response.status_code != 200: raise RuntimeError( f'Request failed to {url}. Code: {response.status_code}.' f' Content: {response.content!r}.') payload = json.JSONDecoder().decode(response.content.decode()) results += payload has_next = 'link' in response.headers and 'rel="next"' in response.headers[ 'link'] page += 1 return results
def remove_label_from_pr(repo: GithubRepository, pull_id: int, label: str) -> bool: """Removes a label from a pull request. References: https://developer.github.com/v3/issues/labels/#remove-a-label-from-an-issue Args: repo: The github repo for the pull request. pull_id: The id for the pull request. label: The label to remove. Raises: RuntimeError: If the request does not return status 200 (success). Returns: True if the label existed and was deleted. False if the label did not exist. """ url = "https://api.github.com/repos/{}/{}/issues/{}/labels/{}".format( repo.organization, repo.name, pull_id, label) response = repo.delete(url) if response.status_code == 404: payload = json.JSONDecoder().decode(response.content.decode()) if payload['message'] == 'Label does not exist': return False if response.status_code == 200: # Removed the label. return True raise RuntimeError('Label remove failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content))
def list_pr_comments(repo: GithubRepository, pull_id: int) -> List[Dict[str, Any]]: """List comments for a given pull request. References: https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue Args: repo: The github repo for the pull request. pull_id: The id of the pull request. Returns: A list of the raw responses for the pull requests. Raises: RuntimeError: If the request does not return status 200 (success). """ url = "https://api.github.com/repos/{}/{}/issues/{}/comments".format( repo.organization, repo.name, pull_id) response = repo.get(url) if response.status_code != 200: raise RuntimeError( 'Comments get failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) payload = json.JSONDecoder().decode(response.content.decode()) return payload
def add_comment(repo: GithubRepository, pull_id: int, text: str) -> None: """Add a comment to a pull request. References: https://developer.github.com/v3/issues/comments/#create-a-comment Arg: rep: The github repo whose pull request should have a comment added to. pull_id: The id of the pull request to comment on. text: The text of the comment. Raises: RuntimeError: If the request does not return status 201 (created). """ url = "https://api.github.com/repos/{}/{}/issues/{}/comments".format( repo.organization, repo.name, pull_id ) data = {'body': text} response = repo.post(url, json=data) if response.status_code != 201: raise RuntimeError( 'Add comment failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content ) )
def fetch_github_pull_request( destination_directory: str, repository: github_repository.GithubRepository, pull_request_number: int, verbose: bool, ) -> prepared_env.PreparedEnv: """Uses content from github to create a dir for testing and comparisons. Args: destination_directory: The location to fetch the contents into. repository: The github repository that the commit lives under. pull_request_number: The id of the pull request to clone. If None, then the master branch is cloned instead. verbose: When set, more progress output is produced. Returns: Commit ids corresponding to content to test/compare. """ branch = 'pull/{}/head'.format(pull_request_number) os.chdir(destination_directory) print('chdir', destination_directory, file=sys.stderr) shell_tools.run_cmd('git', 'init', None if verbose else '--quiet', out=sys.stderr) result = _git_fetch_for_comparison( remote=repository.as_remote(), actual_branch=branch, compare_branch='master', verbose=verbose, ) shell_tools.run_cmd( 'git', 'branch', None if verbose else '--quiet', 'compare_commit', result.compare_commit_id, log_run_to_stderr=verbose, ) shell_tools.run_cmd( 'git', 'checkout', None if verbose else '--quiet', '-b', 'actual_commit', result.actual_commit_id, log_run_to_stderr=verbose, ) return prepared_env.PreparedEnv( github_repo=repository, actual_commit_id=result.actual_commit_id, compare_commit_id=result.compare_commit_id, destination_directory=destination_directory, virtual_env_path=None, )
def delete_comment(repo: GithubRepository, comment_id: int) -> None: """ References: https://developer.github.com/v3/issues/comments/#delete-a-comment """ url = "https://api.github.com/repos/{}/{}/issues/comments/{}".format( repo.organization, repo.name, comment_id) response = repo.delete(url) if response.status_code != 204: raise RuntimeError( 'Comment delete failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content))
def fetch_github_pull_request(destination_directory: str, repository: github_repository.GithubRepository, pull_request_number: int, verbose: bool ) -> prepared_env.PreparedEnv: """Uses content from github to create a dir for testing and comparisons. Args: destination_directory: The location to fetch the contents into. repository: The github repository that the commit lives under. pull_request_number: The id of the pull request to clone. If None, then the master branch is cloned instead. verbose: When set, more progress output is produced. Returns: Commit ids corresponding to content to test/compare. """ branch = 'pull/{}/head'.format(pull_request_number) os.chdir(destination_directory) print('chdir', destination_directory, file=sys.stderr) shell_tools.run_cmd( 'git', 'init', None if verbose else '--quiet', out=sys.stderr) result = _git_fetch_for_comparison(remote=repository.as_remote(), actual_branch=branch, compare_branch='master', verbose=verbose) shell_tools.run_cmd( 'git', 'branch', None if verbose else '--quiet', 'compare_commit', result.compare_commit_id, log_run_to_stderr=verbose) shell_tools.run_cmd( 'git', 'checkout', None if verbose else '--quiet', '-b', 'actual_commit', result.actual_commit_id, log_run_to_stderr=verbose) return prepared_env.PreparedEnv( github_repo=repository, actual_commit_id=result.actual_commit_id, compare_commit_id=result.compare_commit_id, destination_directory=destination_directory, virtual_env_path=None)
def get_repo_ref(repo: GithubRepository, ref: str) -> Dict[str, Any]: """ References: https://developer.github.com/v3/git/refs/#get-a-reference """ url = f"https://api.github.com/repos/{repo.organization}/{repo.name}/git/refs/{ref}" response = repo.get(url) if response.status_code != 200: raise RuntimeError('Refs get failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) payload = json.JSONDecoder().decode(response.content.decode()) return payload
def add_labels_to_pr(repo: GithubRepository, pull_id: int, *labels: str) -> None: """ References: https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue """ url = "https://api.github.com/repos/{}/{}/issues/{}/labels".format( repo.organization, repo.name, pull_id) response = repo.post(url, json=list(labels)) if response.status_code != 200: raise RuntimeError( 'Add labels failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content))
def add_comment(repo: GithubRepository, pull_id: int, text: str) -> None: """ References: https://developer.github.com/v3/issues/comments/#create-a-comment """ url = "https://api.github.com/repos/{}/{}/issues/{}/comments".format( repo.organization, repo.name, pull_id) data = {'body': text} response = repo.post(url, json=data) if response.status_code != 201: raise RuntimeError( 'Add comment failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content))
def edit_comment(repo: GithubRepository, text: str, comment_id: int) -> None: """ References: https://developer.github.com/v3/issues/comments/#edit-a-comment """ url = "https://api.github.com/repos/{}/{}/issues/comments/{}".format( repo.organization, repo.name, comment_id) data = {'body': text} response = repo.patch(url, json=data) if response.status_code != 200: raise RuntimeError( 'Edit comment failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content))
def get_branch_details(repo: GithubRepository, branch: str) -> Any: """ References: https://developer.github.com/v3/repos/branches/#get-branch """ url = "https://api.github.com/repos/{}/{}/branches/{}".format( repo.organization, repo.name, branch) response = repo.get(url) if response.status_code != 200: raise RuntimeError( 'Failed to get branch details. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) return json.JSONDecoder().decode(response.content.decode())
def list_pr_comments(repo: GithubRepository, pull_id: int) -> List[Dict[str, Any]]: """ References: https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue """ url = "https://api.github.com/repos/{}/{}/issues/{}/comments".format( repo.organization, repo.name, pull_id) response = repo.get(url) if response.status_code != 200: raise RuntimeError( 'Comments get failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) payload = json.JSONDecoder().decode(response.content.decode()) return payload
def main(): access_token = os.getenv('CIRQ_BOT_GITHUB_ACCESS_TOKEN') if not access_token: print('CIRQ_BOT_GITHUB_ACCESS_TOKEN not set.', file=sys.stderr) sys.exit(1) repo = GithubRepository( organization='quantumlib', name='cirq', access_token=access_token) log('Watching for automergeable PRs.') while True: duty_cycle(repo) wait_a_tick()
def main(): access_token = os.getenv('CIRQ_BOT_GITHUB_ACCESS_TOKEN') if not access_token: print('CIRQ_BOT_GITHUB_ACCESS_TOKEN not set.', file=sys.stderr) sys.exit(1) repo = GithubRepository( organization='quantumlib', name='cirq', access_token=access_token) log('Watching for automergeable PRs.') problem_seen_times = {} # type: Dict[int, datetime.datetime] while True: duty_cycle(repo, problem_seen_times) wait_for_polling_period()
def main(): access_token = os.getenv(ACCESS_TOKEN_ENV_VARIABLE) if not access_token: print('{} not set.'.format(ACCESS_TOKEN_ENV_VARIABLE), file=sys.stderr) sys.exit(1) repo = GithubRepository(organization=GITHUB_REPO_ORGANIZATION, name=GITHUB_REPO_NAME, access_token=access_token) log('Watching for automergeable PRs.') problem_seen_times = {} # type: Dict[int, datetime.datetime] while True: try: duty_cycle(repo, problem_seen_times) except Exception: # Anything but a keyboard interrupt / system exit. traceback.print_exc() wait_for_polling_period()
def from_github(repo: GithubRepository, pull_id: int) -> 'PullRequestDetails': """ References: https://developer.github.com/v3/pulls/#get-a-single-pull-request """ url = "https://api.github.com/repos/{}/{}/pulls/{}".format( repo.organization, repo.name, pull_id) response = repo.get(url) if response.status_code != 200: raise RuntimeError( 'Pull check failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content)) payload = json.JSONDecoder().decode(response.content.decode()) return PullRequestDetails(payload, repo)
def get_all(repo: GithubRepository, url_func: Callable[[int], str]) -> List[Any]: results: List[Any] = [] page = 0 has_next = True while has_next: url = url_func(page) response = repo.get(url) if response.status_code != 200: raise RuntimeError( f'Request failed to {url}. Code: {response.status_code}.' f' Content: {response.content!r}.') payload = json.JSONDecoder().decode(response.content.decode()) results += payload has_next = 'link' in response.headers and 'rel="next"' in response.headers[ 'link'] page += 1 return results
def delete_comment(repo: GithubRepository, comment_id: int) -> None: """Delete a comment. References: https://developer.github.com/v3/issues/comments/#delete-a-comment Args: repo: The github repo where the comment lives. comment_id: The id of the comment to delete. Raises: RuntimeError: If the request does not return status 204 (no content). """ url = "https://api.github.com/repos/{}/{}/issues/comments/{}".format( repo.organization, repo.name, comment_id) response = repo.delete(url) if response.status_code != 204: raise RuntimeError( 'Comment delete failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content))
def main(): global cla_access_token pull_ids = [int(e) for e in sys.argv[1:]] access_token = os.getenv('CIRQ_BOT_GITHUB_ACCESS_TOKEN') cla_access_token = os.getenv('CIRQ_BOT_ALT_CLA_GITHUB_ACCESS_TOKEN') if not access_token: print('CIRQ_BOT_GITHUB_ACCESS_TOKEN not set.', file=sys.stderr) sys.exit(1) if not cla_access_token: print('CIRQ_BOT_ALT_CLA_GITHUB_ACCESS_TOKEN not set.', file=sys.stderr) sys.exit(1) repo = GithubRepository(organization='quantumlib', name='cirq', access_token=access_token) if pull_ids: auto_merge_multiple_pull_requests(repo, pull_ids) else: watch_for_auto_mergeable_pull_requests(repo)
def remove_label_from_pr(repo: GithubRepository, pull_id: int, label: str) -> bool: """ References: https://developer.github.com/v3/issues/labels/#remove-a-label-from-an-issue """ url = "https://api.github.com/repos/{}/{}/issues/{}/labels/{}".format( repo.organization, repo.name, pull_id, label) response = repo.delete(url) if response.status_code == 404: payload = json.JSONDecoder().decode(response.content.decode()) if payload['message'] == 'Label does not exist': return False if response.status_code == 200: # Removed the label. return True raise RuntimeError('Label remove failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content))
def list_open_pull_requests( repo: GithubRepository, base_branch: Optional[str] = None, per_page: int = 100 ) -> List[PullRequestDetails]: """List open pull requests. Args: repo: The github repo for the pull requests. base_branch: The branch for which to request pull requests. per_page: The number of results to obtain per page. Returns: A list of the pull requests. Raises: RuntimeError: If the request does not return status 200 (success). """ url = ( f"https://api.github.com/repos/{repo.organization}/{repo.name}/pulls" f"?per_page={per_page}" ) data = {'state': 'open'} if base_branch is not None: data['base'] = base_branch response = repo.get(url, json=data) if response.status_code != 200: raise RuntimeError( 'List pulls failed. Code: {}. Content: {!r}.'.format( response.status_code, response.content ) ) pulls = json.JSONDecoder().decode(response.content.decode()) results = [PullRequestDetails(pull, repo) for pull in pulls] # Filtering via the API doesn't seem to work, so we do it ourselves. if base_branch is not None: results = [result for result in results if result.base_branch_name == base_branch] return results