-
Notifications
You must be signed in to change notification settings - Fork 108
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
Adding a details tab to policy bot rules #407
Conversation
37638aa
to
e4407ba
Compare
fed0e44
to
c0661fd
Compare
c0661fd
to
ca1873a
Compare
Now that it matters, I suggest extracting the keys from the map, sorting them, and then iterating through the sorted list in the predicate implementation. We generally still need to use a
It looks like this was added to test the difference between a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a good start! In this initial review, I left some higher-level comments about the overall structure and design, which I'm happy to talk more about when we meet tomorrow. I think we'll also want to look at the visual design, but that's hard to communicate here, so I'll try to make a mockup or drawing.
Later, I'll do a second review looking more at details and style things, but I didn't want to comment on code that might be deleted if the structure changes. But if you prefer, I can do that now so you have everything at once.
a9ff676
to
4c94438
Compare
There was a problem hiding this 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! Looking through this again, I had another idea for how to further simplify the PredicateInfo
concept, hopefully making this easier to understand and extend in the future. Let me know if it make sense to you or if you have any disagreements or questions. I think it can work in all cases, but I haven't actually tried every single predicate.
That's really the only design comment I have, although it is a big one. Look for the comments that start with (1/?)
for discussion on that topic. Everything else is relatively minor comments about code style.
I appreciate you iterating with me on this so far!
policy/approval/approve.go
Outdated
res.Requires = common.Actors{ | ||
Organizations: r.Requires.Organizations, | ||
Teams: r.Requires.Teams, | ||
Users: r.Requires.Users, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
res.Requires = common.Actors{ | |
Organizations: r.Requires.Organizations, | |
Teams: r.Requires.Teams, | |
Users: r.Requires.Users, | |
} | |
res.Requires = r.Requires.Actors |
Embedded fields in Go are implicitly named after their type, so can use this to copy the whole field in one operation.
policy/approval/approve.go
Outdated
Users: r.Requires.Users, | ||
} | ||
|
||
var predicatesInfo []*common.PredicateInfo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: predicateInfos
policy/approval/approve.go
Outdated
|
||
for _, p := range r.Predicates.Predicates() { | ||
satisfied, desc, err := p.Evaluate(ctx, prctx) | ||
satisfied, pPredicateInfo, err := p.Evaluate(ctx, prctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: rename pPredicateInfo
to info
policy/disapproval/disapprove.go
Outdated
res.Requires = common.Actors{ | ||
Organizations: p.Requires.Organizations, | ||
Teams: p.Requires.Teams, | ||
Users: p.Requires.Users, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
res.Requires = common.Actors{ | |
Organizations: p.Requires.Organizations, | |
Teams: p.Requires.Teams, | |
Users: p.Requires.Users, | |
} | |
res.Requires = p.Requires.Actors |
policy/predicate/author.go
Outdated
Type: common.ContributorType, | ||
Name: "HasAuthorIn", | ||
ContributorInfo: &contributorInfo, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(4/?) To give an example of how the proposed PredicateInfo
struct might work in a predicate:
info := PredicateInfo{
ValuesPhrase: "authors",
Values: []string{author},
ConditionPhrase: "meet the required membership conditions",
ConditionsMap: map[string][]string{
"Organizations": pred.Organizations,
"Teams": pred.Teams,
"Users": pred.Users,
},
}
policy/predicate/author.go
Outdated
Type: common.ContributorType, | ||
Name: "AuthorIsOnlyContributor", | ||
ContributorInfo: &contributorInfo, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(5/?) Here's another example:
info := PredicateInfo{
ValuesPhrase: "commits",
Values: []string{fmt.Sprintf("%.10s", c.SHA}, // or the full list of commit SHAs
ConditionPhrase: "meet the authorship requirement",
ConditionValues: []string{
fmt.Sprintf("authored and committed by %s", author),
},
}
policy/predicate/file.go
Outdated
@@ -140,6 +192,14 @@ func (exp ComparisonExpr) MarshalText() ([]byte, error) { | |||
return []byte(fmt.Sprintf("%s %d", op, exp.Value)), nil | |||
} | |||
|
|||
func (exp ComparisonExpr) MarshalTextToString() string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be called String
to match Go conventions (implement the Stringer
interface)
policy/predicate/file.go
Outdated
func (exp ComparisonExpr) MarshalTextToString() string { | ||
res, err := exp.MarshalText() | ||
if err != nil { | ||
return "Unknown" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return "Unknown" | |
return fmt.Sprintf("?? (op:%d) %d"), exp.Op, exp.Value) |
This will give at least a bit of information about what's wrong.
policy/predicate/file.go
Outdated
fileInfo.TotalLimit = string(res[:]) | ||
fileInfo.TotalModifiedLines = additions + deletions | ||
predicateInfo.Description = fmt.Sprintf("modification of (+%d, -%d) does not match any conditions", additions, deletions) | ||
return false, &predicateInfo, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(6/?) One more example
info := PredicateInfo{
ValuesPhrase: "file modifications",
Values: []string{fmt.Sprintf("+%d / -%d (total %d)", additions, deletions, additions+deletions)},
ConditionPhrase: "meet the modification conditions",
ConditionValues: []string{ // filter this to only contain the non-empty conditions
fmt.Sprintf("added %s", pred.Additions),
fmt.Sprintf("deleted %s", pred.Deletions),
fmt.Sprintf("total %s", pred.Total),
}
}
2c06e95
to
c1c287d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the flood of comments, but from a code perspective this looks good! Thanks for implementing the PredicateResult
changes. Almost all of my suggestions are minor things to improve the phrasing of messages. There are also a couple style things that would be good to address.
Beyond that, we can talk about any CSS changes tomorrow and then I think this will be ready to merge.
There's a few more things we could do to make the messages read even better, but I think the current implementation (with the suggestions in this review) is good enough for an initial version, so I don't want to delay this any further.
server/templates/details.html.tmpl
Outdated
<ul class="list-decimal list-inside py-2"> | ||
{{range .PredicateResults}} | ||
<li> | ||
The {{.ValuePhrase}} : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The {{.ValuePhrase}} : | |
The {{.ValuePhrase}}: |
server/templates/details.html.tmpl
Outdated
<ul class="list-none list-inside"> | ||
{{range $key, $value := getRequires .}} | ||
<li class="p-2 text-sm"> | ||
<p> {{$key}} : </p> | ||
<ul class="list-disc list-inside"> | ||
{{range $value}} | ||
<li class="ml-4"> | ||
<a href={{.Link}}>{{.Name}}</a> | ||
</li> | ||
{{end}} | ||
</ul> | ||
</li> | ||
{{end}} | ||
</ul> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you try using the descriptions lists here as well, instead of the p
tag with the key?
server/handler/frontend.go
Outdated
|
||
} | ||
for _, user := range result.Requires.Users { | ||
membershipInfo["Users"] = append(membershipInfo["Users"], Membership{Name: user, Link: githubURL + user}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
membershipInfo["Users"] = append(membershipInfo["Users"], Membership{Name: user, Link: githubURL + user}) | |
membershipInfo["Users"] = append(membershipInfo["Users"], Membership{Name: user, Link: githubURL + "/" + user}) |
server/handler/frontend.go
Outdated
@@ -55,6 +60,10 @@ func LoadTemplates(c *FilesConfig, basePath string) (templatetree.HTMLTree, erro | |||
|
|||
return r | |||
}, | |||
"isRequiresNotEmpty": isRequiresNotEmpty, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: I might call this hasRequires
policy/predicate/author.go
Outdated
predicateResult := common.PredicateResult{ | ||
Satisfied: result, | ||
Description: desc, | ||
ValuePhrase: "Author", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValuePhrase: "Author", | |
ValuePhrase: "authors", |
I know there's only one author, but this will fit better with the list and the rest of the phrasing.
policy/predicate/signature.go
Outdated
@@ -129,11 +191,17 @@ func (pred *HasValidSignaturesByKeys) Evaluate(ctx context.Context, prctx pull.C | |||
} | |||
} | |||
if !isValidKey { | |||
return false, fmt.Sprintf("Key %q does not meet the required key conditions for signing", key), nil | |||
predicateResult.Values = []string{key} | |||
predicateResult.ValuePhrase = "key" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
predicateResult.ValuePhrase = "key" | |
predicateResult.ValuePhrase = "keys" | |
predicateResult.ConditionPhrase = "exist in the set of allowed keys" |
policy/predicate/status.go
Outdated
ValuePhrase: "check", | ||
ConditionPhrase: "has successful status", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValuePhrase: "check", | |
ConditionPhrase: "has successful status", | |
ValuePhrase: "status checks", | |
ConditionPhrase: "exist and pass", |
Here's another place that doesn't set ConditionMap
or ConditionValues
policy/predicate/title.go
Outdated
ConditionPhrase: "meet the pattern requirement", | ||
} | ||
|
||
var MatchPatterns, NotMatchPatterns []string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: local variables start with lowercase letters by convention (matchPatterns
, notMatchPatterns
)
policy/predicate/title.go
Outdated
title := prctx.Title() | ||
|
||
predicateResult := common.PredicateResult{ | ||
Satisfied: false, | ||
ValuePhrase: "title", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValuePhrase: "title", | |
ValuePhrase: "titles", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't there be only 1 title for each pr?
405cdb6
to
9b52479
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working through all the iterations here, this is looking good! I spotted two things that I think are unused and had some minor suggestions on the CSS (although I haven't tested the changes.) Once those are fixed, I think this is ready to merge.
policy/predicate/signature.go
Outdated
} | ||
} | ||
|
||
var keyList []string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be used somewhere? Right now, it looks like you only append to it and never use the resulting list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Previously I returned both keys and the commit hashes. Since now we only want commit hashes, this should be removed. Good catch.
server/assets/css/main.css
Outdated
.list-numbers > li { | ||
@apply ml-2; | ||
} | ||
|
||
.list-star > li:before { | ||
content: "*"; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think both of these rules are unused now
server/handler/frontend.go
Outdated
@@ -73,3 +82,26 @@ func Static(prefix string, c *FilesConfig) http.Handler { | |||
|
|||
return http.StripPrefix(prefix, http.FileServer(http.Dir(dir))) | |||
} | |||
|
|||
func hasRequires(result common.Actors) bool { | |||
if len(result.Organizations) != 0 || len(result.Teams) != 0 || len(result.Users) != 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant to comment on this earlier, but forgot - common.Actors
can also select users by their permissions level, which is a condition we're ignoring at the moment.
I don't think it should be part of this PR, but it would be nice to make a followup change to add minimal support for rules that use permissions. I don't think it's common in the policies you have been using for testing but is pretty common in other policies.
When we do add that support, I think you can replace this function with the existing IsEmpty()
method on common.Actors
.
server/templates/details.html.tmpl
Outdated
<div class="pt-2"></div> | ||
<div class="border-t border-light-gray3"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should be able to combine these div
s by adding mt-2
to the div
with the border. I like to avoid empty div
when I can, and it should be possible here.
server/templates/details.html.tmpl
Outdated
<div class="pt-2"></div> | ||
<div class="border-t border-light-gray3"> | ||
{{if .PredicateResults}} | ||
<div class="pt-2 mt-2"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this only needs pt-2
now that the border is defined on a different element. That will also match the requires div
below.
9b52479
to
7cbede3
Compare
This PR is for adding an expendable details tab to policy bot.
Here is an example of the UI. User can click on the little marker to expand and collapse the details of the rule. This can help user to easily understand why a rule is selected, as well as providing them a link to the users that can approve their PR.
The way policy bot works currently is that it receives a webhook from Github, and then finds the policy written in the repo. For each rule, it determines if it is skipped or selected based on the predicates. It also determines the status of each rule based on the approval/disapproval review the PR received.
Our change adds a struct called
PredicateInfo
. This struct will be produced while the code is evaluating predicate and passed toResult
to get rendered in html. It has two string fields and some pointer fields.Because each predicate requires a different set of data to display as details, we store those in a separate struct and the
PredicateInfo
struct stores the pointer to that struct. The Type field indicates which pointer is not null/used and the Name field is used to generate frontend text, indicating what data is related to the Predicate.There are 15 possible predicates. In the original code, the predicates are grouped by files. Therefore, for each group of predicates, we have a specific struct to store the information of that group.
For example, there are 4 predicates in the
predicate/author.go
file. We have theContributorInfo
struct that stores the data needed by the predicates inauthor.go
. Specifically, each struct needs two types of data, first is that defines the condition to qualify/disqualify a predicate, and the second is the context of the PR used for determining the (dis)qualifications. InContributorInfo
,Organizations
,Teams
andUsers
are data used for qualification purpose andAuthor
,Contributors
are the actual values of the context of the PR.HasAuthorIn
OnlyHasContributorsIn
HasContributorIn
AuthorIsOnlyContributor
If a field is not needed, then we would not assign values to it, and by default in go, it would be
nil
.The context field would store minimal needed data for (dis)qualification. For some predicates, when translate into logic, can be expressed as either
for all
orthere exists
. If the predicate is in the form of for all, e.g.OnlyHasContributorsIn
, the original code has short circuiting so that while looping over all contributors, if the code sees an example of contributor that disqualifies the predicate, then it will return right away. In this case, the Contributors field inContributorInfo
would only store the specific contributor that doesn't qualify the predicate. Otherwise, if all contributors satisfy a condition, then the Contributors field would store all the contributors. The same short circuiting scheme applies tothere exists
, if there exists a value that qualifies a predicate, then we would simply return that value, ignoring other potential candidates.To display the reviewer information, we move the
Requires
struct from approval/disapproval to common package. So now both approval and disapproval uses the sameRequires
struct. This way we can easily pass theRequires
struct toResult
. Previous, theRequires
struct in disapproval does not have a count field. Now we are just setting that number to 1 to keep the same behavior.For the front end part, we added two templates,
result-predicate-details
andresult-requires-details
. They are for displaying the predicate information and review information.getPredicateInfo
andgetPredicateRequirement
in thefrontend.go
file are used to render the text displayed.getPredicateInfo
is used to obtain the context information related to each predicate, specifically, the value we store in the name field. AndgetPredicateRequirement
is for obtaining the values for qualification/disqualification. For example, inHasAuthorIn
predicate, it returns the organizations, teams and users. However, this function does not cover all the predicates. For boolean predicates likeHasSuccessfulStatus
orAuthorIsOnlyContributor
, the explanatory requirement is not needed because the name of the predicate is already explanatory.We also added the
PredicateInfo
to each predicate test. There are two issues worth thinking here.An issue is that using maps as dictionaries in the code would produce non-deterministic behaviors when testing in go. For example, in the
committersInListButAuthorsAreNot
test for theOnlyHasContributorsIn
predicate, bothttest1
andttest2
can be returned as result(because of short circuiting only one of the two will be returned). Therefore, my solution is to use theSubset
function in thetestify
package. However, this cause us to not test equality in the other cases where there is no short circuiting. I do not know what is the best approach to this but two ways I can think of is 1) simply change the code to not use non-deterministic data structures like maps. 2) remove the test of which the result is non-deterministic.Another issue is related to the 'labels do not exist' test in
label_test.go
. I am not sure the point of testing it. I assume when labels do not exist then there is an error, so we can just returnnil
for thePredicateInfo
. Are we just testing a case of error?