forked from kubernetes/test-infra
-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.go
1550 lines (1390 loc) · 57.7 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugins
import (
"errors"
"fmt"
"path"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/test-infra/prow/bugzilla"
"k8s.io/test-infra/prow/errorutil"
"k8s.io/test-infra/prow/labels"
)
const (
defaultBlunderbussReviewerCount = 2
)
// Configuration is the top-level serialization target for plugin Configuration.
type Configuration struct {
// Plugins is a map of repositories (eg "k/k") to lists of
// plugin names.
// You can find a comprehensive list of the default avaulable plugins here
// https://github.com/kubernetes/test-infra/tree/master/prow/plugins
// note that you're also able to add external plugins.
Plugins map[string][]string `json:"plugins,omitempty"`
// ExternalPlugins is a map of repositories (eg "k/k") to lists of
// external plugins.
ExternalPlugins map[string][]ExternalPlugin `json:"external_plugins,omitempty"`
// Owners contains configuration related to handling OWNERS files.
Owners Owners `json:"owners,omitempty"`
// Built-in plugins specific configuration.
Approve []Approve `json:"approve,omitempty"`
UseDeprecatedSelfApprove bool `json:"use_deprecated_2018_implicit_self_approve_default_migrate_before_july_2019,omitempty"`
UseDeprecatedReviewApprove bool `json:"use_deprecated_2018_review_acts_as_approve_default_migrate_before_july_2019,omitempty"`
Blockades []Blockade `json:"blockades,omitempty"`
Blunderbuss Blunderbuss `json:"blunderbuss,omitempty"`
Bugzilla Bugzilla `json:"bugzilla,omitempty"`
Cat Cat `json:"cat,omitempty"`
CherryPickUnapproved CherryPickUnapproved `json:"cherry_pick_unapproved,omitempty"`
ConfigUpdater ConfigUpdater `json:"config_updater,omitempty"`
Dco map[string]*Dco `json:"dco,omitempty"`
Golint Golint `json:"golint,omitempty"`
Goose Goose `json:"goose,omitempty"`
Heart Heart `json:"heart,omitempty"`
Label Label `json:"label,omitempty"`
Lgtm []Lgtm `json:"lgtm,omitempty"`
MilestoneApplier map[string]BranchToMilestone `json:"milestone_applier,omitempty"`
RepoMilestone map[string]Milestone `json:"repo_milestone,omitempty"`
Project ProjectConfig `json:"project_config,omitempty"`
ProjectManager ProjectManager `json:"project_manager,omitempty"`
RequireMatchingLabel []RequireMatchingLabel `json:"require_matching_label,omitempty"`
RequireSIG RequireSIG `json:"requiresig,omitempty"`
Retitle Retitle `json:"retitle,omitempty"`
Slack Slack `json:"slack,omitempty"`
SigMention SigMention `json:"sigmention,omitempty"`
Size Size `json:"size,omitempty"`
Triggers []Trigger `json:"triggers,omitempty"`
Welcome []Welcome `json:"welcome,omitempty"`
Override Override `json:"override"`
}
// Golint holds configuration for the golint plugin
type Golint struct {
// MinimumConfidence is the smallest permissible confidence
// in (0,1] over which problems will be printed. Defaults to
// 0.8, as does the `go lint` tool.
MinimumConfidence *float64 `json:"minimum_confidence,omitempty"`
}
// ExternalPlugin holds configuration for registering an external
// plugin in prow.
type ExternalPlugin struct {
// Name of the plugin.
Name string `json:"name"`
// Endpoint is the location of the external plugin. Defaults to
// the name of the plugin, ie. "http://{{name}}".
Endpoint string `json:"endpoint,omitempty"`
// Events are the events that need to be demuxed by the hook
// server to the external plugin. If no events are specified,
// everything is sent.
Events []string `json:"events,omitempty"`
}
// Blunderbuss defines configuration for the blunderbuss plugin.
type Blunderbuss struct {
// ReviewerCount is the minimum number of reviewers to request
// reviews from. Defaults to requesting reviews from 2 reviewers
// if FileWeightCount is not set.
ReviewerCount *int `json:"request_count,omitempty"`
// MaxReviewerCount is the maximum number of reviewers to request
// reviews from. Defaults to 0 meaning no limit.
MaxReviewerCount int `json:"max_request_count,omitempty"`
// FileWeightCount is the maximum number of reviewers to request
// reviews from. Selects reviewers based on file weighting.
// This and request_count are mutually exclusive options.
FileWeightCount *int `json:"file_weight_count,omitempty"`
// ExcludeApprovers controls whether approvers are considered to be
// reviewers. By default, approvers are considered as reviewers if
// insufficient reviewers are available. If ExcludeApprovers is true,
// approvers will never be considered as reviewers.
ExcludeApprovers bool `json:"exclude_approvers,omitempty"`
// UseStatusAvailability controls whether blunderbuss will consider GitHub's
// status availability when requesting reviews for users. This will use at one
// additional token per successful reviewer (and potentially more depending on
// how many busy reviewers it had to pass over).
UseStatusAvailability bool `json:"use_status_availability,omitempty"`
}
// Owners contains configuration related to handling OWNERS files.
type Owners struct {
// MDYAMLRepos is a list of org and org/repo strings specifying the repos that support YAML
// OWNERS config headers at the top of markdown (*.md) files. These headers function just like
// the config in an OWNERS file, but only apply to the file itself instead of the entire
// directory and all sub-directories.
// The yaml header must be at the start of the file and be bracketed with "---" like so:
/*
---
approvers:
- mikedanese
- thockin
---
*/
MDYAMLRepos []string `json:"mdyamlrepos,omitempty"`
// SkipCollaborators disables collaborator cross-checks and forces both
// the approve and lgtm plugins to use solely OWNERS files for access
// control in the provided repos.
SkipCollaborators []string `json:"skip_collaborators,omitempty"`
// LabelsBlackList holds a list of labels that should not be present in any
// OWNERS file, preventing their automatic addition by the owners-label plugin.
// This check is performed by the verify-owners plugin.
LabelsBlackList []string `json:"labels_blacklist,omitempty"`
}
// MDYAMLEnabled returns a boolean denoting if the passed repo supports YAML OWNERS config headers
// at the top of markdown (*.md) files. These function like OWNERS files but only apply to the file
// itself.
func (c *Configuration) MDYAMLEnabled(org, repo string) bool {
full := fmt.Sprintf("%s/%s", org, repo)
for _, elem := range c.Owners.MDYAMLRepos {
if elem == org || elem == full {
return true
}
}
return false
}
// SkipCollaborators returns a boolean denoting if collaborator cross-checks are enabled for
// the passed repo. If it's true, approve and lgtm plugins rely solely on OWNERS files.
func (c *Configuration) SkipCollaborators(org, repo string) bool {
full := fmt.Sprintf("%s/%s", org, repo)
for _, elem := range c.Owners.SkipCollaborators {
if elem == org || elem == full {
return true
}
}
return false
}
// RequireSIG specifies configuration for the require-sig plugin.
type RequireSIG struct {
// GroupListURL is the URL where a list of the available SIGs can be found.
GroupListURL string `json:"group_list_url,omitempty"`
}
// Retitle specifies configuration for the retitle plugin.
type Retitle struct {
// AllowClosedIssues allows retitling closed/merged issues and PRs.
AllowClosedIssues bool `json:"allow_closed_issues,omitempty"`
}
// SigMention specifies configuration for the sigmention plugin.
type SigMention struct {
// Regexp parses comments and should return matches to team mentions.
// These mentions enable labeling issues or PRs with sig/team labels.
// Furthermore, teams with the following suffixes will be mapped to
// kind/* labels:
//
// * @org/team-bugs --maps to--> kind/bug
// * @org/team-feature-requests --maps to--> kind/feature
// * @org/team-api-reviews --maps to--> kind/api-change
// * @org/team-proposals --maps to--> kind/design
//
// Note that you need to make sure your regexp covers the above
// mentions if you want to use the extra labeling. Defaults to:
// (?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews)
//
// Compiles into Re during config load.
Regexp string `json:"regexp,omitempty"`
Re *regexp.Regexp `json:"-"`
}
// Size specifies configuration for the size plugin, defining lower bounds (in # lines changed) for each size label.
// XS is assumed to be zero.
type Size struct {
S int `json:"s"`
M int `json:"m"`
L int `json:"l"`
Xl int `json:"xl"`
Xxl int `json:"xxl"`
}
// Blockade specifies a configuration for a single blockade.
//
// The configuration for the blockade plugin is defined as a list of these structures.
type Blockade struct {
// Repos are either of the form org/repos or just org.
Repos []string `json:"repos,omitempty"`
// BlockRegexps are regular expressions matching the file paths to block.
BlockRegexps []string `json:"blockregexps,omitempty"`
// ExceptionRegexps are regular expressions matching the file paths that are exceptions to the BlockRegexps.
ExceptionRegexps []string `json:"exceptionregexps,omitempty"`
// Explanation is a string that will be included in the comment left when blocking a PR. This should
// be an explanation of why the paths specified are blockaded.
Explanation string `json:"explanation,omitempty"`
}
// Approve specifies a configuration for a single approve.
//
// The configuration for the approve plugin is defined as a list of these structures.
type Approve struct {
// Repos is either of the form org/repos or just org.
Repos []string `json:"repos,omitempty"`
// IssueRequired indicates if an associated issue is required for approval in
// the specified repos.
IssueRequired bool `json:"issue_required,omitempty"`
// TODO(fejta): delete in June 2019
DeprecatedImplicitSelfApprove *bool `json:"implicit_self_approve,omitempty"`
// RequireSelfApproval requires PR authors to explicitly approve their PRs.
// Otherwise the plugin assumes the author of the PR approves the changes in the PR.
RequireSelfApproval *bool `json:"require_self_approval,omitempty"`
// LgtmActsAsApprove indicates that the lgtm command should be used to
// indicate approval
LgtmActsAsApprove bool `json:"lgtm_acts_as_approve,omitempty"`
// ReviewActsAsApprove should be replaced with its non-deprecated inverse: ignore_review_state.
// TODO(fejta): delete in June 2019
DeprecatedReviewActsAsApprove *bool `json:"review_acts_as_approve,omitempty"`
// IgnoreReviewState causes the approve plugin to ignore the GitHub review state. Otherwise:
// * an APPROVE github review is equivalent to leaving an "/approve" message.
// * A REQUEST_CHANGES github review is equivalent to leaving an /approve cancel" message.
IgnoreReviewState *bool `json:"ignore_review_state,omitempty"`
}
var (
warnImplicitSelfApprove time.Time
warnReviewActsAsApprove time.Time
)
func (a Approve) HasSelfApproval() bool {
if a.DeprecatedImplicitSelfApprove != nil {
warnDeprecated(&warnImplicitSelfApprove, 5*time.Minute, "Please update plugins.yaml to use require_self_approval instead of the deprecated implicit_self_approve before June 2019")
return *a.DeprecatedImplicitSelfApprove
} else if a.RequireSelfApproval != nil {
return !*a.RequireSelfApproval
}
return true
}
func (a Approve) ConsiderReviewState() bool {
if a.DeprecatedReviewActsAsApprove != nil {
warnDeprecated(&warnReviewActsAsApprove, 5*time.Minute, "Please update plugins.yaml to use ignore_review_state instead of the deprecated review_acts_as_approve before June 2019")
return *a.DeprecatedReviewActsAsApprove
} else if a.IgnoreReviewState != nil {
return !*a.IgnoreReviewState
}
return true
}
// Lgtm specifies a configuration for a single lgtm.
// The configuration for the lgtm plugin is defined as a list of these structures.
type Lgtm struct {
// Repos is either of the form org/repos or just org.
Repos []string `json:"repos,omitempty"`
// ReviewActsAsLgtm indicates that a GitHub review of "approve" or "request changes"
// acts as adding or removing the lgtm label
ReviewActsAsLgtm bool `json:"review_acts_as_lgtm,omitempty"`
// StoreTreeHash indicates if tree_hash should be stored inside a comment to detect
// squashed commits before removing lgtm labels
StoreTreeHash bool `json:"store_tree_hash,omitempty"`
// WARNING: This disables the security mechanism that prevents a malicious member (or
// compromised GitHub account) from merging arbitrary code. Use with caution.
//
// StickyLgtmTeam specifies the GitHub team whose members are trusted with sticky LGTM,
// which eliminates the need to re-lgtm minor fixes/updates.
StickyLgtmTeam string `json:"trusted_team_for_sticky_lgtm,omitempty"`
}
// Cat contains the configuration for the cat plugin.
type Cat struct {
// Path to file containing an api key for thecatapi.com
KeyPath string `json:"key_path,omitempty"`
}
// Goose contains the configuration for the goose plugin.
type Goose struct {
// Path to file containing an api key for unsplash.com
KeyPath string `json:"key_path,omitempty"`
}
// Label contains the configuration for the label plugin.
type Label struct {
// AdditionalLabels is a set of additional labels enabled for use
// on top of the existing "kind/*", "priority/*", and "area/*" labels.
AdditionalLabels []string `json:"additional_labels"`
}
// Trigger specifies a configuration for a single trigger.
//
// The configuration for the trigger plugin is defined as a list of these structures.
type Trigger struct {
// Repos is either of the form org/repos or just org.
Repos []string `json:"repos,omitempty"`
// TrustedOrg is the org whose members' PRs will be automatically built for
// PRs to the above repos. The default is the PR's org.
//
// Deprecated: TrustedOrg functionality is deprecated and will be removed in
// January 2020.
TrustedOrg string `json:"trusted_org,omitempty"`
// JoinOrgURL is a link that redirects users to a location where they
// should be able to read more about joining the organization in order
// to become trusted members. Defaults to the GitHub link of TrustedOrg.
JoinOrgURL string `json:"join_org_url,omitempty"`
// OnlyOrgMembers requires PRs and/or /ok-to-test comments to come from org members.
// By default, trigger also include repo collaborators.
OnlyOrgMembers bool `json:"only_org_members,omitempty"`
// IgnoreOkToTest makes trigger ignore /ok-to-test comments.
// This is a security mitigation to only allow testing from trusted users.
IgnoreOkToTest bool `json:"ignore_ok_to_test,omitempty"`
// ElideSkippedContexts makes trigger not post "Skipped" contexts for jobs
// that could run but do not run. Defaults to true.
// THIS FIELD IS DEPRECATED AND WILL BE REMOVED AFTER OCTOBER 2019.
ElideSkippedContexts *bool `json:"elide_skipped_contexts,omitempty"`
}
// Heart contains the configuration for the heart plugin.
type Heart struct {
// Adorees is a list of GitHub logins for members
// for whom we will add emojis to comments
Adorees []string `json:"adorees,omitempty"`
// CommentRegexp is the regular expression for comments
// made by adorees that the plugin adds emojis to.
// If not specified, the plugin will not add emojis to
// any comments.
// Compiles into CommentRe during config load.
CommentRegexp string `json:"commentregexp,omitempty"`
CommentRe *regexp.Regexp `json:"-"`
}
// Milestone contains the configuration options for the milestone and
// milestonestatus plugins.
type Milestone struct {
// ID of the github team for the milestone maintainers (used for setting status labels)
// You can curl the following endpoint in order to determine the github ID of your team
// responsible for maintaining the milestones:
// curl -H "Authorization: token <token>" https://api.github.com/orgs/<org-name>/teams
MaintainersID int `json:"maintainers_id,omitempty"`
MaintainersTeam string `json:"maintainers_team,omitempty"`
MaintainersFriendlyName string `json:"maintainers_friendly_name,omitempty"`
}
// BranchToMilestone is a map of the branch name to the configured milestone for that branch.
// This is used by the milestoneapplier plugin.
type BranchToMilestone map[string]string
// Slack contains the configuration for the slack plugin.
type Slack struct {
MentionChannels []string `json:"mentionchannels,omitempty"`
MergeWarnings []MergeWarning `json:"mergewarnings,omitempty"`
}
// ConfigMapSpec contains configuration options for the configMap being updated
// by the config-updater plugin.
type ConfigMapSpec struct {
// Name of ConfigMap
Name string `json:"name"`
// Key is the key in the ConfigMap to update with the file contents.
// If no explicit key is given, the basename of the file will be used.
Key string `json:"key,omitempty"`
// Namespace in which the configMap needs to be deployed. If no namespace is specified
// it will be deployed to the ProwJobNamespace.
Namespace string `json:"namespace,omitempty"`
// Namespaces in which the configMap needs to be deployed, in addition to the above
// namespace provided, or the default if it is not set.
AdditionalNamespaces []string `json:"additional_namespaces,omitempty"`
// GZIP toggles whether the key's data should be GZIP'd before being stored
// If set to false and the global GZIP option is enabled, this file will
// will not be GZIP'd.
GZIP *bool `json:"gzip,omitempty"`
// Namespaces is the fully resolved list of Namespaces to deploy the ConfigMap in
Namespaces []string `json:"-"`
}
// ConfigUpdater contains the configuration for the config-updater plugin.
type ConfigUpdater struct {
// A map of filename => ConfigMapSpec.
// Whenever a commit changes filename, prow will update the corresponding configmap.
// map[string]ConfigMapSpec{ "/my/path.yaml": {Name: "foo", Namespace: "otherNamespace" }}
// will result in replacing the foo configmap whenever path.yaml changes
Maps map[string]ConfigMapSpec `json:"maps,omitempty"`
// The location of the prow configuration file inside the repository
// where the config-updater plugin is enabled. This needs to be relative
// to the root of the repository, eg. "config/prow/config.yaml" will match
// github.com/kubernetes/test-infra/config/prow/config.yaml assuming the config-updater
// plugin is enabled for kubernetes/test-infra. Defaults to "config/prow/config.yaml".
ConfigFile string `json:"config_file,omitempty"`
// The location of the prow plugin configuration file inside the repository
// where the config-updater plugin is enabled. This needs to be relative
// to the root of the repository, eg. "config/prow/plugins.yaml" will match
// github.com/kubernetes/test-infra/config/prow/plugins.yaml assuming the config-updater
// plugin is enabled for kubernetes/test-infra. Defaults to "config/prow/plugins.yaml".
PluginFile string `json:"plugin_file,omitempty"`
// If GZIP is true then files will be gzipped before insertion into
// their corresponding configmap
GZIP bool `json:"gzip"`
}
// ProjectConfig contains the configuration options for the project plugin
type ProjectConfig struct {
// Org level configs for github projects; key is org name
Orgs map[string]ProjectOrgConfig `json:"project_org_configs,omitempty"`
}
// ProjectOrgConfig holds the github project config for an entire org.
// This can be overridden by ProjectRepoConfig.
type ProjectOrgConfig struct {
// ID of the github project maintainer team for a give project or org
MaintainerTeamID int `json:"org_maintainers_team_id,omitempty"`
// A map of project name to default column; an issue/PR will be added
// to the default column if column name is not provided in the command
ProjectColumnMap map[string]string `json:"org_default_column_map,omitempty"`
// Repo level configs for github projects; key is repo name
Repos map[string]ProjectRepoConfig `json:"project_repo_configs,omitempty"`
}
// ProjectRepoConfig holds the github project config for a github project.
type ProjectRepoConfig struct {
// ID of the github project maintainer team for a give project or org
MaintainerTeamID int `json:"repo_maintainers_team_id,omitempty"`
// A map of project name to default column; an issue/PR will be added
// to the default column if column name is not provided in the command
ProjectColumnMap map[string]string `json:"repo_default_column_map,omitempty"`
}
// ProjectManager represents the config for the ProjectManager plugin, holding top
// level config options, configuration is a hierarchial structure with top level element
// being org or org/repo with the list of projects as its children
type ProjectManager struct {
OrgRepos map[string]ManagedOrgRepo `json:"orgsRepos,omitempty"`
}
// ManagedOrgRepo is used by the ProjectManager plugin to represent an Organisation
// or Repository with a list of Projects
type ManagedOrgRepo struct {
Projects map[string]ManagedProject `json:"projects,omitempty"`
}
// ManagedProject is used by the ProjectManager plugin to represent a Project
// with a list of Columns
type ManagedProject struct {
Columns []ManagedColumn `json:"columns,omitempty"`
}
// ManagedColumn is used by the ProjectQueries plugin to represent a project column
// and the conditions to add a PR to that column
type ManagedColumn struct {
// Either of ID or Name should be specified
ID *int `json:"id,omitempty"`
Name string `json:"name,omitempty"`
// State must be open, closed or all
State string `json:"state,omitempty"`
// all the labels here should match to the incoming event to be bale to add the card to the project
Labels []string `json:"labels,omitempty"`
// Configuration is effective is the issue events repo/Owner/Login matched the org
Org string `json:"org,omitempty"`
}
// MergeWarning is a config for the slackevents plugin's manual merge warnings.
// If a PR is pushed to any of the repos listed in the config then send messages
// to the all the slack channels listed if pusher is NOT in the whitelist.
type MergeWarning struct {
// Repos is either of the form org/repos or just org.
Repos []string `json:"repos,omitempty"`
// List of channels on which a event is published.
Channels []string `json:"channels,omitempty"`
// A slack event is published if the user is not part of the WhiteList.
WhiteList []string `json:"whitelist,omitempty"`
// A slack event is published if the user is not on the branch whitelist
BranchWhiteList map[string][]string `json:"branch_whitelist,omitempty"`
}
// Welcome is config for the welcome plugin.
type Welcome struct {
// Repos is either of the form org/repos or just org.
Repos []string `json:"repos,omitempty"`
// MessageTemplate is the welcome message template to post on new-contributor PRs
// For the info struct see prow/plugins/welcome/welcome.go's PRInfo
MessageTemplate string `json:"message_template,omitempty"`
}
// Dco is config for the DCO (https://developercertificate.org/) checker plugin.
type Dco struct {
// SkipDCOCheckForMembers is used to skip DCO check for trusted org members
SkipDCOCheckForMembers bool `json:"skip_dco_check_for_members,omitempty"`
// TrustedOrg is the org whose members' commits will not be checked for DCO signoff
// if the skip DCO option is enabled. The default is the PR's org.
TrustedOrg string `json:"trusted_org,omitempty"`
// SkipDCOCheckForCollaborators is used to skip DCO check for trusted org members
SkipDCOCheckForCollaborators bool `json:"skip_dco_check_for_collaborators,omitempty"`
}
// CherryPickUnapproved is the config for the cherrypick-unapproved plugin.
type CherryPickUnapproved struct {
// BranchRegexp is the regular expression for branch names such that
// the plugin treats only PRs against these branch names as cherrypick PRs.
// Compiles into BranchRe during config load.
BranchRegexp string `json:"branchregexp,omitempty"`
BranchRe *regexp.Regexp `json:"-"`
// Comment is the comment added by the plugin while adding the
// `do-not-merge/cherry-pick-not-approved` label.
Comment string `json:"comment,omitempty"`
}
// RequireMatchingLabel is the config for the require-matching-label plugin.
type RequireMatchingLabel struct {
// Org is the GitHub organization that this config applies to.
Org string `json:"org,omitempty"`
// Repo is the GitHub repository within Org that this config applies to.
// This fields may be omitted to apply this config across all repos in Org.
Repo string `json:"repo,omitempty"`
// Branch is the branch ref of PRs that this config applies to.
// This field is only valid if `prs: true` and may be omitted to apply this
// config across all branches in the repo or org.
Branch string `json:"branch,omitempty"`
// PRs is a bool indicating if this config applies to PRs.
PRs bool `json:"prs,omitempty"`
// Issues is a bool indicating if this config applies to issues.
Issues bool `json:"issues,omitempty"`
// Regexp is the string specifying the regular expression used to look for
// matching labels.
Regexp string `json:"regexp,omitempty"`
// Re is the compiled version of Regexp. It should not be specified in config.
Re *regexp.Regexp `json:"-"`
// MissingLabel is the label to apply if an issue does not have any label
// matching the Regexp.
MissingLabel string `json:"missing_label,omitempty"`
// MissingComment is the comment to post when we add the MissingLabel to an
// issue. This is typically used to explain why MissingLabel was added and
// how to move forward.
// This field is optional. If unspecified, no comment is created when labeling.
MissingComment string `json:"missing_comment,omitempty"`
// GracePeriod is the amount of time to wait before processing newly opened
// or reopened issues and PRs. This delay allows other automation to apply
// labels before we look for matching labels.
// Defaults to '5s'.
GracePeriod string `json:"grace_period,omitempty"`
GracePeriodDuration time.Duration `json:"-"`
}
// validate checks the following properties:
// - Org, Regexp, MissingLabel, and GracePeriod must be non-empty.
// - Repo does not contain a '/' (should use Org+Repo).
// - At least one of PRs or Issues must be true.
// - Branch only specified if 'prs: true'
// - MissingLabel must not match Regexp.
func (r RequireMatchingLabel) validate() error {
if r.Org == "" {
return errors.New("must specify 'org'")
}
if strings.Contains(r.Repo, "/") {
return errors.New("'repo' may not contain '/'; specify the organization with 'org'")
}
if r.Regexp == "" {
return errors.New("must specify 'regexp'")
}
if r.MissingLabel == "" {
return errors.New("must specify 'missing_label'")
}
if r.GracePeriod == "" {
return errors.New("must specify 'grace_period'")
}
if !r.PRs && !r.Issues {
return errors.New("must specify 'prs: true' and/or 'issues: true'")
}
if !r.PRs && r.Branch != "" {
return errors.New("branch cannot be specified without `prs: true'")
}
if r.Re.MatchString(r.MissingLabel) {
return errors.New("'regexp' must not match 'missing_label'")
}
return nil
}
var warnLock sync.RWMutex // Rare updates and concurrent readers, so reuse the same lock
// warnDeprecated prints a deprecation warning for a particular configuration
// option.
func warnDeprecated(last *time.Time, freq time.Duration, msg string) {
// have we warned within the last freq?
warnLock.RLock()
fresh := time.Now().Sub(*last) <= freq
warnLock.RUnlock()
if fresh { // we've warned recently
return
}
// Warning is stale, will we win the race to warn?
warnLock.Lock()
defer warnLock.Unlock()
now := time.Now() // Recalculate now, we might wait awhile for the lock
if now.Sub(*last) <= freq { // Nope, we lost
return
}
*last = now
logrus.Warn(msg)
}
// Describe generates a human readable description of the behavior that this
// configuration specifies.
func (r RequireMatchingLabel) Describe() string {
str := &strings.Builder{}
fmt.Fprintf(str, "Applies the '%s' label ", r.MissingLabel)
if r.MissingComment == "" {
fmt.Fprint(str, "to ")
} else {
fmt.Fprint(str, "and comments on ")
}
if r.Issues {
fmt.Fprint(str, "Issues ")
if r.PRs {
fmt.Fprint(str, "and ")
}
}
if r.PRs {
if r.Branch != "" {
fmt.Fprintf(str, "'%s' branch ", r.Branch)
}
fmt.Fprint(str, "PRs ")
}
if r.Repo == "" {
fmt.Fprintf(str, "in the '%s' GitHub org ", r.Org)
} else {
fmt.Fprintf(str, "in the '%s/%s' GitHub repo ", r.Org, r.Repo)
}
fmt.Fprintf(str, "that have no labels matching the regular expression '%s'.", r.Regexp)
return str.String()
}
// TriggerFor finds the Trigger for a repo, if one exists
// a trigger can be listed for the repo itself or for the
// owning organization
func (c *Configuration) TriggerFor(org, repo string) Trigger {
orgRepo := fmt.Sprintf("%s/%s", org, repo)
for _, tr := range c.Triggers {
for _, r := range tr.Repos {
if r == org || r == orgRepo {
return tr
}
}
}
var tr Trigger
tr.SetDefaults()
return tr
}
var warnElideSkippedContexts time.Time
func (t *Trigger) SetDefaults() {
truth := true
if t.ElideSkippedContexts == nil {
t.ElideSkippedContexts = &truth
} else {
warnDeprecated(&warnElideSkippedContexts, 5*time.Minute, "elide_skipped_contexts is deprecated and will be removed after Oct. 2019. Skipped contexts are now elided by default.")
}
if t.TrustedOrg != "" && t.JoinOrgURL == "" {
t.JoinOrgURL = fmt.Sprintf("https://github.com/orgs/%s/people", t.TrustedOrg)
}
}
// DcoFor finds the Dco for a repo, if one exists
// a Dco can be listed for the repo itself or for the
// owning organization
func (c *Configuration) DcoFor(org, repo string) *Dco {
if c.Dco[fmt.Sprintf("%s/%s", org, repo)] != nil {
return c.Dco[fmt.Sprintf("%s/%s", org, repo)]
}
if c.Dco[org] != nil {
return c.Dco[org]
}
if c.Dco["*"] != nil {
return c.Dco["*"]
}
return &Dco{}
}
// EnabledReposForPlugin returns the orgs and repos that have enabled the passed plugin.
func (c *Configuration) EnabledReposForPlugin(plugin string) (orgs, repos []string) {
for repo, plugins := range c.Plugins {
found := false
for _, candidate := range plugins {
if candidate == plugin {
found = true
break
}
}
if found {
if strings.Contains(repo, "/") {
repos = append(repos, repo)
} else {
orgs = append(orgs, repo)
}
}
}
return
}
// EnabledReposForExternalPlugin returns the orgs and repos that have enabled the passed
// external plugin.
func (c *Configuration) EnabledReposForExternalPlugin(plugin string) (orgs, repos []string) {
for repo, plugins := range c.ExternalPlugins {
found := false
for _, candidate := range plugins {
if candidate.Name == plugin {
found = true
break
}
}
if found {
if strings.Contains(repo, "/") {
repos = append(repos, repo)
} else {
orgs = append(orgs, repo)
}
}
}
return
}
// SetDefaults sets default options for config updating
func (c *ConfigUpdater) SetDefaults() {
if len(c.Maps) == 0 {
cf := c.ConfigFile
if cf == "" {
cf = "config/prow/config.yaml"
} else {
logrus.Warnf(`config_file is deprecated, please switch to "maps": {"%s": "config"} before July 2018`, cf)
}
pf := c.PluginFile
if pf == "" {
pf = "config/prow/plugins.yaml"
} else {
logrus.Warnf(`plugin_file is deprecated, please switch to "maps": {"%s": "plugins"} before July 2018`, pf)
}
c.Maps = map[string]ConfigMapSpec{
cf: {
Name: "config",
},
pf: {
Name: "plugins",
},
}
}
for name, spec := range c.Maps {
spec.Namespaces = append([]string{spec.Namespace}, spec.AdditionalNamespaces...)
c.Maps[name] = spec
}
}
func (c *Configuration) setDefaults() {
c.ConfigUpdater.SetDefaults()
for repo, plugins := range c.ExternalPlugins {
for i, p := range plugins {
if p.Endpoint != "" {
continue
}
c.ExternalPlugins[repo][i].Endpoint = fmt.Sprintf("http://%s", p.Name)
}
}
if c.Blunderbuss.ReviewerCount == nil && c.Blunderbuss.FileWeightCount == nil {
c.Blunderbuss.ReviewerCount = new(int)
*c.Blunderbuss.ReviewerCount = defaultBlunderbussReviewerCount
}
for i := range c.Triggers {
c.Triggers[i].SetDefaults()
}
if c.SigMention.Regexp == "" {
c.SigMention.Regexp = `(?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews)`
}
if c.Owners.LabelsBlackList == nil {
c.Owners.LabelsBlackList = []string{labels.Approved, labels.LGTM}
}
for _, milestone := range c.RepoMilestone {
if milestone.MaintainersFriendlyName == "" {
milestone.MaintainersFriendlyName = "SIG Chairs/TLs"
}
}
if c.CherryPickUnapproved.BranchRegexp == "" {
c.CherryPickUnapproved.BranchRegexp = `^release-.*$`
}
if c.CherryPickUnapproved.Comment == "" {
c.CherryPickUnapproved.Comment = `This PR is not for the master branch but does not have the ` + "`cherry-pick-approved`" + ` label. Adding the ` + "`do-not-merge/cherry-pick-not-approved`" + ` label.`
}
for i, rml := range c.RequireMatchingLabel {
if rml.GracePeriod == "" {
c.RequireMatchingLabel[i].GracePeriod = "5s"
}
}
}
// validatePluginsDupes will return an error if there are duplicated plugins.
// It is sometimes a sign of misconfiguration and is always useless for a
// plugin to be specified at both the org and repo levels.
func validatePluginsDupes(plugins map[string][]string) error {
var errors []error
for repo, repoConfig := range plugins {
if strings.Contains(repo, "/") {
org := strings.Split(repo, "/")[0]
if dupes := findDuplicatedPluginConfig(repoConfig, plugins[org]); len(dupes) > 0 {
errors = append(errors, fmt.Errorf("plugins %v are duplicated for %s and %s", dupes, repo, org))
}
}
}
return errorutil.NewAggregate(errors...)
}
// ValidatePluginsUnknown will return an error if there are any unrecognized
// plugins configured.
func (c *Configuration) ValidatePluginsUnknown() error {
var errors []error
for _, configuration := range c.Plugins {
for _, plugin := range configuration {
if _, ok := pluginHelp[plugin]; !ok {
errors = append(errors, fmt.Errorf("unknown plugin: %s", plugin))
}
}
}
return errorutil.NewAggregate(errors...)
}
func validateSizes(size Size) error {
if size.S > size.M || size.M > size.L || size.L > size.Xl || size.Xl > size.Xxl {
return errors.New("invalid size plugin configuration - one of the smaller sizes is bigger than a larger one")
}
return nil
}
func findDuplicatedPluginConfig(repoConfig, orgConfig []string) []string {
var dupes []string
for _, repoPlugin := range repoConfig {
for _, orgPlugin := range orgConfig {
if repoPlugin == orgPlugin {
dupes = append(dupes, repoPlugin)
}
}
}
return dupes
}
func validateExternalPlugins(pluginMap map[string][]ExternalPlugin) error {
var errors []string
for repo, plugins := range pluginMap {
if !strings.Contains(repo, "/") {
continue
}
org := strings.Split(repo, "/")[0]
var orgConfig []string
for _, p := range pluginMap[org] {
orgConfig = append(orgConfig, p.Name)
}
var repoConfig []string
for _, p := range plugins {
repoConfig = append(repoConfig, p.Name)
}
if dupes := findDuplicatedPluginConfig(repoConfig, orgConfig); len(dupes) > 0 {
errors = append(errors, fmt.Sprintf("external plugins %v are duplicated for %s and %s", dupes, repo, org))
}
}
if len(errors) > 0 {
return fmt.Errorf("invalid plugin configuration:\n\t%v", strings.Join(errors, "\n\t"))
}
return nil
}
var warnBlunderbussFileWeightCount time.Time
func validateBlunderbuss(b *Blunderbuss) error {
if b.ReviewerCount != nil && b.FileWeightCount != nil {
return errors.New("cannot use both request_count and file_weight_count in blunderbuss")
}
if b.ReviewerCount != nil && *b.ReviewerCount < 1 {
return fmt.Errorf("invalid request_count: %v (needs to be positive)", *b.ReviewerCount)
}
if b.FileWeightCount != nil && *b.FileWeightCount < 1 {
return fmt.Errorf("invalid file_weight_count: %v (needs to be positive)", *b.FileWeightCount)
}
if b.FileWeightCount != nil {
warnDeprecated(&warnBlunderbussFileWeightCount, 5*time.Minute, "file_weight_count is being deprecated in favour of max_request_count. Please ensure your configuration is updated before the end of May 2019.")
}
return nil
}
func validateConfigUpdater(updater *ConfigUpdater) error {
files := sets.NewString()
configMapKeys := map[string]sets.String{}
for file, config := range updater.Maps {
if files.Has(file) {
return fmt.Errorf("file %s listed more than once in config updater config", file)
}
files.Insert(file)
key := config.Key
if key == "" {
key = path.Base(file)
}
if _, ok := configMapKeys[config.Name]; ok {
if configMapKeys[config.Name].Has(key) {
return fmt.Errorf("key %s in configmap %s updated with more than one file", key, config.Name)
}
configMapKeys[config.Name].Insert(key)
} else {
configMapKeys[config.Name] = sets.NewString(key)
}
}
return nil
}
func validateRequireMatchingLabel(rs []RequireMatchingLabel) error {
for i, r := range rs {
if err := r.validate(); err != nil {
return fmt.Errorf("error validating require_matching_label config #%d: %v", i, err)
}
}
return nil
}
func validateProjectManager(pm ProjectManager) error {
projectConfig := pm
// No ProjectManager configuration provided, we have nothing to validate
if len(projectConfig.OrgRepos) == 0 {
return nil
}
for orgRepoName, managedOrgRepo := range pm.OrgRepos {
if len(managedOrgRepo.Projects) == 0 {
return fmt.Errorf("Org/repo: %s, has no projects configured", orgRepoName)
}
for projectName, managedProject := range managedOrgRepo.Projects {
var labelSets []sets.String
if len(managedProject.Columns) == 0 {
return fmt.Errorf("Org/repo: %s, project %s, has no columns configured", orgRepoName, projectName)
}
for _, managedColumn := range managedProject.Columns {