Skip to content

Commit d53f923

Browse files
committed
Phase 3: GitHub API client + all shared commands (Go port)
This PR implements the core GitHub API client and all shared gei subcommands in Go, building on the project skeleton (PR 1) and HTTP/auth foundation (PR 2). ## GitHub API client (pkg/github/) - Custom GraphQL client with pagination, secondary rate limit detection and retry, and navigateJSON() dot-path traversal - 32+ migration API methods via google/go-github REST client: organization, repository, team, mannequin, migration lifecycle - Comprehensive models (models.go) for all API response types ## Shared command infrastructure (internal/cmdutil/) - UserError type for user-friendly error messages - Flag validators: ValidatePaired, ValidateAtLeastOne, ValidateExactlyOne - Migration status constants and classifier (pkg/migration/) ## Commands (cmd/gei/) - wait-for-migration: poll migration status with configurable interval - abort-migration: cancel in-progress migrations - download-logs: fetch and save migration log archives - grant-migrator-role / revoke-migrator-role: manage migration permissions - create-team: create GitHub teams with idempotent slug handling - generate-mannequin-csv: export mannequin data to CSV - reclaim-mannequin: reclaim mannequins from CSV or individual args ## Supporting packages - pkg/download/: migration log download service with temp file cleanup - pkg/mannequin/: mannequin reclamation service with CSV parsing - pkg/version/: version checker against GitHub releases - pkg/status/: GitHub status page availability check ## Testing - 170+ tests across all packages, all passing with -race - External test package convention (package foo_test) - httptest-based test servers for API client tests ## Tooling - golangci-lint v2 configured, 0 issues - Go 1.25.4 added to mise.toml alongside dotnet 8.0.410 - Go binary names added to .gitignore
1 parent b64f0e9 commit d53f923

55 files changed

Lines changed: 9869 additions & 318 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,11 @@ MigrationBackup/
360360
/src/OctoshiftCLI.IntegrationTests/Properties/launchSettings.json
361361
/src/ado2gh/Properties/launchSettings.json
362362

363+
# Go binaries (built from cmd/)
364+
/gei
365+
/ado2gh
366+
/bbs2gh
367+
363368
# Go coverage reports
364369
coverage/
365370
*.out

.golangci.yml

Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,94 @@
11
# golangci-lint configuration for gh-gei Go port
22
# See https://golangci-lint.run/usage/configuration/
3+
version: "2"
34

45
run:
56
timeout: 5m
67
tests: true
78
modules-download-mode: readonly
89

10+
formatters:
11+
enable:
12+
- gofmt
13+
- goimports
14+
- gofumpt
15+
916
linters:
1017
enable:
11-
- gofmt # Checks formatting
12-
- goimports # Checks imports
1318
- govet # Reports suspicious constructs
1419
- errcheck # Checks for unchecked errors
15-
- staticcheck # Static analysis
20+
- staticcheck # Static analysis (includes gosimple, stylecheck)
1621
- unused # Checks for unused code
17-
- gosimple # Simplify code
1822
- ineffassign # Detects ineffectual assignments
19-
- typecheck # Type-checks code
2023
- bodyclose # Checks for HTTP response body close
2124
- noctx # Finds HTTP requests without context
2225
- misspell # Finds commonly misspelled English words
2326
- unconvert # Removes unnecessary type conversions
2427
- goconst # Finds repeated strings that could be constants
2528
- gocyclo # Computes cyclomatic complexities
26-
- gofumpt # Stricter gofmt
2729
- revive # Fast, configurable, extensible linter
2830
- gosec # Security-focused linter
2931
- errname # Checks error naming
3032
- errorlint # Finds misuses of errors
31-
- exportloopref # Checks for pointers to enclosing loop variables
3233
- whitespace # Detects leading/trailing whitespace
3334

34-
linters-settings:
35-
gocyclo:
36-
min-complexity: 15
37-
goconst:
38-
min-len: 3
39-
min-occurrences: 3
40-
misspell:
41-
locale: US
42-
revive:
43-
rules:
44-
- name: exported
45-
severity: warning
46-
disabled: false
47-
- name: package-comments
48-
severity: warning
49-
disabled: false
50-
gosec:
51-
excludes:
52-
- G104 # Audit errors not checked (too noisy)
35+
settings:
36+
gocyclo:
37+
min-complexity: 15
38+
goconst:
39+
min-len: 3
40+
min-occurrences: 3
41+
misspell:
42+
locale: US
43+
revive:
44+
rules:
45+
- name: exported
46+
severity: warning
47+
disabled: false
48+
- name: package-comments
49+
severity: warning
50+
disabled: false
51+
gosec:
52+
excludes:
53+
- G104 # Audit errors not checked (too noisy)
5354

54-
issues:
55-
exclude-use-default: false
56-
max-issues-per-linter: 0
57-
max-same-issues: 0
58-
59-
exclude-rules:
60-
# Exclude some linters from running on tests files
61-
- path: _test\.go
62-
linters:
63-
- gocyclo
64-
- errcheck
65-
- gosec
66-
67-
# Exclude error checks in main.go files (handled by cobra)
68-
- path: cmd/.*/main\.go
69-
text: "Error return value"
70-
linters:
71-
- errcheck
55+
exclusions:
56+
presets:
57+
- comments
58+
- common-false-positives
59+
- legacy
60+
- std-error-handling
61+
rules:
62+
# Exclude some linters from running on tests files
63+
- path: _test\.go
64+
linters:
65+
- gocyclo
66+
- errcheck
67+
- gosec
68+
- revive
69+
# Exclude error checks in main.go files (handled by cobra)
70+
- path: cmd/.*/main\.go
71+
text: "Error return value"
72+
linters:
73+
- errcheck
74+
# Unused functions in skeleton main.go files will be wired up in future PRs
75+
- path: cmd/.*/main\.go
76+
linters:
77+
- unused
78+
# G101 false positives on template constant names containing "Password", "Secret", etc.
79+
- path: pkg/scriptgen/templates\.go
80+
text: "G101"
81+
linters:
82+
- gosec
83+
# Cyclomatic complexity for generate-script command will be addressed when refactoring
84+
- path: cmd/gei/generate_script\.go
85+
linters:
86+
- gocyclo
7287

7388
output:
74-
format: colored-line-number
75-
print-issued-lines: true
76-
print-linter-name: true
89+
formats:
90+
text:
91+
path: stdout
92+
colors: true
93+
print-issued-lines: true
94+
print-linter-name: true

cmd/ado2gh/main.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ package main
22

33
import (
44
"context"
5+
"net/http"
56
"os"
7+
"strings"
68

79
"github.com/github/gh-gei/pkg/env"
810
"github.com/github/gh-gei/pkg/logger"
11+
"github.com/github/gh-gei/pkg/status"
12+
versionpkg "github.com/github/gh-gei/pkg/version"
913
"github.com/spf13/cobra"
1014
)
1115

16+
// contextKey is an unexported type for context keys in this package.
17+
type contextKey string
18+
19+
const loggerKey contextKey = "logger"
20+
1221
var (
1322
version = "dev"
1423
verbose bool
@@ -25,11 +34,16 @@ func newRootCmd() *cobra.Command {
2534
Use: "ado2gh",
2635
Short: "Azure DevOps to GitHub migration CLI",
2736
Long: "Automate end-to-end Azure DevOps Repos to GitHub migrations.",
28-
PersistentPreRun: func(cmd *cobra.Command, args []string) {
37+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
2938
log := logger.New(verbose)
30-
ctx := context.WithValue(cmd.Context(), "logger", log)
39+
ctx := context.WithValue(cmd.Context(), loggerKey, log)
3140
cmd.SetContext(ctx)
3241
log.Debug("Execution started")
42+
43+
checkVersion(ctx, log)
44+
checkGitHubStatus(ctx, log)
45+
46+
return nil
3347
},
3448
SilenceUsage: true,
3549
SilenceErrors: true,
@@ -64,7 +78,7 @@ func newRootCmd() *cobra.Command {
6478
}
6579

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

7791
func checkVersion(ctx context.Context, log *logger.Logger) {
7892
envProvider := getEnvProvider()
79-
if envProvider.SkipVersionCheck() == "true" || envProvider.SkipVersionCheck() == "1" {
93+
skip := envProvider.SkipVersionCheck()
94+
if strings.EqualFold(skip, "true") || skip == "1" {
8095
log.Info("Skipped latest version check due to GEI_SKIP_VERSION_CHECK environment variable")
8196
return
8297
}
83-
log.Info("You are running ado2gh CLI version %s", version)
98+
99+
checker := versionpkg.NewChecker(&http.Client{}, log, version)
100+
isLatest, err := checker.IsLatest(ctx)
101+
if err != nil {
102+
log.Debug("Version check failed: %v", err)
103+
return
104+
}
105+
106+
if !isLatest {
107+
latest, _ := checker.GetLatestVersion(ctx)
108+
log.Info("New version available: %s", latest)
109+
log.Info("You are running ado2gh CLI version %s", version)
110+
}
84111
}
85112

86113
func checkGitHubStatus(ctx context.Context, log *logger.Logger) {
87114
envProvider := getEnvProvider()
88-
if envProvider.SkipStatusCheck() == "true" || envProvider.SkipStatusCheck() == "1" {
115+
skip := envProvider.SkipStatusCheck()
116+
if strings.EqualFold(skip, "true") || skip == "1" {
89117
log.Info("Skipped GitHub status check due to GEI_SKIP_STATUS_CHECK environment variable")
90118
return
91119
}
120+
121+
count, err := status.GetUnresolvedIncidentsCount(ctx, &http.Client{}, "https://www.githubstatus.com")
122+
if err != nil {
123+
log.Debug("GitHub status check failed: %v", err)
124+
return
125+
}
126+
127+
if count > 0 {
128+
log.Warning("GitHub is currently experiencing %d incident(s). Check https://www.githubstatus.com for details.", count)
129+
}
92130
}

cmd/bbs2gh/main.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ package main
22

33
import (
44
"context"
5+
"net/http"
56
"os"
7+
"strings"
68

79
"github.com/github/gh-gei/pkg/env"
810
"github.com/github/gh-gei/pkg/logger"
11+
"github.com/github/gh-gei/pkg/status"
12+
versionpkg "github.com/github/gh-gei/pkg/version"
913
"github.com/spf13/cobra"
1014
)
1115

16+
// contextKey is an unexported type for context keys in this package.
17+
type contextKey string
18+
19+
const loggerKey contextKey = "logger"
20+
1221
var (
1322
version = "dev"
1423
verbose bool
@@ -25,11 +34,16 @@ func newRootCmd() *cobra.Command {
2534
Use: "bbs2gh",
2635
Short: "Bitbucket Server to GitHub migration CLI",
2736
Long: "Migrate repositories from Bitbucket Server and Data Center to GitHub Enterprise Cloud.",
28-
PersistentPreRun: func(cmd *cobra.Command, args []string) {
37+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
2938
log := logger.New(verbose)
30-
ctx := context.WithValue(cmd.Context(), "logger", log)
39+
ctx := context.WithValue(cmd.Context(), loggerKey, log)
3140
cmd.SetContext(ctx)
3241
log.Debug("Execution started")
42+
43+
checkVersion(ctx, log)
44+
checkGitHubStatus(ctx, log)
45+
46+
return nil
3347
},
3448
SilenceUsage: true,
3549
SilenceErrors: true,
@@ -57,7 +71,7 @@ func newRootCmd() *cobra.Command {
5771
}
5872

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

7084
func checkVersion(ctx context.Context, log *logger.Logger) {
7185
envProvider := getEnvProvider()
72-
if envProvider.SkipVersionCheck() == "true" || envProvider.SkipVersionCheck() == "1" {
86+
skip := envProvider.SkipVersionCheck()
87+
if strings.EqualFold(skip, "true") || skip == "1" {
7388
log.Info("Skipped latest version check due to GEI_SKIP_VERSION_CHECK environment variable")
7489
return
7590
}
76-
log.Info("You are running bbs2gh CLI version %s", version)
91+
92+
checker := versionpkg.NewChecker(&http.Client{}, log, version)
93+
isLatest, err := checker.IsLatest(ctx)
94+
if err != nil {
95+
log.Debug("Version check failed: %v", err)
96+
return
97+
}
98+
99+
if !isLatest {
100+
latest, _ := checker.GetLatestVersion(ctx)
101+
log.Info("New version available: %s", latest)
102+
log.Info("You are running bbs2gh CLI version %s", version)
103+
}
77104
}
78105

79106
func checkGitHubStatus(ctx context.Context, log *logger.Logger) {
80107
envProvider := getEnvProvider()
81-
if envProvider.SkipStatusCheck() == "true" || envProvider.SkipStatusCheck() == "1" {
108+
skip := envProvider.SkipStatusCheck()
109+
if strings.EqualFold(skip, "true") || skip == "1" {
82110
log.Info("Skipped GitHub status check due to GEI_SKIP_STATUS_CHECK environment variable")
83111
return
84112
}
113+
114+
count, err := status.GetUnresolvedIncidentsCount(ctx, &http.Client{}, "https://www.githubstatus.com")
115+
if err != nil {
116+
log.Debug("GitHub status check failed: %v", err)
117+
return
118+
}
119+
120+
if count > 0 {
121+
log.Warning("GitHub is currently experiencing %d incident(s). Check https://www.githubstatus.com for details.", count)
122+
}
85123
}

0 commit comments

Comments
 (0)