Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Github reaction emojis on PR comments #2706

Merged
merged 4 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const (
DisableMarkdownFoldingFlag = "disable-markdown-folding"
DisableRepoLockingFlag = "disable-repo-locking"
DiscardApprovalOnPlanFlag = "discard-approval-on-plan"
EmojiReaction = "emoji-reaction"
EnablePolicyChecksFlag = "enable-policy-checks"
EnableRegExpCmdFlag = "enable-regexp-cmd"
EnableDiffMarkdownFormat = "enable-diff-markdown-format"
Expand Down Expand Up @@ -144,6 +145,7 @@ const (
DefaultCheckoutDepth = 0
DefaultBitbucketBaseURL = bitbucketcloud.BaseURL
DefaultDataDir = "~/.atlantis"
DefaultEmojiReaction = "eyes"
DefaultExecutableName = "atlantis"
DefaultMarkdownTemplateOverridesDir = "~/.markdown_templates"
DefaultGHHostname = "github.com"
Expand Down Expand Up @@ -244,6 +246,10 @@ var stringFlags = map[string]stringFlag{
description: "Path to directory to store Atlantis data.",
defaultValue: DefaultDataDir,
},
EmojiReaction: {
description: "Emoji Reaction to use to react to comments",
defaultValue: DefaultEmojiReaction,
},
ExecutableName: {
description: "Comment command executable name.",
defaultValue: DefaultExecutableName,
Expand Down Expand Up @@ -650,7 +656,7 @@ func (s *ServerCmd) Init() *cobra.Command {

c.SetUsageTemplate(usageTmpl(stringFlags, intFlags, boolFlags))
// If a user passes in an invalid flag, tell them what the flag was.
c.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
c.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
s.printErr(err)
return err
})
Expand Down Expand Up @@ -792,6 +798,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) {
if c.BitbucketBaseURL == "" {
c.BitbucketBaseURL = DefaultBitbucketBaseURL
}
if c.EmojiReaction == "" {
c.EmojiReaction = DefaultEmojiReaction
}
if c.ExecutableName == "" {
c.ExecutableName = DefaultExecutableName
}
Expand Down
27 changes: 19 additions & 8 deletions server/controllers/events/events_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ const azuredevopsTestURL = "https://fabrikam.visualstudio.com/DefaultCollection/
// VCSEventsController handles all webhook requests which signify 'events' in the
// VCS host, ex. GitHub.
type VCSEventsController struct {
CommandRunner events.CommandRunner
PullCleaner events.PullCleaner
Logger logging.SimpleLogging
Scope tally.Scope
Parser events.EventParsing
CommentParser events.CommentParsing
ApplyDisabled bool
CommandRunner events.CommandRunner
PullCleaner events.PullCleaner
Logger logging.SimpleLogging
Scope tally.Scope
Parser events.EventParsing
CommentParser events.CommentParsing
ApplyDisabled bool
EmojiReaction string
ExecutableName string
// GithubWebhookSecret is the secret added to this webhook via the GitHub
// UI that identifies this call as coming from GitHub. If empty, no
// request validation is done.
Expand Down Expand Up @@ -309,9 +311,18 @@ func (e *VCSEventsController) HandleGithubCommentEvent(event *github.IssueCommen
}
}

body := event.GetComment().GetBody()

if strings.HasPrefix(body, e.ExecutableName+" ") {
err = e.VCSClient.ReactToComment(baseRepo, *event.Comment.ID, e.EmojiReaction)
if err != nil {
logger.Warn("Failed to react to comment: %s", err)
}
}

// We pass in nil for maybeHeadRepo because the head repo data isn't
// available in the GithubIssueComment event.
return e.handleCommentEvent(logger, baseRepo, nil, nil, user, pullNum, event.Comment.GetBody(), models.Github)
return e.handleCommentEvent(logger, baseRepo, nil, nil, user, pullNum, body, models.Github)
}

// HandleBitbucketCloudCommentEvent handles comment events from Bitbucket.
Expand Down
21 changes: 21 additions & 0 deletions server/controllers/events/events_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,25 @@ func TestPost_GithubCommentSuccess(t *testing.T) {
cr.VerifyWasCalledOnce().RunCommentCommand(baseRepo, nil, nil, user, 1, &cmd)
}

func TestPost_GithubCommentReaction(t *testing.T) {
t.Log("when the event is a github comment with a valid command we call the command handler")
e, v, _, _, p, _, _, vcsClient, cp := setup(t)
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req.Header.Set(githubHeader, "issue_comment")
event := `{"action": "created", "comment": {"body": "atlantis help", "id": 1}}`
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
baseRepo := models.Repo{}
user := models.User{}
cmd := events.CommentCommand{}
When(p.ParseGithubIssueCommentEvent(matchers.AnyPtrToGithubIssueCommentEvent())).ThenReturn(baseRepo, user, 1, nil)
When(cp.Parse("", models.Github)).ThenReturn(events.CommentParseResult{Command: &cmd})
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusOK, "Processing...")

vcsClient.VerifyWasCalledOnce().ReactToComment(baseRepo, 1, "eyes")
}

func TestPost_GithubPullRequestInvalid(t *testing.T) {
t.Log("when the event is a github pull request with invalid data we return a 400")
e, v, _, _, p, _, _, _, _ := setup(t)
Expand Down Expand Up @@ -922,6 +941,8 @@ func setup(t *testing.T) (events_controllers.VCSEventsController, *mocks.MockGit
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
ExecutableName: "atlantis",
EmojiReaction: "eyes",
TestingMode: true,
Logger: logger,
Scope: scope,
Expand Down
10 changes: 10 additions & 0 deletions server/core/config/raw/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const DefaultParallelPolicyCheck = false
// DefaultDeleteSourceBranchOnMerge being false is the default setting whether or not to remove a source branch on merge
const DefaultDeleteSourceBranchOnMerge = false

// DefaultEmojiReaction is the default emoji reaction for repos
const DefaultEmojiReaction = ""

// RepoCfg is the raw schema for repo-level atlantis.yaml config.
type RepoCfg struct {
Version *int `yaml:"version,omitempty"`
Expand All @@ -32,6 +35,7 @@ type RepoCfg struct {
ParallelApply *bool `yaml:"parallel_apply,omitempty"`
ParallelPlan *bool `yaml:"parallel_plan,omitempty"`
DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"`
EmojiReaction *string `yaml:"emoji_reaction,omitempty"`
AllowedRegexpPrefixes []string `yaml:"allowed_regexp_prefixes,omitempty"`
}

Expand Down Expand Up @@ -79,6 +83,11 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
parallelPlan = *r.ParallelPlan
}

emojiReaction := DefaultEmojiReaction
if r.EmojiReaction != nil {
emojiReaction = *r.EmojiReaction
}

return valid.RepoCfg{
Version: *r.Version,
Projects: validProjects,
Expand All @@ -89,5 +98,6 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
ParallelPolicyCheck: parallelPlan,
DeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge,
AllowedRegexpPrefixes: r.AllowedRegexpPrefixes,
EmojiReaction: emojiReaction,
}
}
1 change: 1 addition & 0 deletions server/core/config/valid/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type RepoCfg struct {
ParallelPolicyCheck bool
DeleteSourceBranchOnMerge *bool
RepoLocking *bool
EmojiReaction string
AllowedRegexpPrefixes []string
}

Expand Down
4 changes: 4 additions & 0 deletions server/events/vcs/azuredevops_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ func (g *AzureDevopsClient) CreateComment(repo models.Repo, pullNum int, comment
return nil
}

func (g *AzureDevopsClient) ReactToComment(repo models.Repo, commentID int64, reaction string) error { //nolint: revive
return nil
}

func (g *AzureDevopsClient) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
return nil
}
Expand Down
6 changes: 6 additions & 0 deletions server/events/vcs/bitbucketcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string, co
return err
}

// UpdateComment updates the body of a comment on the merge request.
func (b *Client) ReactToComment(repo models.Repo, commentID int64, reaction string) error { // nolint revive
// TODO: Bitbucket support for reactions
return nil
}

func (b *Client) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
return nil
}
Expand Down
4 changes: 4 additions & 0 deletions server/events/vcs/bitbucketserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string, co
return nil
}

func (b *Client) ReactToComment(repo models.Repo, commentID int64, reaction string) error { // nolint: revive
return nil
}

func (b *Client) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
return nil
}
Expand Down
2 changes: 2 additions & 0 deletions server/events/vcs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type Client interface {
// relative to the repo root, e.g. parent/child/file.txt.
GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error)
CreateComment(repo models.Repo, pullNum int, comment string, command string) error

ReactToComment(repo models.Repo, commentID int64, reaction string) error
HidePrevCommandComments(repo models.Repo, pullNum int, command string) error
PullIsApproved(repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error)
PullIsMergeable(repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error)
Expand Down
9 changes: 8 additions & 1 deletion server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ func (g *GithubClient) CreateComment(repo models.Repo, pullNum int, comment stri
return nil
}

// ReactToComment adds a reaction to a comment.
func (g *GithubClient) ReactToComment(repo models.Repo, commentID int64, reaction string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests

g.logger.Debug("POST /repos/%v/%v/issues/comments/%d/reactions", repo.Owner, repo.Name, commentID)
_, _, err := g.client.Reactions.CreateIssueCommentReaction(g.ctx, repo.Owner, repo.Name, commentID, reaction)
return err
}

func (g *GithubClient) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
var allComments []*github.IssueComment
nextPage := 0
Expand Down Expand Up @@ -395,7 +402,7 @@ func (g *GithubClient) GetCombinedStatusMinusApply(repo models.Repo, pull *githu
return false, errors.Wrap(err, "getting combined status")
}

//iterate over statuses - return false if we find one that isnt "apply" and doesnt have state = "success"
//iterate over statuses - return false if we find one that isn't "apply" and doesn't have state = "success"
for _, r := range status.Statuses {
if strings.HasPrefix(*r.Context, fmt.Sprintf("%s/%s", vcstatusname, command.Apply.String())) {
continue
Expand Down
4 changes: 4 additions & 0 deletions server/events/vcs/gitlab_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ func (g *GitlabClient) CreateComment(repo models.Repo, pullNum int, comment stri
return nil
}

func (g *GitlabClient) ReactToComment(repo models.Repo, commentID int64, reaction string) error { // nolint: revive
return nil
}

func (g *GitlabClient) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
return nil
}
Expand Down
28 changes: 24 additions & 4 deletions server/events/vcs/instrumented_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ func (c *InstrumentedClient) GetModifiedFiles(repo models.Repo, pull models.Pull
}

return files, err

}

func (c *InstrumentedClient) CreateComment(repo models.Repo, pullNum int, comment string, command string) error {
scope := c.StatsScope.SubScope("create_comment")
scope = SetGitScopeTags(scope, repo.FullName, pullNum)
Expand All @@ -127,6 +127,26 @@ func (c *InstrumentedClient) CreateComment(repo models.Repo, pullNum int, commen
executionSuccess.Inc(1)
return nil
}

func (c *InstrumentedClient) ReactToComment(repo models.Repo, commentID int64, reaction string) error {
scope := c.StatsScope.SubScope("react_to_comment")

executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()

executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)

if err := c.Client.ReactToComment(repo, commentID, reaction); err != nil {
executionError.Inc(1)
c.Logger.Err("Unable to react to comment, error: %s", err.Error())
return err
}

executionSuccess.Inc(1)
return nil
}

func (c *InstrumentedClient) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
scope := c.StatsScope.SubScope("hide_prev_plan_comments")
scope = SetGitScopeTags(scope, repo.FullName, pullNum)
Expand All @@ -148,6 +168,7 @@ func (c *InstrumentedClient) HidePrevCommandComments(repo models.Repo, pullNum i
return nil

}

func (c *InstrumentedClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) {
scope := c.StatsScope.SubScope("pull_is_approved")
scope = SetGitScopeTags(scope, repo.FullName, pull.Num)
Expand All @@ -169,8 +190,8 @@ func (c *InstrumentedClient) PullIsApproved(repo models.Repo, pull models.PullRe
}

return approved, err

}

func (c *InstrumentedClient) PullIsMergeable(repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
scope := c.StatsScope.SubScope("pull_is_mergeable")
scope = SetGitScopeTags(scope, repo.FullName, pull.Num)
Expand Down Expand Up @@ -213,8 +234,8 @@ func (c *InstrumentedClient) UpdateStatus(repo models.Repo, pull models.PullRequ

executionSuccess.Inc(1)
return nil

}

func (c *InstrumentedClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error {
scope := c.StatsScope.SubScope("merge_pull")
scope = SetGitScopeTags(scope, pull.BaseRepo.FullName, pull.Num)
Expand All @@ -233,7 +254,6 @@ func (c *InstrumentedClient) MergePull(pull models.PullRequest, pullOptions mode

executionSuccess.Inc(1)
return nil

}

// taken from other parts of the code, would be great to have this in a shared spot
Expand Down
50 changes: 50 additions & 0 deletions server/events/vcs/mocks/mock_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions server/events/vcs/not_configured_vcs_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func (a *NotConfiguredVCSClient) CreateComment(repo models.Repo, pullNum int, co
func (a *NotConfiguredVCSClient) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
return nil
}
func (a *NotConfiguredVCSClient) ReactToComment(repo models.Repo, commentID int64, reaction string) error { // nolint: revive
return nil
}
func (a *NotConfiguredVCSClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) {
return models.ApprovalStatus{}, a.err()
}
Expand Down
Loading