# Copyright 2019 BMW Group # Copyright 2024 Acme Gating, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from graphene import ( Boolean, Field, Int, Interface, List, ObjectType, Schema, String, ) class ID(String): """Github global object ids are strings""" pass class Node(Interface): id = ID(required=True) @classmethod def resolve_type(cls, instance, info): kind = getattr(instance, '_graphene_type', None) if kind == 'BranchProtectionRule': return BranchProtectionRule elif kind == 'Commit': return Commit elif kind == 'CheckSuite': return CheckSuite elif kind == 'PullRequest': return PullRequest class PageInfo(ObjectType): endCursor = String() hasPreviousPage = Boolean() hasNextPage = Boolean() def resolve_endCursor(parent, info): return str(parent['after'] + parent['first']) def resolve_hasPreviousPage(parent, info): return parent['after'] > 0 def resolve_hasNextPage(parent, info): return parent['after'] + parent['first'] < parent['length'] class MatchingRef(ObjectType): name = String() def resolve_name(parent, info): return parent class MatchingRefs(ObjectType): pageInfo = Field(PageInfo) nodes = List(MatchingRef) def resolve_nodes(parent, info): return parent['nodes'] def resolve_pageInfo(parent, info): return parent class BranchProtectionRule(ObjectType): class Meta: interfaces = (Node, ) id = ID(required=True) pattern = String() requiredStatusCheckContexts = List(String) requiresApprovingReviews = Boolean() requiresConversationResolution = Boolean() requiresCodeOwnerReviews = Boolean() matchingRefs = Field(MatchingRefs, first=Int(), after=String(), query=String()) lockBranch = Boolean() def resolve_id(parent, info): return parent.id def resolve_pattern(parent, info): return parent.pattern def resolve_requiredStatusCheckContexts(parent, info): return parent.required_contexts def resolve_requiresApprovingReviews(parent, info): return parent.require_reviews def resolve_requiresConversationResolution(parent, info): return parent.require_conversation_resolution def resolve_requiresCodeOwnerReviews(parent, info): return parent.require_codeowners_review def resolve_lockBranch(parent, info): return parent.lock_branch def resolve_matchingRefs(parent, info, first, after=None, query=None): if after is None: after = '0' after = int(after) values = parent.matching_refs if query: values = [v for v in values if v == query] return dict( length=len(values), nodes=values[after:after + first], first=first, after=after, ) class BranchProtectionRules(ObjectType): pageInfo = Field(PageInfo) nodes = List(BranchProtectionRule) def resolve_nodes(parent, info): return parent['nodes'] def resolve_pageInfo(parent, info): return parent class Actor(ObjectType): login = String() class StatusContext(ObjectType): state = String() context = String() creator = Field(Actor) def resolve_state(parent, info): state = parent.state.upper() return state def resolve_context(parent, info): return parent.context def resolve_creator(parent, info): return parent.creator class Status(ObjectType): contexts = List(StatusContext) def resolve_contexts(parent, info): return parent class CheckRun(ObjectType): class Meta: interfaces = (Node, ) id = ID(required=True) name = String() conclusion = String() def resolve_name(parent, info): return parent.name def resolve_conclusion(parent, info): if parent.conclusion: return parent.conclusion.upper() return None class CheckRuns(ObjectType): nodes = List(CheckRun) pageInfo = Field(PageInfo) def resolve_nodes(parent, info): return parent['nodes'] def resolve_pageInfo(parent, info): return parent class App(ObjectType): slug = String() name = String() class CheckSuite(ObjectType): class Meta: interfaces = (Node, ) id = ID(required=True) app = Field(App) checkRuns = Field(CheckRuns, first=Int(), after=String()) def resolve_app(parent, info): if not parent.runs: return None return parent.runs[0].app def resolve_checkRuns(parent, info, first, after=None): # Github will only return the single most recent check run for # a given name (this is true for REST or graphql), despite the # format of the result being a list. # Since the check runs are ordered from latest to oldest result we # need to traverse the list in reverse order. check_runs_by_name = { "{}:{}".format(cr.app.name, cr.name): cr for cr in reversed(parent.runs) } if after is None: after = '0' after = int(after) values = list(check_runs_by_name.values()) return dict( length=len(values), nodes=values[after:after + first], first=first, after=after, ) class CheckSuites(ObjectType): nodes = List(CheckSuite) pageInfo = Field(PageInfo) def resolve_nodes(parent, info): return parent['nodes'] def resolve_pageInfo(parent, info): return parent class Commit(ObjectType): class Meta: interfaces = (Node, ) id = ID(required=True) status = Field(Status) checkSuites = Field(CheckSuites, first=Int(), after=String()) def resolve_status(parent, info): seen = set() result = [] for status in parent._statuses: if status.context not in seen: seen.add(status.context) result.append(status) # Github returns None if there are no results return result or None def resolve_checkSuites(parent, info, first, after=None): if after is None: after = '0' after = int(after) # Each value is a list of check runs for that suite values = list(parent._check_suites.values()) return dict( length=len(values), nodes=values[after:after + first], first=first, after=after, ) class PullRequestReviewThread(ObjectType): isResolved = Boolean() def resolve_isResolved(parent, info): return parent.resolved class PullRequestReviewThreads(ObjectType): nodes = List(PullRequestReviewThread) pageInfo = Field(PageInfo) def resolve_nodes(parent, info): return parent['nodes'] def resolve_pageInfo(parent, info): return parent class PullRequest(ObjectType): class Meta: interfaces = (Node, ) id = ID(required=True) isDraft = Boolean() reviewDecision = String() mergeable = String() merged = Boolean() reviewThreads = Field(PullRequestReviewThreads, first=Int(), after=String()) def resolve_id(parent, info): return parent.id def resolve_isDraft(parent, info): return parent.draft def resolve_mergeable(parent, info): return "MERGEABLE" if parent.mergeable else "CONFLICTING" def resolve_merged(parent, info): return parent.is_merged def resolve_reviewThreads(parent, info, first, after=None): if after is None: after = '0' after = int(after) values = parent.review_threads return dict( length=len(values), nodes=values[after:after + first], first=first, after=after, ) def resolve_reviewDecision(parent, info): if hasattr(info.context, 'version') and info.context.version: if info.context.version < (2, 21, 0): raise Exception('Field unsupported') # Check branch protection rules if reviews are required org, project = parent.project.split('/') repo = info.context._data.repos[(org, project)] rule = repo._branch_protection_rules.get(parent.branch) if not rule or not rule.require_reviews: # Github returns None if there is no review required return None approvals = [r for r in parent.reviews if r.data['state'] == 'APPROVED'] if approvals: return 'APPROVED' return 'REVIEW_REQUIRED' class Repository(ObjectType): name = String() branchProtectionRules = Field(BranchProtectionRules, first=Int(), after=String()) pullRequest = Field(PullRequest, number=Int(required=True)) object = Field(Commit, expression=String(required=True)) def resolve_name(parent, info): org, name = parent.name.split('/') return name def resolve_branchProtectionRules(parent, info, first, after=None): if after is None: after = '0' after = int(after) values = list(parent._branch_protection_rules.values()) return dict( length=len(values), nodes=values[after:after + first], first=first, after=after, ) def resolve_pullRequest(parent, info, number): return parent.data.pull_requests.get(number) def resolve_object(parent, info, expression): return parent._commits.get(expression) class FakeGithubQuery(ObjectType): repository = Field(Repository, owner=String(required=True), name=String(required=True)) node = Field(Node, id=ID(required=True)) def resolve_repository(root, info, owner, name): return info.context._data.repos.get((owner, name)) def resolve_node(root, info, id): for repo in info.context._data.repos.values(): for rule in repo._branch_protection_rules.values(): if rule.id == id: return rule for commit in repo._commits.values(): if commit.id == id: return commit for suite in commit._check_suites.values(): if suite.id == id: return suite for pr in info.context._data.pull_requests.values(): if pr.id == id: return pr def getGrapheneSchema(): return Schema(query=FakeGithubQuery, types=[ID])