Skip to content

Commit

Permalink
Merge pull request #4 from mona-actions/amenocal/handle-mapping
Browse files Browse the repository at this point in the history
Adds mapping ability for user handle mapping
  • Loading branch information
amenocal authored Oct 3, 2023
2 parents b64517f + 78d1db7 commit f089991
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 17 deletions.
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,30 @@ Flags:

## Usage: Sync

Recreates teams, membership, and team repo roles from a source organization to a target organization.
Recreates teams, membership, and team repo roles from a source organization to a target organization

```bash
Usage:
migrate-teams sync [flags]

Flags:
-h, --help help for sync
-m, --mapping-file string Mapping file path to use for mapping teams members handles
-s, --source-organization string Source Organization to sync teams from
-a, --source-token string Source Organization GitHub token
-a, --source-token string Source Organization GitHub token. Scopes: read:org, read:user, user:email
-t, --target-organization string Target Organization to sync teams from
-b, --target-token string Target Organization GitHub token
-b, --target-token string Target Organization GitHub token. Scopes: admin:org
```

### Mapping File Example

A mapping file can be provided to map member handles in case they are different between source and target.

Example:

```csv
source,target
flastname,firstname.lastname
```

## License
Expand Down
4 changes: 3 additions & 1 deletion cmd/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ var exportCmd = &cobra.Command{
organization := cmd.Flag("organization").Value.String()
token := cmd.Flag("token").Value.String()
filePrefix := cmd.Flag("file-prefix").Value.String()
if filePrefix == "" {
filePrefix = organization
}

// Set ENV variables
os.Setenv("GHMT_SOURCE_ORGANIZATION", organization)
Expand Down Expand Up @@ -48,6 +51,5 @@ func init() {
exportCmd.MarkFlagRequired("token")

exportCmd.Flags().StringP("file-prefix", "f", "", "Output filenames prefix")
exportCmd.MarkFlagRequired("output")

}
9 changes: 7 additions & 2 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,21 @@ var syncCmd = &cobra.Command{
targetOrganization := cmd.Flag("target-organization").Value.String()
sourceToken := cmd.Flag("source-token").Value.String()
targetToken := cmd.Flag("target-token").Value.String()
mappingFile := cmd.Flag("mapping-file").Value.String()

// Set ENV variables
os.Setenv("GHMT_SOURCE_ORGANIZATION", sourceOrganization)
os.Setenv("GHMT_TARGET_ORGANIZATION", targetOrganization)
os.Setenv("GHMT_SOURCE_TOKEN", sourceToken)
os.Setenv("GHMT_TARGET_TOKEN", targetToken)
os.Setenv("GHMT_MAPPING_FILE", mappingFile)

// Bind ENV variables in Viper
viper.BindEnv("SOURCE_ORGANIZATION")
viper.BindEnv("TARGET_ORGANIZATION")
viper.BindEnv("SOURCE_TOKEN")
viper.BindEnv("TARGET_TOKEN")
viper.BindEnv("MAPPING_FILE")

// Call syncTeams
sync.SyncTeams()
Expand All @@ -50,10 +53,12 @@ func init() {
syncCmd.Flags().StringP("target-organization", "t", "", "Target Organization to sync teams from")
syncCmd.MarkFlagRequired("target-organization")

syncCmd.Flags().StringP("source-token", "a", "", "Source Organization GitHub token")
syncCmd.Flags().StringP("source-token", "a", "", "Source Organization GitHub token. Scopes: read:org, read:user, user:email")
syncCmd.MarkFlagRequired("source-token")

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

syncCmd.Flags().StringP("mapping-file", "m", "", "Mapping file path to use for mapping teams members handles")

}
10 changes: 10 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,13 @@ func AddTeamRepository(slug string, repo string, permission string) {
}
}
}

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

fmt.Println("Adding member to team: ", slug, member)
_, _, err := client.Teams.AddTeamMembershipBySlug(context.Background(), viper.Get("TARGET_ORGANIZATION").(string), slug, member, &github.TeamAddTeamMembershipOptions{Role: "member"})
if err != nil {
fmt.Println("Error adding member to team: ", slug, member)
}
}
28 changes: 17 additions & 11 deletions internal/team/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,20 @@ func getTeamRepositories(team string) []Repository {

repositories := make([]Repository, 0)
for _, repository := range data {
// Fixing permission values
permission := "pull"
if repository["Permission"] == "WRITE" {
permission = "push"
} else if repository["Permission"] == "ADMIN" {
permission = "admin"
if repository["Name"] != "" {
// Fixing permission values
permission := "pull"
if repository["Permission"] == "WRITE" {
permission = "push"
} else if repository["Permission"] == "ADMIN" {
permission = "admin"
}

repositories = append(repositories, Repository{
Name: repository["Name"],
Permission: permission,
})
}

repositories = append(repositories, Repository{
Name: repository["Name"],
Permission: permission,
})
}

return repositories
Expand All @@ -101,6 +103,10 @@ func (t Team) CreateTeam() {
for _, repository := range t.Repositories {
api.AddTeamRepository(t.Slug, repository.Name, repository.Permission)
}

for _, member := range t.Members {
api.AddTeamMember(t.Slug, member.Login)
}
}

func (t Teams) ExportTeamMemberships() [][]string {
Expand Down
54 changes: 54 additions & 0 deletions pkg/sync/sync.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package sync

import (
"encoding/csv"
"log"
"os"

"github.com/mona-actions/gh-migrate-teams/internal/team"
"github.com/pterm/pterm"
)
Expand All @@ -14,7 +18,57 @@ func SyncTeams() {
// Create teams in target organization
createTeamsSpinnerSuccess, _ := pterm.DefaultSpinner.Start("Creating teams in target organization...")
for _, team := range teams {
// Map members
if os.Getenv("GHMT_MAPPING_FILE") != "" {
team = mapMembers(team)
}
team.CreateTeam()
}
createTeamsSpinnerSuccess.Success()
}

func mapMembers(team team.Team) team.Team {
for i, member := range team.Members {
// Check if member handle is in mapping file
target_handle, err := getTargetHandle(os.Getenv("GHMT_MAPPING_FILE"), member.Login)
if err != nil {
log.Println("Unable to read or open mapping file")
}
team.Members[i] = updateMemberHandle(member, member.Login, target_handle)
}
return team
}

func updateMemberHandle(member team.Member, source_handle string, target_handle string) team.Member {
// Update member handles
if member.Login == source_handle {
member.Login = target_handle
}
return member
}

func getTargetHandle(filename string, source_handle string) (string, error) {
// Open mapping file
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close()

// Parse mapping file
reader := csv.NewReader(file)
reader.FieldsPerRecord = -1 // Allow variable number of fields per record
records, err := reader.ReadAll()
if err != nil {
return "", err
}

// Find target value for source value
for _, record := range records[1:] {
if record[0] == source_handle {
return record[1], nil
}
}

return source_handle, nil
}

0 comments on commit f089991

Please sign in to comment.