package owners import ( "fmt" "path/filepath" "strings" "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy" "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/approval" "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/common" "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/disapproval" "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/predicate" "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/pull" ) // GeneratePolicyBotConfig generates a single policy-bot.yaml for the repository // from a map of OWNERS file contents and their directories. It sets opinionated // defaults for some rules options: // // - Ignore commits by our instance of bulldozer // - Ignore update commits // - Respect GitHub reviews as an approval method // - Disable GitHub comments as approval method // - Invalidate review on push // // And opinionated defaults for the policy config itself: // // - Enables disapproval, allowing write collaborators on the repo to disapprove // PRs via GitHub review. This is because policy-bot only allows one disapproval // rule per config. // // TODO(aw185176): this could be made more generic by accepting an approval.Options argument and using reflect to merge it with the rules being read. func GeneratePolicyBotConfig(keys []string, ofiles map[string]File, rootOrRules []*approval.Rule) (*policy.Config, error) { ghReview := true p := &policy.Config{ ApprovalRules: []*approval.Rule{}, Policy: policy.Policy{ Disapproval: &disapproval.Policy{ Options: disapproval.Options{ Methods: disapproval.Methods{Disapprove: &common.Methods{ GithubReview: &ghReview, }}, }, Requires: disapproval.Requires{ Actors: common.Actors{ Permissions: []pull.Permission{pull.PermissionWrite}, }, }, }, }, } // generate the approval rules defined by OWNERS files genApproval := approval.Policy{} for _, key := range keys { v, ok := ofiles[key] if !ok { continue } for _, r := range v.Rules { setCommonOptions(r) // prefix all file paths with path of directory where OWNERS file was read var err error if r.Predicates.ChangedFiles != nil { r.Predicates.ChangedFiles, err = prefixChangeFilePaths( r.Predicates.ChangedFiles, key, ) if err != nil { return nil, err } } if r.Predicates.OnlyChangedFiles != nil { r.Predicates.OnlyChangedFiles.Paths, err = prefixRegexp( r.Predicates.OnlyChangedFiles.Paths, key, ) if err != nil { return nil, err } } } p.ApprovalRules = append(p.ApprovalRules, v.Rules...) genApproval = append(genApproval, v.Approval...) } // if no root OR policies are defined, set our generated policies at the root // and return if len(rootOrRules) == 0 { p.Policy.Approval = genApproval return p, nil } // if root OR policies are defined, nest the generated policies under it, // ANDed together rules := make([]interface{}, len(rootOrRules)+1) for i, r := range rootOrRules { setCommonOptions(r) rules[i] = r.Name } rules[len(rootOrRules)] = map[interface{}]interface{}{"and": genApproval} p.Policy.Approval = append(p.Policy.Approval, map[interface{}]interface{}{ "or": rules, }) p.ApprovalRules = append(p.ApprovalRules, rootOrRules...) return p, nil } func prefixChangeFilePaths(files *predicate.ChangedFiles, path string) (*predicate.ChangedFiles, error) { result := &predicate.ChangedFiles{ Paths: make([]common.Regexp, len(files.Paths)), } var err error result.Paths, err = prefixRegexp(files.Paths, path) if err != nil { return nil, err } if len(files.IgnorePaths) == 0 { return result, nil } result.IgnorePaths = make([]common.Regexp, len(files.IgnorePaths)) result.IgnorePaths, err = prefixRegexp(files.IgnorePaths, path) if err != nil { return nil, err } return result, nil } func prefixRegexp(rr []common.Regexp, path string) ([]common.Regexp, error) { for i, r := range rr { var ( rstr = r.String() pattern string ) switch { case strings.HasPrefix(rstr, "^"): pattern = fmt.Sprintf( "^%s", filepath.Join(path, strings.TrimPrefix(rstr, "^")), ) default: pattern = filepath.Join(path, rstr) } re, err := common.NewRegexp(pattern) if err != nil { return nil, err } rr[i] = re } return rr, nil } func setCommonOptions(r *approval.Rule) { ghReview := true r.Options.InvalidateOnPush = true r.Options.IgnoreUpdateMerges = true r.Options.IgnoreCommitsBy = common.Actors{ Users: []string{"edge-bulldozer[bot]"}, } r.Options.Methods = &common.Methods{ GithubReview: &ghReview, Comments: []string{}, } }