diff --git a/prow/plugins/config.go b/prow/plugins/config.go index 293d1cf5c8ac..b16e1de5c958 100644 --- a/prow/plugins/config.go +++ b/prow/plugins/config.go @@ -491,6 +491,8 @@ type Milestone struct { MaintainersID int `json:"maintainers_id,omitempty"` MaintainersTeam string `json:"maintainers_team,omitempty"` MaintainersFriendlyName string `json:"maintainers_friendly_name,omitempty"` + // CodeFreezeMaintainersTeam is the GitHub team allowed to set milestones if code freeze is in place. + CodeFreezeMaintainersTeam string `json:"code_freeze_maintainers_team,omitempty"` } // BranchToMilestone is a map of the branch name to the configured milestone for that branch. diff --git a/prow/plugins/milestone/codefreezechecker/codefreezechecker.go b/prow/plugins/milestone/codefreezechecker/codefreezechecker.go new file mode 100644 index 000000000000..0d9f0200d662 --- /dev/null +++ b/prow/plugins/milestone/codefreezechecker/codefreezechecker.go @@ -0,0 +1,61 @@ +/* +Copyright 2023 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 codefreezechecker + +import ( + "fmt" + "strings" + + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/test-infra/prow/config" +) + +var defaultBranches = []string{"master", "main"} + +// CodeFreezeChecker is the main structure of checking if we're in Code Freeze. +type CodeFreezeChecker struct{} + +// New creates a new CodeFreezeChecker instance. +func New() *CodeFreezeChecker { + return &CodeFreezeChecker{} +} + +// InCodeFreeze returns true if we're in Code Freeze: +// https://github.com/kubernetes/sig-release/blob/2d8a1cc/releases/release_phases.md#code-freeze +// This is being checked if the prow tide configuration has milestone restrictions applied like: +// https://github.com/kubernetes/test-infra/pull/31164/files +func (c *CodeFreezeChecker) InCodeFreeze(prowConfig *config.Config, milestone, org, repo string) bool { + orgRepo := config.OrgRepo{Org: org, Repo: repo} + queries := prowConfig.Tide.Queries.QueryMap().ForRepo(orgRepo) + + for _, query := range queries { + if query.Milestone != milestone { + continue + } + + includedBranches := sets.New(query.IncludedBranches...) + releaseBranchFromMilestone := fmt.Sprintf("release-%s", strings.TrimPrefix(query.Milestone, "v")) + + if includedBranches.Has(releaseBranchFromMilestone) && includedBranches.HasAny(defaultBranches...) { + logrus.Infof("Found code freeze for milestone %s", query.Milestone) + return true + } + } + + return false +} diff --git a/prow/plugins/milestone/codefreezechecker/codefreezechecker_test.go b/prow/plugins/milestone/codefreezechecker/codefreezechecker_test.go new file mode 100644 index 000000000000..a6ac0345f033 --- /dev/null +++ b/prow/plugins/milestone/codefreezechecker/codefreezechecker_test.go @@ -0,0 +1,122 @@ +/* +Copyright 2023 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 codefreezechecker + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/test-infra/prow/config" +) + +func TestInCodeFreeze(t *testing.T) { + const ( + testOrg = "org" + testRepo = "repo" + testMilestone = "v1.29" + testReleaseBranch = "release-1.29" + ) + t.Parallel() + for _, tc := range []struct { + name string + config *config.Config + isInCodeFreezte bool + }{ + { + name: "in code freeze", + config: &config.Config{ + ProwConfig: config.ProwConfig{ + Tide: config.Tide{ + TideGitHubConfig: config.TideGitHubConfig{ + Queries: config.TideQueries{ + { + Milestone: testMilestone, + Repos: []string{fmt.Sprintf("%s/%s", testOrg, testRepo)}, + IncludedBranches: []string{testReleaseBranch, defaultBranches[0]}, + }, + }, + }, + }, + }, + }, + isInCodeFreezte: true, + }, + { + name: "different milestone", + config: &config.Config{ + ProwConfig: config.ProwConfig{ + Tide: config.Tide{ + TideGitHubConfig: config.TideGitHubConfig{ + Queries: config.TideQueries{ + { + Milestone: "different", + Repos: []string{fmt.Sprintf("%s/%s", testOrg, testRepo)}, + IncludedBranches: []string{testReleaseBranch, defaultBranches[0]}, + }, + }, + }, + }, + }, + }, + isInCodeFreezte: false, + }, + { + name: "different repo", + config: &config.Config{ + ProwConfig: config.ProwConfig{ + Tide: config.Tide{ + TideGitHubConfig: config.TideGitHubConfig{ + Queries: config.TideQueries{ + { + Milestone: testMilestone, + Repos: []string{"bar"}, + IncludedBranches: []string{testReleaseBranch, defaultBranches[0]}, + }, + }, + }, + }, + }, + }, + isInCodeFreezte: false, + }, + { + name: "different included branches", + config: &config.Config{ + ProwConfig: config.ProwConfig{ + Tide: config.Tide{ + TideGitHubConfig: config.TideGitHubConfig{ + Queries: config.TideQueries{ + { + Milestone: testMilestone, + Repos: []string{fmt.Sprintf("%s/%s", testOrg, testRepo)}, + IncludedBranches: []string{"some", "other", "branches"}, + }, + }, + }, + }, + }, + }, + isInCodeFreezte: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + res := New().InCodeFreeze(tc.config, testMilestone, testOrg, testRepo) + assert.Equal(t, tc.isInCodeFreezte, res) + }) + } +} diff --git a/prow/plugins/milestone/milestone.go b/prow/plugins/milestone/milestone.go index 7b91d82cd465..e3f7c0f597f3 100644 --- a/prow/plugins/milestone/milestone.go +++ b/prow/plugins/milestone/milestone.go @@ -26,10 +26,12 @@ import ( "github.com/sirupsen/logrus" + "k8s.io/test-infra/prow/config" prowconfig "k8s.io/test-infra/prow/config" "k8s.io/test-infra/prow/github" "k8s.io/test-infra/prow/pluginhelp" "k8s.io/test-infra/prow/plugins" + "k8s.io/test-infra/prow/plugins/milestone/codefreezechecker" ) const pluginName = "milestone" @@ -85,7 +87,7 @@ func helpProvider(config *plugins.Configuration, enabledRepos []prowconfig.OrgRe } func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error { - return handle(pc.GitHubClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone) + return handle(pc.GitHubClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone, pc.Config) } func BuildMilestoneMap(milestones []github.Milestone) map[string]int { @@ -95,7 +97,7 @@ func BuildMilestoneMap(milestones []github.Milestone) map[string]int { } return m } -func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, repoMilestone map[string]plugins.Milestone) error { +func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, repoMilestone map[string]plugins.Milestone, prowConfig *config.Config) error { if e.Action != github.GenericCommentActionCreated { return nil } @@ -117,19 +119,10 @@ func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, r if err != nil { return err } + found := false - for _, person := range milestoneMaintainers { - login := github.NormLogin(e.User.Login) - if github.NormLogin(person.Login) == login { - found = true - break - } - } - if !found { - // not in the milestone maintainers team - msg := fmt.Sprintf(mustBeAuthorized, org, milestone.MaintainersTeam, org, milestone.MaintainersTeam, milestone.MaintainersFriendlyName) - return gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)) - } + team := milestone.MaintainersTeam + inCodeFreeze := false milestones, err := gc.ListMilestones(org, repo) if err != nil { @@ -138,6 +131,42 @@ func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, r } proposedMilestone := milestoneMatch[1] + if milestone.CodeFreezeMaintainersTeam != "" { + codeFreezeMaintainers, err := gc.ListTeamMembersBySlug(org, milestone.CodeFreezeMaintainersTeam, github.RoleAll) + if err != nil { + return fmt.Errorf("retrieve code freeze maintainers: %w", err) + } + + inCodeFreeze = codefreezechecker.New().InCodeFreeze(prowConfig, proposedMilestone, org, repo) + if inCodeFreeze { + team = milestone.CodeFreezeMaintainersTeam + + for _, maintainer := range codeFreezeMaintainers { + login := github.NormLogin(e.User.Login) + if github.NormLogin(maintainer.Login) == login { + found = true + break + } + } + } + } + + if !inCodeFreeze { + for _, person := range milestoneMaintainers { + login := github.NormLogin(e.User.Login) + if github.NormLogin(person.Login) == login { + found = true + break + } + } + } + + if !found { + // not in the milestone maintainers team + msg := fmt.Sprintf(mustBeAuthorized, org, team, org, team, milestone.MaintainersFriendlyName) + return gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)) + } + // special case, if the clear keyword is used if proposedMilestone == clearKeyword { if err := gc.ClearMilestone(org, repo, e.Number); err != nil { diff --git a/prow/plugins/milestone/milestone_test.go b/prow/plugins/milestone/milestone_test.go index a7ea64eeffc3..54c4e0315cd0 100644 --- a/prow/plugins/milestone/milestone_test.go +++ b/prow/plugins/milestone/milestone_test.go @@ -21,6 +21,7 @@ import ( "github.com/sirupsen/logrus" + "k8s.io/test-infra/prow/config" "k8s.io/test-infra/prow/github" "k8s.io/test-infra/prow/github/fakegithub" "k8s.io/test-infra/prow/plugins" @@ -124,7 +125,7 @@ func TestMilestoneStatus(t *testing.T) { repoMilestone["org/repo"] = plugins.Milestone{MaintainersTeam: maintainersTeamName} } - if err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, repoMilestone); err != nil { + if err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, repoMilestone, &config.Config{}); err != nil { t.Errorf("(%s): Unexpected error from handle: %v.", tc.name, err) continue }