Skip to content

Commit

Permalink
revamps sync command; not takes path to repo list
Browse files Browse the repository at this point in the history
  • Loading branch information
amenocal committed Jul 23, 2024
1 parent 79367a2 commit c3b63f4
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 43 deletions.
41 changes: 31 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Creates a JSON file of the releases tied to a repository
```bash
gh migrate-releases export --hostname github.example.com -o <org-name> --repository <repo-name> --token <token>
```
```

```bash
Usage:
migrate-releases export [flags]

Expand All @@ -42,19 +43,38 @@ Recreates releases,from a source repository to a target repository
gh migrate-releases sync --source-hostname github.example.com --source-organization <source-org> --source-token <source-token> --repository <repo-name> --target-organization <target-org> --target-token <target-token> --mapping-file "path/to/user-mappings.csv"
```

```bash
```txt
Usage:
migrate-releases sync [flags]
Flags:
-h, --help help for sync
-m, --mapping-file string Mapping file path to use for mapping members handles
-r, --repository string repository to export/import releases from/to
-u, --source-hostname string GitHub Enterprise source hostname url (optional) Ex. github.example.com
-s, --source-organization string Source Organization to sync releases from
-a, --source-token string Source Organization GitHub token. Scopes: read:org, read:user, user:email
-t, --target-organization string Target Organization to sync releases from
-b, --target-token string Target Organization GitHub token. Scopes: admin:org
-h, --help help for sync
-m, --mapping-file string Mapping file path to use for mapping members handles
-r, --repository string repository to export/import releases from/to; can't be used with --repository-list
-l, --repository-list-file string file path that contains list of repositories to export/import releases from/to; can't be used with --repository
-u, --source-hostname string GitHub Enterprise source hostname url (optional) Ex. github.example.com
-s, --source-organization string Source Organization to sync releases from
-a, --source-token string Source Organization GitHub token. Scopes: read:org, read:user, user:email
-t, --target-organization string Target Organization to sync releases from
-b, --target-token string Target Organization GitHub token. Scopes: admin:org
```

### Repository List Example

A list of repositories can be provided to sync releases from multiple repositories to many repositories in a single target.

Example:

```txt
https://github.example.com/owner/repo-name
https://github.example.com/owner/repo-name2
```

or

```txt
owner/repo-name
owner/repo-name2
```

### Mapping File Example
Expand All @@ -68,6 +88,7 @@ source,target
flastname,firstname.lastname
```


## License

- [MIT](./license) (c) [Mona-Actions](https://github.com/mona-actions)
Expand Down
8 changes: 6 additions & 2 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var syncCmd = &cobra.Command{
ghHostname := cmd.Flag("source-hostname").Value.String()
repository := cmd.Flag("repository").Value.String()
mappingFile := cmd.Flag("mapping-file").Value.String()
repositoryList := cmd.Flag("repository-list").Value.String()

// Set ENV variables
os.Setenv("GHMT_SOURCE_ORGANIZATION", sourceOrganization)
Expand All @@ -34,6 +35,7 @@ var syncCmd = &cobra.Command{
os.Setenv("GHMT_SOURCE_HOSTNAME", ghHostname)
os.Setenv("GHMT_REPOSITORY", repository)
os.Setenv("GHMT_MAPPING_FILE", mappingFile)
os.Setenv("GHMT_REPOSITORY_LIST", repositoryList)

// Bind ENV variables in Viper
viper.BindEnv("SOURCE_ORGANIZATION")
Expand All @@ -43,6 +45,7 @@ var syncCmd = &cobra.Command{
viper.BindEnv("SOURCE_HOSTNAME")
viper.BindEnv("REPOSITORY")
viper.BindEnv("MAPPING_FILE")
viper.BindEnv("REPOSITORY_LIST")

// Call syncreleases
sync.SyncReleases()
Expand All @@ -65,8 +68,9 @@ func init() {
syncCmd.Flags().StringP("target-token", "b", "", "Target Organization GitHub token. Scopes: admin:org")
syncCmd.MarkFlagRequired("target-token")

syncCmd.Flags().StringP("repository", "r", "", "repository to export/import releases from/to")
syncCmd.MarkFlagRequired("repository")
syncCmd.Flags().StringP("repository", "r", "", "repository to export/import releases from/to; can't be used with --repository-list")

syncCmd.Flags().StringP("repository-list-file", "l", "", "file path that contains list of repositories to export/import releases from/to; can't be used with --repository")

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

Expand Down
8 changes: 4 additions & 4 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func newGHRestClient(token string, hostname string) *github.Client {
return client
}

func GetSourceRepositoryReleases() ([]*github.RepositoryRelease, error) {
func GetSourceRepositoryReleases(owner string, repository string) ([]*github.RepositoryRelease, error) {
client := newGHRestClient(viper.GetString("source_token"), viper.GetString("source_hostname"))

ctx := context.WithValue(context.Background(), github.SleepUntilPrimaryRateLimitResetWhenRateLimited, true)
Expand All @@ -61,7 +61,7 @@ func GetSourceRepositoryReleases() ([]*github.RepositoryRelease, error) {
opts := &github.ListOptions{PerPage: 100}

for {
releases, resp, err := client.Repositories.ListReleases(ctx, viper.Get("SOURCE_ORGANIZATION").(string), viper.Get("REPOSITORY").(string), opts)
releases, resp, err := client.Repositories.ListReleases(ctx, owner, repository, opts)
if err != nil {
return allReleases, fmt.Errorf("error getting releases: %v", err)
}
Expand Down Expand Up @@ -183,11 +183,11 @@ func DownloadFileFromURL(url, fileName, token string) error {
return err
}

func CreateRelease(release *github.RepositoryRelease) (*github.RepositoryRelease, error) {
func CreateRelease(repository string, release *github.RepositoryRelease) (*github.RepositoryRelease, error) {
client := newGHRestClient(viper.GetString("TARGET_TOKEN"), "")

ctx := context.WithValue(context.Background(), github.SleepUntilPrimaryRateLimitResetWhenRateLimited, true)
newRelease, _, err := client.Repositories.CreateRelease(ctx, viper.Get("TARGET_ORGANIZATION").(string), viper.Get("REPOSITORY").(string), release)
newRelease, _, err := client.Repositories.CreateRelease(ctx, viper.Get("TARGET_ORGANIZATION").(string), repository, release)
if err != nil {
if strings.Contains(err.Error(), "already_exists") {
return nil, fmt.Errorf("release already exists: %v", release.GetName())
Expand Down
30 changes: 30 additions & 0 deletions internal/files/files.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package files

import (
"bufio"
"encoding/json"
"net/url"
"os"
"strings"
)

func OpenFile(fileName string) (*os.File, error) {
Expand Down Expand Up @@ -41,3 +44,30 @@ func CreateJSON(data interface{}, filename string) error {

return nil
}

// read repository list from file assuming each line is a repository
func ReadRepositoryListFromFile(fileName string) ([]string, error) {
file, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer file.Close()

var repositories []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
repo := scanner.Text()
parsedURL, err := url.Parse(repo)
if err != nil {
return nil, err
}
path := strings.TrimPrefix(parsedURL.Path, "/")
repositories = append(repositories, path)
}

if err := scanner.Err(); err != nil {
return nil, err
}

return repositories, nil
}
7 changes: 5 additions & 2 deletions pkg/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"github.com/mona-actions/gh-migrate-releases/internal/api"
"github.com/mona-actions/gh-migrate-releases/internal/files"
"github.com/pterm/pterm"
"github.com/spf13/viper"
)

func CreateJSONs() {
// Get all teams from source organization
fetchReleasesSpinner, _ := pterm.DefaultSpinner.Start("Fetching teams from organization...")
releases, err := api.GetSourceRepositoryReleases()
fetchReleasesSpinner, _ := pterm.DefaultSpinner.Start("Fetching releases from repository...")
repository := viper.GetString("REPOSITORY")
owner := viper.GetString("SOURCE_ORGANIZATION")
releases, err := api.GetSourceRepositoryReleases(owner, repository)
if err != nil {
pterm.Fatal.Printf("Error getting releases: %v", err)
}
Expand Down
125 changes: 100 additions & 25 deletions pkg/sync/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,101 @@ import (
"strings"

"github.com/mona-actions/gh-migrate-releases/internal/api"
"github.com/mona-actions/gh-migrate-releases/internal/files"
"github.com/mona-actions/gh-migrate-releases/internal/mapping"
"github.com/pterm/pterm"
"github.com/spf13/viper"
)

func SyncReleases() {
// Get all releases from source repository
fetchReleasesSpinner, _ := pterm.DefaultSpinner.Start("Fetching releases from repository: ", viper.GetString("REPOSITORY"))
releases, err := api.GetSourceRepositoryReleases()
checkVars()

var totalReleases, totalFailed int

if viper.GetString("REPOSITORY_LIST") != "" {
// Read repository list from file
repositories, err := files.ReadRepositoryListFromFile(viper.GetString("REPOSITORY_LIST"))
if err != nil {
pterm.Error.Printf("Error reading repository list: %v", err)
os.Exit(1)
}

// Loop through each repository in the list
for _, repository := range repositories {

releasesCount, failedReleases, err := migrateRepositoryReleases(repository)
if err != nil {
pterm.Error.Printf("Error migrating repository releases: %v", err)
}

totalReleases += releasesCount
totalFailed += failedReleases

}
} else if viper.GetString("REPOSITORY") != "" {
// Migrate releases from a single repository
repository := viper.GetString("REPOSITORY")

releasesCount, failedReleases, err := migrateRepositoryReleases(repository)
if err != nil {
pterm.Error.Printf("Error migrating repository releases: %v", err)
}

totalReleases += releasesCount
totalFailed += failedReleases

} else {
pterm.Error.Println("Error: No repository or repository list specified")
os.Exit(1)
}

if os.Getenv("CI") == "true" && os.Getenv("GITHUB_ACTIONS") == "true" {
// Print in a README Table format the number of releases created
message := fmt.Sprintf(
"| No. of Releases | Succeeded | Failed |\n"+
"| --------------- | --------- | ------ |\n"+
"| %d | %d | %d |\n",
totalReleases, totalReleases-totalFailed, totalFailed,
)
organization, repository, issueNumber, err := api.GetDatafromGitHubContext()
if err != nil {
pterm.Error.Printf("Error getting issue number: %v", err)
}
err = api.WriteToIssue(organization, repository, issueNumber, message)
if err != nil {
pterm.Error.Printf("Error writing releases table to issue: %v", err)
}
} else {
pterm.Info.Printf("Total Releases: %d\n", totalReleases)
pterm.Info.Printf("Succeeded: %d\n", totalReleases-totalFailed)
pterm.Info.Printf("Failed: %d\n", totalFailed)

}

}

func checkVars() {
//check that repository and repository list are not sent at the same time
if viper.GetString("REPOSITORY") != "" && viper.GetString("REPOSITORY_LIST") != "" {
pterm.Error.Println("Error: Cannot specify both a repository and a repository list")
os.Exit(1)
}
}

func migrateRepositoryReleases(repository string) (int, int, error) {
var owner string
// if repository includes owner, split it
if strings.Contains(repository, "/") {
repositoryParts := strings.Split(repository, "/")
owner = repositoryParts[0]
repository = repositoryParts[1]
} else {
owner = viper.GetString("SOURCE_ORGANIZATION")
}

fetchReleasesSpinner, _ := pterm.DefaultSpinner.Start("Fetching releases from repository: ", repository)
releases, err := api.GetSourceRepositoryReleases(owner, repository)
if err != nil {
pterm.Error.Printf("Error getting releases: %v", err)
fetchReleasesSpinner.Fail()
Expand All @@ -23,8 +109,9 @@ func SyncReleases() {
fetchReleasesSpinner.Success()

// Create releases in target repository
createReleasesSpinner, _ := pterm.DefaultSpinner.Start("Creating releases in target repository...", viper.GetString("REPOSITORY"))
createReleasesSpinner, _ := pterm.DefaultSpinner.Start("Creating releases in target repository...", repository)
var failed int
releasesCount := len(releases)
//loop through each release and create it in the target repository
for _, release := range releases {
createReleasesSpinner.UpdateText("Creating release: " + release.GetName())
Expand All @@ -39,7 +126,8 @@ func SyncReleases() {
pterm.Warning.Printf("Error modifying release body: %v", err)
}
// Create release api call
newRelease, err := api.CreateRelease(release)
newRepository := repository
newRelease, err := api.CreateRelease(newRepository, release)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
pterm.Info.Printf("Release already exists: %v... skipping", release.GetName())
Expand All @@ -66,29 +154,16 @@ func SyncReleases() {
createReleasesSpinner.Fail()
}
}

}

if os.Getenv("CI") == "true" && os.Getenv("GITHUB_ACTIONS") == "true" {
// Print in a README Table format the number of releases created
message := fmt.Sprintf(
"```\n"+
"| No. of Releases | Succeeded | Failed |\n"+
"| --------------- | --------- | ------ |\n"+
"| %d | %d | %d |\n"+
"```",
len(releases), len(releases)-failed, failed,
)
organization, repository, issueNumber, err := api.GetDatafromGitHubContext()
if err != nil {
pterm.Error.Printf("Error getting issue number: %v", err)
}
err = api.WriteToIssue(organization, repository, issueNumber, message)
if err != nil {
pterm.Error.Printf("Error writing releases table to issue: %v", err)
}
if failed > 0 {
createReleasesSpinner.UpdateText("Some Releases failed to create")
createReleasesSpinner.Fail()
return releasesCount, failed, fmt.Errorf("some releases failed to create")
} else {
createReleasesSpinner.UpdateText("All Releases created successfully!")
createReleasesSpinner.Success()
return releasesCount, failed, nil
}

createReleasesSpinner.UpdateText("All Releases created successfully!")
createReleasesSpinner.Success()
}

0 comments on commit c3b63f4

Please sign in to comment.