Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
8 changes: 8 additions & 0 deletions tools/github-membership/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/trodge/magic-modules/membership-tools/tools/github-membership

go 1.25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
go 1.25
go 1.23

consistency with other packages


require (
github.com/google/go-cmp v0.7.0
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
)
4 changes: 4 additions & 0 deletions tools/github-membership/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
119 changes: 119 additions & 0 deletions tools/github-membership/membership.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2023 Google LLC. All Rights Reserved.
*
* 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 membership

import (
"fmt"
"math/rand"
"slices"
"time"

"golang.org/x/exp/maps"
)

type UserType int64

const (
CommunityUserType UserType = iota
GooglerUserType
CoreContributorUserType
)

func (ut UserType) String() string {
switch ut {
case GooglerUserType:
return "Googler"
case CoreContributorUserType:
return "Core Contributor"
default:
return "Community Contributor"
}
}

func (gh *Client) GetUserType(user string) UserType {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

undefined: ClientcompilerUndeclaredName

Should this define a client interface that has this function?

if IsCoreContributor(user) {
fmt.Println("User is a core contributor")
return CoreContributorUserType
}

if gh.IsTeamMember("GoogleCloudPlatform", "terraform", user) {
fmt.Println("User is an active member of the 'terraform' team in 'GoogleCloudPlatform' organization")
return GooglerUserType
} else {
fmt.Printf("User '%s' is not an active member of the 'terraform' team in 'GoogleCloudPlatform' organization\n", user)
}

if gh.IsOrgMember(user, "GoogleCloudPlatform") {
fmt.Println("User is a GCP org member")
return GooglerUserType
}

if gh.IsOrgMember(user, "googlers") {
fmt.Println("User is a googlers org member")
return GooglerUserType
}

return CommunityUserType
}

// Check if a user is team member to not request a random reviewer
func IsCoreContributor(user string) bool {
_, isTrustedContributor := trustedContributors[user]
return IsCoreReviewer(user) || isTrustedContributor
}

func IsCoreReviewer(user string) bool {
_, isCoreReviewer := reviewerRotation[user]
return isCoreReviewer
}

// GetRandomReviewer returns a random available reviewer (optionally excluding some people from the reviewer pool)
func GetRandomReviewer(excludedReviewers []string) string {
availableReviewers := AvailableReviewers(excludedReviewers)
reviewer := availableReviewers[rand.Intn(len(availableReviewers))]
return reviewer
}

func AvailableReviewers(excludedReviewers []string) []string {
return available(time.Now(), reviewerRotation, excludedReviewers)
}

func available(nowTime time.Time, reviewerRotation map[string]ReviewerConfig, excludedReviewers []string) []string {
excludedReviewers = append(excludedReviewers, onVacation(nowTime, reviewerRotation)...)
notExcluded := make(map[string]struct{}, len(reviewerRotation))
for reviewer := range reviewerRotation {
notExcluded[reviewer] = struct{}{}
}
for _, excluded := range excludedReviewers {
delete(notExcluded, excluded)
}
ret := maps.Keys(notExcluded)
slices.Sort(ret)
return ret
}

func onVacation(nowTime time.Time, reviewerRotation map[string]ReviewerConfig) []string {
var onVacationList []string
for reviewer, config := range reviewerRotation {
for _, v := range config.vacations {
if nowTime.Before(v.GetStart(config.timezone)) || nowTime.After(v.GetEnd(config.timezone)) {
continue
}
onVacationList = append(onVacationList, reviewer)
}
}
return onVacationList
}
146 changes: 146 additions & 0 deletions tools/github-membership/membership_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package membership

import "time"

type date struct {
year int
month int
day int
}

func newDate(year, month, day int) date {
return date{
year: year,
month: month,
day: day,
}
}

type Vacation struct {
startDate, endDate date
}

// GetStart returns a time corresponding to the beginning of the start date in the given timezone.
func (v Vacation) GetStart(timezone *time.Location) time.Time {
if timezone == nil {
timezone = usPacific
}
return time.Date(v.startDate.year, time.Month(v.startDate.month), v.startDate.day, 0, 0, 0, 0, timezone)
}

// GetEnd returns a time corresponding to the end of the end date in the given timezone
func (v Vacation) GetEnd(timezone *time.Location) time.Time {
if timezone == nil {
timezone = usPacific
}
return time.Date(v.endDate.year, time.Month(v.endDate.month), v.endDate.day, 0, 0, 0, 0, timezone).AddDate(0, 0, 1).Add(-1 * time.Millisecond)
}

type ReviewerConfig struct {
// timezone controls the timezone for vacation start / end dates. Default: US/Pacific.
timezone *time.Location

// vacations allows specifying times when new reviews should not be requested of the reviewer.
// Existing PRs will still have reviews re-requested.
// Both startDate and endDate are inclusive.
// Example: taking vacation from 2024-03-28 to 2024-04-02.
// {
// vacations: []Vacation{
// startDate: newDate(2024, 3, 28),
// endDate: newDate(2024, 4, 2),
// },
// },
vacations []Vacation
}

var (
usPacific, _ = time.LoadLocation("US/Pacific")
usCentral, _ = time.LoadLocation("US/Central")
usEastern, _ = time.LoadLocation("US/Eastern")
london, _ = time.LoadLocation("Europe/London")

// This is for the random-assignee rotation.
reviewerRotation = map[string]ReviewerConfig{
"BBBmau": {
vacations: []Vacation{
{
startDate: newDate(2025, 4, 7),
endDate: newDate(2025, 4, 11),
},
},
},
"c2thorn": {
vacations: []Vacation{
{
startDate: newDate(2025, 4, 9),
endDate: newDate(2025, 4, 15),
},
},
},
"hao-nan-li": {
vacations: []Vacation{},
},
"melinath": {
vacations: []Vacation{},
},
"NickElliot": {
vacations: []Vacation{},
},
"rileykarson": {
vacations: []Vacation{
{
startDate: newDate(2025, 2, 25),
endDate: newDate(2025, 3, 10),
},
},
},
"roaks3": {
vacations: []Vacation{},
},
"ScottSuarez": {
vacations: []Vacation{},
},
"shuyama1": {
vacations: []Vacation{
{
startDate: newDate(2025, 5, 23),
endDate: newDate(2025, 5, 30),
},
},
},
"SirGitsalot": {
vacations: []Vacation{
{
startDate: newDate(2025, 1, 18),
endDate: newDate(2025, 1, 25),
},
},
},
"slevenick": {
vacations: []Vacation{
{
startDate: newDate(2025, 5, 22),
endDate: newDate(2025, 6, 7),
},
},
},
"trodge": {
vacations: []Vacation{},
},
"zli82016": {
vacations: []Vacation{
{
startDate: newDate(2025, 1, 15),
endDate: newDate(2025, 2, 9),
},
},
},
}

// This is for new team members who are onboarding
trustedContributors = map[string]struct{}{
"bbasata": struct{}{},
"jaylonmcshan03": struct{}{},
"malhotrasagar2212": struct{}{},
}
)
Loading
Loading