@@ -17,31 +17,151 @@ package githubrepo
1717import (
1818 "context"
1919 "fmt"
20+ "strings"
21+ "sync"
2022
2123 "github.com/google/go-github/v38/github"
24+ "github.com/shurcooL/githubv4"
2225
2326 "github.com/ossf/scorecard/v4/clients"
2427 sce "github.com/ossf/scorecard/v4/errors"
28+ "github.com/ossf/scorecard/v4/log"
2529)
2630
31+ //nolint:govet
32+ type checkRunsGraphqlData struct {
33+ Repository struct {
34+ Object struct {
35+ Commit struct {
36+ History struct {
37+ Nodes []struct {
38+ AssociatedPullRequests struct {
39+ Nodes []struct {
40+ HeadRefOid githubv4.String
41+ Commits struct {
42+ Nodes []struct {
43+ Commit struct {
44+ CheckSuites struct {
45+ Nodes []struct {
46+ App struct {
47+ Slug githubv4.String
48+ }
49+ Conclusion githubv4.CheckConclusionState
50+ Status githubv4.CheckStatusState
51+ }
52+ } `graphql:"checkSuites(first: $checksToAnalyze)"`
53+ }
54+ }
55+ } `graphql:"commits(last:1)"`
56+ }
57+ } `graphql:"associatedPullRequests(first: $pullRequestsToAnalyze)"`
58+ }
59+ } `graphql:"history(first: $commitsToAnalyze)"`
60+ } `graphql:"... on Commit"`
61+ } `graphql:"object(expression: $commitExpression)"`
62+ } `graphql:"repository(owner: $owner, name: $name)"`
63+ RateLimit struct {
64+ Cost * int
65+ }
66+ }
67+
68+ type checkRunsByRef = map [string ][]clients.CheckRun
69+
2770type checkrunsHandler struct {
28- client * github.Client
29- ctx context.Context
30- repourl * repoURL
71+ client * github.Client
72+ graphClient * githubv4.Client
73+ ctx context.Context
74+ repourl * repoURL
75+ commitDepth int
76+ logger * log.Logger
77+ checkData * checkRunsGraphqlData
78+ checkRunsByRef checkRunsByRef
79+ setupOnce * sync.Once
80+ errSetup error
3181}
3282
33- func (handler * checkrunsHandler ) init (ctx context.Context , repourl * repoURL ) {
83+ func (handler * checkrunsHandler ) init (ctx context.Context , repourl * repoURL , commitDepth int ) {
3484 handler .ctx = ctx
3585 handler .repourl = repourl
86+ handler .commitDepth = commitDepth
87+ handler .logger = log .NewLogger (log .DefaultLevel )
88+ handler .checkData = new (checkRunsGraphqlData )
89+ handler .setupOnce = new (sync.Once )
90+ handler .checkRunsByRef = checkRunsByRef {}
91+ }
92+
93+ func (handler * checkrunsHandler ) setup () error {
94+ handler .setupOnce .Do (func () {
95+ commitExpression := handler .repourl .commitExpression ()
96+ vars := map [string ]interface {}{
97+ "owner" : githubv4 .String (handler .repourl .owner ),
98+ "name" : githubv4 .String (handler .repourl .repo ),
99+ "pullRequestsToAnalyze" : githubv4 .Int (pullRequestsToAnalyze ),
100+ "commitsToAnalyze" : githubv4 .Int (handler .commitDepth ),
101+ "commitExpression" : githubv4 .String (commitExpression ),
102+ "checksToAnalyze" : githubv4 .Int (checksToAnalyze ),
103+ }
104+ // TODO(#2224):
105+ // sast and ci checks causes cache miss if commits dont match number of check runs.
106+ // paging for this needs to be implemented if using higher than 100 --number-of-commits
107+ if handler .commitDepth > 99 {
108+ vars ["commitsToAnalyze" ] = githubv4 .Int (99 )
109+ }
110+ if err := handler .graphClient .Query (handler .ctx , handler .checkData , vars ); err != nil {
111+ // quit early without setting crsErrSetup for "Resource not accessible by integration" error
112+ // for whatever reason, this check doesn't work with a GITHUB_TOKEN, only a PAT
113+ if strings .Contains (err .Error (), "Resource not accessible by integration" ) {
114+ return
115+ }
116+ handler .errSetup = err
117+ return
118+ }
119+ handler .checkRunsByRef = parseCheckRuns (handler .checkData )
120+ })
121+ return handler .errSetup
36122}
37123
38124func (handler * checkrunsHandler ) listCheckRunsForRef (ref string ) ([]clients.CheckRun , error ) {
125+ if err := handler .setup (); err != nil {
126+ return nil , fmt .Errorf ("error during graphqlHandler.setupCheckRuns: %w" , err )
127+ }
128+ if crs , ok := handler .checkRunsByRef [ref ]; ok {
129+ return crs , nil
130+ }
131+ msg := fmt .Sprintf ("listCheckRunsForRef cache miss: %s/%s:%s" , handler .repourl .owner , handler .repourl .repo , ref )
132+ handler .logger .Info (msg )
133+
39134 checkRuns , _ , err := handler .client .Checks .ListCheckRunsForRef (
40135 handler .ctx , handler .repourl .owner , handler .repourl .repo , ref , & github.ListCheckRunsOptions {})
41136 if err != nil {
42137 return nil , sce .WithMessage (sce .ErrScorecardInternal , fmt .Sprintf ("ListCheckRunsForRef: %v" , err ))
43138 }
44- return checkRunsFrom (checkRuns ), nil
139+ handler .checkRunsByRef [ref ] = checkRunsFrom (checkRuns )
140+ return handler .checkRunsByRef [ref ], nil
141+ }
142+
143+ func parseCheckRuns (data * checkRunsGraphqlData ) checkRunsByRef {
144+ checkCache := checkRunsByRef {}
145+ for _ , commit := range data .Repository .Object .Commit .History .Nodes {
146+ for _ , pr := range commit .AssociatedPullRequests .Nodes {
147+ var crs []clients.CheckRun
148+ for _ , c := range pr .Commits .Nodes {
149+ for _ , checkRun := range c .Commit .CheckSuites .Nodes {
150+ crs = append (crs , clients.CheckRun {
151+ // the REST API returns lowercase. the graphQL API returns upper
152+ Status : strings .ToLower (string (checkRun .Status )),
153+ Conclusion : strings .ToLower (string (checkRun .Conclusion )),
154+ App : clients.CheckRunApp {
155+ Slug : string (checkRun .App .Slug ),
156+ },
157+ })
158+ }
159+ }
160+ headRef := string (pr .HeadRefOid )
161+ checkCache [headRef ] = crs
162+ }
163+ }
164+ return checkCache
45165}
46166
47167func checkRunsFrom (data * github.ListCheckRunsResults ) []clients.CheckRun {
0 commit comments