def validate_config(config_path: str) -> None: """ parse and output the json representation of a Kodiak config """ cfg_text = Path(config_path).read_text() cfg_file = V1.parse_toml(cfg_text) assert isinstance(cfg_file, V1) click.echo(cfg_file.json(indent=2))
def test_bad_file() -> None: res = V1.parse_toml("something[invalid[") assert isinstance(res, toml.TomlDecodeError) res = V1.parse_toml("version = 20") assert isinstance(res, pydantic.ValidationError) # we should return an error when we try to parse a different version res = V1.parse_toml("version = 20") assert isinstance(res, pydantic.ValidationError) # we should always require that the version is specified, even if we provide defaults for everything else res = V1.parse_toml("") assert isinstance(res, pydantic.ValidationError) res = V1.parse_toml("merge.automerge_label = 123") assert isinstance(res, pydantic.ValidationError)
def test_get_markdown_for_config_toml_error() -> None: config = "[[[ version = 12" error = V1.parse_toml(config) assert not isinstance(error, V1) markdown = get_markdown_for_config(error, config_str=config, git_path="master:.kodiak.toml") assert markdown == load_config_fixture("toml-error.md").read_text()
async def get_config_for_ref( self, *, ref: str, org_repo_default_branch: str | None) -> CfgInfo | None: repo_root_config_expression = create_root_config_file_expression( branch=ref) repo_github_config_expression = create_github_config_file_expression( branch=ref) org_root_config_expression: str | None = None org_github_config_file_expression: str | None = None if org_repo_default_branch is not None: org_root_config_expression = create_root_config_file_expression( branch=org_repo_default_branch) org_github_config_file_expression = create_github_config_file_expression( branch=org_repo_default_branch) res = await self.send_query( query=GET_CONFIG_QUERY, variables=dict( owner=self.owner, repo=self.repo, rootConfigFileExpression=repo_root_config_expression, githubConfigFileExpression=repo_github_config_expression, orgRootConfigFileExpression=org_root_config_expression, orgGithubConfigFileExpression=org_github_config_file_expression, ), installation_id=self.installation_id, ) if res is None: return None data = res.get("data") if data is None: self.log.error("could not fetch default branch name", res=res) return None parsed_config = parse_config(data) if parsed_config is None: return None def get_file_expression() -> str: assert parsed_config is not None if parsed_config.kind == "repo_root": return repo_root_config_expression if parsed_config.kind == "repo_github": return repo_github_config_expression if parsed_config.kind == "org_root": assert org_root_config_expression is not None return org_root_config_expression if parsed_config.kind == "org_github": assert org_github_config_file_expression is not None return org_github_config_file_expression raise Exception(f"unknown config kind {parsed_config.kind!r}") return CfgInfo( parsed=V1.parse_toml(parsed_config.text), text=parsed_config.text, file_expression=get_file_expression(), )
def test_bad_file() -> None: with pytest.raises(toml.TomlDecodeError): V1.parse_toml("something[invalid[") with pytest.raises(ValueError): # we should raise an error when we try to parse a different version V1.parse_toml("version = 20") with pytest.raises(ValueError): # we should always require that the version is specified, even if we provide defaults for everything else V1.parse_toml("") with pytest.raises(ValueError): V1.parse_toml("merge.automerge_label = 123")
async def get_config_for_ref(self, *, ref: str) -> CfgInfo | None: root_config_file_expression = create_root_config_file_expression( branch=ref) github_config_file_expression = create_github_config_file_expression( branch=ref) res = await self.send_query( query=GET_CONFIG_QUERY, variables=dict( owner=self.owner, repo=self.repo, rootConfigFileExpression=root_config_file_expression, githubConfigFileExpression=github_config_file_expression, ), installation_id=self.installation_id, ) if res is None: return None data = res.get("data") errors = res.get("errors") if errors is not None or data is None: self.log.error("could not fetch default branch name", res=res) return None try: parsed = ConfigQueryResponse.parse_obj(data) except pydantic.ValidationError: self.log.exception("problem parsing api response for config") return None if not parsed.repository: return None if (parsed.repository.rootConfigFile is not None and parsed.repository.rootConfigFile.text is not None): config_file_expression = root_config_file_expression config_text = parsed.repository.rootConfigFile.text elif (parsed.repository.githubConfigFile is not None and parsed.repository.githubConfigFile.text is not None): config_file_expression = github_config_file_expression config_text = parsed.repository.githubConfigFile.text else: return None return CfgInfo( parsed=V1.parse_toml(config_text), text=config_text, file_expression=config_file_expression, )
async def get_event_info( self, branch_name: str, pr_number: int ) -> Optional[EventInfoResponse]: """ Retrieve all the information we need to evaluate a pull request This is basically the "do-all-the-things" query """ log = self.log.bind(pr=pr_number) root_config_file_expression = create_root_config_file_expression( branch=branch_name ) github_config_file_expression = create_github_config_file_expression( branch=branch_name ) res = await self.send_query( query=GET_EVENT_INFO_QUERY, variables=dict( owner=self.owner, repo=self.repo, rootConfigFileExpression=root_config_file_expression, githubConfigFileExpression=github_config_file_expression, PRNumber=pr_number, ), installation_id=self.installation_id, ) if res is None: return None data = res.get("data") errors = res.get("errors") if errors is not None or data is None: log.error("could not fetch event info", res=res) return None repository = get_repo(data=data) if not repository: log.warning("could not find repository") return None subscription = await self.get_subscription() root_config_str = get_root_config_str(repo=repository) github_config_str = get_github_config_str(repo=repository) if root_config_str is not None: config_str = root_config_str config_file_expression = root_config_file_expression elif github_config_str is not None: config_str = github_config_str config_file_expression = github_config_file_expression else: # NOTE(chdsbd): we don't want to show a message for this as the lack # of a config allows kodiak to be selectively installed log.info("could not find configuration file") return None pull_request = get_pull_request(repo=repository) if not pull_request: log.warning("Could not find PR") return None config = V1.parse_toml(config_str) # update the dictionary to match what we need for parsing pull_request["labels"] = get_labels(pr=pull_request) pull_request["latest_sha"] = get_sha(pr=pull_request) pull_request["number"] = pr_number try: pr = PullRequest.parse_obj(pull_request) except ValueError: log.warning("Could not parse pull request") return None branch_protection = get_branch_protection( repo=repository, ref_name=pr.baseRefName ) partial_reviews = get_reviews(pr=pull_request) reviews_with_permissions = await self.get_reviewers_and_permissions( reviews=partial_reviews ) return EventInfoResponse( config=config, config_str=config_str, config_file_expression=config_file_expression, pull_request=pr, repository=RepoInfo( merge_commit_allowed=repository.get("mergeCommitAllowed", False), rebase_merge_allowed=repository.get("rebaseMergeAllowed", False), squash_merge_allowed=repository.get("squashMergeAllowed", False), delete_branch_on_merge=repository.get("deleteBranchOnMerge") is True, is_private=repository.get("isPrivate") is True, ), subscription=subscription, branch_protection=branch_protection, review_requests=get_requested_reviews(pr=pull_request), reviews=reviews_with_permissions, status_contexts=get_status_contexts(pr=pull_request), check_runs=get_check_runs(pr=pull_request), head_exists=get_head_exists(pr=pull_request), valid_signature=get_valid_signature(pr=pull_request), valid_merge_methods=get_valid_merge_methods(repo=repository), )
def config(config_file: str) -> V1: return V1.parse_toml(config_file)
async def get_event_info( self, config_file_expression: str, pr_number: int) -> typing.Optional[EventInfoResponse]: """ Retrieve all the information we need to evaluate a pull request This is basically the "do-all-the-things" query """ log = logger.bind(repo=f"{self.owner}/{self.repo}", pr=pr_number, install=self.installation_id) res = await self.send_query( query=GET_EVENT_INFO_QUERY, variables=dict( owner=self.owner, repo=self.repo, configFileExpression=config_file_expression, PRNumber=pr_number, ), installation_id=self.installation_id, ) if res is None: return None data = res.get("data") errors = res.get("errors") if errors is not None or data is None: log.error("could not fetch event info", res=res) return None repository = get_repo(data=data) if not repository: log.warning("could not find repository") return None config_str = get_config_str(repo=repository) if not config_str: log.warning("could not find configuration file") return None try: config = V1.parse_toml(config_str) except ValueError: log.warning("could not parse configuration") return None pull_request = get_pull_request(repo=repository) if not pull_request: log.warning("Could not find PR") return None # update the dictionary to match what we need for parsing pull_request["labels"] = get_labels(pr=pull_request) pull_request["latest_sha"] = get_sha(pr=pull_request) pull_request["number"] = pr_number try: pr = PullRequest.parse_obj(pull_request) except ValueError: log.warning("Could not parse pull request") return None branch_protection = get_branch_protection(repo=repository, ref_name=pr.baseRefName) return EventInfoResponse( config=config, pull_request=pr, repo=RepoInfo( merge_commit_allowed=repository.get("mergeCommitAllowed", False), rebase_merge_allowed=repository.get("rebaseMergeAllowed", False), squash_merge_allowed=repository.get("squashMergeAllowed", False), ), branch_protection=branch_protection, review_requests_count=get_review_requests_count(pr=pull_request), reviews=get_reviews(pr=pull_request), status_contexts=get_status_contexts(pr=pull_request), check_runs=get_check_runs(pr=pull_request), head_exists=get_head_exists(pr=pull_request), valid_signature=get_valid_signature(pr=pull_request), valid_merge_methods=get_valid_merge_methods(repo=repository), )
def config( config_file: str ) -> Union[V1, pydantic.ValidationError, toml.TomlDecodeError]: return V1.parse_toml(config_file)