Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
24 changes: 15 additions & 9 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ This is a C# based repository that produces several CLIs that are used by custom
### Go Port Directories
- `cmd/gei/`, `cmd/ado2gh/`, `cmd/bbs2gh/`: Go CLI entry points
- `pkg/scriptgen/`: PowerShell script generation (ported from C#)
- `pkg/github/`: GitHub API client (REST + GraphQL)
- `pkg/logger/`, `pkg/env/`: Shared Go packages
- `internal/cmdutil/`: Command utility helpers
- `internal/sharedcmd/`: Shared commands (download-logs, version, wait-for-migration, etc.)

## Key Guidelines
1. Follow C# best practices and idiomatic patterns
Expand All @@ -38,14 +40,18 @@ This is a C# based repository that produces several CLIs that are used by custom

## Go Port Sync Requirements

**Current state:** The Go port has the base framework and `generate-script` commands for all three CLIs. Script generation has full behavioral parity with C#.
**Current state:** The Go port has `generate-script` commands, the GitHub API client, and shared commands (download-logs, version, wait-for-migration, grant-migrator-role, revoke-migrator-role, create-team, add-team-members, lock-ado-repo, disable-ado-repo, configure-autolink).

**When making C# changes to script generation logic:**
- If you modify `GenerateScriptCommandHandler.cs` in any of the three CLIs, you MUST make the corresponding change in Go:
- `src/gei/Commands/GenerateScript/` → `cmd/gei/generate_script.go` + `pkg/scriptgen/generator.go`
- `src/ado2gh/Commands/GenerateScript/` → `cmd/ado2gh/generate_script.go`
- `src/bbs2gh/Commands/GenerateScript/` → `cmd/bbs2gh/generate_script.go`
- Run `go test ./...` to verify the Go changes compile and tests pass
- Generated PowerShell scripts must be identical between C# and Go
**When making C# changes, check if the Go port needs updating:**

**When making other C# changes:** No Go sync required yet. The remaining commands are not yet ported.
| C# Area | Go Equivalent | Sync Required? |
|----------|--------------|----------------|
| `GenerateScriptCommandHandler.cs` (any CLI) | `cmd/{cli}/generate_script.go` + `pkg/scriptgen/generator.go` | **Yes** — scripts must be identical |
| `src/Octoshift/Services/GithubApi.cs` | `pkg/github/client.go` | **Yes** — API behavior must match |
| `src/Octoshift/Services/GithubClient.cs` | `pkg/github/client.go` | **Yes** — HTTP/auth behavior must match |
| Shared commands in `src/Octoshift/Commands/` | `internal/sharedcmd/` | **Yes** — command behavior must match |
| `src/gei/Commands/DownloadLogs/` | `cmd/gei/download_logs.go` | **Yes** |
| ADO/BBS API clients or commands | Not yet ported | No |
| `migrate-repo` commands | Not yet ported | No |

**Testing:** Run `go test ./...` to verify Go changes. Run `golangci-lint run` to check for lint issues.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ MigrationBackup/
/src/OctoshiftCLI.IntegrationTests/Properties/launchSettings.json
/src/ado2gh/Properties/launchSettings.json

# Go binaries (built from cmd/)
/gei
/ado2gh
/bbs2gh

# Go coverage reports
coverage/
*.out
Expand Down
50 changes: 44 additions & 6 deletions cmd/ado2gh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ package main

import (
"context"
"net/http"
"os"
"strings"

"github.com/github/gh-gei/pkg/env"
"github.com/github/gh-gei/pkg/logger"
"github.com/github/gh-gei/pkg/status"
versionpkg "github.com/github/gh-gei/pkg/version"
"github.com/spf13/cobra"
)

// contextKey is an unexported type for context keys in this package.
type contextKey string

const loggerKey contextKey = "logger"

var (
version = "dev"
verbose bool
Expand All @@ -25,11 +34,16 @@ func newRootCmd() *cobra.Command {
Use: "ado2gh",
Short: "Azure DevOps to GitHub migration CLI",
Long: "Automate end-to-end Azure DevOps Repos to GitHub migrations.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
log := logger.New(verbose)
ctx := context.WithValue(cmd.Context(), "logger", log)
ctx := context.WithValue(cmd.Context(), loggerKey, log)
cmd.SetContext(ctx)
log.Debug("Execution started")

checkVersion(ctx, log)
checkGitHubStatus(ctx, log)

return nil
},
SilenceUsage: true,
SilenceErrors: true,
Expand Down Expand Up @@ -64,7 +78,7 @@ func newRootCmd() *cobra.Command {
}

func getLogger(cmd *cobra.Command) *logger.Logger {
if log, ok := cmd.Context().Value("logger").(*logger.Logger); ok {
if log, ok := cmd.Context().Value(loggerKey).(*logger.Logger); ok {
return log
}
return logger.New(false)
Expand All @@ -76,17 +90,41 @@ func getEnvProvider() *env.Provider {

func checkVersion(ctx context.Context, log *logger.Logger) {
envProvider := getEnvProvider()
if envProvider.SkipVersionCheck() == "true" || envProvider.SkipVersionCheck() == "1" {
skip := envProvider.SkipVersionCheck()
if strings.EqualFold(skip, "true") || skip == "1" {
log.Info("Skipped latest version check due to GEI_SKIP_VERSION_CHECK environment variable")
return
}
log.Info("You are running ado2gh CLI version %s", version)

checker := versionpkg.NewChecker(&http.Client{}, log, version)
isLatest, err := checker.IsLatest(ctx)
if err != nil {
log.Debug("Version check failed: %v", err)
return
}

if !isLatest {
latest, _ := checker.GetLatestVersion(ctx)
log.Info("New version available: %s", latest)
log.Info("You are running ado2gh CLI version %s", version)
}
}

func checkGitHubStatus(ctx context.Context, log *logger.Logger) {
envProvider := getEnvProvider()
if envProvider.SkipStatusCheck() == "true" || envProvider.SkipStatusCheck() == "1" {
skip := envProvider.SkipStatusCheck()
if strings.EqualFold(skip, "true") || skip == "1" {
log.Info("Skipped GitHub status check due to GEI_SKIP_STATUS_CHECK environment variable")
return
}

count, err := status.GetUnresolvedIncidentsCount(ctx, &http.Client{}, "https://www.githubstatus.com")
if err != nil {
log.Debug("GitHub status check failed: %v", err)
return
}

if count > 0 {
log.Warning("GitHub is currently experiencing %d incident(s). Check https://www.githubstatus.com for details.", count)
}
}
50 changes: 44 additions & 6 deletions cmd/bbs2gh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ package main

import (
"context"
"net/http"
"os"
"strings"

"github.com/github/gh-gei/pkg/env"
"github.com/github/gh-gei/pkg/logger"
"github.com/github/gh-gei/pkg/status"
versionpkg "github.com/github/gh-gei/pkg/version"
"github.com/spf13/cobra"
)

// contextKey is an unexported type for context keys in this package.
type contextKey string

const loggerKey contextKey = "logger"

var (
version = "dev"
verbose bool
Expand All @@ -25,11 +34,16 @@ func newRootCmd() *cobra.Command {
Use: "bbs2gh",
Short: "Bitbucket Server to GitHub migration CLI",
Long: "Migrate repositories from Bitbucket Server and Data Center to GitHub Enterprise Cloud.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
log := logger.New(verbose)
ctx := context.WithValue(cmd.Context(), "logger", log)
ctx := context.WithValue(cmd.Context(), loggerKey, log)
cmd.SetContext(ctx)
log.Debug("Execution started")

checkVersion(ctx, log)
checkGitHubStatus(ctx, log)

return nil
},
SilenceUsage: true,
SilenceErrors: true,
Expand Down Expand Up @@ -57,7 +71,7 @@ func newRootCmd() *cobra.Command {
}

func getLogger(cmd *cobra.Command) *logger.Logger {
if log, ok := cmd.Context().Value("logger").(*logger.Logger); ok {
if log, ok := cmd.Context().Value(loggerKey).(*logger.Logger); ok {
return log
}
return logger.New(false)
Expand All @@ -69,17 +83,41 @@ func getEnvProvider() *env.Provider {

func checkVersion(ctx context.Context, log *logger.Logger) {
envProvider := getEnvProvider()
if envProvider.SkipVersionCheck() == "true" || envProvider.SkipVersionCheck() == "1" {
skip := envProvider.SkipVersionCheck()
if strings.EqualFold(skip, "true") || skip == "1" {
log.Info("Skipped latest version check due to GEI_SKIP_VERSION_CHECK environment variable")
return
}
log.Info("You are running bbs2gh CLI version %s", version)

checker := versionpkg.NewChecker(&http.Client{}, log, version)
isLatest, err := checker.IsLatest(ctx)
if err != nil {
log.Debug("Version check failed: %v", err)
return
}

if !isLatest {
latest, _ := checker.GetLatestVersion(ctx)
log.Info("New version available: %s", latest)
log.Info("You are running bbs2gh CLI version %s", version)
}
}

func checkGitHubStatus(ctx context.Context, log *logger.Logger) {
envProvider := getEnvProvider()
if envProvider.SkipStatusCheck() == "true" || envProvider.SkipStatusCheck() == "1" {
skip := envProvider.SkipStatusCheck()
if strings.EqualFold(skip, "true") || skip == "1" {
log.Info("Skipped GitHub status check due to GEI_SKIP_STATUS_CHECK environment variable")
return
}

count, err := status.GetUnresolvedIncidentsCount(ctx, &http.Client{}, "https://www.githubstatus.com")
if err != nil {
log.Debug("GitHub status check failed: %v", err)
return
}

if count > 0 {
log.Warning("GitHub is currently experiencing %d incident(s). Check https://www.githubstatus.com for details.", count)
}
}
63 changes: 63 additions & 0 deletions cmd/gei/abort_migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"context"
"strings"

"github.com/github/gh-gei/internal/cmdutil"
"github.com/github/gh-gei/pkg/logger"
"github.com/spf13/cobra"
)

// migrationAborter is the consumer-defined interface for aborting migrations.
type migrationAborter interface {
AbortMigration(ctx context.Context, id string) (bool, error)
}

// newAbortMigrationCmd creates the abort-migration cobra command.
func newAbortMigrationCmd(gh migrationAborter, log *logger.Logger) *cobra.Command {
var migrationID string

cmd := &cobra.Command{
Use: "abort-migration",
Short: "Aborts a repository migration that is queued or in progress",
Long: "Aborts a repository migration that is queued or in progress.",
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateAbortMigrationID(migrationID); err != nil {
return err
}
return runAbortMigration(cmd.Context(), gh, log, migrationID)
},
}

cmd.Flags().StringVar(&migrationID, "migration-id", "",
"The ID of the migration to abort, starting with RM_. Organization migrations, where the ID starts with OM_, are not supported.")
cmd.Flags().String("github-target-pat", "", "Personal access token for the target GitHub instance")
cmd.Flags().String("target-api-url", "", "API URL for the target GitHub instance")

return cmd
}

func validateAbortMigrationID(id string) error {
if strings.TrimSpace(id) == "" {
return cmdutil.NewUserError("--migration-id must be provided")
}
if !strings.HasPrefix(id, repoMigrationIDPrefix) {
return cmdutil.NewUserErrorf(
"Invalid migration ID: %s. Only repository migration IDs starting with RM_ are supported.", id)
}
return nil
}

func runAbortMigration(ctx context.Context, gh migrationAborter, log *logger.Logger, migrationID string) error {
success, err := gh.AbortMigration(ctx, migrationID)
if err != nil {
return err
}
if !success {
log.Errorf("Failed to abort migration %s", migrationID)
return nil
}
log.Info("Migration %s was canceled", migrationID)
return nil
}
Loading
Loading