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
9 changes: 6 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ 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 `generate-script` commands, the GitHub API client, shared commands, and cloud storage clients (Azure Blob, AWS S3, GitHub-owned multipart upload). Archive upload orchestration and GHES version checking are also ported.
**Current state:** The `gei` CLI is fully ported to Go, including `migrate-repo`, `migrate-org`, and all alert migration commands. The GitHub API client, shared commands, and cloud storage clients are also ported.

**When making C# changes, check if the Go port needs updating:**

| C# Area | Go Equivalent | Sync Required? |
|----------|--------------|----------------|
| `src/gei/Commands/` (any command) | `cmd/gei/` | **Yes** — all gei commands are ported |
| `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 |
Expand All @@ -56,7 +57,9 @@ This is a C# based repository that produces several CLIs that are used by custom
| `src/Octoshift/Services/AwsApi.cs` | `pkg/storage/aws/client.go` | **Yes** — upload behavior must match |
| `src/Octoshift/Services/HttpDownloadService.cs` | `pkg/storage/ghowned/client.go` | **Yes** — multipart upload must match |
| `src/Octoshift/Services/ArchiveUploader.cs` | `pkg/archive/uploader.go` | **Yes** — orchestration must match |
| ADO/BBS API clients or commands | Not yet ported | No |
| `migrate-repo` commands | Not yet ported | No |
| ADO API client (`src/Octoshift/Services/AdoApi.cs`) | Not yet ported | No |
| BBS API client (`src/Octoshift/Services/BbsApi.cs`) | Not yet ported | No |
| `ado2gh` commands | Not yet ported | No |
| `bbs2gh` commands | Not yet ported | No |

**Testing:** Run `go test ./...` to verify Go changes. Run `golangci-lint run` to check for lint issues.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,9 @@ MigrationBackup/
/gei
/ado2gh
/bbs2gh
cmd/gei/gei
cmd/ado2gh/ado2gh
cmd/bbs2gh/bbs2gh

# Go coverage reports
coverage/
Expand Down
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ linters:
text: "G402"
linters:
- gosec
# Cyclomatic complexity for migrate-repo: validation and orchestration are inherently branchy
- path: cmd/gei/migrate_repo\.go
linters:
- gocyclo

output:
formats:
Expand Down
2 changes: 1 addition & 1 deletion cmd/gei/generate_script.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func runGenerateScript(ctx context.Context, opts *generateScriptOptions, log *lo
// Create GitHub client for source
sourceAPIURL := opts.ghesAPIURL
if sourceAPIURL == "" {
sourceAPIURL = "https://api.github.com"
sourceAPIURL = defaultGitHubAPIURL
}

clientOpts := []github.Option{
Expand Down
9 changes: 5 additions & 4 deletions cmd/gei/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@ func newRootCmd() *cobra.Command {

// Add commands
rootCmd.AddCommand(newGenerateScriptCmd())
rootCmd.AddCommand(newMigrateRepoCmdLive())
rootCmd.AddCommand(newMigrateOrgCmdLive())

rootCmd.AddCommand(newMigrateSecretAlertsCmdLive())
rootCmd.AddCommand(newMigrateCodeScanningCmdLive())

// Additional commands will be implemented in subsequent phases
// rootCmd.AddCommand(newMigrateRepoCmd())
// rootCmd.AddCommand(newMigrateOrgCmd())
// rootCmd.AddCommand(newWaitForMigrationCmd())
// rootCmd.AddCommand(newAbortMigrationCmd())
// rootCmd.AddCommand(newDownloadLogsCmd())
// rootCmd.AddCommand(newMigrateSecretAlertsCmd())
// rootCmd.AddCommand(newMigrateCodeScanningAlertsCmd())
// rootCmd.AddCommand(newGenerateMannequinCSVCmd())
// rootCmd.AddCommand(newReclaimMannequinCmd())
// rootCmd.AddCommand(newGrantMigratorRoleCmd())
Expand Down
178 changes: 178 additions & 0 deletions cmd/gei/migrate_code_scanning.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package main

import (
"context"
"strings"

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

// codeScanningMigrator is the consumer-defined interface for migrating code scanning alerts.
type codeScanningMigrator interface {
MigrateCodeScanningAlerts(ctx context.Context, sourceOrg, sourceRepo, targetOrg, targetRepo string, dryRun bool) error
}

// newMigrateCodeScanningCmd creates the migrate-code-scanning-alerts cobra command.
func newMigrateCodeScanningCmd(svc codeScanningMigrator, log *logger.Logger) *cobra.Command {
var (
sourceOrg string
sourceRepo string
targetOrg string
targetRepo string
targetAPIURL string
ghesAPIURL string
noSSLVerify bool
githubSrcPAT string
githubTgtPAT string
dryRun bool
)

cmd := &cobra.Command{
Use: "migrate-code-scanning-alerts",
Short: "Migrates code-scanning analyses, alert states, and dismissed-reasons",
Long: "Migrates all code-scanning analyses, alert states and possible dismissed-reasons for the default branch. This lets you migrate the history of code-scanning alerts to the target repository.",
RunE: func(cmd *cobra.Command, _ []string) error {
if err := validateCodeScanningArgs(sourceOrg, sourceRepo, targetOrg, targetRepo, log); err != nil {
return err
}
return runMigrateCodeScanning(cmd.Context(), svc, log, sourceOrg, sourceRepo, targetOrg, targetRepo, dryRun)
},
}

cmd.Flags().StringVar(&sourceOrg, "source-org", "", "Source GitHub organization (REQUIRED)")
cmd.Flags().StringVar(&sourceRepo, "source-repo", "", "Source repository name (REQUIRED)")
cmd.Flags().StringVar(&targetOrg, "target-org", "", "Target GitHub organization (REQUIRED)")
cmd.Flags().StringVar(&targetRepo, "target-repo", "", "Target repository name (defaults to source-repo)")
cmd.Flags().StringVar(&targetAPIURL, "target-api-url", "", "API URL for the target GitHub instance (defaults to https://api.github.com)")
cmd.Flags().StringVar(&ghesAPIURL, "ghes-api-url", "", "API endpoint for GHES instance")
cmd.Flags().BoolVar(&noSSLVerify, "no-ssl-verify", false, "Disable SSL verification for GHES")
cmd.Flags().StringVar(&githubSrcPAT, "github-source-pat", "", "Personal access token for the source GitHub instance")
cmd.Flags().StringVar(&githubTgtPAT, "github-target-pat", "", "Personal access token for the target GitHub instance")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Execute in dry run mode without making actual changes")

return cmd
}

func validateCodeScanningArgs(sourceOrg, sourceRepo, targetOrg, targetRepo string, log *logger.Logger) error {
if err := cmdutil.ValidateRequired(sourceOrg, "--source-org"); err != nil {
return err
}
if err := cmdutil.ValidateRequired(sourceRepo, "--source-repo"); err != nil {
return err
}
if err := cmdutil.ValidateRequired(targetOrg, "--target-org"); err != nil {
return err
}
if err := cmdutil.ValidateNoURL(sourceOrg, "--source-org"); err != nil {
return err
}
if err := cmdutil.ValidateNoURL(targetOrg, "--target-org"); err != nil {
return err
}
if err := cmdutil.ValidateNoURL(sourceRepo, "--source-repo"); err != nil {
return err
}
if err := cmdutil.ValidateNoURL(targetRepo, "--target-repo"); err != nil {
return err
}
return nil
}

func runMigrateCodeScanning(ctx context.Context, svc codeScanningMigrator, log *logger.Logger, sourceOrg, sourceRepo, targetOrg, targetRepo string, dryRun bool) error {
// Default target-repo to source-repo
if strings.TrimSpace(targetRepo) == "" {
targetRepo = sourceRepo
log.Info("Since target-repo is not provided, source-repo value will be used for target-repo.")
}

log.Info("Migrating Repo Code Scanning Alerts...")

if err := svc.MigrateCodeScanningAlerts(ctx, sourceOrg, sourceRepo, targetOrg, targetRepo, dryRun); err != nil {
return err
}

if !dryRun {
log.Success("Code scanning alerts successfully migrated.")
}
return nil
}

// newMigrateCodeScanningCmdLive creates the migrate-code-scanning-alerts command with real deps.
func newMigrateCodeScanningCmdLive() *cobra.Command {
var (
sourceOrg string
sourceRepo string
targetOrg string
targetRepo string
targetAPIURL string
ghesAPIURL string
noSSLVerify bool
githubSrcPAT string
githubTgtPAT string
dryRun bool
)

cmd := &cobra.Command{
Use: "migrate-code-scanning-alerts",
Short: "Migrates code-scanning analyses, alert states, and dismissed-reasons",
Long: "Migrates all code-scanning analyses, alert states and possible dismissed-reasons for the default branch. This lets you migrate the history of code-scanning alerts to the target repository.",
RunE: func(cmd *cobra.Command, _ []string) error {
log := getLogger(cmd)
ctx := cmd.Context()
envProv := env.New()

if err := validateCodeScanningArgs(sourceOrg, sourceRepo, targetOrg, targetRepo, log); err != nil {
return err
}

// Resolve tokens from flags or environment
sourcePAT := resolveAlertSourceToken(githubSrcPAT, githubTgtPAT, envProv)
targetPAT := resolveAlertTargetToken(githubTgtPAT, envProv)

// Build source client
sourceAPIURL := ghesAPIURL
if sourceAPIURL == "" {
sourceAPIURL = defaultGitHubAPIURL
}
sourceOpts := []github.Option{
github.WithAPIURL(sourceAPIURL),
github.WithLogger(log),
}
if noSSLVerify {
sourceOpts = append(sourceOpts, github.WithNoSSLVerify())
}
sourceGH := github.NewClient(sourcePAT, sourceOpts...)

// Build target client
tgtAPI := targetAPIURL
if tgtAPI == "" {
tgtAPI = defaultGitHubAPIURL
}
targetGH := github.NewClient(targetPAT,
github.WithAPIURL(tgtAPI),
github.WithLogger(log),
)

svc := alerts.NewCodeScanningService(sourceGH, targetGH, log)
return runMigrateCodeScanning(ctx, svc, log, sourceOrg, sourceRepo, targetOrg, targetRepo, dryRun)
},
}

cmd.Flags().StringVar(&sourceOrg, "source-org", "", "Source GitHub organization (REQUIRED)")
cmd.Flags().StringVar(&sourceRepo, "source-repo", "", "Source repository name (REQUIRED)")
cmd.Flags().StringVar(&targetOrg, "target-org", "", "Target GitHub organization (REQUIRED)")
cmd.Flags().StringVar(&targetRepo, "target-repo", "", "Target repository name (defaults to source-repo)")
cmd.Flags().StringVar(&targetAPIURL, "target-api-url", "", "API URL for the target GitHub instance (defaults to https://api.github.com)")
cmd.Flags().StringVar(&ghesAPIURL, "ghes-api-url", "", "API endpoint for GHES instance")
cmd.Flags().BoolVar(&noSSLVerify, "no-ssl-verify", false, "Disable SSL verification for GHES")
cmd.Flags().StringVar(&githubSrcPAT, "github-source-pat", "", "Personal access token for the source GitHub instance")
cmd.Flags().StringVar(&githubTgtPAT, "github-target-pat", "", "Personal access token for the target GitHub instance")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Execute in dry run mode without making actual changes")

return cmd
}
Loading
Loading