Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
17 changes: 10 additions & 7 deletions cmd/deepsource/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,24 @@ func mainRun() (exitCode int) {
func run() int {
v.SetBuildInfo(version, Date, buildMode)

// Check for available updates and notify (skip when running "update" itself)
// Notify about available updates from a previous check (instant, disk-only read).
// Then kick off a background check for the next invocation.
isUpdateCmd := len(os.Args) >= 2 && os.Args[1] == "update"
if !isUpdateCmd && update.ShouldCheckForUpdate() {
client := &http.Client{Timeout: 3 * time.Second}
if err := update.CheckForUpdate(client); err != nil {
debug.Log("update: %v", err)
}

state, err := update.ReadUpdateState()
if err != nil {
debug.Log("update: %v", err)
}
if state != nil {
fmt.Fprintln(os.Stderr, pterm.Yellow(fmt.Sprintf("Update available: v%s, run '%s update' to install.", state.Version, filepath.Base(os.Args[0]))))
fmt.Fprintln(os.Stderr, pterm.Yellow(fmt.Sprintf("Update available: v%s → v%s, run '%s update' to install.", version, state.Version, filepath.Base(os.Args[0]))))
}

go func() {
client := &http.Client{Timeout: 3 * time.Second}
if err := update.CheckForUpdate(client); err != nil {
debug.Log("update: %v", err)
}
}()
}

exitCode := 0
Expand Down
2 changes: 1 addition & 1 deletion command/auth/login/pat_login_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/fatih/color"
)

func (opts *LoginOptions) startPATLoginFlow(svc *authsvc.Service, cfg *config.CLIConfig, token string) error {
func (_ *LoginOptions) startPATLoginFlow(svc *authsvc.Service, cfg *config.CLIConfig, token string) error {
Comment thread
sourya-deepsource marked this conversation as resolved.
Outdated
cfg.Token = token

viewer, err := svc.GetViewer(context.Background(), cfg)
Expand Down
43 changes: 41 additions & 2 deletions command/issues/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ func (opts *IssuesOptions) Run(ctx context.Context, cmd *cobra.Command) error {
opts.client = client
opts.remote = remote

issuesList, err := opts.resolveIssues(ctx, client, remote)
issuesList, err := opts.resolveIssuesWithRetry(ctx, client, remote)
if err != nil {
return err
}
Expand Down Expand Up @@ -283,6 +283,41 @@ func (opts *IssuesOptions) Run(ctx context.Context, cmd *cobra.Command) error {
return nil
}

// resolveIssuesWithRetry calls resolveIssues and, for monorepos with an
// auto-detected sub-repo path, progressively strips path segments on
// "Repository does not exist" errors until a match is found.
func (opts *IssuesOptions) resolveIssuesWithRetry(ctx context.Context, client *deepsource.Client, remote *vcs.RemoteData) ([]issues.Issue, error) {
issuesList, err := opts.resolveIssues(ctx, client, remote)
if err == nil {
return issuesList, nil
}

if strings.Contains(err.Error(), "This repository is a monorepo") {
return nil, fmt.Errorf("This is a monorepo. Use --repo to specify a sub-project.\n\nHint: %s", err.Error())
}

if !strings.Contains(err.Error(), "Repository does not exist") || remote.SubRepoSuffix == "" {
return nil, err
}

parts := strings.Split(remote.SubRepoSuffix, ":")
for len(parts) > 1 {
Comment thread
sourya-deepsource marked this conversation as resolved.
Outdated
parts = parts[:len(parts)-1]
remote.SubRepoSuffix = strings.Join(parts, ":")
baseName := strings.SplitN(remote.RepoName, ":", 2)[0]
remote.RepoName = baseName + ":" + remote.SubRepoSuffix

issuesList, err = opts.resolveIssues(ctx, client, remote)
if err == nil {
return issuesList, nil
}
if !strings.Contains(err.Error(), "Repository does not exist") {
return nil, err
}
}
return nil, err
}

func (opts *IssuesOptions) resolveIssues(ctx context.Context, client *deepsource.Client, remote *vcs.RemoteData) ([]issues.Issue, error) {
serverFilters := opts.buildServerFilters()
prFilters := opts.buildPRFilters()
Expand Down Expand Up @@ -314,7 +349,11 @@ func (opts *IssuesOptions) resolveIssues(ctx context.Context, client *deepsource
case ab.PRNumber > 0:
opts.PRNumber = ab.PRNumber
opts.CommitOid = ab.CommitOid
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, ab.PRNumber, prFilters)
if ab.Fallback {
issuesList, err = client.GetRunIssuesFlat(ctx, ab.CommitOid, serverFilters)
} else {
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, ab.PRNumber, prFilters)
}
case ab.UseRepo:
issuesList, err = client.GetIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider)
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"repository": {
"analysisRuns": {
"edges": [
{
"node": {
"runUid": "run-uid-running-01",
"commitOid": "831701c7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"branchName": "feature/new-auth",
"status": "RUNNING",
"createdAt": "2025-03-12T14:00:00Z",
"finishedAt": null,
"updatedAt": "2025-03-12T14:01:00Z",
"summary": {
"occurrencesIntroduced": 0,
"occurrencesResolved": 0,
"occurrencesSuppressed": 0
},
"reportCard": null
}
},
{
"node": {
"runUid": "run-uid-completed-01",
"commitOid": "862df9f2e3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8",
"branchName": "feature/new-auth",
"status": "SUCCESS",
"createdAt": "2025-03-12T12:00:00Z",
"finishedAt": "2025-03-12T12:05:00Z",
"updatedAt": "2025-03-12T12:05:00Z",
"summary": {
"occurrencesIntroduced": 2,
"occurrencesResolved": 0,
"occurrencesSuppressed": 0
},
"reportCard": null
}
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": null
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"repository": {
"analysisRuns": {
"edges": [
{
"node": {
"runUid": "run-uid-running-01",
"commitOid": "831701c7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"branchName": "feature/new-auth",
"status": "RUNNING",
"createdAt": "2025-03-12T14:00:00Z",
"finishedAt": null,
"updatedAt": "2025-03-12T14:01:00Z",
"summary": {
"occurrencesIntroduced": 0,
"occurrencesResolved": 0,
"occurrencesSuppressed": 0
},
"reportCard": null
}
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": null
}
}
}
}
98 changes: 98 additions & 0 deletions command/issues/tests/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,104 @@ func TestIssuesMultipleFilters(t *testing.T) {
}
}

// TestIssuesPRFallbackUsesRunIssues verifies that when auto-branch resolution
// detects a PR AND the current run is in-progress (fallback), the code fetches
// issues via GetRunIssuesFlat (commit-scoped) instead of GetPRIssues.
// Regression test for ticket #6884175.
func TestIssuesPRFallbackUsesRunIssues(t *testing.T) {
Comment thread
sourya-deepsource marked this conversation as resolved.
cfgMgr := testutil.CreateTestConfigManager(t, "test-token", "deepsource.com", "test@example.com")

// Load golden files for the multi-step mock.
prFoundData, err := os.ReadFile(goldenPath("get_pr_by_branch_found_response.json"))
if err != nil {
t.Fatalf("failed to read PR golden file: %v", err)
}
runsFirstData, err := os.ReadFile(goldenPath("get_analysis_runs_pr_fallback_first_response.json"))
if err != nil {
t.Fatalf("failed to read first runs golden file: %v", err)
}
runsCompletedData, err := os.ReadFile(goldenPath("get_analysis_runs_pr_fallback_completed_response.json"))
if err != nil {
t.Fatalf("failed to read completed runs golden file: %v", err)
}
commitScopeData, err := os.ReadFile(goldenPath("commit_scope_response.json"))
if err != nil {
t.Fatalf("failed to read commit scope golden file: %v", err)
}

mock := graphqlclient.NewMockClient()
analysisRunsCalls := 0
mock.QueryFunc = func(_ context.Context, query string, _ map[string]any, result any) error {
switch {
case strings.Contains(query, "pullRequests("):
return json.Unmarshal(prFoundData, result)
case strings.Contains(query, "query GetAnalysisRuns("):
analysisRunsCalls++
if analysisRunsCalls == 1 {
// First call (ResolveLatestRunForBranch, limit=1): RUNNING run
return json.Unmarshal(runsFirstData, result)
}
// Second call (ResolveLatestCompletedRun, limit=10): RUNNING + SUCCESS
return json.Unmarshal(runsCompletedData, result)
case strings.Contains(query, "checks {"):
// GetRunIssuesFlat for the fallback commit
return json.Unmarshal(commitScopeData, result)
default:
t.Fatalf("unexpected query: %s", query)
return nil
}
}
client := deepsource.NewWithGraphQLClient(mock)

var buf bytes.Buffer
deps := &cmddeps.Deps{
Client: client,
ConfigMgr: cfgMgr,
Stdout: &buf,
BranchNameFunc: func() (string, error) {
return "feature/new-auth", nil
},
HasUnpushedCommitsFunc: func() bool { return false },
HasUncommittedChangesFunc: func() bool { return false },
}

cmd := issuesCmd.NewCmdIssuesWithDeps(deps)
cmd.SetArgs([]string{"--repo", "gh/testowner/testrepo", "--output", "json"})

if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}

got := buf.String()

// Verify the fallback info message is present.
if !strings.Contains(got, "Analysis is running on commit") {
t.Errorf("expected fallback info message, got: %q", got)
}

// Extract the JSON array from the output (after the info message line).
// The JSON starts at the first '[' character.
jsonStart := strings.Index(got, "[")
if jsonStart < 0 {
t.Fatalf("no JSON array found in output: %s", got)
}

var issues []map[string]any
if err := json.Unmarshal([]byte(got[jsonStart:]), &issues); err != nil {
t.Fatalf("failed to parse JSON output: %v\nraw output: %s", err, got)
}

// Must NOT be empty — this was the bug.
if len(issues) == 0 {
t.Fatal("expected non-empty issues from fallback commit, got empty array")
}

// Verify we got the expected issues from commit_scope_response.json.
if issues[0]["issue_code"] != "GO-W1007" {
t.Errorf("expected first issue GO-W1007, got %v", issues[0]["issue_code"])
}
}

func TestIssuesRunInProgress(t *testing.T) {
cfgMgr := testutil.CreateTestConfigManager(t, "test-token", "deepsource.com", "test@example.com")
mock := testutil.MockQueryFunc(t, map[string]string{
Expand Down
22 changes: 16 additions & 6 deletions command/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package update
import (
"fmt"
"net/http"
"runtime"
"time"

"github.com/deepsourcelabs/cli/buildinfo"
"github.com/deepsourcelabs/cli/internal/update"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

Expand All @@ -29,7 +31,7 @@ func runUpdate(cmd *cobra.Command) error {
return fmt.Errorf("checking for updates: %w", err)
}

state, err := update.ReadUpdateState()
state, err := update.PrepareUpdate()
if err != nil {
return fmt.Errorf("reading update state: %w", err)
}
Expand All @@ -40,17 +42,25 @@ func runUpdate(cmd *cobra.Command) error {
return nil
}

fmt.Fprintf(w, "Updating to v%s...\n", state.Version)
fmt.Fprintln(w, pterm.Green("✓")+" Platform: "+runtime.GOOS+"/"+runtime.GOARCH)
fmt.Fprintln(w, pterm.Green("✓")+" Version: v"+state.Version)

applyClient := &http.Client{Timeout: 30 * time.Second}
newVer, err := update.ApplyUpdate(applyClient)
data, err := update.DownloadUpdate(applyClient, state)
if err != nil {
return fmt.Errorf("applying update: %w", err)
return fmt.Errorf("downloading update: %w", err)
}
fmt.Fprintln(w, pterm.Green("✓")+" Downloaded")

if newVer != "" {
fmt.Fprintf(w, "Updated to v%s\n", newVer)
if err := update.VerifyUpdate(data, state); err != nil {
return fmt.Errorf("verifying update: %w", err)
}
fmt.Fprintln(w, pterm.Green("✓")+" Checksum verified")

if err := update.ExtractAndInstall(data, state.ArchiveURL); err != nil {
return fmt.Errorf("installing update: %w", err)
}
fmt.Fprintln(w, pterm.Green("✓")+" Installed")

return nil
}
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (cfg *CLIConfig) SetTokenExpiry(str string) {
cfg.TokenExpiresIn = t.UTC()
}

func (cfg CLIConfig) IsExpired() bool {
func (cfg *CLIConfig) IsExpired() bool {
if cfg.TokenExpiresIn.IsZero() {
return false
}
Expand Down
18 changes: 18 additions & 0 deletions deepsource/issues/queries/pr_issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ const fetchPRIssuesQuery = `query GetPRIssues(
category
severity
explanation
issue {
analyzer {
name
shortcode
}
}
}
}
pageInfo {
Expand Down Expand Up @@ -77,6 +83,12 @@ type PRIssuesListResponse struct {
Category string `json:"category"`
Severity string `json:"severity"`
Explanation string `json:"explanation"`
Issue *struct {
Analyzer struct {
Name string `json:"name"`
Shortcode string `json:"shortcode"`
} `json:"analyzer"`
} `json:"issue"`
} `json:"node"`
} `json:"edges"`
PageInfo pagination.PageInfo `json:"pageInfo"`
Expand Down Expand Up @@ -139,6 +151,12 @@ func (r *PRIssuesListRequest) Do(ctx context.Context) ([]issues.Issue, error) {
},
},
}
if node.Issue != nil {
issue.Analyzer = issues.AnalyzerMeta{
Name: node.Issue.Analyzer.Name,
Shortcode: node.Issue.Analyzer.Shortcode,
}
}
allIssues = append(allIssues, issue)
}

Expand Down
Loading
Loading