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: ignore edited comments with new option #357

Merged

Conversation

devinburnette
Copy link
Contributor

Closes #356

As discussed in the attached issue, this feature would allow a new boolean option ignore_edited_comments: that when set to true would ignore both issue comments and review comments that have been edited when evaluating the approval rules. This helps guard against potential situations where a user could edit a comment to gain approval outside of the intended approval rules construct.

I went with adding the updated_at field we get back from the graphql response because that makes it easy to determine quickly if a comment has been edited irrespective of whom the comment was edited by (which was also an intentional choice).

I went with putting it in the approval part of the code because it seemed like that was where it best fit naturally given we already have access to the rule options and the candidates and this is the place we seem to already be doing other types of candidate filtering. This would have been harder to implement over in the server handlers as we don't have access to the options yet. Let me know what you think of this approach, open to other ideas, but this seemed the easiest to implement.

Copy link
Member

@bluekeyes bluekeyes left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! Overall, this looks good, but I had one suggestion about where to place the filtering logic.

Comment on lines 228 to 233
if r.Options.IgnoreEditedComments {
if c.UpdatedAt != c.CreatedAt {
log.Debug().Str("user", c.User).Msg("ignoring approval because comment was edited")
continue
}
}
Copy link
Member

Choose a reason for hiding this comment

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

While I believe this will work here, I think it might be more correct to perform this filtering as part of the filteredCandidates function. That will make sure they are ignored before almost any other processing happens and logically, if we're ignoring these comments, they should never appear as "candidates" for approval.

Comment on lines 278 to 293
if candidate.CreatedAt.After(*last.PushedAt) {
allowedCandidates = append(allowedCandidates, candidate)
if r.Options.IgnoreEditedComments {
if candidate.UpdatedAt != candidate.CreatedAt {
log.Debug().Msg("discarded candidate because it was an edited comment")
continue
}
}
if r.Options.InvalidateOnPush {
if candidate.CreatedAt.Before(*last.PushedAt) {
log.Debug().Msgf("discarded candidate because it was invalidated by push of %s at %s",
last.SHA,
last.PushedAt.Format(time.RFC3339))
continue
}
}
allowedCandidates = append(allowedCandidates, candidate)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bluekeyes like this? or were you thinking something different?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, this is the right direction, but I think it needs a few more adjustments.

Comment on lines 278 to 293
if candidate.CreatedAt.After(*last.PushedAt) {
allowedCandidates = append(allowedCandidates, candidate)
if r.Options.IgnoreEditedComments {
if candidate.UpdatedAt != candidate.CreatedAt {
log.Debug().Msg("discarded candidate because it was an edited comment")
continue
}
}
if r.Options.InvalidateOnPush {
if candidate.CreatedAt.Before(*last.PushedAt) {
log.Debug().Msgf("discarded candidate because it was invalidated by push of %s at %s",
last.SHA,
last.PushedAt.Format(time.RFC3339))
continue
}
}
allowedCandidates = append(allowedCandidates, candidate)
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, this is the right direction, but I think it needs a few more adjustments.

@@ -256,7 +257,7 @@ func (r *Rule) filteredCandidates(ctx context.Context, prctx pull.Context) ([]*c
}
sort.Stable(common.CandidatesByCreationTime(candidates))

if !r.Options.InvalidateOnPush {
if !r.Options.IgnoreEditedComments && !r.Options.InvalidateOnPush {
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to treat these as two separate conditions: the code on L264-275 is only relevant for InvalidateOnPush, so ideally we only run that when the option is enabled. There's also a situation (L269) where we could return early without applying IgnoreEditedComments filtering.

It might make sense to extract each filter into a new helper function so that we can directly update candidates, something like:

candidates, err := r.Options.GetMethods().Candidates(ctx, prctx)
// ...

if r.Options.IgnoreEditedComments {
  candidates, err = r.filterEditedCandidates(ctx, prctx, candidates)
  if err != nil {
    return nil, err
  }
}
if r.Options.InvalidateOnPush {
  candidates, err = r.filterInvalidCandidates(ctx, prctx, candidates)
  if err != nil {
    return nil, err
  }
}

return candidates

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i see, i missed L269, will give this approach a shot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i guess i was just trying to avoid looping over the candidates multiple times

Comment on lines +276 to +300
func (r *Rule) filterEditedCandidates(ctx context.Context, prctx pull.Context, candidates []*common.Candidate) ([]*common.Candidate, error) {
log := zerolog.Ctx(ctx)

if !r.Options.IgnoreEditedComments {
return candidates, nil
}

var allowedCandidates []*common.Candidate
for _, candidate := range candidates {
if r.Options.IgnoreEditedComments {
if candidate.UpdatedAt == candidate.CreatedAt {
allowedCandidates = append(allowedCandidates, candidate)
}
}
}

log.Debug().Msgf("discarded %d candidates with edited comments",
len(candidates)-len(allowedCandidates))

return allowedCandidates, nil
}

func (r *Rule) filterInvalidCandidates(ctx context.Context, prctx pull.Context, candidates []*common.Candidate) ([]*common.Candidate, error) {
log := zerolog.Ctx(ctx)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bluekeyes updated with the suggested approach here.

Copy link
Member

@bluekeyes bluekeyes left a comment

Choose a reason for hiding this comment

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

Thanks for the updates - this looks good to me! I think iterating over candidates multiple times is fine given the expected low number and the relative cost compared to all the API requests that have to be made.

I'll try to test this out in our environment and make a release by the end of the week. If you'd like to test in your environment before that and you're using Docker, you can pull the snapshot tag from Docker Hub once this merges and the build completes.

@bluekeyes bluekeyes merged commit a73549d into palantir:develop Jan 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: ignore edited comments with new option
2 participants