Skip to content

Commit 7becd0d

Browse files
kpauljosephJayashree138shreyasHpandyaAgrek11ANIRUDH-333
authored andcommitted
feat: add retry logic to scorecard API for commit-specific queries (#13)
feat: retry logic, normalizeRepoName to scorecard api when querying the Scorecard API with a specific commitSHA that returns 404, retry the request without the commit parameter to get the latest scorecard for the default branch. extract fetchFromAPI helper for HTTP request logic add retry-without-commit logic in getScoreFromAPI update tests to cover retry behavior and edge cases --------- Co-authored-by: Jayashree O <jaishu138@gmail.com> Co-authored-by: Shreyas Pandya <pandyashreyas1@gmail.com> Co-authored-by: Abhishek Agrawal <abhishek.yours4@gmail.com> Co-authored-by: Anirudh Edpuganti <aniedpuganti@gmail.com> Signed-off-by: Paul Joseph <k.paul.joseph@gmail.com>
1 parent c07abf8 commit 7becd0d

File tree

2 files changed

+75
-29
lines changed

2 files changed

+75
-29
lines changed

pkg/certifier/scorecard/scorecardRunner.go

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,38 @@ package scorecard
1818
import (
1919
"context"
2020
"fmt"
21-
"io"
22-
"net/http"
23-
"net/url"
24-
"os"
25-
"time"
26-
2721
"github.com/guacsec/guac/pkg/logging"
2822
"github.com/ossf/scorecard/v4/checker"
2923
"github.com/ossf/scorecard/v4/checks"
3024
"github.com/ossf/scorecard/v4/log"
3125
sc "github.com/ossf/scorecard/v4/pkg"
26+
"io"
27+
"net/http"
28+
"net/url"
29+
"os"
30+
"strings"
31+
"time"
3232
)
3333

34+
const githubPrefix = "github.com/"
35+
3436
// scorecardRunner is a struct that implements the Scorecard interface.
3537
type scorecardRunner struct {
3638
ctx context.Context
3739
}
3840

41+
// normalizeRepoName ensures the repo name has the github.com/ prefix required by the Scorecard API.
42+
// If the prefix is missing, it is added. If the repo name already has the prefix, it is returned as-is.
43+
func normalizeRepoName(repoName string) string {
44+
if strings.HasPrefix(repoName, githubPrefix) {
45+
return repoName
46+
}
47+
return githubPrefix + repoName
48+
}
49+
3950
func (s scorecardRunner) GetScore(repoName, commitSHA, tag string) (*sc.ScorecardResult, error) {
4051
logger := logging.FromContext(s.ctx)
52+
repoName = normalizeRepoName(repoName)
4153

4254
// First try API approach
4355
logger.Debugf("Attempting to fetch scorecard from API for repo: %s, commit: %s", repoName, commitSHA)
@@ -67,30 +79,44 @@ func (s scorecardRunner) GetScore(repoName, commitSHA, tag string) (*sc.Scorecar
6779
func (s scorecardRunner) getScoreFromAPI(repoName, commitSHA, tag string) (*sc.ScorecardResult, error) {
6880
logger := logging.FromContext(s.ctx)
6981

70-
// The Scorecard API only supports commit SHAs, not tags.
71-
// If a tag is provided without a commitSHA, we cannot use the API
72-
// and must fall back to local computation to avoid returning incorrect results.
82+
// If tag is provided without a valid commitSHA, skip API and use local computation
83+
// The API cannot resolve tags, but computeScore can look up the commit for a tag
7384
if (commitSHA == "" || commitSHA == "HEAD") && tag != "" {
74-
logger.Debugf("Cannot use API for tag %s without commit SHA - will fall back to local computation", tag)
75-
return nil, fmt.Errorf("scorecard API does not support tags; commit SHA required for tag %s", tag)
85+
logger.Debugf("Tag %s provided without commit SHA - skipping API, will use local computation", tag)
86+
return nil, fmt.Errorf("tag provided without commit SHA; falling back to local computation for tag %s", tag)
7687
}
7788

78-
url, err := url.JoinPath("https://api.securityscorecards.dev", "projects", repoName)
89+
baseURL, err := url.JoinPath("https://api.securityscorecards.dev", "projects", repoName)
7990
if err != nil {
8091
return nil, err
8192
}
8293

94+
// If commitSHA is provided, try with it first
8395
if commitSHA != "" && commitSHA != "HEAD" {
84-
url += "?commit=" + commitSHA
96+
urlWithCommit := baseURL + "?commit=" + commitSHA
97+
result, err := s.fetchFromAPI(urlWithCommit)
98+
if err == nil {
99+
return result, nil
100+
}
101+
logger.Debugf("API call with commit %s failed, retrying without commit: %v", commitSHA, err)
85102
}
86103

87-
logger.Debugf("Making API request to: %s", url)
104+
result, err := s.fetchFromAPI(baseURL)
105+
if err != nil {
106+
return nil, err
107+
}
108+
return result, nil
109+
}
110+
111+
func (s scorecardRunner) fetchFromAPI(apiURL string) (*sc.ScorecardResult, error) {
112+
logger := logging.FromContext(s.ctx)
113+
logger.Debugf("Making API request to: %s", apiURL)
88114

89115
httpClient := &http.Client{
90116
Timeout: 30 * time.Second,
91117
}
92118

93-
req, err := http.NewRequestWithContext(s.ctx, http.MethodGet, url, nil)
119+
req, err := http.NewRequestWithContext(s.ctx, http.MethodGet, apiURL, nil)
94120
if err != nil {
95121
return nil, fmt.Errorf("failed to create request: %w", err)
96122
}
@@ -111,7 +137,7 @@ func (s scorecardRunner) getScoreFromAPI(repoName, commitSHA, tag string) (*sc.S
111137
logger.Debugf("API response status code: %d", resp.StatusCode)
112138

113139
if resp.StatusCode == http.StatusNotFound {
114-
return nil, fmt.Errorf("Scorecard for repo %s not found in scorecard API", repoName)
140+
return nil, fmt.Errorf("scorecard not found in API")
115141
}
116142

117143
if resp.StatusCode >= 400 {
@@ -198,4 +224,4 @@ func NewScorecardRunner(ctx context.Context) (Scorecard, error) {
198224
return scorecardRunner{
199225
ctx,
200226
}, nil
201-
}
227+
}

pkg/certifier/scorecard/scorecardRunner_test.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ func Test_scorecardRunner_GetScore(t *testing.T) {
6666
}
6767
}
6868

69-
// Test_scorecardRunner_getScoreFromAPI tests the early return logic
70-
// for tags without commit SHAs, which doesn't require network access.
69+
// Test_scorecardRunner_getScoreFromAPI tests the API fetch logic with retry behavior.
70+
// Tests that require network access use a well-known repo (ossf/scorecard).
7171

7272
func Test_scorecardRunner_getScoreFromAPI(t *testing.T) {
7373
tests := []struct {
@@ -79,20 +79,42 @@ func Test_scorecardRunner_getScoreFromAPI(t *testing.T) {
7979
errContains string
8080
}{
8181
{
82-
name: "tag without commit SHA returns error",
83-
repoName: "test/repo",
82+
name: "valid repo without commit returns latest scorecard",
83+
repoName: "github.com/ossf/scorecard",
84+
commitSHA: "",
85+
tag: "",
86+
wantErr: false,
87+
},
88+
{
89+
name: "tag without commit SHA skips API for local computation",
90+
repoName: "github.com/ossf/scorecard",
8491
commitSHA: "",
85-
tag: "v1.0.0",
92+
tag: "v4.10.4",
8693
wantErr: true,
87-
errContains: "scorecard API does not support tags",
94+
errContains: "tag provided without commit SHA",
8895
},
8996
{
90-
name: "tag with HEAD commit SHA returns error",
91-
repoName: "test/repo",
97+
name: "tag with HEAD skips API for local computation",
98+
repoName: "github.com/ossf/scorecard",
9299
commitSHA: "HEAD",
93-
tag: "v1.0.0",
100+
tag: "v4.10.4",
101+
wantErr: true,
102+
errContains: "tag provided without commit SHA",
103+
},
104+
{
105+
name: "non-existent repo returns error",
106+
repoName: "github.com/nonexistent/nonexistent-repo-12345",
107+
commitSHA: "",
108+
tag: "",
94109
wantErr: true,
95-
errContains: "scorecard API does not support tags",
110+
errContains: "scorecard not found in API",
111+
},
112+
{
113+
name: "invalid commit SHA falls back to latest scorecard",
114+
repoName: "github.com/ossf/scorecard",
115+
commitSHA: "0000000000000000000000000000000000000000",
116+
tag: "",
117+
wantErr: false,
96118
},
97119
}
98120

@@ -101,8 +123,6 @@ func Test_scorecardRunner_getScoreFromAPI(t *testing.T) {
101123
ctx := context.Background()
102124
runner := scorecardRunner{ctx: ctx}
103125

104-
// Run the actual API call - these tests only cover edge cases
105-
// that return early without making network requests
106126
got, err := runner.getScoreFromAPI(tt.repoName, tt.commitSHA, tt.tag)
107127

108128
if (err != nil) != tt.wantErr {

0 commit comments

Comments
 (0)