Skip to content

Commit fccfbea

Browse files
Migrate PR issues to new flattened API with server-side filters (#270)
* Migrate PR issues to new flattened API with server-side filters - Switch from `issueOccurrences` to `issues` field in PR issues GraphQL query - Flatten response structure, remove nested issue/analyzer objects - Add source, category, severity, and q filter params to PR issues query - Add buildPRFilters() to map CLI flags to server-side filter params - Update GetPRIssues signature to accept filter params - Update golden files and tests to match new schema * Handle nil issues list before filtering - Return early when issuesList is nil to avoid passing nil to filterIssues * Improve "no runs found" messages with actionable hints - Suggest checking if branch has been pushed/analyzed when no runs exist - Replace generic "no completed runs" with "analysis still in progress" - Point users to --default-branch as a fallback option - Update test assertions to match new message text * Bump version to 2.0.41 * Suppress empty-results message when showing fallback data - Add Fallback field to AutoBranchResult to track when results come from a previous completed run while a new analysis is in progress - Propagate fallback state to issues, metrics, and vulnerabilities commands - Skip "no results found" message in fallback mode since it's misleading when the data is from an older run, not the current commit * Bump version to 2.0.42
1 parent 093ba87 commit fccfbea

File tree

12 files changed

+128
-87
lines changed

12 files changed

+128
-87
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.0.40
1+
2.0.42

command/cmdutil/resolve_run.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func resolveRunFromCommits(
6767
}
6868
if len(allRuns) == 0 {
6969
return nil, fmt.Errorf(
70-
"no analysis runs found for branch %q.\nTry: --default-branch, --commit <sha>, or --pr <number>",
70+
"no analysis runs found for branch %q. Has this branch been pushed and analyzed on DeepSource?\nTry: --default-branch, --commit <sha>, or --pr <number>",
7171
branchName,
7272
)
7373
}
@@ -223,6 +223,7 @@ type AutoBranchResult struct {
223223
PRNumber int // >0 if a PR was detected for the branch
224224
UseRepo bool // true when the caller should fall back to repo-level (default branch) data
225225
Empty bool // true when there are no results (timeout, no completed runs)
226+
Fallback bool // true when showing results from a previous completed run while a new analysis is in progress
226227
}
227228

228229
// ResolveAutoBranch encapsulates the shared "default" branch resolution logic
@@ -286,12 +287,13 @@ func resolveWithPR(
286287
return nil, fallbackErr
287288
}
288289
if completedRun == nil {
289-
style.Infof(w, "No completed analysis runs found for branch %q.", branchName)
290+
style.Infof(w, "Analysis is still in progress for branch %q. Try again shortly, or use --default-branch to see results from the default branch.", branchName)
290291
result.Empty = true
291292
return result, nil
292293
}
293294
style.Infof(w, "Analysis is running on commit %s. Showing results from the last analyzed commit (%s).", run.CommitOid[:8], completedRun.CommitOid[:8])
294295
result.CommitOid = completedRun.CommitOid
296+
result.Fallback = true
295297
return result, nil
296298
}
297299
}
@@ -336,12 +338,13 @@ func resolveWithoutPR(
336338
return nil, fallbackErr
337339
}
338340
if run == nil {
339-
style.Infof(w, "No completed analysis runs found for branch %q.", branchName)
341+
style.Infof(w, "Analysis is still in progress for branch %q. Try again shortly, or use --default-branch to see results from the default branch.", branchName)
340342
result.Empty = true
341343
return result, nil
342344
}
343345
style.Infof(w, "Analysis is running on commit %s. Showing results from the last analyzed commit (%s).", commitOid[:8], run.CommitOid[:8])
344346
commitOid = run.CommitOid
347+
result.Fallback = true
345348
}
346349
if IsRunTimedOut(finalStatus) {
347350
style.Warnf(w, "Analysis timed out for branch %q.", branchName)

command/cmdutil/resolve_run_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,8 +491,8 @@ func TestResolveWithPR_PendingNoCompletedRuns(t *testing.T) {
491491
if !got.Empty {
492492
t.Fatal("expected Empty=true when no completed runs exist")
493493
}
494-
if !strings.Contains(buf.String(), "No completed analysis runs found") {
495-
t.Errorf("expected 'no completed runs' message, got: %s", buf.String())
494+
if !strings.Contains(buf.String(), "Analysis is still in progress") {
495+
t.Errorf("expected 'analysis still in progress' message, got: %s", buf.String())
496496
}
497497
}
498498

command/issues/issues.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type IssuesOptions struct {
4242
DefaultBranch bool
4343
repoSlug string
4444
autoDetectedBranch string
45+
fallback bool
4546
issues []issues.Issue
4647
deps *cmddeps.Deps
4748
client *deepsource.Client
@@ -254,6 +255,9 @@ func (opts *IssuesOptions) Run(ctx context.Context) error {
254255
if err != nil {
255256
return err
256257
}
258+
if issuesList == nil {
259+
return nil
260+
}
257261

258262
issuesList = opts.filterIssues(issuesList)
259263

@@ -280,14 +284,15 @@ func (opts *IssuesOptions) Run(ctx context.Context) error {
280284

281285
func (opts *IssuesOptions) resolveIssues(ctx context.Context, client *deepsource.Client, remote *vcs.RemoteData) ([]issues.Issue, error) {
282286
serverFilters := opts.buildServerFilters()
287+
prFilters := opts.buildPRFilters()
283288

284289
var issuesList []issues.Issue
285290
var err error
286291
switch {
287292
case opts.CommitOid != "":
288293
issuesList, err = client.GetRunIssuesFlat(ctx, opts.CommitOid, serverFilters)
289294
case opts.PRNumber > 0:
290-
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, opts.PRNumber)
295+
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, opts.PRNumber, prFilters)
291296
case opts.DefaultBranch:
292297
issuesList, err = client.GetIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider)
293298
default:
@@ -303,11 +308,12 @@ func (opts *IssuesOptions) resolveIssues(ctx context.Context, client *deepsource
303308
return nil, nil
304309
}
305310
opts.autoDetectedBranch = ab.BranchName
311+
opts.fallback = ab.Fallback
306312
switch {
307313
case ab.PRNumber > 0:
308314
opts.PRNumber = ab.PRNumber
309315
opts.CommitOid = ab.CommitOid
310-
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, ab.PRNumber)
316+
issuesList, err = client.GetPRIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider, ab.PRNumber, prFilters)
311317
case ab.UseRepo:
312318
issuesList, err = client.GetIssues(ctx, remote.Owner, remote.RepoName, remote.VCSProvider)
313319
default:
@@ -375,6 +381,25 @@ func (opts *IssuesOptions) buildServerFilters() issuesQuery.RunIssuesFlatParams
375381
return params
376382
}
377383

384+
// buildPRFilters returns PRIssuesListParams with server-side filters set
385+
// for any filter that has exactly one value.
386+
func (opts *IssuesOptions) buildPRFilters() issuesQuery.PRIssuesListParams {
387+
var params issuesQuery.PRIssuesListParams
388+
if len(opts.SourceFilters) == 1 {
389+
v := normalizeEnumValue(opts.SourceFilters[0])
390+
params.Source = &v
391+
}
392+
if len(opts.CategoryFilters) == 1 {
393+
v := normalizeEnumValue(opts.CategoryFilters[0])
394+
params.Category = &v
395+
}
396+
if len(opts.SeverityFilters) == 1 {
397+
v := normalizeEnumValue(opts.SeverityFilters[0])
398+
params.Severity = &v
399+
}
400+
return params
401+
}
402+
378403
// --- Filters ---
379404

380405
func (opts *IssuesOptions) hasFilters() bool {
@@ -538,6 +563,9 @@ func groupIssuesByCategory(issuesList []issues.Issue) map[string][]issues.Issue
538563

539564
func (opts *IssuesOptions) outputHuman(_ context.Context) error {
540565
if len(opts.issues) == 0 {
566+
if opts.fallback {
567+
return nil
568+
}
541569
if opts.hasFilters() {
542570
style.Infof(opts.stdout(), "No issues matched the provided filters in %s on %s.", opts.repoSlug, opts.scopeLabel())
543571
} else {

command/issues/tests/golden_files/pr_scope_output.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"description": "Return value of io.ReadAll is not checked for errors",
99
"category": "BUG_RISK",
1010
"severity": "MAJOR",
11-
"source": "",
12-
"analyzer": "go"
11+
"source": "static",
12+
"analyzer": ""
1313
},
1414
{
1515
"path": "internal/vcs/remotes.go",
@@ -20,7 +20,7 @@
2020
"description": "Constructing HTTP request with user-controlled URL allows SSRF",
2121
"category": "SECURITY",
2222
"severity": "MAJOR",
23-
"source": "",
24-
"analyzer": "go"
23+
"source": "ai",
24+
"analyzer": ""
2525
}
2626
]

command/issues/tests/golden_files/pr_scope_response.json

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,39 @@
11
{
22
"repository": {
33
"pullRequest": {
4-
"issueOccurrences": {
4+
"issues": {
55
"edges": [
66
{
77
"node": {
8+
"source": "static",
89
"path": "cmd/deepsource/main.go",
910
"beginLine": 42,
1011
"endLine": 42,
1112
"title": "Unchecked error return value of os.ReadFile",
12-
"issue": {
13-
"shortcode": "GO-W1007",
14-
"shortDescription": "Return value of io.ReadAll is not checked for errors",
15-
"category": "BUG_RISK",
16-
"severity": "MAJOR",
17-
"analyzer": {
18-
"name": "Go",
19-
"shortcode": "go"
20-
}
21-
}
13+
"shortcode": "GO-W1007",
14+
"category": "BUG_RISK",
15+
"severity": "MAJOR",
16+
"explanation": "Return value of io.ReadAll is not checked for errors"
2217
}
2318
},
2419
{
2520
"node": {
21+
"source": "ai",
2622
"path": "internal/vcs/remotes.go",
2723
"beginLine": 87,
2824
"endLine": 91,
2925
"title": "HTTP request built with user-controlled URL",
30-
"issue": {
31-
"shortcode": "GO-S1010",
32-
"shortDescription": "Constructing HTTP request with user-controlled URL allows SSRF",
33-
"category": "SECURITY",
34-
"severity": "MAJOR",
35-
"analyzer": {
36-
"name": "Go",
37-
"shortcode": "go"
38-
}
39-
}
26+
"shortcode": "GO-S1010",
27+
"category": "SECURITY",
28+
"severity": "MAJOR",
29+
"explanation": "Constructing HTTP request with user-controlled URL allows SSRF"
4030
}
4131
}
42-
]
32+
],
33+
"pageInfo": {
34+
"hasNextPage": false,
35+
"endCursor": null
36+
}
4337
}
4438
}
4539
}

command/issues/tests/issues_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ func TestIssuesAutoDetectBranch(t *testing.T) {
6161
func TestIssuesAutoDetectPR(t *testing.T) {
6262
cfgMgr := testutil.CreateTestConfigManager(t, "test-token", "deepsource.com", "test@example.com")
6363
mock := testutil.MockQueryFunc(t, map[string]string{
64-
"pullRequests(": goldenPath("get_pr_by_branch_found_response.json"),
65-
"query GetAnalysisRuns(": goldenPath("get_analysis_runs_response.json"),
66-
"issueOccurrences(first:": goldenPath("pr_scope_response.json"),
64+
"pullRequests(": goldenPath("get_pr_by_branch_found_response.json"),
65+
"query GetAnalysisRuns(": goldenPath("get_analysis_runs_response.json"),
66+
"query GetPRIssues(": goldenPath("pr_scope_response.json"),
6767
})
6868
client := deepsource.NewWithGraphQLClient(mock)
6969

@@ -185,7 +185,7 @@ func TestIssuesCommitScope(t *testing.T) {
185185
func TestIssuesPRScope(t *testing.T) {
186186
cfgMgr := testutil.CreateTestConfigManager(t, "test-token", "deepsource.com", "test@example.com")
187187
mock := testutil.MockQueryFunc(t, map[string]string{
188-
"issueOccurrences(first:": goldenPath("pr_scope_response.json"),
188+
"query GetPRIssues(": goldenPath("pr_scope_response.json"),
189189
})
190190
client := deepsource.NewWithGraphQLClient(mock)
191191

@@ -447,8 +447,8 @@ func TestIssuesRunInProgress(t *testing.T) {
447447
got := buf.String()
448448
// In non-TTY (test runner), in-progress auto-falls back to last completed run.
449449
// Since the mock only has a PENDING run, no completed run is found.
450-
if !strings.Contains(got, "No completed analysis runs found") {
451-
t.Errorf("expected fallback 'no completed runs' message, got: %q", got)
450+
if !strings.Contains(got, "Analysis is still in progress") {
451+
t.Errorf("expected 'analysis still in progress' message, got: %q", got)
452452
}
453453
}
454454

command/metrics/metrics.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type MetricsOptions struct {
3434
LimitArg int
3535
repoSlug string
3636
autoDetectedBranch string
37+
fallback bool
3738
repoMetrics []metrics.RepositoryMetric
3839
runMetrics *metrics.RunMetrics
3940
prMetrics *metrics.PRMetrics
@@ -246,6 +247,7 @@ func (opts *MetricsOptions) resolveMetrics(ctx context.Context, client *deepsour
246247
return nil
247248
}
248249
opts.autoDetectedBranch = ab.BranchName
250+
opts.fallback = ab.Fallback
249251
switch {
250252
case ab.PRNumber > 0:
251253
opts.PRNumber = ab.PRNumber
@@ -359,6 +361,9 @@ func (opts *MetricsOptions) outputHuman() error {
359361
w := opts.stdout()
360362

361363
if len(metricsList) == 0 {
364+
if opts.fallback {
365+
return nil
366+
}
362367
style.Infof(w, "No metrics found in %s on %s.", opts.repoSlug, opts.scopeLabel())
363368
return nil
364369
}

command/reportcard/reportcard.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ func (opts *ReportCardOptions) resolveByPR(ctx context.Context, client *deepsour
219219
return err
220220
}
221221
if completed == nil {
222-
style.Infof(opts.stdout(), "No completed analysis runs found for branch %q.", branch)
222+
style.Infof(opts.stdout(), "Analysis is still in progress for branch %q. Try again shortly, or use --default-branch to see results from the default branch.", branch)
223223
return nil
224224
}
225225
style.Infof(opts.stdout(), "Analysis is running on commit %s. Showing results from the last analyzed commit (%s).", run.CommitOid[:8], completed.CommitOid[:8])
@@ -276,7 +276,7 @@ func (opts *ReportCardOptions) resolveByCurrentBranch(ctx context.Context, clien
276276
return err
277277
}
278278
if completed == nil {
279-
style.Infof(opts.stdout(), "No completed analysis runs found for branch %q.", branchName)
279+
style.Infof(opts.stdout(), "Analysis is still in progress for branch %q. Try again shortly, or use --default-branch to see results from the default branch.", branchName)
280280
return nil
281281
}
282282
style.Infof(opts.stdout(), "Analysis is running on commit %s. Showing results from the last analyzed commit (%s).", run.CommitOid[:8], completed.CommitOid[:8])

command/vulnerabilities/vulnerabilities.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type VulnerabilitiesOptions struct {
3434
SeverityFilters []string
3535
repoSlug string
3636
autoDetectedBranch string
37+
fallback bool
3738
repoVulns []vulnerabilities.VulnerabilityOccurrence
3839
runVulns *vulnerabilities.RunVulns
3940
prVulns *vulnerabilities.PRVulns
@@ -234,6 +235,7 @@ func (opts *VulnerabilitiesOptions) resolveVulnerabilities(ctx context.Context,
234235
return nil
235236
}
236237
opts.autoDetectedBranch = ab.BranchName
238+
opts.fallback = ab.Fallback
237239
switch {
238240
case ab.PRNumber > 0:
239241
opts.PRNumber = ab.PRNumber
@@ -342,6 +344,9 @@ func (opts *VulnerabilitiesOptions) outputHuman() error {
342344
vulnsList := opts.getVulns()
343345

344346
if len(vulnsList) == 0 {
347+
if opts.fallback {
348+
return nil
349+
}
345350
if opts.hasFilters() {
346351
style.Infof(opts.stdout(), "No vulnerabilities matched the provided filters in %s on %s.", opts.repoSlug, opts.scopeLabel())
347352
} else {

0 commit comments

Comments
 (0)