Skip to content

Commit b4908fc

Browse files
feat/support dir flag (#2067)
* support dir flags * add docs page for comment args * Update docs/ce/reference/comment-args.mdx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update docs/ce/reference/comment-args.mdx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update docs/ce/reference/comment-args.mdx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update libs/ci/generic/comment_utils_test.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update ee/backend/controllers/bitbucket.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update ee/backend/controllers/gitlab.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update libs/ci/generic/comment_utils.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * remove func and complete comment * fix error message --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent 5429070 commit b4908fc

File tree

13 files changed

+383
-63
lines changed

13 files changed

+383
-63
lines changed

backend/controllers/github.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,7 +1458,7 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
14581458
"branchName", prBranchName,
14591459
)
14601460

1461-
processEventResult, err := generic.ProcessIssueCommentEvent(issueNumber, commentBody, config, projectsGraph, ghService)
1461+
processEventResult, err := generic.ProcessIssueCommentEvent(issueNumber, config, projectsGraph, ghService)
14621462
if err != nil {
14631463
slog.Error("Error processing issue comment event",
14641464
"issueNumber", issueNumber,
@@ -1467,10 +1467,19 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
14671467
commentReporterManager.UpdateComment(fmt.Sprintf(":x: Error processing event: %v", err))
14681468
return fmt.Errorf("error processing event")
14691469
}
1470-
impactedProjectsForComment := processEventResult.ImpactedProjectsForComment
14711470
impactedProjectsSourceMapping := processEventResult.ImpactedProjectsSourceMapping
14721471
allImpactedProjects := processEventResult.AllImpactedProjects
14731472

1473+
impactedProjectsForComment, err := generic.FilterOutProjectsFromComment(allImpactedProjects, commentBody)
1474+
if err != nil {
1475+
slog.Error("Error filtering out projects from comment",
1476+
"issueNumber", issueNumber,
1477+
"error", err,
1478+
)
1479+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: Error filtering out projects from comment: %v", err))
1480+
return fmt.Errorf("error filtering out projects from comment")
1481+
}
1482+
14741483
slog.Info("Issue comment event processed successfully",
14751484
"issueNumber", issueNumber,
14761485
"impactedProjectCount", len(impactedProjectsForComment),

docs/ce/reference/comment-args.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
title: Comment Args
3+
---
4+
5+
Digger comments such as `digger plan` and `digger apply` accept the following flags:
6+
7+
```
8+
-d value
9+
directory (short form)
10+
-directory value
11+
directory (long form)
12+
-p value
13+
-p value
14+
project (short form)
15+
-project value
16+
-project value
17+
project (long form)
18+
-layer value (only for layering)
19+
layer to plan or apply (default -1)
20+
```
21+
22+
The -p and -d flags can be specified multiple times. In the case of projects they will select the projects from the flags.
23+
Same goes for directories. Some example comments with flags:
24+
25+
```
26+
digger plan -p dev_vpc
27+
```
28+
29+
```
30+
digger plan --project dev_vpc
31+
```
32+
33+
```
34+
digger plan -d dev/vpc
35+
```
36+
37+
```
38+
digger plan --directory dev/vpc
39+
```
40+
41+
```
42+
digger plan -p dev_vpc -p staging_vpc
43+
```
44+
45+
```
46+
digger plan -p dev_vpc -d staging/vpc
47+
```

docs/mint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@
128128
"ce/reference/action-inputs",
129129
"ce/reference/api",
130130
"ce/reference/terraform.lock",
131-
"ce/securing-digger/spec-signing"
131+
"ce/securing-digger/spec-signing",
132+
"ce/reference/comment-args"
132133
]
133134
},
134135
{

ee/backend/controllers/bitbucket.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,18 +215,24 @@ func handleIssueCommentEventBB(bitbucketProvider utils.BitbucketProvider, payloa
215215
return fmt.Errorf("error while fetching branch name")
216216
}
217217

218-
processIssueCommentResult, err := generic.ProcessIssueCommentEvent(issueNumber, commentBody, config, projectsGraph, bbService)
218+
processIssueCommentResult, err := generic.ProcessIssueCommentEvent(issueNumber, config, projectsGraph, bbService)
219219
if err != nil {
220220
log.Printf("Error processing event: %v", err)
221221
utils.InitCommentReporter(bbService, issueNumber, fmt.Sprintf(":x: Error processing event: %v", err))
222222
return fmt.Errorf("error processing event")
223223
}
224224
log.Printf("Bitbucket IssueComment event processed successfully\n")
225225

226-
impactedProjectsForComment := processIssueCommentResult.ImpactedProjectsForComment
227226
impactedProjectsSourceMapping := processIssueCommentResult.ImpactedProjectsSourceMapping
228227
allImpactedProjects := processIssueCommentResult.AllImpactedProjects
229228

229+
impactedProjectsForComment, err := generic.FilterOutProjectsFromComment(allImpactedProjects, commentBody)
230+
if err != nil {
231+
log.Printf("error filtering out projects from comment issueNumber: %v, error: %v", issueNumber, err)
232+
utils.InitCommentReporter(bbService, issueNumber, fmt.Sprintf(":x: Error filtering out projects from comment: %v", err))
233+
return fmt.Errorf("error filtering out projects from comment")
234+
}
235+
230236
// perform unlocking in backend
231237
if config.PrLocks {
232238
for _, project := range impactedProjectsForComment {

ee/backend/controllers/gitlab.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,17 +400,24 @@ func handleIssueCommentEvent(gitlabProvider utils.GitlabProvider, payload *gitla
400400
return fmt.Errorf("error while fetching branch name")
401401
}
402402

403-
processIssueCommentEventResult, err := generic.ProcessIssueCommentEvent(issueNumber, commentBody, config, projectsGraph, glService)
403+
processIssueCommentEventResult, err := generic.ProcessIssueCommentEvent(issueNumber, config, projectsGraph, glService)
404404
if err != nil {
405405
log.Printf("Error processing event: %v", err)
406406
utils.InitCommentReporter(glService, issueNumber, fmt.Sprintf(":x: Error processing event: %v", err))
407407
return fmt.Errorf("error processing event")
408408
}
409409
log.Printf("GitHub IssueComment event processed successfully\n")
410410

411-
impactedProjectsForComment := processIssueCommentEventResult.ImpactedProjectsForComment
412411
impactedProjectsSourceMapping := processIssueCommentEventResult.ImpactedProjectsSourceMapping
413412
allImpactedProjects := processIssueCommentEventResult.AllImpactedProjects
413+
414+
impactedProjectsForComment, err := generic.FilterOutProjectsFromComment(allImpactedProjects, commentBody)
415+
if err != nil {
416+
log.Printf("error filtering out projects from comment issueNumber: %v, error: %v", issueNumber, err)
417+
utils.InitCommentReporter(glService, issueNumber, fmt.Sprintf(":x: Error filtering out projects from comment: %v", err))
418+
return fmt.Errorf("error filtering out projects from comment")
419+
}
420+
414421
// perform unlocking in backend
415422
if config.PrLocks {
416423
for _, project := range impactedProjectsForComment {

libs/ci/generic/comment_filter.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package generic
2+
3+
import (
4+
"fmt"
5+
"github.com/diggerhq/digger/libs/digger_config"
6+
"github.com/samber/lo"
7+
)
8+
9+
func FilterOutProjectsFromComment(impactedProjects []digger_config.Project, comment string) ([]digger_config.Project, error) {
10+
var filteredProjects []digger_config.Project
11+
commentParts, valid, err := ParseDiggerCommentFlags(comment)
12+
if !valid {
13+
return nil, fmt.Errorf("invalid comment: %v", err)
14+
}
15+
if err != nil {
16+
return nil, fmt.Errorf("failed to parse comment %v", err)
17+
}
18+
19+
// filtering by layer
20+
if commentParts.Layer != -1 {
21+
filteredProjects = lo.Filter(impactedProjects, func(project digger_config.Project, _ int) bool {
22+
return int(project.Layer) == commentParts.Layer
23+
})
24+
return filteredProjects, nil
25+
}
26+
27+
// filtering by projects and directories
28+
if commentParts.Projects != nil || commentParts.Directories != nil {
29+
if commentParts.Projects != nil {
30+
// check that projects are in the list
31+
for _, project := range commentParts.Projects {
32+
if !lo.ContainsBy(impactedProjects, func(p digger_config.Project) bool {
33+
return p.Name == project
34+
}) {
35+
return nil, fmt.Errorf("project %v not found in the list of impacted projects", project)
36+
}
37+
}
38+
filteredProjects = lo.Filter(impactedProjects, func(project digger_config.Project, _ int) bool {
39+
return lo.Contains(commentParts.Projects, project.Name)
40+
})
41+
}
42+
if commentParts.Directories != nil {
43+
filteredDirectoriesProjects := lo.Filter(impactedProjects, func(project digger_config.Project, _ int) bool {
44+
return lo.Contains(commentParts.Directories, project.Dir)
45+
})
46+
filteredProjects = append(filteredProjects, filteredDirectoriesProjects...)
47+
}
48+
49+
return filteredProjects, nil
50+
}
51+
52+
// if nothing specified in flags, we will return the original list
53+
return impactedProjects, nil
54+
55+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package generic
2+
3+
import (
4+
"github.com/diggerhq/digger/libs/digger_config"
5+
"reflect"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestFilterOutProjectsFromComment(t *testing.T) {
11+
projects := []digger_config.Project{
12+
{Name: "proj1", Dir: "/app", Layer: 1},
13+
{Name: "proj2", Dir: "/db", Layer: 2},
14+
}
15+
16+
tests := []struct {
17+
name string
18+
comment string
19+
want []digger_config.Project
20+
wantErr bool
21+
errorText string
22+
}{
23+
{
24+
name: "No flags returns all projects",
25+
comment: "digger plan",
26+
want: projects,
27+
},
28+
{
29+
name: "Filter by layer",
30+
comment: "digger plan --layer 1",
31+
want: []digger_config.Project{{Name: "proj1", Dir: "/app", Layer: 1}},
32+
},
33+
{
34+
name: "Filter by project",
35+
comment: "digger plan --project proj1",
36+
want: []digger_config.Project{{Name: "proj1", Dir: "/app", Layer: 1}},
37+
},
38+
{
39+
name: "Filter by directory",
40+
comment: "digger plan --directory /app",
41+
want: []digger_config.Project{{Name: "proj1", Dir: "/app", Layer: 1}},
42+
},
43+
{
44+
name: "Invalid project name error",
45+
comment: "digger plan --project unknown",
46+
wantErr: true,
47+
errorText: "project unknown not found",
48+
},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
got, err := FilterOutProjectsFromComment(projects, tt.comment)
54+
55+
if tt.wantErr {
56+
if err == nil {
57+
t.Errorf("expected error, got nil")
58+
} else if !strings.Contains(err.Error(), tt.errorText) {
59+
t.Errorf("expected error containing %q, got %v", tt.errorText, err)
60+
}
61+
return
62+
}
63+
64+
if err != nil {
65+
t.Errorf("unexpected error: %v", err)
66+
return
67+
}
68+
69+
if !reflect.DeepEqual(got, tt.want) {
70+
t.Errorf("expected %v, got %v", tt.want, got)
71+
}
72+
})
73+
}
74+
}

libs/ci/generic/comment_utils.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package generic
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"github.com/google/shlex"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
type CommentParts struct {
12+
Projects []string
13+
Layer int
14+
Directories []string
15+
}
16+
17+
type multiFlag []string
18+
19+
func (m *multiFlag) String() string {
20+
return strings.Join(*m, ",")
21+
}
22+
23+
func (m *multiFlag) Set(value string) error {
24+
*m = append(*m, value)
25+
return nil
26+
}
27+
28+
// singleUseInt rejects multiple occurrences of the same flag.
29+
type singleUseInt struct {
30+
val int
31+
seen bool
32+
name string // for better error messages
33+
}
34+
35+
func (s *singleUseInt) String() string { return strconv.Itoa(s.val) }
36+
func (s *singleUseInt) Set(v string) error {
37+
if s.seen {
38+
return fmt.Errorf("--%s specified multiple times", s.name)
39+
}
40+
i, err := strconv.Atoi(v)
41+
if err != nil {
42+
return fmt.Errorf("invalid value for --%s: %w", s.name, err)
43+
}
44+
s.val = i
45+
s.seen = true
46+
return nil
47+
}
48+
49+
// ParseDiggerCommentFlags parse the flags in the comment such as -p and -d and --layer
50+
// validates that the right number of flags are specified
51+
// Does not validate the "digger plan" part of the command that is left to a prior function
52+
func ParseDiggerCommentFlags(comment string) (*CommentParts, bool, error) {
53+
comment = strings.TrimSpace(strings.ToLower(comment))
54+
55+
args, err := shlex.Split(comment)
56+
if err != nil {
57+
return nil, false, fmt.Errorf("failed to split input %v", err)
58+
59+
}
60+
61+
if len(args) < 2 {
62+
return nil, false, fmt.Errorf("incorrect operation specified: (%v) err: %v", comment, err)
63+
}
64+
65+
fs := flag.NewFlagSet("digger", flag.ContinueOnError)
66+
67+
var projects multiFlag
68+
var directories multiFlag
69+
layer := &singleUseInt{val: -1, name: "layer"}
70+
71+
fs.Var(&projects, "p", "project (short form)")
72+
fs.Var(&projects, "project", "project (long form)")
73+
74+
fs.Var(&directories, "d", "directory (short form)")
75+
fs.Var(&directories, "directory", "directory (long form)")
76+
77+
fs.Var(layer, "layer", "layer to plan or apply")
78+
79+
err = fs.Parse(args[2:])
80+
if err != nil {
81+
return nil, false, fmt.Errorf("failed to parse input %v", comment)
82+
}
83+
84+
// ❗ Disallow mixing --layer with -p or -d
85+
if layer.val != -1 && (len(projects) > 0 || len(directories) > 0) {
86+
return nil, false, fmt.Errorf("cannot mix --layer with -p or -d")
87+
}
88+
89+
return &CommentParts{
90+
Projects: projects,
91+
Layer: layer.val,
92+
Directories: directories,
93+
}, true, nil
94+
}

0 commit comments

Comments
 (0)