Skip to content

Commit 6df0106

Browse files
authored
Implement remote and default config loading (#264)
Use the go-githubapp/appconfig package to support remote configuration references and organization-level defaults. To make this simpler to implement, there are two changes to existing behavior: 1. If any config is invalid, Bulldozer stops processing the PR and logs a warning. Previously, it would fall back to global config (if set) in this case. 2. All defined config paths may contain either v1 or v0 configuration. Previously, some paths could only contain one version or the other. Since v0 config is undocumented and has been deprecated for a long-time, I don't expect this to impact anything. I also did some refactoring, moving the fetching logic to the handler package while leaving the parsing logic in the bulldozer package. This mirrors the layout in policy-bot.
1 parent 9a2fda3 commit 6df0106

File tree

12 files changed

+745
-284
lines changed

12 files changed

+745
-284
lines changed

README.md

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,25 @@ The behavior of the bot is configured by a `.bulldozer.yml` file at the root of
5858
the repository. The file name and location are configurable when running your
5959
own instance of the server.
6060

61-
The `.bulldozer.yml` file is read from the most recent commit on the target
62-
branch of each pull request. If `bulldozer` cannot find a configuration file,
63-
it will take no action. This means it is safe to enable the `bulldozer` on all
64-
repositories in an organization.
61+
- The file is read from the most recent commit on the _target_ branch of each
62+
pull request.
63+
64+
- The file may contain a reference to a configuration in a different
65+
repository (see [Remote Configuration](#remote-configuration).)
66+
67+
- If the file does not exist in the repository, `bulldozer` tries to load a
68+
shared `bulldozer.yml` file at the root of the `.github` repository in the
69+
same organization. You can change this path and repository name when running
70+
your own instance of the server.
71+
72+
- You can also define a global default configuration when running your own
73+
instance of the server. This is used if the repository or the shared
74+
organization repository do not define any configuration.
75+
76+
- If configuration does not exist in the repository or in the shared
77+
organization repository and the server does not have a default configuration,
78+
`bulldozer` does not act on the pull request. This means it is safe to enable
79+
`bulldozer` on all repositories in an organization.
6580

6681
### bulldozer.yml Specification
6782

@@ -171,6 +186,30 @@ update:
171186
ignore_drafts: false
172187
```
173188
189+
#### Remote Configuration
190+
191+
You can use a remote configuration by specifying a repository and an optional
192+
path and Git reference. Place the following in the repository's
193+
`.bulldozer.yml` file instead of the normal configuration:
194+
195+
```yaml
196+
# The remote repository to read the configuration file from. This is required,
197+
# and must be in "org/repo-name" form. Must be a public repository.
198+
remote: org/repo-name
199+
200+
# The path to the configuration file in the remote repository. If not set,
201+
# uses the default configuration path.
202+
path: path/to/bulldozer.yml
203+
204+
# The branch (or tag, or commit hash) that should be used on the remote
205+
# repository. If not set, uses the default branch of the repository.
206+
ref: main
207+
```
208+
209+
The remote file must contain `bulldozer` configuration and cannot be another
210+
remote reference. However, the organization-level default configuration may be
211+
a remote reference.
212+
174213
## FAQ
175214

176215
#### Can I specify both `ignore` and `trigger`?

bulldozer/config.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2021 Palantir Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package bulldozer
16+
17+
import (
18+
"github.com/pkg/errors"
19+
"gopkg.in/yaml.v2"
20+
)
21+
22+
func ParseConfig(c []byte) (*Config, error) {
23+
config, v1err := parseConfigV1(c)
24+
if v1err == nil {
25+
return config, nil
26+
}
27+
28+
config, v0err := parseConfigV0(c)
29+
if v0err == nil {
30+
return config, nil
31+
}
32+
33+
// Encourage v1 usage by reporting the v1 parsing error in all cases
34+
return nil, v1err
35+
}
36+
37+
func parseConfigV1(bytes []byte) (*Config, error) {
38+
var config Config
39+
if err := yaml.UnmarshalStrict(bytes, &config); err != nil {
40+
return nil, errors.Wrapf(err, "failed to unmarshal configuration")
41+
}
42+
43+
// Merge old signals configurations if they exist when the new values aren't present
44+
if config.Merge.Blacklist.Enabled() && !config.Merge.Ignore.Enabled() {
45+
config.Merge.Ignore = config.Merge.Blacklist
46+
}
47+
if config.Merge.Whitelist.Enabled() && !config.Merge.Trigger.Enabled() {
48+
config.Merge.Trigger = config.Merge.Whitelist
49+
}
50+
if config.Update.Blacklist.Enabled() && !config.Update.Ignore.Enabled() {
51+
config.Update.Ignore = config.Update.Blacklist
52+
}
53+
if config.Update.Whitelist.Enabled() && !config.Update.Trigger.Enabled() {
54+
config.Update.Trigger = config.Update.Whitelist
55+
}
56+
57+
if config.Version != 1 {
58+
return nil, errors.Errorf("unexpected version %d, expected 1", config.Version)
59+
}
60+
61+
return &config, nil
62+
}
63+
64+
func parseConfigV0(bytes []byte) (*Config, error) {
65+
var configv0 ConfigV0
66+
if err := yaml.UnmarshalStrict(bytes, &configv0); err != nil {
67+
return nil, errors.Wrapf(err, "failed to unmarshal v0 configuration")
68+
}
69+
70+
var config Config
71+
switch configv0.Mode {
72+
case ModeWhitelistV0:
73+
config = Config{
74+
Version: 1,
75+
Update: UpdateConfig{
76+
Trigger: Signals{
77+
Labels: []string{"update me", "update-me", "update_me"},
78+
},
79+
},
80+
Merge: MergeConfig{
81+
Trigger: Signals{
82+
Labels: []string{"merge when ready", "merge-when-ready", "merge_when_ready"},
83+
},
84+
DeleteAfterMerge: configv0.DeleteAfterMerge,
85+
AllowMergeWithNoChecks: false,
86+
Method: configv0.Strategy,
87+
},
88+
}
89+
if config.Merge.Method == SquashAndMerge {
90+
config.Merge.Options.Squash = &SquashOptions{
91+
Body: SummarizeCommits,
92+
}
93+
}
94+
case ModeBlacklistV0:
95+
config = Config{
96+
Version: 1,
97+
Update: UpdateConfig{
98+
Trigger: Signals{
99+
Labels: []string{"update me", "update-me", "update_me"},
100+
},
101+
},
102+
Merge: MergeConfig{
103+
Ignore: Signals{
104+
Labels: []string{"wip", "do not merge", "do-not-merge", "do_not_merge"},
105+
},
106+
DeleteAfterMerge: configv0.DeleteAfterMerge,
107+
AllowMergeWithNoChecks: false,
108+
Method: configv0.Strategy,
109+
},
110+
}
111+
if config.Merge.Method == SquashAndMerge {
112+
config.Merge.Options.Squash = &SquashOptions{
113+
Body: SummarizeCommits,
114+
}
115+
}
116+
case ModeBodyV0:
117+
config = Config{
118+
Version: 1,
119+
Update: UpdateConfig{
120+
Trigger: Signals{
121+
Labels: []string{"update me", "update-me", "update_me"},
122+
},
123+
},
124+
Merge: MergeConfig{
125+
Trigger: Signals{
126+
CommentSubstrings: []string{"==MERGE_WHEN_READY=="},
127+
},
128+
DeleteAfterMerge: configv0.DeleteAfterMerge,
129+
AllowMergeWithNoChecks: false,
130+
Method: configv0.Strategy,
131+
},
132+
}
133+
if config.Merge.Method == SquashAndMerge {
134+
config.Merge.Options.Squash = &SquashOptions{
135+
Body: PullRequestBody,
136+
MessageDelimiter: "==COMMIT_MSG==",
137+
}
138+
}
139+
default:
140+
return nil, errors.Errorf("unknown v0 mode: %q", configv0.Mode)
141+
}
142+
143+
return &config, nil
144+
}

0 commit comments

Comments
 (0)