Skip to content

Commit

Permalink
Plugin to add issue and PR to GitHub Project
Browse files Browse the repository at this point in the history
  • Loading branch information
taragu committed Mar 7, 2019
1 parent b61b69d commit 96ff294
Show file tree
Hide file tree
Showing 8 changed files with 1,017 additions and 6 deletions.
21 changes: 18 additions & 3 deletions prow/github/client.go
Expand Up @@ -2588,9 +2588,9 @@ func (c *Client) MoveProjectCard(projectCardID int, newColumnID int) error {
c.log("MoveProjectCard", projectCardID, newColumnID)
_, err := c.request(&request{
method: http.MethodPost,
path: fmt.Sprintf("/projects/columns/cards/:%s/moves", projectCardID),
path: fmt.Sprintf("/projects/columns/cards/:%d/moves", projectCardID),
accept: "application/vnd.github.symmetra-preview+json", // allow the description field -- https://developer.github.com/changes/2018-02-22-label-description-search-preview/
requestBody: Label{column_id: newColumnID},
requestBody: fmt.Sprintf("{column_id: %d}", newColumnID),
exitCodes: []int{201},
}, nil)
return err
Expand All @@ -2604,8 +2604,23 @@ func (c *Client) DeleteProjectCard(projectCardID int) error {
_, err := c.request(&request{
method: http.MethodDelete,
accept: "application/vnd.github.symmetra-preview+json", // allow the description field -- https://developer.github.com/changes/2018-02-22-label-description-search-preview/
path: fmt.Sprintf("/projects/columns/cards/:%s", projectCardID),
path: fmt.Sprintf("/projects/columns/cards/:%d", projectCardID),
exitCodes: []int{204},
}, nil)
return err
}

// TeamHasMember checks if a user belongs to a team
func (c *Client) TeamHasMember(teamID int, memberLogin string) (bool, error) {
c.log("TeamHasMember", teamID, memberLogin)
projectMaintainers, err := c.ListTeamMembers(teamID, RoleAll)
if err != nil {
return false, err
}
for _, person := range projectMaintainers {
if NormLogin(person.Login) == NormLogin(memberLogin) {
return true, nil
}
}
return false, nil
}
175 changes: 175 additions & 0 deletions prow/github/fakegithub/fakegithub.go
Expand Up @@ -79,6 +79,22 @@ type FakeClient struct {

// A list of refs that got deleted via DeleteRef
RefsDeleted []struct{ Org, Repo, Ref string }

// A map of repo names to projects
RepoProjects map[string][]github.Project

// A map of project name to columns
ProjectColumnsMap map[string][]github.ProjectColumn

// Maps column ID to the list of project cards
ColumnCardsMap map[int][]github.ProjectCard

// Maps project name to maps of column ID to columnName
ColumnIDMap map[string]map[int]string

// The project and column names for an issue or PR
Project string
Column string
}

// BotName returns authenticated login.
Expand Down Expand Up @@ -411,3 +427,162 @@ func (f *FakeClient) ListPRCommits(org, repo string, prNumber int) ([]github.Rep
k := fmt.Sprintf("%s/%s#%d", org, repo, prNumber)
return f.CommitMap[k], nil
}

// GetRepoProjects returns the list of projects under a repo.
func (f *FakeClient) GetRepoProjects(owner, repo string) ([]github.Project, error) {
return f.RepoProjects[fmt.Sprintf("%s/%s", owner, repo)], nil
}

// GetOrgProjects returns the list of projects under an org
func (f *FakeClient) GetOrgProjects(org string) ([]github.Project, error) {
return f.RepoProjects[fmt.Sprintf("%s/*", org)], nil
}

// GetProjectColumns returns the list of columns for a given project.
func (f *FakeClient) GetProjectColumns(projectID int) ([]github.ProjectColumn, error) {
// Get project name
for _, projects := range f.RepoProjects {
for _, project := range projects {
if projectID == project.ID {
return f.ProjectColumnsMap[project.Name], nil
}
}
}
return nil, fmt.Errorf("Cannot find project ID")
}

// CreateProjectCard creates a project card under a given column.
func (f *FakeClient) CreateProjectCard(columnID int, projectCard github.ProjectCard) (*github.ProjectCard, error) {
if f.ColumnCardsMap == nil {
f.ColumnCardsMap = make(map[int][]github.ProjectCard)
}

for project, columnIDMap := range f.ColumnIDMap {
columnName, exists := columnIDMap[columnID]
if exists {
f.ColumnCardsMap[columnID] = append(
f.ColumnCardsMap[columnID],
projectCard,
)
f.Column = columnName
f.Project = project
return &projectCard, nil
}
}
return nil, fmt.Errorf("Provided column %d does not exist, ColumnIDMap is %v", columnID, f.ColumnIDMap)
}

// DeleteProjectCard deletes the project card of a specific issue or PR
func (f *FakeClient) DeleteProjectCard(projectCardID int) error {
if f.ColumnCardsMap == nil {
return fmt.Errorf("Project card doesn't exist")
}
f.Project = ""
f.Column = ""
newCards := []github.ProjectCard{}
oldColumnID := -1
for column, cards := range f.ColumnCardsMap {
removalIndex := -1
for i, existingCard := range cards {
if existingCard.ContentID == projectCardID {
oldColumnID = column
removalIndex = i
break
}
}
if removalIndex != -1 {
newCards = cards
newCards[removalIndex] = newCards[len(newCards)-1]
newCards = newCards[:len(newCards)-1]
break
}
}
// Update the old column's list of project cards
if oldColumnID != -1 {
f.ColumnCardsMap[oldColumnID] = newCards
}
return nil
}

func (f *FakeClient) GetColumnProjectCard(columnID int, cardNumber int) (*github.ProjectCard, error) {
if f.ColumnCardsMap == nil {
f.ColumnCardsMap = make(map[int][]github.ProjectCard)
}
for _, existingCard := range f.ColumnCardsMap[columnID] {
if existingCard.ContentID == cardNumber {
return &existingCard, nil
}
}
return nil, nil
}

func (f *FakeClient) GetRepos(org string, isUser bool) ([]github.Repo, error) {
return []github.Repo{
{
Owner: github.User{
Login: "kubernetes",
},
Name: "kubernetes",
},
{
Owner: github.User{
Login: "kubernetes",
},
Name: "community",
},
}, nil
}

// MoveProjectCard moves a specific project card to a specified column in the same project
func (f *FakeClient) MoveProjectCard(projectCardID int, newColumnID int) error {
// Remove project card from old column
newCards := []github.ProjectCard{}
oldColumnID := -1
projectCard := github.ProjectCard{}
for column, cards := range f.ColumnCardsMap {
removalIndex := -1
for i, existingCard := range cards {
if existingCard.ContentID == projectCardID {
oldColumnID = column
removalIndex = i
projectCard = existingCard
break
}
}
if removalIndex != -1 {
newCards = cards
newCards[removalIndex] = newCards[len(newCards)-1]
newCards = newCards[:len(newCards)-1]
}
}
if oldColumnID != -1 {
// Update the old column's list of project cards
f.ColumnCardsMap[oldColumnID] = newCards
}

for project, columnIDMap := range f.ColumnIDMap {
if columnName, exists := columnIDMap[newColumnID]; exists {
// Add project card to new column
f.ColumnCardsMap[newColumnID] = append(
f.ColumnCardsMap[newColumnID],
projectCard,
)
f.Column = columnName
f.Project = project
break
}
}

return nil
}

// TeamHasMember checks if a user belongs to a team
func (f *FakeClient) TeamHasMember(teamID int, memberLogin string) (bool, error) {
teamMembers, _ := f.ListTeamMembers(teamID, github.RoleAll)
for _, member := range teamMembers {
if member.Login == memberLogin {
return true, nil
}
}
return false, nil
}
23 changes: 23 additions & 0 deletions prow/plugins.yaml
Expand Up @@ -222,6 +222,29 @@ repo_milestone:
maintainers_id: 2460384
maintainers_team: kubernetes-milestone-maintainers

project_config:
project_org_configs:
kubernetes:
org_maintainers_team_id: 2460384
org_maintainers_team_name: kubernetes-milestone-maintainers
org_default_column_map:
1.14 CI Signal:
Start here
KEP Implementation Tracking:
To do
project_repo_configs:
kubernetes:
repo_maintainers_team_id: 2460384
repo_maintainers_team_name: kubernetes-milestone-maintainers
repo_default_column_map:
component-base:
To do
Workloads:
Backlog
kubeflow:
org_maintainers_team_id: 2460384
org_maintainers_team_name: kubernetes-milestone-maintainers

config_updater:
maps:
label_sync/labels.yaml:
Expand Down
1 change: 1 addition & 0 deletions prow/plugins/BUILD.bazel
Expand Up @@ -79,6 +79,7 @@ filegroup(
"//prow/plugins/override:all-srcs",
"//prow/plugins/owners-label:all-srcs",
"//prow/plugins/pony:all-srcs",
"//prow/plugins/project:all-srcs",
"//prow/plugins/releasenote:all-srcs",
"//prow/plugins/require-matching-label:all-srcs",
"//prow/plugins/requiresig:all-srcs",
Expand Down
72 changes: 69 additions & 3 deletions prow/plugins/config.go
Expand Up @@ -64,6 +64,7 @@ type Configuration struct {
Label *Label `json:"label,omitempty"`
Lgtm []Lgtm `json:"lgtm,omitempty"`
RepoMilestone map[string]Milestone `json:"repo_milestone,omitempty"`
Project ProjectConfig `json:"project_config,omitempty"`
RequireMatchingLabel []RequireMatchingLabel `json:"require_matching_label,omitempty"`
RequireSIG RequireSIG `json:"requiresig,omitempty"`
Slack Slack `json:"slack,omitempty"`
Expand Down Expand Up @@ -299,7 +300,7 @@ func (a Approve) ConsiderReviewState() bool {
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"
// 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
Expand All @@ -308,7 +309,7 @@ type Lgtm struct {
// 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,
// 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"`
}
Expand Down Expand Up @@ -337,7 +338,7 @@ type Trigger struct {
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.
// 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.
Expand Down Expand Up @@ -418,6 +419,33 @@ type ConfigUpdater struct {
PluginFile string `json:"plugin_file,omitempty"`
}

// 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"`
}

// 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.
Expand Down Expand Up @@ -891,3 +919,41 @@ func (c *Configuration) Validate() error {

return nil
}

func (pluginConfig *ProjectConfig) GetMaintainerTeam(org string, repo string) int {
for orgName, orgConfig := range pluginConfig.Orgs {
if org == orgName {
// look for repo level configs first because repo level config overrides org level configs
for repoName, repoConfig := range orgConfig.Repos {
if repo == repoName {
return repoConfig.MaintainerTeamID
}
}
return orgConfig.MaintainerTeamID
}
}
return -1
}

func (pluginConfig *ProjectConfig) GetColumnMap(org string, repo string) map[string]string {
for orgName, orgConfig := range pluginConfig.Orgs {
if org == orgName {
for repoName, repoConfig := range orgConfig.Repos {
if repo == repoName {
return repoConfig.ProjectColumnMap
}
}
return orgConfig.ProjectColumnMap
}
}
return nil
}

func (pluginConfig *ProjectConfig) GetOrgColumnMap(org string) map[string]string {
for orgName, orgConfig := range pluginConfig.Orgs {
if org == orgName {
return orgConfig.ProjectColumnMap
}
}
return nil
}

0 comments on commit 96ff294

Please sign in to comment.