Skip to content
Open

V2 #264

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
4 changes: 2 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ jobs:

steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ^1.18
go-version: '1.24'

- name: Check out code into the Go module directory
uses: actions/checkout@v2
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ jobs:
submodules: 'true'

- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: '1.24'

- name: Setup environment variables
run: |-
Expand Down
75 changes: 75 additions & 0 deletions ARCHITECTURE_REFACTORING_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Architecture Refactoring Plan: DeepSource CLI

## Progress Tracker

### Phase 1: Foundation & Output System (Week 1-2)
- [x] 1. Create `internal/interfaces/` package with all interface definitions
- [x] 2. Create `internal/container/` package with dependency container
- [x] 3. Create `internal/adapters/` with production implementations
- [x] 4. Create `internal/errors/` with structured error types
- [x] 5. Implement dual output system (user-facing + diagnostic logging)
- [x] 6. Add context.Context propagation support
- [x] 7. Add comprehensive documentation

### Phase 2a: SDK Foundation (Week 2-3)
- [x] 1. Create centralized GraphQL client interface in `deepsource/graphql.go`
- [x] 2. Build GraphQL client wrapper with auth/header management
- [x] 3. Create structured error types for GraphQL operations
- [x] 4. Create mock implementations for testing
- [x] 5. Refactor auth mutations (register, request PAT, refresh)

### Phase 2b: SDK Queries (Week 3-4)
- [x] 1. Refactor analyzers queries
- [x] 2. Refactor transformers queries
- [x] 3. Refactor repository queries
- [x] 4. Refactor issues queries
- [x] 5. Remove duplicate `IGQLClient` interfaces
- [x] 6. Update Client facade to use new interface

### Phase 3: Config & Auth Services (Week 4-5)
- [x] 1. Refactor `config/` package to use interfaces (remove global state)
- [x] 2. Create `internal/services/auth/` service
- [x] 3. Refactor auth subcommands (login, logout, refresh, status) to use service
- [x] 4. Add configuration file support (.deepsource-cli.yaml)
- [x] 5. Implement secrets management (keychain integration)
- [x] 6. Add unit tests for config and auth services

### Phase 4: Report Command Service (Week 5-6)
- [x] 1. Create `internal/services/report/` package
- [x] 2. Extract business logic from `command/report/report.go`
- [x] 3. Refactor `command/report/report.go` to thin wrapper
- [x] 4. Write unit tests for service
- [x] 5. Add progress indicators for upload/compression
- [x] 6. Enhance error messages with actionable guidance
- [x] 7. Add `--output` flag support (json/yaml/table)

### Phase 5a: Issues & Repo Commands (Week 6-7)
- [x] 1. Create `internal/services/issues/` service
- [x] 2. Create `internal/services/repo/` service
- [x] 3. Refactor issues subcommands (list, list-file)
- [x] 4. Refactor repo subcommands (view, status)
- [x] 5. Add output format support to all commands
- [x] 6. Add unit tests for services

### Phase 5b: Config & Version Commands (Week 7-8)
- [x] 1. Create `internal/services/config/` service (generate, validate)
- [x] 2. Refactor config subcommands
- [x] 3. Refactor version command
- [x] 4. Update integration tests to use container
- [x] 5. Remove caching layer from scope

## Verification Tracker

### Run Results
- [ ] `go test ./...` (fails in sandbox: `utils` TestFetchOIDCTokenFromProvider cannot bind httptest listener)

### CLI Flows
- [ ] Auth: login, logout, refresh, status
- [ ] Config: generate, validate
- [ ] Report: key/value, value-file, analyzer type, OIDC
- [ ] Issues: list (table/json/csv/sarif)
- [ ] Repo: status, view (table/json/yaml)
- [ ] Version command

### OIDC Coverage
- [ ] `utils/fetch_oidc_token.go` flows (supported providers)
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PACKAGE_NAME := github.com/deepsourcelabs/cli
GOLANG_CROSS_VERSION ?= v1.21.6
GOLANG_CROSS_VERSION ?= v1.24.0

SYSROOT_DIR ?= sysroots
SYSROOT_ARCHIVE ?= sysroots.tar.bz2
Expand All @@ -15,7 +15,7 @@ test:
echo "\n====TESTING DEEPSOURCE PACKAGE====\n"
CGO_ENABLED=1 go test -v ./deepsource/tests/...
echo "\n====TESTING CONFIG VALIDATOR PACKAGE====\n"
go test -v ./configvalidator/... -count=1
go test -v ./internal/configvalidator/... -count=1
echo "\n====CALCULATING TEST COVERAGE FOR ENTIRE PACKAGE====\n"
go test -v -coverprofile=coverage.out -count=1 ./...

Expand Down
2 changes: 2 additions & 0 deletions command/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/deepsourcelabs/cli/command/auth/logout"
"github.com/deepsourcelabs/cli/command/auth/refresh"
"github.com/deepsourcelabs/cli/command/auth/status"
"github.com/deepsourcelabs/cli/command/auth/whoami"
)

// Options holds the metadata.
Expand All @@ -22,5 +23,6 @@ func NewCmdAuth() *cobra.Command {
cmd.AddCommand(logout.NewCmdLogout())
cmd.AddCommand(refresh.NewCmdRefresh())
cmd.AddCommand(status.NewCmdStatus())
cmd.AddCommand(whoami.NewCmdWhoAmI())
return cmd
}
22 changes: 13 additions & 9 deletions command/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (

"github.com/MakeNowJust/heredoc"
"github.com/deepsourcelabs/cli/config"
"github.com/deepsourcelabs/cli/utils"
"github.com/deepsourcelabs/cli/internal/cli/args"
"github.com/deepsourcelabs/cli/internal/cli/prompt"
"github.com/deepsourcelabs/cli/internal/cli/style"
authsvc "github.com/deepsourcelabs/cli/internal/services/auth"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -34,7 +37,7 @@ func NewCmdLogin() *cobra.Command {

Use %[3]s to authenticate with a specific DeepSource instance, for example:
%[4]s
`, utils.Yellow("--with-token"), utils.Cyan("deepsource auth login --with-token dsp_abcd"), utils.Yellow("--hostname"), utils.Cyan("deepsource auth login --hostname my_instance"))
`, style.Yellow("--with-token"), style.Cyan("deepsource auth login --with-token dsp_abcd"), style.Yellow("--hostname"), style.Cyan("deepsource auth login --hostname my_instance"))

opts := LoginOptions{
AuthTimedOut: false,
Expand All @@ -47,7 +50,7 @@ func NewCmdLogin() *cobra.Command {
Use: "login",
Short: "Log in to DeepSource using Command Line Interface",
Long: doc,
Args: utils.NoArgs,
Args: args.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return opts.Run()
},
Expand All @@ -63,8 +66,9 @@ func NewCmdLogin() *cobra.Command {

// Run executes the auth command and starts the login flow if not already authenticated
func (opts *LoginOptions) Run() (err error) {
svc := authsvc.NewService(config.DefaultManager())
// Fetch config
cfg, _ := config.GetConfig()
cfg, _ := svc.LoadConfig()
opts.User = cfg.User
opts.TokenExpired = cfg.IsExpired()

Expand Down Expand Up @@ -92,7 +96,7 @@ func (opts *LoginOptions) Run() (err error) {
if !opts.TokenExpired {
// The user is already logged in, confirm re-authentication.
msg := fmt.Sprintf("You're already logged into DeepSource as %s. Do you want to re-authenticate?", opts.User)
response, err := utils.ConfirmFromUser(msg, "")
response, err := prompt.ConfirmFromUser(msg, "")
if err != nil {
return fmt.Errorf("Error in fetching response. Please try again.")
}
Expand All @@ -105,12 +109,12 @@ func (opts *LoginOptions) Run() (err error) {

// If PAT is passed, start the login flow through PAT
if opts.PAT != "" {
return opts.startPATLoginFlow(cfg, opts.PAT)
return opts.startPATLoginFlow(svc, cfg, opts.PAT)
}

// Condition 2
// `startLoginFlow` implements the authentication flow for the CLI
return opts.startLoginFlow(cfg)
return opts.startLoginFlow(svc, cfg)
}

func (opts *LoginOptions) handleInteractiveLogin() error {
Expand All @@ -121,13 +125,13 @@ func (opts *LoginOptions) handleInteractiveLogin() error {
hostPromptHelpText := "The hostname of the DeepSource instance to authenticate with"

// Display prompt to user
loginType, err := utils.SelectFromOptions(loginPromptMessage, loginPromptHelpText, accountTypes)
loginType, err := prompt.SelectFromOptions(loginPromptMessage, loginPromptHelpText, accountTypes)
if err != nil {
return err
}
// Prompt the user for hostname only in the case of on-premise
if loginType == "DeepSource Enterprise" {
opts.HostName, err = utils.GetSingleLineInput(hostPromptMessage, hostPromptHelpText)
opts.HostName, err = prompt.GetSingleLineInput(hostPromptMessage, hostPromptHelpText)
if err != nil {
return err
}
Expand Down
41 changes: 9 additions & 32 deletions command/auth/login/login_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import (

"github.com/cli/browser"
"github.com/deepsourcelabs/cli/config"
"github.com/deepsourcelabs/cli/deepsource"
"github.com/deepsourcelabs/cli/deepsource/auth"
authsvc "github.com/deepsourcelabs/cli/internal/services/auth"
"github.com/fatih/color"
)

// Starts the login flow for the CLI
func (opts *LoginOptions) startLoginFlow(cfg *config.CLIConfig) error {
func (opts *LoginOptions) startLoginFlow(svc *authsvc.Service, cfg *config.CLIConfig) error {
// Register the device and get a device code through the response
ctx := context.Background()
deviceRegistrationResponse, err := registerDevice(ctx)
deviceRegistrationResponse, err := registerDevice(ctx, svc, cfg)
if err != nil {
return err
}
Expand All @@ -37,7 +37,7 @@ func (opts *LoginOptions) startLoginFlow(cfg *config.CLIConfig) error {

// Fetch the PAT using the device registration resonse
var tokenData *auth.PAT
tokenData, opts.AuthTimedOut, err = fetchPAT(ctx, deviceRegistrationResponse)
tokenData, opts.AuthTimedOut, err = fetchPAT(ctx, deviceRegistrationResponse, svc, cfg)
if err != nil {
return err
}
Expand All @@ -54,32 +54,18 @@ func (opts *LoginOptions) startLoginFlow(cfg *config.CLIConfig) error {
cfg.SetTokenExpiry(tokenData.Expiry)

// Having stored the data in the global Cfg object, write it into the config file present in the local filesystem
err = cfg.WriteFile()
err = svc.SaveConfig(cfg)
if err != nil {
return fmt.Errorf("Error in writing authentication data to a file. Exiting...")
}
return nil
}

func registerDevice(ctx context.Context) (*auth.Device, error) {
// Fetching DeepSource client in order to interact with SDK
deepsource, err := deepsource.New(deepsource.ClientOpts{
Token: config.Cfg.Token,
HostName: config.Cfg.Host,
})
if err != nil {
return nil, err
}

// Send a mutation to register device and get the device code
res, err := deepsource.RegisterDevice(ctx)
if err != nil {
return nil, err
}
return res, nil
func registerDevice(ctx context.Context, svc *authsvc.Service, cfg *config.CLIConfig) (*auth.Device, error) {
return svc.RegisterDevice(ctx, cfg)
}

func fetchPAT(ctx context.Context, deviceRegistrationData *auth.Device) (*auth.PAT, bool, error) {
func fetchPAT(ctx context.Context, deviceRegistrationData *auth.Device, svc *authsvc.Service, cfg *config.CLIConfig) (*auth.PAT, bool, error) {
var tokenData *auth.PAT
var err error
defaultUserName := "user"
Expand All @@ -103,23 +89,14 @@ func fetchPAT(ctx context.Context, deviceRegistrationData *auth.Device) (*auth.P
}
userDescription := fmt.Sprintf("CLI PAT for %s@%s", userName, hostName)

// Fetching DeepSource client in order to interact with SDK
deepsource, err := deepsource.New(deepsource.ClientOpts{
Token: config.Cfg.Token,
HostName: config.Cfg.Host,
})
if err != nil {
return nil, authTimedOut, err
}

// Keep polling the mutation at a certain interval till the expiry timeperiod
ticker := time.NewTicker(time.Duration(deviceRegistrationData.Interval) * time.Second)
pollStartTime := time.Now()

// Polling for fetching PAT
func() {
for range ticker.C {
tokenData, err = deepsource.Login(ctx, deviceRegistrationData.Code, userDescription)
tokenData, err = svc.RequestPAT(ctx, cfg, deviceRegistrationData.Code, userDescription)
if err == nil {
authTimedOut = false
return
Expand Down
5 changes: 3 additions & 2 deletions command/auth/login/pat_login_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import (
"fmt"

"github.com/deepsourcelabs/cli/config"
authsvc "github.com/deepsourcelabs/cli/internal/services/auth"
)

// Starts the login flow for the CLI (using PAT)
func (opts *LoginOptions) startPATLoginFlow(cfg *config.CLIConfig, token string) error {
func (opts *LoginOptions) startPATLoginFlow(svc *authsvc.Service, cfg *config.CLIConfig, token string) error {
// set personal access token (PAT)
cfg.Token = token

// Having stored the data in the global Cfg object, write it into the config file present in the local filesystem
err := cfg.WriteFile()
err := svc.SaveConfig(cfg)
if err != nil {
return fmt.Errorf("Error in writing authentication data to a file. Exiting...")
}
Expand Down
16 changes: 9 additions & 7 deletions command/auth/logout/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"fmt"

"github.com/deepsourcelabs/cli/config"
"github.com/deepsourcelabs/cli/utils"
"github.com/deepsourcelabs/cli/internal/cli/args"
"github.com/deepsourcelabs/cli/internal/cli/prompt"
authsvc "github.com/deepsourcelabs/cli/internal/services/auth"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
Expand All @@ -17,7 +19,7 @@ func NewCmdLogout() *cobra.Command {
cmd := &cobra.Command{
Use: "logout",
Short: "Logout of your active DeepSource account",
Args: utils.NoArgs,
Args: args.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts := LogoutOptions{}
return opts.Run()
Expand All @@ -27,8 +29,9 @@ func NewCmdLogout() *cobra.Command {
}

func (opts *LogoutOptions) Run() error {
svc := authsvc.NewService(config.DefaultManager())
// Fetch config
cfg, err := config.GetConfig()
cfg, err := svc.LoadConfig()
if err != nil {
return fmt.Errorf("Error while reading DeepSource CLI config : %v", err)
}
Expand All @@ -39,18 +42,17 @@ func (opts *LogoutOptions) Run() error {

// Confirm from the user if they want to logout
logoutConfirmationMsg := "Are you sure you want to log out of DeepSource account?"
response, err := utils.ConfirmFromUser(logoutConfirmationMsg, "")
response, err := prompt.ConfirmFromUser(logoutConfirmationMsg, "")
if err != nil {
return err
}

// If response is true, delete the config file => logged out the user
if response {
err := cfg.Delete()
if err != nil {
if err := svc.DeleteConfig(); err != nil {
return err
}
}
pterm.Info.Println("Logged out from DeepSource (deepsource.io)")
pterm.Println("Logged out from DeepSource (deepsource.io)")
return nil
}
Loading
Loading