Skip to content

Commit 3f0df3d

Browse files
committed
implement logic for checking CI status for a given commit and PR approvals
1 parent 0ef96ce commit 3f0df3d

File tree

2 files changed

+222
-3
lines changed

2 files changed

+222
-3
lines changed

internal/cmd/match_criteria.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package cmd
22

33
import (
4+
"context"
5+
"fmt"
46
"regexp"
57
"strings"
8+
9+
"github.com/cli/go-gh/v2/pkg/api"
10+
graphql "github.com/cli/shurcooL-graphql"
611
)
712

813
// checks if a PR matches all filtering criteria
@@ -113,3 +118,206 @@ func labelsMatchCriteria(prLabels []struct{ Name string }) bool {
113118

114119
return true
115120
}
121+
122+
// GraphQL response structure for PR status info
123+
type prStatusResponse struct {
124+
Data struct {
125+
Repository struct {
126+
PullRequest struct {
127+
ReviewDecision string `json:"reviewDecision"`
128+
Commits struct {
129+
Nodes []struct {
130+
Commit struct {
131+
StatusCheckRollup *struct {
132+
State string `json:"state"`
133+
} `json:"statusCheckRollup"`
134+
} `json:"commit"`
135+
} `json:"nodes"`
136+
} `json:"commits"`
137+
} `json:"pullRequest"`
138+
} `json:"repository"`
139+
} `json:"data"`
140+
Errors []struct {
141+
Message string `json:"message"`
142+
} `json:"errors,omitempty"`
143+
}
144+
145+
// GetPRStatusInfo fetches both CI status and approval status using GitHub's GraphQL API
146+
func GetPRStatusInfo(ctx context.Context, graphQlClient *api.GraphQLClient, owner, repo string, prNumber int) (*prStatusResponse, error) {
147+
// Check for context cancellation
148+
select {
149+
case <-ctx.Done():
150+
return nil, ctx.Err()
151+
default:
152+
// Continue processing
153+
}
154+
155+
// Define a struct with embedded graphql query
156+
var query struct {
157+
Repository struct {
158+
PullRequest struct {
159+
ReviewDecision string
160+
Commits struct {
161+
Nodes []struct {
162+
Commit struct {
163+
StatusCheckRollup *struct {
164+
State string
165+
}
166+
}
167+
}
168+
} `graphql:"commits(last: 1)"`
169+
} `graphql:"pullRequest(number: $prNumber)"`
170+
} `graphql:"repository(owner: $owner, name: $repo)"`
171+
}
172+
173+
// Prepare GraphQL query variables
174+
variables := map[string]interface{}{
175+
"owner": graphql.String(owner),
176+
"repo": graphql.String(repo),
177+
"prNumber": graphql.Int(prNumber),
178+
}
179+
180+
// Execute GraphQL query
181+
err := graphQlClient.Query("PullRequestStatus", &query, variables)
182+
if err != nil {
183+
return nil, fmt.Errorf("GraphQL query failed: %w", err)
184+
}
185+
186+
// Convert to our response format
187+
response := &prStatusResponse{}
188+
response.Data.Repository.PullRequest.ReviewDecision = query.Repository.PullRequest.ReviewDecision
189+
190+
if len(query.Repository.PullRequest.Commits.Nodes) > 0 {
191+
response.Data.Repository.PullRequest.Commits.Nodes = make([]struct {
192+
Commit struct {
193+
StatusCheckRollup *struct {
194+
State string `json:"state"`
195+
} `json:"statusCheckRollup"`
196+
} `json:"commit"`
197+
}, len(query.Repository.PullRequest.Commits.Nodes))
198+
199+
for i, node := range query.Repository.PullRequest.Commits.Nodes {
200+
if node.Commit.StatusCheckRollup != nil {
201+
response.Data.Repository.PullRequest.Commits.Nodes[i].Commit.StatusCheckRollup = &struct {
202+
State string `json:"state"`
203+
}{
204+
State: node.Commit.StatusCheckRollup.State,
205+
}
206+
}
207+
}
208+
}
209+
210+
return response, nil
211+
}
212+
213+
// HasPassingCI checks if a pull request has passing CI
214+
func HasPassingCI(ctx context.Context, graphQlClient *api.GraphQLClient, owner, repo string, prNumber int) (bool, error) {
215+
// Check for context cancellation
216+
select {
217+
case <-ctx.Done():
218+
return false, ctx.Err()
219+
default:
220+
// Continue processing
221+
}
222+
223+
// Get PR status info using GraphQL
224+
response, err := GetPRStatusInfo(ctx, graphQlClient, owner, repo, prNumber)
225+
if err != nil {
226+
return false, err
227+
}
228+
229+
// Get the commit status check info
230+
commits := response.Data.Repository.PullRequest.Commits.Nodes
231+
if len(commits) == 0 {
232+
Logger.Debug("No commits found for PR", "repo", fmt.Sprintf("%s/%s", owner, repo), "pr", prNumber)
233+
return false, nil
234+
}
235+
236+
// Get status check info
237+
statusCheckRollup := commits[0].Commit.StatusCheckRollup
238+
if statusCheckRollup == nil {
239+
Logger.Debug("No status checks found for PR", "repo", fmt.Sprintf("%s/%s", owner, repo), "pr", prNumber)
240+
return true, nil // If no checks defined, consider it passing
241+
}
242+
243+
// Check if status is SUCCESS
244+
if statusCheckRollup.State != "SUCCESS" {
245+
Logger.Debug("PR failed CI check", "repo", fmt.Sprintf("%s/%s", owner, repo), "pr", prNumber, "status", statusCheckRollup.State)
246+
return false, nil
247+
}
248+
249+
return true, nil
250+
}
251+
252+
// HasApproval checks if a pull request has been approved
253+
func HasApproval(ctx context.Context, graphQlClient *api.GraphQLClient, owner, repo string, prNumber int) (bool, error) {
254+
// Check for context cancellation
255+
select {
256+
case <-ctx.Done():
257+
return false, ctx.Err()
258+
default:
259+
// Continue processing
260+
}
261+
262+
// Get PR status info using GraphQL
263+
response, err := GetPRStatusInfo(ctx, graphQlClient, owner, repo, prNumber)
264+
if err != nil {
265+
return false, err
266+
}
267+
268+
reviewDecision := response.Data.Repository.PullRequest.ReviewDecision
269+
Logger.Debug("PR review decision", "repo", fmt.Sprintf("%s/%s", owner, repo), "pr", prNumber, "decision", reviewDecision)
270+
271+
// Check the review decision
272+
switch reviewDecision {
273+
case "APPROVED":
274+
return true, nil
275+
case "": // When no reviews are required
276+
Logger.Debug("PR has no required reviewers", "repo", fmt.Sprintf("%s/%s", owner, repo), "pr", prNumber)
277+
return true, nil // If no reviews required, consider it approved
278+
default:
279+
// Any other decision (REVIEW_REQUIRED, CHANGES_REQUESTED, etc.)
280+
Logger.Debug("PR not approved", "repo", fmt.Sprintf("%s/%s", owner, repo), "pr", prNumber, "decision", reviewDecision)
281+
return false, nil
282+
}
283+
}
284+
285+
// PrMeetsRequirements checks if a PR meets additional requirements beyond basic criteria
286+
func PrMeetsRequirements(ctx context.Context, graphQlClient *api.GraphQLClient, owner, repo string, prNumber int) (bool, error) {
287+
// If no additional requirements are specified, the PR meets requirements
288+
if !requireCI && !mustBeApproved {
289+
return true, nil
290+
}
291+
292+
// Check for context cancellation
293+
select {
294+
case <-ctx.Done():
295+
return false, ctx.Err()
296+
default:
297+
// Continue processing
298+
}
299+
300+
// Check CI status if required
301+
if requireCI {
302+
passing, err := HasPassingCI(ctx, graphQlClient, owner, repo, prNumber)
303+
if err != nil {
304+
return false, err
305+
}
306+
if !passing {
307+
return false, nil
308+
}
309+
}
310+
311+
// Check approval status if required
312+
if mustBeApproved {
313+
approved, err := HasApproval(ctx, graphQlClient, owner, repo, prNumber)
314+
if err != nil {
315+
return false, err
316+
}
317+
if !approved {
318+
return false, nil
319+
}
320+
}
321+
322+
return true, nil
323+
}

internal/cmd/root.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ func runCombine(cmd *cobra.Command, args []string) error {
163163
func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string) error {
164164
// Create GitHub API client
165165
restClient, err := api.DefaultRESTClient()
166+
graphQlClient, err := api.DefaultGraphQLClient()
166167
if err != nil {
167168
return fmt.Errorf("failed to create REST client: %w", err)
168169
}
@@ -180,7 +181,7 @@ func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string
180181
Logger.Debug("Processing repository", "repo", repo)
181182

182183
// Process the repository
183-
if err := processRepository(ctx, restClient, spinner, repo); err != nil {
184+
if err := processRepository(ctx, restClient, graphQlClient, spinner, repo); err != nil {
184185
if ctx.Err() != nil {
185186
// If the context was cancelled, stop processing
186187
return ctx.Err()
@@ -195,7 +196,7 @@ func executeCombineCommand(ctx context.Context, spinner *Spinner, repos []string
195196
}
196197

197198
// processRepository handles a single repository's PRs
198-
func processRepository(ctx context.Context, client *api.RESTClient, spinner *Spinner, repo string) error {
199+
func processRepository(ctx context.Context, client *api.RESTClient, graphQlClient *api.GraphQLClient, spinner *Spinner, repo string) error {
199200
// Parse owner and repo name
200201
parts := strings.Split(repo, "/")
201202
if len(parts) != 2 {
@@ -259,7 +260,17 @@ func processRepository(ctx context.Context, client *api.RESTClient, spinner *Spi
259260
continue
260261
}
261262

262-
// TODO: Implement CI/approval status checking
263+
// Check if PR meets additional requirements (CI, approval)
264+
meetsRequirements, err := PrMeetsRequirements(ctx, graphQlClient, owner, repoName, pull.Number)
265+
if err != nil {
266+
Logger.Warn("Failed to check PR requirements", "repo", repo, "pr", pull.Number, "error", err)
267+
continue
268+
}
269+
270+
if !meetsRequirements {
271+
// Skip this PR as it doesn't meet CI/approval requirements
272+
continue
273+
}
263274

264275
matchedPRs = append(matchedPRs, struct {
265276
Number int

0 commit comments

Comments
 (0)