From 7570e54fda6171920ab686a67e914d6f160587f5 Mon Sep 17 00:00:00 2001 From: Jai Pradeesh Date: Tue, 24 Mar 2026 09:23:44 -0700 Subject: [PATCH] Add --commit flag to report command - Allow specifying commit SHA directly, skipping git detection - Useful for CI environments where git is not available - Falls back to existing git/env var resolution when not set --- command/report/report.go | 6 ++- internal/services/report/service.go | 13 ++++-- internal/services/report/service_test.go | 53 ++++++++++++++++++++++++ internal/services/report/types.go | 1 + 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/command/report/report.go b/command/report/report.go index 0c9a51469..09a459f35 100644 --- a/command/report/report.go +++ b/command/report/report.go @@ -20,6 +20,7 @@ import ( type ReportOptions struct { Analyzer string AnalyzerType string + CommitOID string Key string Value string ValueFile string @@ -110,6 +111,8 @@ func NewCmdReportWithDeps(deps *container.Container) *cobra.Command { cmd.Flags().StringVar(&opts.AnalyzerType, "analyzer-type", "", "type of the analyzer (example: community)") + cmd.Flags().StringVar(&opts.CommitOID, "commit", "", "commit SHA to report against (skips git detection)") + cmd.Flags().StringVar(&opts.Key, "key", "", "shortcode of the language (example: go)") cmd.Flags().StringVar(&opts.Value, "value", "", "value of the artifact") @@ -165,6 +168,7 @@ func (opts *ReportOptions) Run(ctx context.Context, svc *reportsvc.Service, outp result, err := svc.Report(ctx, reportsvc.Options{ Analyzer: opts.Analyzer, AnalyzerType: opts.AnalyzerType, + CommitOID: opts.CommitOID, Key: opts.Key, Value: opts.Value, ValueFile: opts.ValueFile, @@ -193,7 +197,7 @@ func setReportUsageFunc(cmd *cobra.Command) { title string flags []string }{ - {"Artifact", []string{"analyzer", "analyzer-type", "key", "value", "value-file"}}, + {"Artifact", []string{"analyzer", "analyzer-type", "commit", "key", "value", "value-file"}}, {"Authentication", []string{"use-oidc", "oidc-provider", "oidc-request-token", "oidc-request-url", "host"}}, {"Output", []string{"output"}}, {"General", []string{"skip-verify", "help"}}, diff --git a/internal/services/report/service.go b/internal/services/report/service.go index ce4aa55c9..c63597e13 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -88,10 +88,15 @@ func (s *Service) Report(ctx context.Context, opts Options) (*Result, error) { return nil, uerr } - headCommitOID, warning, err := s.git.GetHead(currentDir) - if err != nil { - s.capture(err) - return nil, errors.New("Unable to get commit OID HEAD. Make sure you are running the CLI from a git repository") + var headCommitOID, warning string + if opts.CommitOID != "" { + headCommitOID = opts.CommitOID + } else { + headCommitOID, warning, err = s.git.GetHead(currentDir) + if err != nil { + s.capture(err) + return nil, errors.New("Unable to get commit OID HEAD. Make sure you are running the CLI from a git repository or use --commit flag") + } } artifactValue, err := s.resolveArtifactValue(opts) diff --git a/internal/services/report/service_test.go b/internal/services/report/service_test.go index c93d9e39d..440855c9b 100644 --- a/internal/services/report/service_test.go +++ b/internal/services/report/service_test.go @@ -369,6 +369,59 @@ func TestReportCreateArtifactEmptyError(t *testing.T) { assert.Contains(t, err.Error(), "raw response") } +func TestReportExplicitCommitSkipsGit(t *testing.T) { + tempDir := t.TempDir() + artifactPath := filepath.Join(tempDir, "coverage.xml") + assert.NoError(t, os.WriteFile(artifactPath, []byte(""), 0o644)) + + var capturedCommit string + httpClient := &mockHTTPClient{DoFunc: func(req *http.Request) (*http.Response, error) { + body, _ := io.ReadAll(req.Body) + _ = req.Body.Close() + if bytes.Contains(body, []byte("ArtifactMetadataInput")) { + payload := `{"data":{"__type":{"inputFields":[]}}}` + return httpResponse(200, payload), nil + } + if bytes.Contains(body, []byte("createArtifact")) { + var q ReportQuery + _ = json.Unmarshal(body, &q) + capturedCommit = q.Variables.Input.CommitOID + payload := `{"data":{"createArtifact":{"ok":true,"message":"ok","error":""}}}` + return httpResponse(200, payload), nil + } + return httpResponse(400, `{"error":"unexpected"}`), nil + }} + + // Git client returns an error — simulates no git repo available + git := adapters.NewMockGitClient() + git.SetError(errors.New("not a git repository")) + env := adapters.NewMockEnvironment() + env.Set("DEEPSOURCE_DSN", "https://token@localhost:8080") + + svc := NewService(ServiceDeps{ + GitClient: git, + HTTPClient: httpClient, + FileSystem: adapters.NewOSFileSystem(), + Environment: env, + Sentry: adapters.NewNoOpSentry(), + Output: adapters.NewBufferOutput(), + Workdir: func() (string, error) { return tempDir, nil }, + }) + + result, err := svc.Report(context.Background(), Options{ + Analyzer: "test-coverage", + Key: "python", + ValueFile: artifactPath, + CommitOID: "deadbeef1234567890abcdef1234567890abcdef", + }) + + assert.NoError(t, err) + if assert.NotNil(t, result) { + assert.Equal(t, "ok", result.Message) + } + assert.Equal(t, "deadbeef1234567890abcdef1234567890abcdef", capturedCommit) +} + func TestCaptureSkipsUserErrors(t *testing.T) { captured := false mockSentry := &captureSentry{onCapture: func(_ error) { captured = true }} diff --git a/internal/services/report/types.go b/internal/services/report/types.go index 5415ba375..c589926f5 100644 --- a/internal/services/report/types.go +++ b/internal/services/report/types.go @@ -36,6 +36,7 @@ type QueryResponse struct { type Options struct { Analyzer string AnalyzerType string + CommitOID string Key string Value string ValueFile string