feat: generate and upload trivy SBOM into codacy#200
feat: generate and upload trivy SBOM into codacy#200franciscoovazevedo wants to merge 1 commit intomainfrom
Conversation
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Documentation | 1 minor |
| Complexity | 3 medium |
🟢 Metrics 53 complexity · 2 duplication
Metric Results Complexity 53 Duplication 2
🟢 Coverage 80.92% diff coverage · +1.46% coverage variation
Metric Results Coverage variation ✅ +1.46% coverage variation (-0.50%) Diff coverage ✅ 80.92% diff coverage (50.00%) Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (9b6cc83) 5997 1310 21.84% Head commit (a7fc362) 6149 (+152) 1433 (+123) 23.30% (+1.46%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#200) 152 123 80.92% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes. Give us feedback
There was a problem hiding this comment.
Pull request overview
Adds a new CLI command to generate (via Trivy) and upload an SPDX JSON SBOM to Codacy, reusing .codacy/cli-config.yaml metadata when available.
Changes:
- Extend
.codacy/cli-config.yamlparsing to include provider/org/repository and expose aGetCliConfig()accessor. - Add
upload-sbomcommand to generate/read an SBOM and upload it to Codacy’s image SBOM endpoint. - Add unit tests for image ref parsing and SBOM upload behaviors; skip
codacy.yamlvalidation forupload-sbom.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
config/config.go |
Extends CLI config model and adds GetCliConfig() for full YAML parsing. |
cmd/validation.go |
Skips codacy.yaml validation for the new upload-sbom command. |
cmd/upload_sbom.go |
Implements SBOM generation (Trivy) and multipart upload to Codacy. |
cmd/upload_sbom_test.go |
Adds tests for parsing and SBOM upload flow (but currently includes an external network call). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cmd/upload_sbom.go
Outdated
| // Fill provider/organization/repository from cli-config.yaml if not set via flags | ||
| if sbomProvider == "" || sbomOrg == "" { | ||
| if cliConfig, err := config.Config.GetCliConfig(); err == nil { | ||
| if sbomProvider == "" && cliConfig.Provider != "" { | ||
| sbomProvider = cliConfig.Provider | ||
| } | ||
| if sbomOrg == "" && cliConfig.Organization != "" { | ||
| sbomOrg = cliConfig.Organization |
There was a problem hiding this comment.
GetCliConfig() errors (e.g., permission denied or YAML parse errors) are silently ignored here, which can hide the real reason provider/org weren’t loaded and results in a misleading “--provider/--organization required” message. Consider surfacing non-IsNotExist errors (warn and continue, or fail fast with the underlying error) to help users fix a broken .codacy/cli-config.yaml.
| func uploadSBOMToCodacy(sbomPath, imageName, tag string) error { | ||
| url := fmt.Sprintf("https://app.codacy.com/api/v3/organizations/%s/%s/image-sboms", | ||
| sbomProvider, sbomOrg) | ||
|
|
There was a problem hiding this comment.
uploadSBOMToCodacy relies on package-level globals (sbomProvider, sbomOrg, sbomAPIToken, etc.) instead of its parameters, which makes the function harder to test, non-reentrant, and more error-prone (state leaks between invocations). Consider passing provider/org/token/repo/env (and optionally an *http.Client/base URL) as explicit parameters or encapsulating them in a struct.
| writer.WriteField("imageName", imageName) | ||
| writer.WriteField("tag", tag) | ||
|
|
||
| // Add optional fields | ||
| if sbomRepoName != "" { | ||
| writer.WriteField("repositoryName", sbomRepoName) | ||
| } | ||
| if sbomEnv != "" { | ||
| writer.WriteField("environment", sbomEnv) |
There was a problem hiding this comment.
Errors returned by writer.WriteField(...) are ignored. If these writes fail (e.g., due to an earlier multipart error), the request will be sent missing required/optional fields with no indication why. Handle and return these errors (especially for required fields like imageName and tag).
| writer.WriteField("imageName", imageName) | |
| writer.WriteField("tag", tag) | |
| // Add optional fields | |
| if sbomRepoName != "" { | |
| writer.WriteField("repositoryName", sbomRepoName) | |
| } | |
| if sbomEnv != "" { | |
| writer.WriteField("environment", sbomEnv) | |
| if err := writer.WriteField("imageName", imageName); err != nil { | |
| return fmt.Errorf("failed to write imageName field: %w", err) | |
| } | |
| if err := writer.WriteField("tag", tag); err != nil { | |
| return fmt.Errorf("failed to write tag field: %w", err) | |
| } | |
| // Add optional fields | |
| if sbomRepoName != "" { | |
| if err := writer.WriteField("repositoryName", sbomRepoName); err != nil { | |
| return fmt.Errorf("failed to write repositoryName field: %w", err) | |
| } | |
| } | |
| if sbomEnv != "" { | |
| if err := writer.WriteField("environment", sbomEnv); err != nil { | |
| return fmt.Errorf("failed to write environment field: %w", err) | |
| } |
| resp, err := http.DefaultClient.Do(req) | ||
| if err != nil { | ||
| return fmt.Errorf("request failed: %w", err) | ||
| } |
There was a problem hiding this comment.
Using http.DefaultClient without a timeout can cause the CLI to hang indefinitely on network issues. Prefer a dedicated http.Client{Timeout: ...} (there’s already a 10s timeout used in codacy-client/client.go) and/or allow injecting the client for tests.
cmd/upload_sbom_test.go
Outdated
| ss := saveSBOMState() | ||
| defer ss.restore() | ||
|
|
||
| server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| assert.Equal(t, "POST", r.Method) | ||
| assert.Equal(t, "my-token", r.Header.Get("api-token")) | ||
| assert.Equal(t, "application/json", r.Header.Get("Accept")) | ||
|
|
||
| err := r.ParseMultipartForm(10 << 20) | ||
| assert.NoError(t, err) | ||
| assert.Equal(t, "myapp", r.FormValue("imageName")) | ||
| assert.Equal(t, "v1.0", r.FormValue("tag")) | ||
| assert.Equal(t, "my-repo", r.FormValue("repositoryName")) | ||
| assert.Equal(t, "prod", r.FormValue("environment")) | ||
|
|
||
| _, _, err = r.FormFile("sbom") | ||
| assert.NoError(t, err) | ||
|
|
||
| w.WriteHeader(http.StatusNoContent) | ||
| })) | ||
| defer server.Close() | ||
|
|
||
| // Create a temp SBOM file | ||
| tmpFile, err := os.CreateTemp("", "test-sbom-*.json") | ||
| assert.NoError(t, err) | ||
| tmpFile.WriteString(`{"spdxVersion": "SPDX-2.3"}`) | ||
| tmpFile.Close() | ||
| defer os.Remove(tmpFile.Name()) | ||
|
|
||
| sbomAPIToken = "my-token" | ||
| sbomRepoName = "my-repo" | ||
| sbomEnv = "prod" | ||
|
|
||
| // This hits the real Codacy URL (can't easily override), so it will fail with a connection error. | ||
| // Verifies the function doesn't panic and correctly builds the request. | ||
| err = uploadSBOMToCodacy(tmpFile.Name(), "myapp", "v1.0") | ||
| assert.Error(t, err) |
There was a problem hiding this comment.
This test starts an httptest server but the production code always posts to https://app.codacy.com/..., so the test still performs a real network call and can be flaky/slow. Refactor uploadSBOMToCodacy to accept a base URL and/or http.Client so the test can hit server.URL and assert success (nil error) instead of asserting an error.
| ss := saveSBOMState() | |
| defer ss.restore() | |
| server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| assert.Equal(t, "POST", r.Method) | |
| assert.Equal(t, "my-token", r.Header.Get("api-token")) | |
| assert.Equal(t, "application/json", r.Header.Get("Accept")) | |
| err := r.ParseMultipartForm(10 << 20) | |
| assert.NoError(t, err) | |
| assert.Equal(t, "myapp", r.FormValue("imageName")) | |
| assert.Equal(t, "v1.0", r.FormValue("tag")) | |
| assert.Equal(t, "my-repo", r.FormValue("repositoryName")) | |
| assert.Equal(t, "prod", r.FormValue("environment")) | |
| _, _, err = r.FormFile("sbom") | |
| assert.NoError(t, err) | |
| w.WriteHeader(http.StatusNoContent) | |
| })) | |
| defer server.Close() | |
| // Create a temp SBOM file | |
| tmpFile, err := os.CreateTemp("", "test-sbom-*.json") | |
| assert.NoError(t, err) | |
| tmpFile.WriteString(`{"spdxVersion": "SPDX-2.3"}`) | |
| tmpFile.Close() | |
| defer os.Remove(tmpFile.Name()) | |
| sbomAPIToken = "my-token" | |
| sbomRepoName = "my-repo" | |
| sbomEnv = "prod" | |
| // This hits the real Codacy URL (can't easily override), so it will fail with a connection error. | |
| // Verifies the function doesn't panic and correctly builds the request. | |
| err = uploadSBOMToCodacy(tmpFile.Name(), "myapp", "v1.0") | |
| assert.Error(t, err) | |
| t.Skip("uploadSBOMToCodacy currently posts to the fixed Codacy URL; refactor it to accept an injectable base URL and/or http.Client so this test can use httptest and assert success") |
b38549f to
a7fc362
Compare
No description provided.