Add a branch regex matcher to the server side repo config (#1383)

* Add a branch regex to the repo.yaml config to allow Atlantis to
accept only pull requests with a given base branch (the branch
a PR would merge _into_).
* `branch` is optional. By default its unset and Atlantis will match
webhooks for pull requests for any branch (no change)

Match any PR,

```
repos:
  - id: /.*/
    branch: /.*/
```

Match only PRs with master or main base branch.

```
repos:
  - id: /.*/
    branch: /(main|master)/
```

Some repos have special pull request branching practices. For example,
plan/apply from feature branches merging into master, but separate flows
for merging master into a release branch (where Atlantis isn't used).
Adding a regex allows for flexible workflow options. For example, you
could have Atlantis ignore "release" branches.
This commit is contained in:
Dalton Hubble
2021-02-24 12:52:30 -08:00
committed by GitHub
parent 832afeaec1
commit 00a5bc0fc0
6 changed files with 74 additions and 2 deletions

View File

@@ -28,6 +28,10 @@ repos:
# Repo ID's are of the form {VCS hostname}/{org}/{repo name}, ex.
# github.com/runatlantis/atlantis.
- id: /.*/
# branch is an regex matching pull requests by base branch
# (the branch the pull request is getting merged into).
# By default, all branches are matched
branch: /.*/
# apply_requirements sets the Apply Requirements for all repos that match.
apply_requirements: [approved, mergeable]

View File

@@ -34,7 +34,7 @@ func (w *DefaultPreWorkflowHooksCommandRunner) RunPreHooks(
preWorkflowHooks := make([]*valid.PreWorkflowHook, 0)
for _, repo := range w.GlobalCfg.Repos {
if repo.IDMatches(baseRepo.ID()) && len(repo.PreWorkflowHooks) > 0 {
if repo.IDMatches(baseRepo.ID()) && repo.BranchMatches(pull.BaseBranch) && len(repo.PreWorkflowHooks) > 0 {
preWorkflowHooks = append(preWorkflowHooks, repo.PreWorkflowHooks...)
}
}

View File

@@ -1014,11 +1014,17 @@ func TestParseGlobalCfg(t *testing.T) {
- apply_requirements: []`,
expErr: "repos: (0: (id: cannot be blank.).).",
},
"invalid regex": {
"invalid id regex": {
input: `repos:
- id: /?/`,
expErr: "repos: (0: (id: parsing: /?/: error parsing regexp: missing argument to repetition operator: `?`.).).",
},
"invalid branch regex": {
input: `repos:
- id: /.*/
branch: /?/`,
expErr: "repos: (0: (branch: parsing: /?/: error parsing regexp: missing argument to repetition operator: `?`.).).",
},
"workflow doesn't exist": {
input: `repos:
- id: /.*/
@@ -1115,6 +1121,7 @@ repos:
allowed_overrides: [apply_requirements, workflow]
allow_custom_workflows: true
- id: /.*/
branch: /(master|main)/
pre_workflow_hooks:
- run: custom workflow command
workflows:
@@ -1155,6 +1162,7 @@ policies:
},
{
IDRegex: regexp.MustCompile(".*"),
BranchRegex: regexp.MustCompile("(master|main)"),
PreWorkflowHooks: preWorkflowHooks,
},
},
@@ -1228,6 +1236,7 @@ workflows:
Repos: []valid.Repo{
{
IDRegex: regexp.MustCompile(".*"),
BranchRegex: regexp.MustCompile(".*"),
PreWorkflowHooks: []*valid.PreWorkflowHook{},
ApplyRequirements: []string{},
Workflow: &valid.Workflow{
@@ -1301,6 +1310,10 @@ workflows:
Assert(t, expRepo.IDRegex.String() == actRepo.IDRegex.String(),
"%q != %q for repos[%d]", expRepo.IDRegex.String(), actRepo.IDRegex.String(), i)
}
if expRepo.BranchRegex != nil {
Assert(t, expRepo.BranchRegex.String() == actRepo.BranchRegex.String(),
"%q != %q for repos[%d]", expRepo.BranchRegex.String(), actRepo.BranchRegex.String(), i)
}
}
})
}

View File

@@ -20,6 +20,7 @@ type GlobalCfg struct {
// Repo is the raw schema for repos in the server-side repo config.
type Repo struct {
ID string `yaml:"id" json:"id"`
Branch string `yaml:"branch" json:"branch"`
ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"`
PreWorkflowHooks []PreWorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"`
Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"`
@@ -121,6 +122,11 @@ func (r Repo) HasRegexID() bool {
return strings.HasPrefix(r.ID, "/") && strings.HasSuffix(r.ID, "/")
}
// HasRegexBranch returns true if a branch regex was set.
func (r Repo) HasRegexBranch() bool {
return strings.HasPrefix(r.Branch, "/") && strings.HasSuffix(r.Branch, "/")
}
func (r Repo) Validate() error {
idValid := func(value interface{}) error {
id := value.(string)
@@ -131,6 +137,15 @@ func (r Repo) Validate() error {
return errors.Wrapf(err, "parsing: %s", id)
}
branchValid := func(value interface{}) error {
branch := value.(string)
if !r.HasRegexBranch() {
return nil
}
_, err := regexp.Compile(branch[1 : len(branch)-1])
return errors.Wrapf(err, "parsing: %s", branch)
}
overridesValid := func(value interface{}) error {
overrides := value.([]string)
for _, o := range overrides {
@@ -149,6 +164,7 @@ func (r Repo) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.ID, validation.Required, validation.By(idValid)),
validation.Field(&r.Branch, validation.By(branchValid)),
validation.Field(&r.AllowedOverrides, validation.By(overridesValid)),
validation.Field(&r.ApplyRequirements, validation.By(validApplyReq)),
validation.Field(&r.Workflow, validation.By(workflowExists)),
@@ -166,6 +182,13 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo {
id = r.ID
}
var branchRegex *regexp.Regexp
if r.HasRegexBranch() {
withoutSlashes := r.Branch[1 : len(r.Branch)-1]
// Safe to use MustCompile because we test it in Validate().
branchRegex = regexp.MustCompile(withoutSlashes)
}
var workflow *valid.Workflow
if r.Workflow != nil {
// This key is guaranteed to exist because we test for it in
@@ -184,6 +207,7 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo {
return valid.Repo{
ID: id,
IDRegex: idRegex,
BranchRegex: branchRegex,
ApplyRequirements: r.ApplyRequirements,
PreWorkflowHooks: preWorkflowHooks,
Workflow: workflow,

View File

@@ -34,6 +34,7 @@ type Repo struct {
// IDRegex is the regex match for this config.
// If ID is set then this will be nil.
IDRegex *regexp.Regexp
BranchRegex *regexp.Regexp
ApplyRequirements []string
PreWorkflowHooks []*PreWorkflowHook
Workflow *Workflow
@@ -123,6 +124,7 @@ func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq boo
Repos: []Repo{
{
IDRegex: regexp.MustCompile(".*"),
BranchRegex: regexp.MustCompile(".*"),
ApplyRequirements: applyReqs,
PreWorkflowHooks: preWorkflowHooks,
Workflow: &defaultWorkflow,
@@ -158,6 +160,14 @@ func (r Repo) IDMatches(otherID string) bool {
return r.IDRegex.MatchString(otherID)
}
// BranchMatches returns true if the branch other matches a branch regex (if preset).
func (r Repo) BranchMatches(other string) bool {
if r.BranchRegex == nil {
return true
}
return r.BranchRegex.MatchString(other)
}
// IDString returns a string representation of this config.
func (r Repo) IDString() string {
if r.ID != "" {

View File

@@ -50,6 +50,7 @@ func TestNewGlobalCfg(t *testing.T) {
Repos: []valid.Repo{
{
IDRegex: regexp.MustCompile(".*"),
BranchRegex: regexp.MustCompile(".*"),
ApplyRequirements: []string{},
Workflow: &expDefaultWorkflow,
AllowedWorkflows: []string{},
@@ -108,6 +109,7 @@ func TestNewGlobalCfg(t *testing.T) {
// For each test, we change our expected cfg based on the parameters.
exp := deepcopy.Copy(baseCfg).(valid.GlobalCfg)
exp.Repos[0].IDRegex = regexp.MustCompile(".*") // deepcopy doesn't copy the regex.
exp.Repos[0].BranchRegex = regexp.MustCompile(".*")
if c.allowRepoCfg {
exp.Repos[0].AllowCustomWorkflows = Bool(true)
@@ -132,6 +134,10 @@ func TestNewGlobalCfg(t *testing.T) {
Assert(t, expRepo.IDRegex.String() == actRepo.IDRegex.String(),
"%q != %q for repos[%d]", expRepo.IDRegex.String(), actRepo.IDRegex.String(), i)
}
if expRepo.BranchRegex != nil {
Assert(t, expRepo.BranchRegex.String() == actRepo.BranchRegex.String(),
"%q != %q for repos[%d]", expRepo.BranchRegex.String(), actRepo.BranchRegex.String(), i)
}
}
})
}
@@ -726,6 +732,21 @@ func TestRepo_IDString(t *testing.T) {
Equals(t, "/regex.*/", (valid.Repo{IDRegex: regexp.MustCompile("regex.*")}).IDString())
}
func TestRepo_BranchMatches(t *testing.T) {
// Test matches when no branch regex is set.
Equals(t, true, (valid.Repo{}).BranchMatches("main"))
// Test regexes.
Equals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile(".*")}).BranchMatches("main"))
Equals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile("main")}).BranchMatches("main"))
Equals(t, false, (valid.Repo{BranchRegex: regexp.MustCompile("^main$")}).BranchMatches("foo-main"))
Equals(t, false, (valid.Repo{BranchRegex: regexp.MustCompile("^main$")}).BranchMatches("main-foo"))
Equals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile("(main|master)")}).BranchMatches("main"))
Equals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile("(main|master)")}).BranchMatches("master"))
Equals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile("release")}).BranchMatches("release-stage"))
Equals(t, false, (valid.Repo{BranchRegex: regexp.MustCompile("release")}).BranchMatches("main"))
}
// String is a helper routine that allocates a new string value
// to store v and returns a pointer to it.
func String(v string) *string { return &v }