@@ -16,6 +16,7 @@ package githubrepo
1616
1717import (
1818 "context"
19+ "errors"
1920 "fmt"
2021 "strings"
2122 "sync"
@@ -25,18 +26,22 @@ import (
2526
2627 "github.com/ossf/scorecard/v4/clients"
2728 sce "github.com/ossf/scorecard/v4/errors"
29+ "github.com/ossf/scorecard/v4/log"
2830)
2931
3032const (
3133 pullRequestsToAnalyze = 1
34+ checksToAnalyze = 30
3235 issuesToAnalyze = 30
3336 issueCommentsToAnalyze = 30
3437 reviewsToAnalyze = 30
3538 labelsToAnalyze = 30
3639 commitsToAnalyze = 30
3740)
3841
39- //nolint: govet
42+ var errNotCached = errors .New ("result not cached" )
43+
44+ //nolint:govet
4045type graphqlData struct {
4146 Repository struct {
4247 IsArchived githubv4.Boolean
@@ -121,34 +126,77 @@ type graphqlData struct {
121126 }
122127}
123128
129+ //nolint:govet
130+ type checkRunsGraphqlData struct {
131+ Repository struct {
132+ Object struct {
133+ Commit struct {
134+ History struct {
135+ Nodes []struct {
136+ AssociatedPullRequests struct {
137+ Nodes []struct {
138+ HeadRefOid githubv4.String
139+ Commits struct {
140+ Nodes []struct {
141+ Commit struct {
142+ CheckSuites struct {
143+ Nodes []struct {
144+ App struct {
145+ Slug githubv4.String
146+ }
147+ Conclusion githubv4.CheckConclusionState
148+ Status githubv4.CheckStatusState
149+ }
150+ } `graphql:"checkSuites(first: $checksToAnalyze)"`
151+ }
152+ }
153+ } `graphql:"commits(last:1)"`
154+ }
155+ } `graphql:"associatedPullRequests(first: $pullRequestsToAnalyze)"`
156+ }
157+ } `graphql:"history(first: $commitsToAnalyze)"`
158+ } `graphql:"... on Commit"`
159+ } `graphql:"object(expression: $commitExpression)"`
160+ } `graphql:"repository(owner: $owner, name: $name)"`
161+ RateLimit struct {
162+ Cost * int
163+ }
164+ }
165+
166+ type checkRunCache = map [string ][]clients.CheckRun
167+
124168type graphqlHandler struct {
125- client * githubv4.Client
126- data * graphqlData
127- once * sync.Once
128- ctx context.Context
129- errSetup error
130- repourl * repoURL
131- commits []clients.Commit
132- issues []clients.Issue
133- archived bool
169+ checkRuns checkRunCache
170+ client * githubv4.Client
171+ data * graphqlData
172+ setupOnce * sync.Once
173+ checkData * checkRunsGraphqlData
174+ setupCheckRunsOnce * sync.Once
175+ errSetupCheckRuns error
176+ logger * log.Logger
177+ ctx context.Context
178+ errSetup error
179+ repourl * repoURL
180+ commits []clients.Commit
181+ issues []clients.Issue
182+ archived bool
134183}
135184
136185func (handler * graphqlHandler ) init (ctx context.Context , repourl * repoURL ) {
137186 handler .ctx = ctx
138187 handler .repourl = repourl
139188 handler .data = new (graphqlData )
140189 handler .errSetup = nil
141- handler .once = new (sync.Once )
190+ handler .setupOnce = new (sync.Once )
191+ handler .checkData = new (checkRunsGraphqlData )
192+ handler .setupCheckRunsOnce = new (sync.Once )
193+ handler .checkRuns = checkRunCache {}
194+ handler .logger = log .NewLogger (log .DefaultLevel )
142195}
143196
144197func (handler * graphqlHandler ) setup () error {
145- handler .once .Do (func () {
146- commitExpression := handler .repourl .commitSHA
147- if strings .EqualFold (handler .repourl .commitSHA , clients .HeadSHA ) {
148- // TODO(#575): Confirm that this works as expected.
149- commitExpression = fmt .Sprintf ("heads/%s" , handler .repourl .defaultBranch )
150- }
151-
198+ handler .setupOnce .Do (func () {
199+ commitExpression := handler .commitExpression ()
152200 vars := map [string ]interface {}{
153201 "owner" : githubv4 .String (handler .repourl .owner ),
154202 "name" : githubv4 .String (handler .repourl .repo ),
@@ -174,13 +222,54 @@ func (handler *graphqlHandler) setup() error {
174222 return handler .errSetup
175223}
176224
225+ func (handler * graphqlHandler ) setupCheckRuns () error {
226+ handler .setupCheckRunsOnce .Do (func () {
227+ commitExpression := handler .commitExpression ()
228+ vars := map [string ]interface {}{
229+ "owner" : githubv4 .String (handler .repourl .owner ),
230+ "name" : githubv4 .String (handler .repourl .repo ),
231+ "pullRequestsToAnalyze" : githubv4 .Int (pullRequestsToAnalyze ),
232+ "commitsToAnalyze" : githubv4 .Int (commitsToAnalyze ),
233+ "commitExpression" : githubv4 .String (commitExpression ),
234+ "checksToAnalyze" : githubv4 .Int (checksToAnalyze ),
235+ }
236+ if err := handler .client .Query (handler .ctx , handler .checkData , vars ); err != nil {
237+ // quit early without setting crsErrSetup for "Resource not accessible by integration" error
238+ // for whatever reason, this check doesn't work with a GITHUB_TOKEN, only a PAT
239+ if strings .Contains (err .Error (), "Resource not accessible by integration" ) {
240+ return
241+ }
242+ handler .errSetupCheckRuns = err
243+ return
244+ }
245+ handler .checkRuns = parseCheckRuns (handler .checkData )
246+ })
247+ return handler .errSetupCheckRuns
248+ }
249+
177250func (handler * graphqlHandler ) getCommits () ([]clients.Commit , error ) {
178251 if err := handler .setup (); err != nil {
179252 return nil , fmt .Errorf ("error during graphqlHandler.setup: %w" , err )
180253 }
181254 return handler .commits , nil
182255}
183256
257+ func (handler * graphqlHandler ) cacheCheckRunsForRef (ref string , crs []clients.CheckRun ) {
258+ handler .checkRuns [ref ] = crs
259+ }
260+
261+ func (handler * graphqlHandler ) listCheckRunsForRef (ref string ) ([]clients.CheckRun , error ) {
262+ if err := handler .setupCheckRuns (); err != nil {
263+ return nil , fmt .Errorf ("error during graphqlHandler.setupCheckRuns: %w" , err )
264+ }
265+ if crs , ok := handler .checkRuns [ref ]; ok {
266+ return crs , nil
267+ }
268+ msg := fmt .Sprintf ("listCheckRunsForRef cache miss: %s/%s:%s" , handler .repourl .owner , handler .repourl .repo , ref )
269+ handler .logger .Info (msg )
270+ return nil , errNotCached
271+ }
272+
184273func (handler * graphqlHandler ) getIssues () ([]clients.Issue , error ) {
185274 if ! strings .EqualFold (handler .repourl .commitSHA , clients .HeadSHA ) {
186275 return nil , fmt .Errorf ("%w: ListIssues only supported for HEAD queries" , clients .ErrUnsupportedFeature )
@@ -201,7 +290,39 @@ func (handler *graphqlHandler) isArchived() (bool, error) {
201290 return handler .archived , nil
202291}
203292
204- //nolint
293+ func (handler * graphqlHandler ) commitExpression () string {
294+ if strings .EqualFold (handler .repourl .commitSHA , clients .HeadSHA ) {
295+ // TODO(#575): Confirm that this works as expected.
296+ return fmt .Sprintf ("heads/%s" , handler .repourl .defaultBranch )
297+ }
298+ return handler .repourl .commitSHA
299+ }
300+
301+ func parseCheckRuns (data * checkRunsGraphqlData ) checkRunCache {
302+ checkCache := checkRunCache {}
303+ for _ , commit := range data .Repository .Object .Commit .History .Nodes {
304+ for _ , pr := range commit .AssociatedPullRequests .Nodes {
305+ var crs []clients.CheckRun
306+ for _ , c := range pr .Commits .Nodes {
307+ for _ , checkRun := range c .Commit .CheckSuites .Nodes {
308+ crs = append (crs , clients.CheckRun {
309+ // the REST API returns lowercase. the graphQL API returns upper
310+ Status : strings .ToLower (string (checkRun .Status )),
311+ Conclusion : strings .ToLower (string (checkRun .Conclusion )),
312+ App : clients.CheckRunApp {
313+ Slug : string (checkRun .App .Slug ),
314+ },
315+ })
316+ }
317+ }
318+ headRef := string (pr .HeadRefOid )
319+ checkCache [headRef ] = crs
320+ }
321+ }
322+ return checkCache
323+ }
324+
325+ //nolint:all
205326func commitsFrom (data * graphqlData , repoOwner , repoName string ) ([]clients.Commit , error ) {
206327 ret := make ([]clients.Commit , 0 )
207328 for _ , commit := range data .Repository .Object .Commit .History .Nodes {
0 commit comments