Skip to content

Commit

Permalink
Merge pull request #28 from mona-actions/amenocal/github-app-auth
Browse files Browse the repository at this point in the history
  • Loading branch information
amenocal authored Nov 21, 2024
2 parents c8d9a16 + 3618572 commit b4844e5
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 18 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b #v4.1.4
- uses: cli/gh-extension-precompile@v1
- uses: cli/gh-extension-precompile@v2
with:
go_version: 1.21
go_version: 1.23
17 changes: 14 additions & 3 deletions cmd/byRepos.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ Copyright © 2024 NAME HERE <EMAIL ADDRESS>
package cmd

import (
"fmt"
"os"

"github.com/mona-actions/gh-migrate-teams/pkg/sync"
Expand All @@ -20,7 +19,6 @@ var byReposCmd = &cobra.Command{
It will migrate all the teams that have access to the repositories in the list.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("byRepos called")

targetOrganization := cmd.Flag("target-organization").Value.String()
sourceToken := cmd.Flag("source-token").Value.String()
Expand All @@ -29,6 +27,8 @@ var byReposCmd = &cobra.Command{
ghHostname := cmd.Flag("source-hostname").Value.String()
repoFile := cmd.Flag("from-file").Value.String()
skipTeams := cmd.Flag("skip-teams").Value.String()
tAppId := cmd.Flag("target-app-id").Value.String()
tInstallationId := cmd.Flag("target-installation-id").Value.String()

// Set ENV variables
os.Setenv("GHMT_TARGET_ORGANIZATION", targetOrganization)
Expand All @@ -38,6 +38,8 @@ var byReposCmd = &cobra.Command{
os.Setenv("GHMT_SOURCE_HOSTNAME", ghHostname)
os.Setenv("GHMT_REPO_FILE", repoFile)
os.Setenv("GHMT_SKIP_TEAMS", skipTeams)
os.Setenv("GHMT_TARGET_APP_ID", tAppId)
os.Setenv("GHMT_TARGET_INSTALLATION_ID", tInstallationId)

// Bind ENV variables in Viper
viper.BindEnv("TARGET_ORGANIZATION")
Expand All @@ -48,6 +50,9 @@ var byReposCmd = &cobra.Command{
viper.BindEnv("USER_SYNC")
viper.BindEnv("SKIP_TEAMS")
viper.BindEnv("REPO_FILE")
viper.BindEnv("TARGET_PRIVATE_KEY")
viper.BindEnv("TARGET_APP_ID")
viper.BindEnv("TARGET_INSTALLATION_ID")

sync.SyncTeamsByRepo()
},
Expand All @@ -66,7 +71,6 @@ func init() {
byReposCmd.MarkFlagRequired("source-token")

byReposCmd.Flags().StringP("target-token", "b", "", "Target Organization GitHub token. Scopes: admin:org")
byReposCmd.MarkFlagRequired("target-token")

byReposCmd.Flags().StringP("from-file", "f", "repositories.txt", "File path to use for repository list")
byReposCmd.MarkFlagRequired("from-file")
Expand All @@ -76,4 +80,11 @@ func init() {
byReposCmd.Flags().BoolP("skip-teams", "k", false, "Skips adding members and repos to teams that already exist to save on API requests (default \"false\")")

byReposCmd.Flags().StringP("source-hostname", "u", os.Getenv("SOURCE_HOST"), "GitHub Enterprise source hostname url (optional) Ex. https://github.example.com")

byReposCmd.Flags().StringP("target-private-key", "p", "", "Private key for GitHub App authentication. Ideally set as an env variable: 'GHMT_TARGET_PRIVATE_KEY'")
viper.BindPFlag("TARGET_PRIVATE_KEY", byReposCmd.Flags().Lookup("target-private-key"))

byReposCmd.Flags().StringP("target-app-id", "i", "", "GitHub App ID")

byReposCmd.Flags().Int64P("target-installation-id", "l", 0, "GitHub App Installation ID")
}
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
module github.com/mona-actions/gh-migrate-teams

go 1.21
go 1.23

toolchain go1.21.6
toolchain go1.23.3

require (
github.com/gofri/go-github-ratelimit v1.1.0
github.com/google/go-github/v62 v62.0.0
github.com/jferrl/go-githubauth v1.1.1
github.com/pterm/pterm v0.12.79
github.com/shurcooL/githubv4 v0.0.0-20240429030203-be2daab69064
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
golang.org/x/oauth2 v0.20.0
golang.org/x/oauth2 v0.24.0
)

require (
Expand All @@ -20,6 +21,8 @@ require (
atomicgo.dev/schedule v0.1.0 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/go-github/v64 v64.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
Expand Down
16 changes: 14 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,29 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gofri/go-github-ratelimit v1.1.0 h1:ijQ2bcv5pjZXNil5FiwglCg8wc9s8EgjTmNkqjw8nuk=
github.com/gofri/go-github-ratelimit v1.1.0/go.mod h1:OnCi5gV+hAG/LMR7llGhU7yHt44se9sYgKPnafoL7RY=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg=
github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jferrl/go-githubauth v1.1.1 h1:HfF3eeWFL+9jV9KHAatBaEnFGm9R2LkTqo5Z2GcDk20=
github.com/jferrl/go-githubauth v1.1.1/go.mod h1:FC1jqgik3xdaZDg8CUmGbvDwfP/egXkrq6Ygl9pSz/Y=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
Expand All @@ -64,6 +72,8 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/migueleliasweb/go-github-mock v1.0.1 h1:amLEECVny28RCD1ElALUpQxrAimamznkg9rN2O7t934=
github.com/migueleliasweb/go-github-mock v1.0.1/go.mod h1:8PJ7MpMoIiCBBNpuNmvndHm0QicjsE+hjex1yMGmjYQ=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
Expand Down Expand Up @@ -141,8 +151,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -173,6 +183,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
Expand Down
73 changes: 65 additions & 8 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,58 @@ import (
"context"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"

"github.com/gofri/go-github-ratelimit/github_ratelimit"
"github.com/google/go-github/v62/github"
"github.com/jferrl/go-githubauth"
"github.com/shurcooL/githubv4"
"github.com/spf13/viper"
"golang.org/x/oauth2"
)

func newHTTPClient() *http.Client {

token := viper.GetString("TARGET_TOKEN")
appId := viper.GetString("TARGET_APP_ID")
privateKey := []byte(viper.GetString("TARGET_PRIVATE_KEY"))
installationId := viper.GetInt64("TARGET_INSTALLATION_ID")

// check that Target token or GitHub App values are set
if token != "" && (appId == "" || len(privateKey) == 0 || installationId == 0) {
log.Fatalf("Please provide a target token or a target GitHub App ID and private key")
}

if appId != "" && len(privateKey) != 0 && installationId != 0 {
// GitHub App authentication

appIdInt, err := strconv.ParseInt(appId, 10, 64)
if err != nil {
log.Fatalf("Error converting app ID to int64: %v", err)
}
appToken, err := githubauth.NewApplicationTokenSource(appIdInt, privateKey)
if err != nil {
log.Fatalf("Error creating app token: %v", err)
}

installationToken := githubauth.NewInstallationTokenSource(installationId, appToken)

// Create HTTP client with automatic token refresh
httpClient := oauth2.NewClient(context.Background(), installationToken)

return httpClient

} else {
// Personal access token authentication
token := viper.GetString("TARGET_TOKEN")
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
return oauth2.NewClient(context.Background(), src)
}
}

type RateLimitAwareGraphQLClient struct {
client *githubv4.Client
}
Expand Down Expand Up @@ -77,7 +119,19 @@ func newGHGraphqlClient(token string) *RateLimitAwareGraphQLClient {
}
}

func newGHRestClient(token string) *github.Client {
func newGHRestClient() *github.Client {
httpClient := newHTTPClient()
rateLimiter, err := github_ratelimit.NewRateLimitWaiterClient(httpClient.Transport)

if err != nil {
panic(err)
}

return github.NewClient(rateLimiter)
}

func newSourceGHRestClient() *github.Client {
token := viper.GetString("SOURCE_TOKEN")
ctx := context.Background()
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(ctx, ts)
Expand Down Expand Up @@ -349,7 +403,7 @@ func GetRepositoryCollaborators(repository string) []map[string]string {
}

func CreateTeam(name string, description string, privacy string, parentTeamName string) error {
client := newGHRestClient(viper.GetString("TARGET_TOKEN"))
client := newGHRestClient()

t := github.NewTeam{Name: name, Description: &description, Privacy: &privacy}
if parentTeamName != "" {
Expand Down Expand Up @@ -377,7 +431,7 @@ func CreateTeam(name string, description string, privacy string, parentTeamName
}

func AddTeamRepository(slug string, repo string, permission string) {
client := newGHRestClient(viper.GetString("TARGET_TOKEN"))
client := newGHRestClient()

fmt.Println("Adding repository to team: ", slug, repo, permission)

Expand All @@ -396,7 +450,7 @@ func AddTeamRepository(slug string, repo string, permission string) {
}

func AddTeamMember(slug string, member string, role string) {
client := newGHRestClient(viper.GetString("TARGET_TOKEN"))
client := newGHRestClient()

role = strings.ToLower(role) // lowercase to match github api
fmt.Println("Adding member to team: ", slug, member, role)
Expand All @@ -409,7 +463,7 @@ func AddTeamMember(slug string, member string, role string) {
}

func GetTeamId(TeamName string) (int64, error) {
client := newGHRestClient(viper.GetString("TARGET_TOKEN"))
client := newGHRestClient()

ctx := context.WithValue(context.Background(), github.SleepUntilPrimaryRateLimitResetWhenRateLimited, true)
team, _, err := client.Teams.GetTeamBySlug(ctx, viper.Get("TARGET_ORGANIZATION").(string), TeamName)
Expand All @@ -421,7 +475,7 @@ func GetTeamId(TeamName string) (int64, error) {
}

func GetRepositoryTeams(owner string, repo string) ([]*github.Team, error) {
client := newGHRestClient(viper.GetString("SOURCE_TOKEN"))
client := newSourceGHRestClient()
ctx := context.WithValue(context.Background(), github.SleepUntilPrimaryRateLimitResetWhenRateLimited, true)

// Get teams for the repository
Expand All @@ -434,20 +488,23 @@ func GetRepositoryTeams(owner string, repo string) ([]*github.Team, error) {
}

func GetAuthenticatedUser() (*github.User, error) {
client := newGHRestClient(viper.GetString("TARGET_TOKEN"))
client := newGHRestClient()
ctx := context.Background()

// Get authenticated user
user, _, err := client.Users.Get(ctx, "")

if err != nil {
if strings.Contains(err.Error(), "403 Resource not accessible by integration") {
return nil, nil
}
return nil, err
}
return user, nil
}

func RemoveTeamMember(slug string, member string) error {
client := newGHRestClient(viper.GetString("TARGET_TOKEN"))
client := newGHRestClient()
ctx := context.WithValue(context.Background(), github.SleepUntilPrimaryRateLimitResetWhenRateLimited, true)

_, err := client.Teams.RemoveTeamMembershipBySlug(ctx, viper.Get("TARGET_ORGANIZATION").(string), slug, member)
Expand Down

0 comments on commit b4844e5

Please sign in to comment.