Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 217 additions & 20 deletions tools/codegen/cmd/featuregate-test-analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const (
// required pass rate.
// nearly all current tests pass 99% of the time, but in a two week window we lack enough data to say.
requiredPassRateOfTestsPerVariant = 0.95

// required pass rate for "install should succeed" test
requiredPassRateForInstallTest = 1.0
Comment thread
sadasu marked this conversation as resolved.
Outdated
)

type FeatureGateTestAnalyzerOptions struct {
Expand Down Expand Up @@ -181,7 +184,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
clusterProfiles := recentlyEnabledFeatureGatesToClusterProfiles[enabledFeatureGate]
md.Title(1, enabledFeatureGate)

testingResults, err := listTestResultFor(enabledFeatureGate, clusterProfiles)
testingResults, installTestLevelData, err := listTestResultFor(enabledFeatureGate, clusterProfiles)
if err != nil {
return err
}
Expand All @@ -190,7 +193,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {

validationResults := checkIfTestingIsSufficient(enabledFeatureGate, testingResults)

// Separate warnings from blocking errors
// Separate warnings and blocking errors
blockingErrors := []error{}
warnings := []error{}
for _, vr := range validationResults {
Expand All @@ -201,24 +204,62 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error {
}
}

// For Install feature gates, report "install should succeed: overall" test statistics first
if strings.Contains(enabledFeatureGate, "Install") {
md.Text("")
fmt.Fprintf(o.Out, "\n")
md.Textf("**Install test statistics for \"install should succeed: overall\":**\n")
fmt.Fprintf(o.Out, "Install test statistics for \"install should succeed: overall\":\n")
jobVariants := make([]JobVariant, 0, len(testingResults))
for jobVariant := range testingResults {
jobVariants = append(jobVariants, jobVariant)
}
sort.Slice(jobVariants, func(i, j int) bool {
return jobVariants[i].String() < jobVariants[j].String()
})
for _, jobVariant := range jobVariants {
installTest := installTestLevelData[jobVariant]
if installTest == nil {
md.Textf(" - %v: test not found\n", jobVariant)
fmt.Fprintf(o.Out, " %v: test not found\n", jobVariant)
} else if installTest.TotalRuns > 0 {
passPercent := float32(installTest.SuccessfulRuns) / float32(installTest.TotalRuns)
displayActual := int(passPercent * 100)
md.Textf(" - %v: passed %d%% (%d/%d runs)\n", jobVariant, displayActual, installTest.SuccessfulRuns, installTest.TotalRuns)
fmt.Fprintf(o.Out, " %v: passed %d%% (%d/%d runs)\n", jobVariant, displayActual, installTest.SuccessfulRuns, installTest.TotalRuns)
} else {
md.Textf(" - %v: 0 runs\n", jobVariant)
fmt.Fprintf(o.Out, " %v: 0 runs\n", jobVariant)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
md.Text("")
fmt.Fprintf(o.Out, "\n")
}

if len(validationResults) == 0 {
md.Textf("Sufficient CI testing for %q.\n", enabledFeatureGate)
fmt.Fprintf(o.Out, "Sufficient CI testing for %q.\n", enabledFeatureGate)
} else {
if len(blockingErrors) > 0 {
md.Textf("INSUFFICIENT CI testing for %q.\n", enabledFeatureGate)
fmt.Fprintf(o.Out, "INSUFFICIENT CI testing for %q.\n", enabledFeatureGate)
} else {
} else if len(warnings) > 0 {
md.Textf("CI testing issues found for %q (non-blocking warnings).\n", enabledFeatureGate)
fmt.Fprintf(o.Out, "CI testing issues found for %q (non-blocking warnings).\n", enabledFeatureGate)
} else {
md.Textf("Sufficient CI testing for %q.\n", enabledFeatureGate)
fmt.Fprintf(o.Out, "Sufficient CI testing for %q.\n", enabledFeatureGate)
}

md.Textf("* At least five tests are expected for a feature\n")
md.Textf("* Tests must be be run on every TechPreview platform (ask for an exception if your feature doesn't support a variant)")
md.Textf("* All tests must run at least 14 times on every platform")
md.Textf("* All tests must pass at least 95%% of the time")
md.Textf("* JobTier must be one of: standard, informing, blocking, candidate (candidate is allowed but produces a warning as it is not covered by Component Readiness)\n")
md.Text("")
if len(blockingErrors) > 0 || len(warnings) > 0 {
md.Textf("* At least five tests are expected for a feature\n")
md.Textf("* Tests must be be run on every TechPreview platform (ask for an exception if your feature doesn't support a variant)")
md.Textf("* All tests must run at least 14 times on every platform")
md.Textf("* All tests must pass at least 95%% of the time")
md.Textf("* For Install feature gates, the \"install should succeed: overall\" test must pass at least 100%% of the time")
md.Textf("* JobTier must be one of: standard, informing, blocking, candidate (candidate is allowed but produces a warning as it is not covered by Component Readiness)\n")
md.Text("")
}

if len(warnings) > 0 {
md.Textf("**Non-blocking warnings (optional variants):**\n")
Expand Down Expand Up @@ -394,6 +435,11 @@ func checkIfTestingIsSufficient(featureGate string, testingResults map[JobVarian
}

for _, testResults := range testedVariant.TestResults {
// Skip "install should succeed: overall" for Install feature gates - it has special validation below
if strings.Contains(featureGate, "Install") && testResults.TestName == "install should succeed: overall" {
continue
}

if testResults.TotalRuns < requiredNumberOfTestRunsPerVariant {
results = append(results, ValidationResult{
Error: fmt.Errorf("error: %q only has %d runs, need at least %d runs for %q on %v",
Expand All @@ -415,6 +461,38 @@ func checkIfTestingIsSufficient(featureGate string, testingResults map[JobVarian
})
}
}

// For Install feature gates, validate "install should succeed: overall" test
if strings.Contains(featureGate, "Install") {
installTest := testResultByName(testedVariant.TestResults, "install should succeed: overall")
if installTest == nil {
results = append(results, ValidationResult{
Error: fmt.Errorf("warning: \"install should succeed: overall\" test not found for Install feature gate %q on %v",
featureGate, jobVariant),
IsWarning: true,
})
} else {
if installTest.TotalRuns < requiredNumberOfTestRunsPerVariant {
results = append(results, ValidationResult{
Error: fmt.Errorf("error: \"install should succeed: overall\" only has %d runs, need at least %d runs for %q on %v",
installTest.TotalRuns, requiredNumberOfTestRunsPerVariant, featureGate, jobVariant),
IsWarning: isOptional,
})
}
if installTest.TotalRuns > 0 {
passPercent := float32(installTest.SuccessfulRuns) / float32(installTest.TotalRuns)
if passPercent < requiredPassRateForInstallTest {
displayExpected := int(requiredPassRateForInstallTest * 100)
displayActual := int(passPercent * 100)
results = append(results, ValidationResult{
Error: fmt.Errorf("error: \"install should succeed: overall\" only passed %d%%, need at least %d%% for %q on %v",
displayActual, displayExpected, featureGate, jobVariant),
IsWarning: isOptional,
})
}
}
}
}
}

return results
Expand Down Expand Up @@ -622,6 +700,20 @@ type JobVariant struct {
Optional bool // If true, validation failures for this variant are non-blocking warnings
}

func (jv JobVariant) String() string {
result := fmt.Sprintf("cloud=%s arch=%s topology=%s", jv.Cloud, jv.Architecture, jv.Topology)
if jv.NetworkStack != "" {
result += fmt.Sprintf(" network=%s", jv.NetworkStack)
}
if jv.OS != "" {
result += fmt.Sprintf(" os=%s", jv.OS)
}
if jv.Optional {
result += " optional=true"
}
return result
}

type OrderedJobVariants []JobVariant

func (a OrderedJobVariants) Len() int { return len(a) }
Expand Down Expand Up @@ -694,6 +786,7 @@ type TestResults struct {
type ValidationResult struct {
Error error
IsWarning bool // if true, this is a non-blocking warning (for optional variants)
IsInfo bool // if true, this is informational telemetry (e.g., install test pass percentage)
}

func testResultByName(results []TestResults, testName string) *TestResults {
Expand Down Expand Up @@ -736,10 +829,11 @@ func validateJobTiers(jobVariant JobVariant) error {
return nil
}

func listTestResultFor(featureGate string, clusterProfiles sets.Set[string]) (map[JobVariant]*TestingResults, error) {
func listTestResultFor(featureGate string, clusterProfiles sets.Set[string]) (map[JobVariant]*TestingResults, map[JobVariant]*TestResults, error) {
fmt.Printf("Query sippy for all test run results for feature gate %q on clusterProfile %q\n", featureGate, sets.List(clusterProfiles))

results := map[JobVariant]*TestingResults{}
installTestLevelData := map[JobVariant]*TestResults{}

var jobVariantsToCheck []JobVariant
if clusterProfiles.Has("Hypershift") && !nonHypershiftPlatforms.MatchString(featureGate) {
Expand All @@ -760,19 +854,28 @@ func listTestResultFor(featureGate string, clusterProfiles sets.Set[string]) (ma
// Validate all variants before making expensive API calls
for _, jobVariant := range jobVariantsToCheck {
if err := validateJobTiers(jobVariant); err != nil {
return nil, err
return nil, nil, err
}
}

for _, jobVariant := range jobVariantsToCheck {
jobVariantResults, err := listTestResultForVariant(featureGate, jobVariant)
if err != nil {
return nil, err
return nil, nil, err
}
results[jobVariant] = jobVariantResults

// For Install feature gates, also get test-level data for "install should succeed: overall"
if strings.Contains(featureGate, "Install") {
installTestData, err := getInstallTestLevelData(featureGate, jobVariant)
if err != nil {
return nil, nil, err
}
installTestLevelData[jobVariant] = installTestData
}
}

return results, nil
return results, installTestLevelData, nil
}

func filterVariants(featureGate string, variantsList ...[]JobVariant) []JobVariant {
Expand Down Expand Up @@ -862,15 +965,111 @@ func getRelease() (string, error) {
return getLatestRelease()
}

func listTestResultForVariant(featureGate string, jobVariant JobVariant) (*TestingResults, error) {
// Substring here matches for both [OCPFeatureGate:...] and [FeatureGate:...]
testPattern := fmt.Sprintf("FeatureGate:%s]", featureGate)
func getInstallTestLevelData(featureGate string, jobVariant JobVariant) (*TestResults, error) {
testPattern := "install should succeed: overall"
queries := sippy.QueriesForWithCapability(jobVariant.Cloud, jobVariant.Architecture, jobVariant.Topology,
jobVariant.NetworkStack, jobVariant.OS, jobVariant.JobTiers, testPattern, featureGate)

defaultTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}

sippyClient := &http.Client{
Timeout: 2 * time.Minute,
Transport: defaultTransport,
}
Comment on lines +1071 to +1086

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first.
ast-grep outline tools/codegen/cmd/featuregate-test-analyzer.go --view expanded > /tmp/outline.txt
sed -n '1,200p' /tmp/outline.txt

# Read the relevant section around the transport/client setup.
sed -n '900,1040p' tools/codegen/cmd/featuregate-test-analyzer.go

# Find where sippyClient or defaultTransport is used.
rg -n "sippyClient|defaultTransport|InsecureSkipVerify|TLSClientConfig|ProxyFromEnvironment" tools/codegen/cmd/featuregate-test-analyzer.go

Repository: openshift/api

Length of output: 9751


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant slice with line numbers.
nl -ba tools/codegen/cmd/featuregate-test-analyzer.go | sed -n '930,1025p'

# Locate all references to Sippy-related networking in the repo.
rg -n "sippy|InsecureSkipVerify|TLSClientConfig" tools/codegen/cmd -g '!**/*_test.go'

Repository: openshift/api

Length of output: 191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('tools/codegen/cmd/featuregate-test-analyzer.go')
text = p.read_text()
for key in ['InsecureSkipVerify', 'sippyClient', 'defaultTransport']:
    print(f'## {key}')
    for i, line in enumerate(text.splitlines(), 1):
        if key in line:
            start = max(1, i-8)
            end = min(len(text.splitlines()), i+12)
            for j in range(start, end+1):
                print(f'{j:4d}: {text.splitlines()[j-1]}')
            print()
            break
PY

Repository: openshift/api

Length of output: 2425


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any trusted-root configuration or explanatory comments near Sippy clients.
rg -n "RootCAs|InsecureSkipVerify|sippy\.dptools\.openshift\.org|crypto/x509|x509\.NewCertPool|AppendCertsFromPEM" tools/codegen/cmd tools -g '!**/*_test.go'

# Show the neighboring code around the other two Sippy client setups in this file.
python3 - <<'PY'
from pathlib import Path
text = Path('tools/codegen/cmd/featuregate-test-analyzer.go').read_text().splitlines()
for anchor in [1054, 1175]:
    print(f"\n## around line {anchor}")
    for j in range(anchor-6, anchor+18):
        if 1 <= j <= len(text):
            print(f"{j:4d}: {text[j-1]}")
PY

Repository: openshift/api

Length of output: 18642


Restore TLS verification for the Sippy clients. InsecureSkipVerify: true leaves these http.Transport setups open to on-path forgery of Sippy telemetry, which can skew promotion decisions. Remove the override or load the trusted Sippy CA bundle instead.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 958-960: MinVersionis missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{
InsecureSkipVerify: true,
}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures

(missing-ssl-minversion-go)

🪛 OpenGrep (1.25.0)

[ERROR] 959-961: TLS certificate verification is disabled via InsecureSkipVerify. This allows man-in-the-middle attacks. Remove InsecureSkipVerify or set it to false.

(coderabbit.tls.go-insecure-skip-verify)


[ERROR] 959-961: TLS certificate verification is disabled via InsecureSkipVerify. This allows man-in-the-middle attacks. Remove InsecureSkipVerify or set it to false.

(coderabbit.tls.go-insecure-skip-verify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/codegen/cmd/featuregate-test-analyzer.go` around lines 952 - 967,
Restore certificate verification in the Sippy client transport by removing the
InsecureSkipVerify override from the tls.Config used by defaultTransport, or
configure it with the trusted Sippy CA bundle. Keep the existing http.Client and
transport settings unchanged.

Source: Linters/SAST tools


release, err := getRelease()
if err != nil {
return nil, fmt.Errorf("couldn't fetch latest release version: %w", err)
}

var installTestResult *TestResults
for _, currQuery := range queries {
currURL := &url.URL{
Scheme: "https",
Host: "sippy.dptools.openshift.org",
Path: "api/tests",
}
queryParams := currURL.Query()
queryParams.Add("release", release)
queryParams.Add("period", "default")
filterJSON, err := json.Marshal(currQuery)
if err != nil {
return nil, err
}
queryParams.Add("filter", string(filterJSON))
currURL.RawQuery = queryParams.Encode()

req, err := http.NewRequest(http.MethodGet, currURL.String(), nil)
if err != nil {
return nil, err
}

response, err := sippyClient.Do(req)
if err != nil {
return nil, err
}
Comment on lines +1110 to +1118

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="tools/codegen/cmd/featuregate-test-analyzer.go"

echo "== File size =="
wc -l "$file"

echo "== Around cited lines =="
sed -n '940,1035p' "$file"

echo "== Search for request construction and Sippy calls =="
rg -n "NewRequestWithContext|NewRequest\\(|sippyClient\\.Do\\(|ctx\\b|context\\.Context" "$file"

echo "== Function signatures in file =="
ast-grep outline "$file" --view expanded

Repository: openshift/api

Length of output: 8150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="tools/codegen/cmd/featuregate-test-analyzer.go"

echo "== getInstallTestLevelData callers =="
rg -n "getInstallTestLevelData\\(" "$file" -A 3 -B 3

echo "== lines around second request site =="
sed -n '1030,1115p' "$file"

echo "== Run path and cancellation handling =="
sed -n '120,170p' "$file"

echo "== any explicit ctx checks or deadlines in request helpers =="
rg -n "context\\.With|ctx\\.Done|select \\{|NewRequestWithContext|http\\.Client\\{.*Timeout|Timeout:" "$file" -A 2 -B 2

Repository: openshift/api

Length of output: 7318


Propagate ctx into the Sippy requests. In tools/codegen/cmd/featuregate-test-analyzer.go:947-999, these calls use http.NewRequest, so cancellation from Run won't stop the Sippy fetches and each one can keep running until the 2m client timeout. Thread ctx through the helper and switch to http.NewRequestWithContext.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/codegen/cmd/featuregate-test-analyzer.go` around lines 991 - 999,
Thread the context parameter from Run through the Sippy-fetching helper to the
request construction near http.NewRequest, and replace http.NewRequest with
http.NewRequestWithContext using ctx. Preserve the existing request URL, method,
error handling, and response processing while ensuring cancellation from Run
stops in-flight Sippy requests.

Source: Path instructions

if response.StatusCode < 200 || response.StatusCode > 299 {
return nil, fmt.Errorf("error getting sippy results (status=%d) for: %v", response.StatusCode, currURL.String())
}
queryResultBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
response.Body.Close()
Comment on lines +1115 to +1126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="tools/codegen/cmd/featuregate-test-analyzer.go"

echo "== surrounding lines =="
sed -n '960,1035p' "$file" | cat -n

echo
echo "== body close usages in file =="
rg -n "response\.Body\.Close|defer .*Body\.Close|Close\(\)" "$file"

Repository: openshift/api

Length of output: 3188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="tools/codegen/cmd/featuregate-test-analyzer.go"

printf '== surrounding lines ==\n'
sed -n '960,1035p' "$file" | cat -n

printf '\n== body close usages in file ==\n'
rg -n "response\.Body\.Close|defer .*Body\.Close|Close\(\)" "$file" || true

Repository: openshift/api

Length of output: 3188


Close response.Body on every path

response.Body.Close() only runs after ReadAll; move it to a defer immediately after Do so the non-2xx and read-error returns also clean up, and surface the close error instead of dropping it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/codegen/cmd/featuregate-test-analyzer.go` around lines 996 - 1007,
Update the response handling around sippyClient.Do to defer closing
response.Body immediately after a successful request, before status validation
or io.ReadAll. Ensure the deferred cleanup surfaces any close error rather than
discarding it, while preserving the existing non-2xx and read-error returns.

Source: Path instructions


testInfos := []sippy.SippyTestInfo{}
if err := json.Unmarshal(queryResultBytes, &testInfos); err != nil {
return nil, err
}

for _, currTest := range testInfos {
if installTestResult == nil {
installTestResult = &TestResults{
TestName: currTest.Name,
}
}

// Accumulate results across multiple JobTier queries
if currTest.CurrentRuns >= requiredNumberOfTestRunsPerVariant {
installTestResult.TotalRuns += currTest.CurrentRuns
installTestResult.SuccessfulRuns += currTest.CurrentSuccesses
installTestResult.FailedRuns += currTest.CurrentFailures
installTestResult.FlakedRuns += currTest.CurrentFlakes
} else {
installTestResult.TotalRuns += currTest.CurrentRuns + currTest.PreviousRuns
installTestResult.SuccessfulRuns += currTest.CurrentSuccesses + currTest.PreviousSuccesses
installTestResult.FailedRuns += currTest.CurrentFailures + currTest.PreviousFailures
installTestResult.FlakedRuns += currTest.CurrentFlakes + currTest.PreviousFlakes
}
}
}

return installTestResult, nil
}

func listTestResultForVariant(featureGate string, jobVariant JobVariant) (*TestingResults, error) {
// Feature gates used by the installer don't need separate tests, use the overall install tests
if strings.Contains(featureGate, "Install") {
return verifyJobBasedFeatureGatePromotion(featureGate, jobVariant)
}

var testPattern string
var queries []*sippy.SippyQueryStruct

// Substring here matches for both [OCPFeatureGate:...] and [FeatureGate:...]
testPattern = fmt.Sprintf("FeatureGate:%s]", featureGate)
queries = sippy.QueriesFor(jobVariant.Cloud, jobVariant.Architecture, jobVariant.Topology,
jobVariant.NetworkStack, jobVariant.OS, jobVariant.JobTiers, testPattern)
fmt.Printf("Query sippy for all test run results for pattern %q on variant %#v\n", testPattern, jobVariant)

defaultTransport := &http.Transport{
Expand All @@ -892,12 +1091,10 @@ func listTestResultForVariant(featureGate string, jobVariant JobVariant) (*Testi

testNameToResults := map[string]*TestResults{}
hasCandidateTierResults := false
queries := sippy.QueriesFor(jobVariant.Cloud, jobVariant.Architecture, jobVariant.Topology, jobVariant.NetworkStack, jobVariant.OS, jobVariant.JobTiers, testPattern)
release, err := getRelease()
if err != nil {
return nil, fmt.Errorf("couldn't fetch latest release version: %w", err)
}
fmt.Printf("Querying sippy release %s for test run results\n", release)

for _, currQuery := range queries {
currURL := &url.URL{
Expand Down Expand Up @@ -1139,7 +1336,7 @@ func getJobsForFeatureGateFromSippy(client *http.Client, release, featureGate st
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected a 200 OK status code but got %s", resp.StatusCode)
return nil, fmt.Errorf("expected a 200 OK status code but got %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
Expand All @@ -1165,7 +1362,7 @@ func getJobRunsFromSippy(client *http.Client, release, jobName string) ([]sippy.
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected a 200 OK status code but got %s", resp.StatusCode)
return nil, fmt.Errorf("expected a 200 OK status code but got %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
Expand Down
Loading