Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/invite after cherry pick pr created #1011

Merged
merged 21 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e029eff
refactor(externalplugins/cherrypicker): extract pull request create m…
wuhuizuo Aug 23, 2022
d373219
refactor(externalplugins/cherrypicker): extract `cherryPickCommit` me…
wuhuizuo Aug 23, 2022
2f6cf6e
refactor(externalplugins/cherrypicker): extract `newCherryPickLock` m…
wuhuizuo Aug 23, 2022
b0e01fe
feat(externalplugins/cherrypicker): implement collaborator invitation
wuhuizuo Aug 23, 2022
80ab3b9
chore: fix lint issues
wuhuizuo Aug 24, 2022
0f92522
refactor(cherrypicker): move invite logic and test codes to new files
wuhuizuo Aug 26, 2022
7011ea5
test(cherrypicker): add tests to increase coverage
wuhuizuo Aug 26, 2022
129c36f
chore: try to make codcov passing
wuhuizuo Aug 26, 2022
ce22239
fix(cherrypicker): fix invite repo
wuhuizuo Aug 26, 2022
b17a724
test(cherrypicker): add unit tests
wuhuizuo Aug 26, 2022
346fb63
fix(cherrypicker): review issues
wuhuizuo Aug 31, 2022
870bc1a
revert: delete `ReadHeaderTimeout` setting in http.Server
wuhuizuo Aug 31, 2022
f6eccde
refactor(cherrypicker): change param type in `newExtGithubClient`
wuhuizuo Sep 6, 2022
6d15174
refactor(cherrypicker): change param type in `AddCollaborator`
wuhuizuo Sep 6, 2022
df7bc53
style(cherrypicker): fix spell in test codes
wuhuizuo Sep 6, 2022
82dbd3c
style(cherrypicker): keep var name style
wuhuizuo Sep 6, 2022
7d236e6
test(cherrypicker): add test data for `TestInviteIC`
wuhuizuo Sep 9, 2022
b1bb326
test(cherrypicker): add opened invitation juge
wuhuizuo Sep 9, 2022
9969546
Update internal/pkg/externalplugins/cherrypicker/cherrypicker_invite.go
wuhuizuo Sep 13, 2022
613af9a
fix(cherrypicker): fix for condition in method `ListRepoInvitations`
wuhuizuo Sep 13, 2022
9b13d8b
docs(plugins/cherrypicker): update doc in md files and codes
wuhuizuo Sep 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions cmd/ticommunitycherrypicker/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"context"
"time"

gc "github.com/google/go-github/v29/github"
"golang.org/x/oauth2"
"k8s.io/test-infra/prow/github"

"github.com/ti-community-infra/tichi/internal/pkg/externalplugins/cherrypicker"
)

const (
timeoutForAddCollaborator = 5 * time.Second
timeoutForListInvitations = 5 * time.Second
)

type oauth2TokenSource func() []byte

// Token implement interface oauth2.TokenSource.
func (o oauth2TokenSource) Token() (*oauth2.Token, error) {
return &oauth2.Token{AccessToken: string(o())}, nil
}

func newExtGithubClient(client github.Client, tokenGenerator oauth2TokenSource) cherrypicker.GithubClient {
ctx := context.Background()

ts := oauth2.ReuseTokenSource(nil, tokenGenerator)
cc := gc.NewClient(oauth2.NewClient(ctx, ts))

return &extendGithubClient{
Client: client,
rs: cc.Repositories,
}
}

type extendGithubClient struct {
github.Client
rs *gc.RepositoriesService
}

// AddCollaborator to repository.
func (c *extendGithubClient) AddCollaborator(org, repo, user string, permission github.RepoPermissionLevel) error {
ctx, cancel := context.WithTimeout(context.TODO(), timeoutForAddCollaborator)
defer cancel()

options := &gc.RepositoryAddCollaboratorOptions{Permission: string(permission)}
_, _, err := c.rs.AddCollaborator(ctx, org, repo, user, options)
return err
}

// ListRepoInvitations list repository invitations.
func (c *extendGithubClient) ListRepoInvitations(org, repo string) ([]*gc.RepositoryInvitation, error) {
ctx, cancel := context.WithTimeout(context.TODO(), timeoutForListInvitations)
defer cancel()

var invitations []*gc.RepositoryInvitation
for page, nextPage := 1, 1; nextPage > 0; page++ {
data, res, err := c.rs.ListInvitations(ctx, org, repo, &gc.ListOptions{PerPage: 100, Page: page})
if err != nil {
return nil, err
}

invitations = append(invitations, data...)
if res == nil {
nextPage = 0
} else {
nextPage = res.NextPage
}
}

return invitations, nil
}
5 changes: 3 additions & 2 deletions cmd/ticommunitycherrypicker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,16 @@ func main() {
log.WithError(err).Fatal("Error listing bot repositories.")
}

tokenGenerator := secretAgent.GetTokenGenerator(o.github.TokenPath)
server := &cherrypicker.Server{
WebhookSecretGenerator: secretAgent.GetTokenGenerator(o.webhookSecretFile),
GitHubTokenGenerator: secretAgent.GetTokenGenerator(o.github.TokenPath),
GitHubTokenGenerator: tokenGenerator,
BotUser: botUser,
Email: email,
ConfigAgent: epa,

GitClient: git.ClientFactoryFrom(gitClient),
GitHubClient: githubClient,
GitHubClient: newExtGithubClient(githubClient, tokenGenerator),
Log: log,

Bare: &http.Client{},
Expand Down
1 change: 0 additions & 1 deletion cmd/ticommunitymerge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ func main() {
helpProvider := merge.HelpProvider(epa)
externalplugins.ServeExternalPluginHelp(mux, log, helpProvider)
httpServer := &http.Server{Addr: ":" + strconv.Itoa(o.port), Handler: mux}

defer interrupts.WaitForGracefulShutdown()
interrupts.ListenAndServe(httpServer, 5*time.Second)
}
Expand Down
1 change: 1 addition & 0 deletions docs/en/plugins/cherrypicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This plugin is primarily responsible for cherry-picking code and creating PRs wi

- `allow_all` set to true, all GitHub users can trigger `/cherry-pick some-branch`
- `allow_all` set to false, only members of the repo's Org can trigger `/cherry-pick some-branch`
- **only** members of the repo's Org can trigger `/cherry-pick-invite` on cherry-pick pull requests.

## Design

Expand Down
1 change: 1 addition & 0 deletions docs/plugins/cherrypicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ti-community-cherrypicker 将帮助我们自动的 cherry-pick PR 的改动到

- `allow_all` 配置为 true,所有 GitHub 用户都可以触发 `/cherry-pick some-branch`
- `allow_all` 配置为 false,则只有该 repo 所在 Org 的成员可以触发 `/cherry-pick some-branch`
- **只有** 该 repo 所在 Org 的成员可以在 cherry-pick PR 上触发 `/cherry-pick-invite`.

## 设计思路

Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.18
require (
github.com/gin-gonic/gin v1.7.0
github.com/mroth/weightedrand v0.4.1
github.com/pkg/errors v0.9.1
github.com/shurcooL/githubv4 v0.0.0-20191102174205-af46314aec7b
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/sirupsen/logrus v1.9.0
Expand Down Expand Up @@ -53,6 +54,7 @@ require (
github.com/gomodule/redigo v1.7.0 // indirect
github.com/google/btree v1.0.0 // indirect
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/go-github/v29 v29.0.3
github.com/google/go-querystring v1.0.0 // indirect
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea // indirect
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7 // indirect
Expand All @@ -76,7 +78,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.7.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.10.0 // indirect
Expand All @@ -93,7 +94,7 @@ require (
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
golang.org/x/mod v0.3.0 // indirect
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
golang.org/x/text v0.3.6 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ github.com/google/go-containerregistry v0.1.1/go.mod h1:npTSyywOeILcgWqd+rvtzGWf
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-github/v27 v27.0.6/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0=
github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
github.com/google/go-github/v29 v29.0.3 h1:IktKCTwU//aFHnpA+2SLIi7Oo9uhAzgsdZNbcAqhgdc=
github.com/google/go-github/v29 v29.0.3/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
github.com/google/go-licenses v0.0.0-20191112164736-212ea350c932/go.mod h1:16wa6pRqNDUIhOtwF0GcROVqMeXHZJ7H6eGDFUh5Pfk=
github.com/google/go-licenses v0.0.0-20200227160636-0fa8c766a591/go.mod h1:JWeTIGPLQ9gF618ZOdlUitd1gRR/l99WOkHOlmR/UVA=
Expand Down
80 changes: 58 additions & 22 deletions internal/pkg/externalplugins/cherrypicker/cherrypicker.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ import (
"sync"
"time"

gc "github.com/google/go-github/v29/github"
"github.com/sirupsen/logrus"
tiexternalplugins "github.com/ti-community-infra/tichi/internal/pkg/externalplugins"
"github.com/ti-community-infra/tichi/internal/pkg/externalplugins/utils"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/test-infra/prow/config"
Expand All @@ -46,19 +45,35 @@ import (
"k8s.io/test-infra/prow/pluginhelp/externalplugins"
"k8s.io/test-infra/prow/plugins"
"k8s.io/utils/exec"

tiexternalplugins "github.com/ti-community-infra/tichi/internal/pkg/externalplugins"
"github.com/ti-community-infra/tichi/internal/pkg/externalplugins/utils"
)

const PluginName = "ti-community-cherrypicker"
const (
PluginName = "ti-community-cherrypicker"

pluginDescription = `The cherrypicker plugin is used for cherry-pick PRs across branches.
For every successful cherry-pick invocation a new PR is opened against the target branch and assigned to the requestor.
The plugin will send an repo collaborator invitation if the requestor is not a collaborator.
`

upstreamRemoteName = "upstream"
collaboratorPermission = "push"
cherryPickInviteExample = "/cherry-pick-invite"
cherryPickBranchFmt = "cherry-pick-%d-to-%s"
cherryPickTipFmt = "This is an automated cherry-pick of #%d"
cherryPickInviteNotifyMsgTpl = `@%s Please accept the invitation then you can push to the cherry-pick pull requests.
Comment with "%s" if the invitation is expired.
%s`
)

var (
cherryPickRe = regexp.MustCompile(`(?m)^(?:/cherrypick|/cherry-pick)\s+(.+)$`)
cherryPickBranchFmt = "cherry-pick-%d-to-%s"
cherryPickTipFmt = "This is an automated cherry-pick of #%d"
cherryPickRe = regexp.MustCompile(`(?m)^(?:/cherrypick|/cherry-pick)\s+(.+)$`)
cherryPickInviteRe = regexp.MustCompile(`(?m)^(?:/cherrypick|/cherry-pick)-invite\b`)
)

const upstreamRemoteName = "upstream"

type githubClient interface {
type GithubClient interface {
AddLabels(org, repo string, number int, labels ...string) error
AssignIssue(org, repo string, number int, logins []string) error
CreateComment(org, repo string, number int, comment string) error
Expand All @@ -75,6 +90,10 @@ type githubClient interface {
ListIssueComments(org, repo string, number int) ([]github.IssueComment, error)
GetIssueLabels(org, repo string, number int) ([]github.Label, error)
ListOrgMembers(org, role string) ([]github.TeamMember, error)
ListRepoInvitations(org, repo string) ([]*gc.RepositoryInvitation, error)
IsCollaborator(org, repo, user string) (bool, error)
// AddCollaborator invite collaborator to repo with permission(pull, triage, push, maintain, admin)
AddCollaborator(org, repo, user string, permission github.RepoPermissionLevel) error
}

// HelpProvider constructs the PluginHelp for this plugin that takes into account enabled repositories.
Expand Down Expand Up @@ -135,12 +154,10 @@ func HelpProvider(epa *tiexternalplugins.ConfigAgent) externalplugins.ExternalPl
}

pluginHelp := &pluginhelp.PluginHelp{
Description: "The cherrypicker plugin is used for cherry-pick PRs across branches. " +
"For every successful cherry-pick invocation a new PR is opened " +
"against the target branch and assigned to the requestor. ",
Config: configInfo,
Snippet: yamlSnippet,
Events: []string{tiexternalplugins.PullRequestEvent, tiexternalplugins.IssueCommentEvent},
Description: pluginDescription,
Config: configInfo,
Snippet: yamlSnippet,
Events: []string{tiexternalplugins.PullRequestEvent, tiexternalplugins.IssueCommentEvent},
}

pluginHelp.AddCommand(pluginhelp.Command{
Expand All @@ -152,6 +169,14 @@ func HelpProvider(epa *tiexternalplugins.ConfigAgent) externalplugins.ExternalPl
WhoCanUse: "Members of the trusted organization for the repo or anyone(depends on the AllowAll configuration).",
Examples: []string{"/cherrypick release-3.9", "/cherry-pick release-1.15"},
})
pluginHelp.AddCommand(pluginhelp.Command{
Usage: cherryPickInviteExample,
Description: "Request a collaborator invitation" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Description: "Request a collaborator invitation" +
Description: "Request a collaborator invitation (Only works in cherry-pick pull requests)." +

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next PR

"This command works in cherry-pick pull requests.",
Featured: true,
WhoCanUse: "Members of the trusted organization for the repo.",
Examples: []string{cherryPickInviteExample},
})

return pluginHelp, nil
}
Expand All @@ -168,7 +193,7 @@ type Server struct {
GitClient git.ClientFactory
// Used for unit testing
Push func(forkName, newBranch string, force bool) error
GitHubClient githubClient
GitHubClient GithubClient
Log *logrus.Entry
ConfigAgent *tiexternalplugins.ConfigAgent

Expand Down Expand Up @@ -215,7 +240,7 @@ func (s *Server) handleEvent(eventType, eventGUID string, payload []byte) error
return err
}
go func() {
if err := s.handleIssueComment(l, ic); err != nil {
if err := s.handleIssueComment(l, &ic); err != nil {
s.Log.WithError(err).WithFields(l.Data).Info("Cherry-pick failed.")
}
}()
Expand All @@ -225,7 +250,7 @@ func (s *Server) handleEvent(eventType, eventGUID string, payload []byte) error
return err
}
go func() {
if err := s.handlePullRequest(l, pr); err != nil {
if err := s.handlePullRequest(l, &pr); err != nil {
s.Log.WithError(err).WithFields(l.Data).Info("Cherry-pick failed.")
}
}()
Expand All @@ -235,12 +260,20 @@ func (s *Server) handleEvent(eventType, eventGUID string, payload []byte) error
return nil
}

func (s *Server) handleIssueComment(l *logrus.Entry, ic github.IssueCommentEvent) error {
func (s *Server) handleIssueComment(l *logrus.Entry, ic *github.IssueCommentEvent) error {
// Only consider new comments in PRs.
if !ic.Issue.IsPullRequest() || ic.Action != github.IssueCommentActionCreated {
return nil
}

if cherryPickInviteRe.MatchString(ic.Comment.Body) {
return s.inviteCollaborator(ic)
}

return s.handleIssueCherryPickComment(l, ic)
}

func (s *Server) handleIssueCherryPickComment(l *logrus.Entry, ic *github.IssueCommentEvent) error {
org := ic.Repo.Owner.Login
repo := ic.Repo.Name
num := ic.Issue.Number
Expand Down Expand Up @@ -338,7 +371,7 @@ func (s *Server) handleIssueComment(l *logrus.Entry, ic github.IssueCommentEvent
return nil
}

func (s *Server) handlePullRequest(log *logrus.Entry, pre github.PullRequestEvent) error {
func (s *Server) handlePullRequest(log *logrus.Entry, pre *github.PullRequestEvent) error {
// Only consider merged PRs.
pr := pre.PullRequest
if !pr.Merged || pr.MergeSHA == nil {
Expand Down Expand Up @@ -472,8 +505,9 @@ func (s *Server) handlePullRequest(log *logrus.Entry, pre github.PullRequestEven
return utilerrors.NewAggregate(errs)
}

//nolint:gocyclo
// TODO: refactoring to reduce complexity.
//
//nolint:gocyclo
func (s *Server) handle(logger *logrus.Entry, requestor string,
comment *github.IssueComment, org, repo, targetBranch string, pr *github.PullRequest) error {
num := pr.Number
Expand Down Expand Up @@ -708,6 +742,7 @@ func (s *Server) handle(logger *logrus.Entry, requestor string,
return nil
}

// TODO(wuhuizuo): reduce param count
func (s *Server) createComment(l *logrus.Entry, org, repo string,
num int, comment *github.IssueComment, resp string) error {
if err := func() error {
Expand All @@ -724,6 +759,7 @@ func (s *Server) createComment(l *logrus.Entry, org, repo string,
}

// createIssue creates an issue on GitHub.
// TODO(wuhuizuo): reduce param count
func (s *Server) createIssue(l *logrus.Entry, org, repo, title, body string, num int,
comment *github.IssueComment, labels, assignees []string) error {
issueNum, err := s.GitHubClient.CreateIssue(org, repo, title, body, 0, labels, assignees)
Expand Down Expand Up @@ -777,7 +813,7 @@ func normalize(input string) string {
}

// createCherryPickCommitMessage creates the commit message for the cherry-pick commit.
func createCherryPickCommitMessage(gc githubClient, log *logrus.Entry, copyIssueNumbers bool,
func createCherryPickCommitMessage(gc GithubClient, log *logrus.Entry, copyIssueNumbers bool,
org, repo string, num int, mergeSHA *string) string {
cherryPickCommitMessage := fmt.Sprintf(cherryPickTipFmt, num)

Expand Down
Loading