-
Notifications
You must be signed in to change notification settings - Fork 11
feat: Accept repository token for remote config OD-497 #206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -336,7 +336,7 @@ func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error { | |
| "toolConfigPath": repoConfigPath, | ||
| }) | ||
| return nil | ||
| } else if (!cliLocalMode && initFlags.ApiToken != "") || cliLocalMode { | ||
| } else if (!cliLocalMode && initFlags.HasRemoteToken()) || cliLocalMode { | ||
| if err := configsetup.CreateToolConfigurationFile(toolName, initFlags); err != nil { | ||
| return fmt.Errorf("failed to create config file for tool %s: %w", toolName, err) | ||
| } | ||
|
|
@@ -351,7 +351,7 @@ func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error { | |
| logger.Debug("Config file not found for tool, using tool defaults", logrus.Fields{ | ||
| "tool": toolName, | ||
| "toolConfigPath": toolConfigPath, | ||
| "message": "No API token provided", | ||
| "message": "No API token or project token provided", | ||
| }) | ||
| } | ||
| } else if err != nil { | ||
|
|
@@ -507,6 +507,8 @@ var analyzeCmd = &cobra.Command{ | |
|
|
||
| Supports API token, provider, and repository flags to automatically fetch tool configurations from Codacy API if they don't exist locally.`, | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| cmdutils.PrepareRemoteFlags(cmd, &initFlags) | ||
|
|
||
| // Validate paths before proceeding | ||
| if err := validatePaths(args); err != nil { | ||
| fmt.Println(err) | ||
|
|
@@ -519,7 +521,7 @@ Supports API token, provider, and repository flags to automatically fetch tool c | |
| log.Fatalf("Failed to get current working directory: %v", err) | ||
| } | ||
|
|
||
| cliLocalMode := len(initFlags.ApiToken) == 0 | ||
| cliLocalMode := !initFlags.HasRemoteToken() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK The |
||
|
|
||
| var toolsToRun map[string]*plugins.ToolInfo | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,68 @@ | ||
| package cmdutils | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "log" | ||
| "os" | ||
|
|
||
| "codacy/cli-v2/domain" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // ProjectTokenEnvVar is also the codacy-coverage-reporter's variable, so it is often | ||
| // exported for unrelated reasons - see ResolveProjectToken. | ||
| const ProjectTokenEnvVar = "CODACY_PROJECT_TOKEN" | ||
|
|
||
| // AddCloudFlags adds the common cloud-related flags to a cobra command. | ||
| // The flags will be bound to the provided flags struct. | ||
| func AddCloudFlags(cmd *cobra.Command, flags *domain.InitFlags) { | ||
| cmd.Flags().StringVar(&flags.ApiToken, "api-token", "", "Optional Codacy API token. If defined, configurations will be fetched from Codacy") | ||
| cmd.Flags().StringVar(&flags.Provider, "provider", "", "Provider (e.g., gh, bb, gl) to fetch configurations from Codacy. Required when api-token is provided") | ||
| cmd.Flags().StringVar(&flags.Organization, "organization", "", "Remote organization name to fetch configurations from Codacy. Required when api-token is provided") | ||
| cmd.Flags().StringVar(&flags.Repository, "repository", "", "Remote repository name to fetch configurations from Codacy. Required when api-token is provided") | ||
| cmd.Flags().StringVar(&flags.ApiToken, "api-token", "", "Optional Codacy account API token. If defined, configurations will be fetched from Codacy") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK Suggestion: The flag registration logic in |
||
| cmd.Flags().StringVar(&flags.ProjectToken, "project-token", "", "Optional Codacy repository token, an alternative to api-token for fetching configurations from Codacy. Falls back to CODACY_PROJECT_TOKEN when provider, organization and repository are given. See https://docs.codacy.com/codacy-api/api-tokens/#repository-api-tokens") | ||
| cmd.Flags().StringVar(&flags.Provider, "provider", "", "Provider (e.g., gh, bb, gl) to fetch configurations from Codacy. Required when api-token or project-token is provided") | ||
| cmd.Flags().StringVar(&flags.Organization, "organization", "", "Remote organization name to fetch configurations from Codacy. Required when api-token or project-token is provided") | ||
| cmd.Flags().StringVar(&flags.Repository, "repository", "", "Remote repository name to fetch configurations from Codacy. Required when api-token or project-token is provided") | ||
| } | ||
|
|
||
| // ResolveProjectToken falls back to CODACY_PROJECT_TOKEN, but only once the repository | ||
| // coordinates are known. The variable is the coverage reporter's and is routinely exported | ||
| // CI-wide, so on its own it must not switch a local run into remote mode. | ||
| func ResolveProjectToken(flags *domain.InitFlags) { | ||
| if flags.HasRemoteToken() || !flags.HasRepositoryCoordinates() { | ||
| return | ||
| } | ||
| flags.ProjectToken = os.Getenv(ProjectTokenEnvVar) | ||
| } | ||
|
|
||
| // ValidateRemoteFlags rejects a token given without the repository coordinates every | ||
| // remote read needs, so commands fail with a clear message instead of requesting a URL | ||
| // with empty path segments. | ||
| func ValidateRemoteFlags(flags domain.InitFlags) error { | ||
| if !flags.HasRemoteToken() || flags.HasRepositoryCoordinates() { | ||
| return nil | ||
| } | ||
| return errors.New("when using --api-token or --project-token, you must also provide --provider, --organization, and --repository flags") | ||
| } | ||
|
|
||
| // exit is swappable so the failure path stays testable. | ||
| var exit = os.Exit | ||
|
|
||
| // PrepareRemoteFlags resolves the environment fallback and rejects an incomplete remote | ||
| // setup, so every command that takes cloud flags behaves the same way. | ||
| func PrepareRemoteFlags(cmd *cobra.Command, flags *domain.InitFlags) { | ||
| ResolveProjectToken(flags) | ||
|
|
||
| err := ValidateRemoteFlags(*flags) | ||
| if err == nil { | ||
| return | ||
| } | ||
|
|
||
| fmt.Printf("Error: %s.\n", err) | ||
| fmt.Println("Please provide all required flags and try again.") | ||
| fmt.Println() | ||
| if errHelp := cmd.Help(); errHelp != nil { | ||
| log.Printf("Warning: Failed to display command help: %v", errHelp) | ||
| } | ||
| exit(1) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| package cmdutils | ||
|
|
||
| import ( | ||
| "io" | ||
| "testing" | ||
|
|
||
| "codacy/cli-v2/domain" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func completeFlags() domain.InitFlags { | ||
| return domain.InitFlags{Provider: "gh", Organization: "org", Repository: "repo"} | ||
| } | ||
|
|
||
| func TestAddCloudFlags(t *testing.T) { | ||
| t.Setenv(ProjectTokenEnvVar, "token-from-env") | ||
|
|
||
| cmd := &cobra.Command{Use: "test"} | ||
| flags := domain.InitFlags{} | ||
| AddCloudFlags(cmd, &flags) | ||
|
|
||
| for _, name := range []string{"api-token", "project-token", "provider", "organization", "repository"} { | ||
| assert.NotNil(t, cmd.Flags().Lookup(name), "flag %s should be registered", name) | ||
| } | ||
|
|
||
| // the environment must not leak in at registration time - see ResolveProjectToken | ||
| assert.Equal(t, "", cmd.Flags().Lookup("project-token").DefValue) | ||
| assert.Equal(t, domain.InitFlags{}, flags) | ||
| } | ||
|
|
||
| func TestAddCloudFlagsBindsValues(t *testing.T) { | ||
| cmd := &cobra.Command{Use: "test", Run: func(*cobra.Command, []string) {}} | ||
| flags := domain.InitFlags{} | ||
| AddCloudFlags(cmd, &flags) | ||
|
|
||
| cmd.SetArgs([]string{"--project-token", "tok", "--provider", "gh", "--organization", "org", "--repository", "repo"}) | ||
| assert.NoError(t, cmd.Execute()) | ||
|
|
||
| assert.Equal(t, domain.InitFlags{ProjectToken: "tok", Provider: "gh", Organization: "org", Repository: "repo"}, flags) | ||
| assert.NoError(t, ValidateRemoteFlags(flags)) | ||
| } | ||
|
|
||
| func TestResolveProjectToken(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| env string | ||
| flags domain.InitFlags | ||
| expectedToken string | ||
| }{ | ||
| {"environment applies once coordinates are known", "env-token", completeFlags(), "env-token"}, | ||
| {"environment ignored without coordinates", "env-token", domain.InitFlags{}, ""}, | ||
| {"environment ignored with partial coordinates", "env-token", domain.InitFlags{Provider: "gh", Organization: "org"}, ""}, | ||
| {"explicit flag wins over environment", "env-token", domain.InitFlags{ProjectToken: "flag-token", Provider: "gh", Organization: "org", Repository: "repo"}, "flag-token"}, | ||
| {"account token is not overwritten", "env-token", domain.InitFlags{ApiToken: "api", Provider: "gh", Organization: "org", Repository: "repo"}, ""}, | ||
| {"unset environment leaves flags alone", "", completeFlags(), ""}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Setenv(ProjectTokenEnvVar, tt.env) | ||
|
|
||
| flags := tt.flags | ||
| ResolveProjectToken(&flags) | ||
|
|
||
| assert.Equal(t, tt.expectedToken, flags.ProjectToken) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestValidateRemoteFlags(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| flags domain.InitFlags | ||
| expectError bool | ||
| }{ | ||
| {"no token needs no coordinates", domain.InitFlags{}, false}, | ||
| {"no token with partial coordinates", domain.InitFlags{Provider: "gh"}, false}, | ||
| {"account token with coordinates", domain.InitFlags{ApiToken: "api", Provider: "gh", Organization: "org", Repository: "repo"}, false}, | ||
| {"repository token with coordinates", domain.InitFlags{ProjectToken: "project", Provider: "gh", Organization: "org", Repository: "repo"}, false}, | ||
| {"repository token without coordinates", domain.InitFlags{ProjectToken: "project"}, true}, | ||
| {"account token without coordinates", domain.InitFlags{ApiToken: "api"}, true}, | ||
| {"token missing provider", domain.InitFlags{ProjectToken: "project", Organization: "org", Repository: "repo"}, true}, | ||
| {"token missing organization", domain.InitFlags{ProjectToken: "project", Provider: "gh", Repository: "repo"}, true}, | ||
| {"token missing repository", domain.InitFlags{ProjectToken: "project", Provider: "gh", Organization: "org"}, true}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := ValidateRemoteFlags(tt.flags) | ||
| if tt.expectError { | ||
| assert.Error(t, err) | ||
| } else { | ||
| assert.NoError(t, err) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // stopped reports whether PrepareRemoteFlags terminated the command. The exit stub panics | ||
| // so a helper that kept running past it would fail the assertion instead of passing. | ||
| func stopped(t *testing.T, flags *domain.InitFlags) bool { | ||
| t.Helper() | ||
|
|
||
| original := exit | ||
| defer func() { exit = original }() | ||
|
|
||
| type exitCall struct{ code int } | ||
| exit = func(code int) { panic(exitCall{code}) } | ||
|
|
||
| cmd := &cobra.Command{Use: "test"} | ||
| cmd.SetOut(io.Discard) | ||
|
|
||
| didExit := false | ||
| func() { | ||
| defer func() { | ||
| if r := recover(); r != nil { | ||
| call, ok := r.(exitCall) | ||
| assert.True(t, ok, "unexpected panic: %v", r) | ||
| assert.Equal(t, 1, call.code) | ||
| didExit = true | ||
| } | ||
| }() | ||
| PrepareRemoteFlags(cmd, flags) | ||
| }() | ||
|
|
||
| return didExit | ||
| } | ||
|
|
||
| func TestPrepareRemoteFlags(t *testing.T) { | ||
| t.Run("stops on a token without coordinates", func(t *testing.T) { | ||
| flags := domain.InitFlags{ProjectToken: "tok"} | ||
| assert.True(t, stopped(t, &flags)) | ||
| }) | ||
|
|
||
| t.Run("continues without any token", func(t *testing.T) { | ||
| flags := domain.InitFlags{} | ||
| assert.False(t, stopped(t, &flags)) | ||
| }) | ||
|
|
||
| t.Run("continues when the environment token is ignored", func(t *testing.T) { | ||
| t.Setenv(ProjectTokenEnvVar, "env-token") | ||
|
|
||
| flags := domain.InitFlags{} | ||
| assert.False(t, stopped(t, &flags)) | ||
| assert.Equal(t, "", flags.ProjectToken) | ||
| }) | ||
|
|
||
| t.Run("adopts the environment token with coordinates", func(t *testing.T) { | ||
| t.Setenv(ProjectTokenEnvVar, "env-token") | ||
|
|
||
| flags := completeFlags() | ||
| assert.False(t, stopped(t, &flags)) | ||
| assert.Equal(t, "env-token", flags.ProjectToken) | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,7 +103,7 @@ func CreateConfigurationFilesForDiscoveredTools(discoveredToolNames map[string]s | |
| currentCliMode = "local" // Default to local | ||
| } | ||
|
|
||
| if currentCliMode == "remote" && initFlags.ApiToken != "" { | ||
| if currentCliMode == "remote" && initFlags.HasRemoteToken() { | ||
| // Remote mode - create configurations based on cloud repository settings | ||
| return createRemoteToolConfigurationsForDiscovered(discoveredToolNames, initFlags) | ||
| } | ||
|
|
@@ -200,7 +200,7 @@ func createDefaultConfigurationsForSpecificTools(discoveredToolNames map[string] | |
| // createToolConfigurationsForUUIDs creates tool configurations for specific UUIDs | ||
| func createToolConfigurationsForUUIDs(uuids []string, toolsConfigDir string, initFlags domain.InitFlags) error { | ||
| for _, uuid := range uuids { | ||
| patternsConfig, err := codacyclient.GetToolPatternsConfig(initFlags, uuid, true) | ||
| patternsConfig, err := codacyclient.GetToolPatternsConfig(domain.InitFlags{}, uuid, true) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK The |
||
| if err != nil { | ||
| logToolConfigWarning(uuid, "Failed to get default patterns", err) | ||
| continue | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -98,7 +98,7 @@ func CreateToolConfigurationFile(toolName string, flags domain.InitFlags) error | |||||
| return fmt.Errorf("tool '%s' not found in supported tools", toolName) | ||||||
| } | ||||||
|
|
||||||
| patternsConfig, err := codacyclient.GetToolPatternsConfig(flags, toolUUID, true) | ||||||
| patternsConfig, err := codacyclient.GetToolPatternsConfig(domain.InitFlags{}, toolUUID, true) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM RISK The
Suggested change
|
||||||
| if err != nil { | ||||||
| return fmt.Errorf("failed to get default patterns: %w", err) | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚪ LOW RISK
Suggestion: This condition
(!cliLocalMode && initFlags.HasRemoteToken()) || cliLocalModealways evaluates totruebecausecliLocalModeis the negation ofHasRemoteToken(). This makes theelseblock on line 350 unreachable dead code, including the log message on line 354. The logic should be simplified or corrected to ensure the intended fallback behavior works.