Skip to content

Commit

Permalink
feat(bot): support older GraphQL schemas (#734)
Browse files Browse the repository at this point in the history
The `requiresConversationResolution` BranchProtectionRule isn't available until later GitHub Enterprise (GHE) versions, so we check the API before trying to use it.

I think in the future, if we use new fields we should update this logic to check for the field availability to ensure better GHE compatibility.

resolves #727
related #728
  • Loading branch information
chdsbd authored Sep 24, 2021
1 parent f74676c commit afd6e59
Showing 1 changed file with 73 additions and 6 deletions.
79 changes: 73 additions & 6 deletions bot/kodiak/queries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class GraphQLError(TypedDict):
path: Optional[List[str]]


class GraphQLResponse(TypedDict):
class GraphQLResponse(TypedDict, total=False):
data: Optional[Dict[Any, Any]]
errors: Optional[List[GraphQLError]]

Expand Down Expand Up @@ -149,7 +149,8 @@ def parse_config(data: dict[Any, Any]) -> ParsedConfig | None:
return None


GET_EVENT_INFO_QUERY = """
def get_event_info_query(requires_conversation_resolution: bool) -> str:
return """
query GetEventInfo($owner: String!, $repo: String!, $PRNumber: Int!) {
repository(owner: $owner, name: $repo) {
branchProtectionRules(first: 100) {
Expand All @@ -166,7 +167,7 @@ def parse_config(data: dict[Any, Any]) -> ParsedConfig | None:
requiresStrictStatusChecks
requiresCodeOwnerReviews
requiresCommitSignatures
requiresConversationResolution
%(requiresConversationResolution)s
restrictsPushes
pushAllowances(first: 100) {
nodes {
Expand Down Expand Up @@ -305,7 +306,11 @@ def parse_config(data: dict[Any, Any]) -> ParsedConfig | None:
}
}
"""
""" % dict(
requiresConversationResolution="requiresConversationResolution"
if requires_conversation_resolution
else ""
)


def get_org_config_default_branch(data: dict[Any, Any]) -> str | None:
Expand Down Expand Up @@ -473,7 +478,7 @@ class BranchProtectionRule(BaseModel):
requiresStrictStatusChecks: bool
requiresCodeOwnerReviews: bool
requiresCommitSignatures: bool
requiresConversationResolution: bool
requiresConversationResolution: Optional[bool]
restrictsPushes: bool
pushAllowances: NodeListPushAllowance

Expand Down Expand Up @@ -794,6 +799,14 @@ class CfgInfo:
file_expression: str


@dataclass
class ApiFeatures:
requires_conversation_resolution: bool


_api_features_cache: ApiFeatures | None = None


class ThrottlerProtocol(Protocol):
async def __aenter__(self) -> None:
...
Expand Down Expand Up @@ -867,6 +880,54 @@ async def send_query(
return None
return cast(GraphQLResponse, res.json())

async def get_api_features(self) -> ApiFeatures | None:
"""
Check if we can use recently added schema fields.
For GitHub Enterprise installations, the latest GraphQL fields may not
be available.
To query the GraphQL API, we need an installation token, so for the
first client to make an API request, we use their credentials to view
schema metadata and cache the results.
"""
global _api_features_cache # pylint: disable=global-statement
if _api_features_cache is not None:
return _api_features_cache
res = await self.send_query(
query="""
query {
__type(name:"BranchProtectionRule") {
fields(includeDeprecated: true) {
name
}
}
}
""",
variables=dict(),
installation_id=self.installation_id,
)
if res is None:
self.log.warning("failed to fetching api features")
return None
errors = res.get("errors")
data = res.get("data")
if errors or not data:
self.log.warning("errors fetching api features", errors=errors, data=data)
return None

try:
fields = data["__type"]["fields"]
except (TypeError, KeyError):
self.log.warning("problem parsing api features", exc_info=True)
return None
_api_features_cache = ApiFeatures(
requires_conversation_resolution=any(
field["name"] == "requiresConversationResolution" for field in fields
)
)
return _api_features_cache

async def get_permissions_for_username(self, username: str) -> Permission:
headers = await get_headers(
session=self.session, installation_id=self.installation_id
Expand Down Expand Up @@ -1013,8 +1074,14 @@ async def get_event_info(self, pr_number: int) -> Optional[EventInfoResponse]:

log = self.log.bind(pr=pr_number)

api_features = await self.get_api_features()

res = await self.send_query(
query=GET_EVENT_INFO_QUERY,
query=get_event_info_query(
requires_conversation_resolution=api_features.requires_conversation_resolution
if api_features
else True
),
variables=dict(owner=self.owner, repo=self.repo, PRNumber=pr_number),
installation_id=self.installation_id,
)
Expand Down

0 comments on commit afd6e59

Please sign in to comment.