Skip to content
Open
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
46 changes: 46 additions & 0 deletions cmd/ld-find-code-refs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,47 @@ var extinctions = &cobra.Command{
},
}

var aliasCmd = &cobra.Command{
Use: "alias",
Example: "ld-find-code-refs alias --flag-key my-flag",
Short: "Generate aliases for feature flags",
RunE: func(cmd *cobra.Command, args []string) error {
err := o.InitYAMLForAlias()
if err != nil {
return err
}

opts, err := o.GetOptions()
if err != nil {
return err
}

// Get the flag-key value from the command
flagKey, _ := cmd.Flags().GetString("flag-key")

// Create AliasOptions from the global options
aliasOpts := &o.AliasOptions{
Dir: opts.Dir,
ProjKey: opts.ProjKey,
Projects: opts.Projects,
AccessToken: opts.AccessToken,
BaseUri: opts.BaseUri,
FlagKey: flagKey,
Debug: opts.Debug,
UserAgent: opts.UserAgent,
}

// Validate the alias options (not the global options)
err = aliasOpts.ValidateAliasOptions()
if err != nil {
return err
}

log.Init(opts.Debug)
return coderefs.GenerateAliases(*aliasOpts)
},
}

var cmd = &cobra.Command{
Use: "ld-find-code-refs",
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -90,8 +131,13 @@ func main() {
if err := o.Init(cmd.PersistentFlags()); err != nil {
panic(err)
}

// Add the flag-key flag to the alias command
aliasCmd.Flags().String("flag-key", "", "Generate aliases for a specific flag key (local mode)")

cmd.AddCommand(prune)
cmd.AddCommand(extinctions)
cmd.AddCommand(aliasCmd)

if err := cmd.Execute(); err != nil {
os.Exit(1)
Expand Down
98 changes: 98 additions & 0 deletions coderefs/coderefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

"github.com/launchdarkly/ld-find-code-refs/v2/aliases"
"github.com/launchdarkly/ld-find-code-refs/v2/internal/git"
"github.com/launchdarkly/ld-find-code-refs/v2/internal/helpers"
"github.com/launchdarkly/ld-find-code-refs/v2/internal/ld"
Expand Down Expand Up @@ -206,3 +207,100 @@ func runExtinctions(opts options.Options, matcher search.Matcher, branch ld.Bran
}
}
}

// GenerateAliases generates aliases for flags based on the provided options
func GenerateAliases(opts options.AliasOptions) error {
absPath, err := validation.NormalizeAndValidatePath(opts.Dir)
if err != nil {
return fmt.Errorf("could not validate directory option: %s", err)
}

log.Info.Printf("absolute directory path: %s", absPath)

var flagKeys []string

if opts.FlagKey != "" {
// Local Mode: Generate aliases for a single flag key
log.Info.Printf("generating aliases for flag key: %s", opts.FlagKey)
flagKeys = []string{opts.FlagKey}
} else {
// API-Connected Mode: Fetch all flags from the API
log.Info.Printf("fetching all flags from LaunchDarkly API")

ldApi := ld.InitApiClient(ld.ApiOptions{
ApiKey: opts.AccessToken,
BaseUri: opts.BaseUri,
UserAgent: helpers.GetUserAgent(opts.UserAgent),
})

// Determine project key
projKey := opts.ProjKey
if projKey == "" && len(opts.Projects) > 0 {
projKey = opts.Projects[0].Key
}

flags, err := ldApi.GetFlagKeyList(projKey, false) // Include archived flags
if err != nil {
return fmt.Errorf("could not retrieve flag keys from LaunchDarkly for project `%s`: %w", projKey, err)
}
flagKeys = flags
log.Info.Printf("retrieved %d flags from LaunchDarkly", len(flagKeys))
}

// Load alias configuration from .launchdarkly/coderefs.yaml
// We need to get the full options to access the aliases
fullOpts, err := options.GetOptions()
if err != nil {
return fmt.Errorf("error loading configuration: %w", err)
}

var aliasConfigs []options.Alias

// Check if we have project-specific aliases
if len(fullOpts.Projects) > 0 {
for _, project := range fullOpts.Projects {
aliasConfigs = append(aliasConfigs, project.Aliases...)
}
} else {
// Use global aliases
aliasConfigs = fullOpts.Aliases
}

if len(aliasConfigs) == 0 {
log.Warning.Printf("no alias configurations found in .launchdarkly/coderefs.yaml")
return nil
}

// Generate aliases
aliasMap, err := aliases.GenerateAliases(flagKeys, aliasConfigs, absPath)
if err != nil {
return fmt.Errorf("error generating aliases: %w", err)
}

// Print the results
if opts.FlagKey != "" {
// Local mode: print aliases for the single flag
if aliases, exists := aliasMap[opts.FlagKey]; exists && len(aliases) > 0 {
fmt.Printf("Aliases for flag '%s':\n", opts.FlagKey)
for _, alias := range aliases {
fmt.Printf(" - %s\n", alias)
}
} else {
fmt.Printf("No aliases found for flag '%s'\n", opts.FlagKey)
}
} else {
// API-connected mode: print aliases for all flags
fmt.Printf("Generated aliases for %d flags:\n\n", len(flagKeys))
for _, flagKey := range flagKeys {
if aliases, exists := aliasMap[flagKey]; exists && len(aliases) > 0 {
fmt.Printf("Flag '%s':\n", flagKey)
for _, alias := range aliases {
fmt.Printf(" - %s\n", alias)
}
fmt.Println()
}
}
}

return nil
}
46 changes: 46 additions & 0 deletions coderefs/coderefs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package coderefs

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/launchdarkly/ld-find-code-refs/v2/internal/ld"
"github.com/launchdarkly/ld-find-code-refs/v2/internal/log"
"github.com/launchdarkly/ld-find-code-refs/v2/options"
)

func init() {
Expand Down Expand Up @@ -54,3 +57,46 @@ func Test_calculateStaleBranches(t *testing.T) {
})
}
}

func Test_GenerateAliases(t *testing.T) {
// Create a temporary directory for testing
tmpDir, err := os.MkdirTemp("", "alias-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)

// Create .launchdarkly directory
ldDir := filepath.Join(tmpDir, ".launchdarkly")
err = os.MkdirAll(ldDir, 0755)
require.NoError(t, err)

// Create a simple coderefs.yaml file with alias configuration
configContent := `aliases:
- type: camelcase
- type: snakecase
`
configPath := filepath.Join(ldDir, "coderefs.yaml")
err = os.WriteFile(configPath, []byte(configContent), 0644)
require.NoError(t, err)

t.Run("local mode with flag key", func(t *testing.T) {
opts := options.AliasOptions{
Dir: tmpDir,
FlagKey: "test-flag",
}

// This should not return an error for local mode
err := GenerateAliases(opts)
require.NoError(t, err)
})

t.Run("validation error for invalid dir", func(t *testing.T) {
opts := options.AliasOptions{
Dir: "/nonexistent/directory/path",
FlagKey: "test-flag",
}

err := GenerateAliases(opts)
require.Error(t, err)
assert.Contains(t, err.Error(), "could not validate directory option")
})
}
52 changes: 52 additions & 0 deletions options/alias_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package options

import (
"fmt"
"os"
)

// AliasOptions contains the options specific to the alias command
type AliasOptions struct {
Dir string `mapstructure:"dir"`
ProjKey string `mapstructure:"projkey"`
Projects []Project `mapstructure:"projects"`
AccessToken string `mapstructure:"accessToken"`
BaseUri string `mapstructure:"baseUri"`
FlagKey string `mapstructure:"flagKey"`
Debug bool `mapstructure:"debug"`
UserAgent string `mapstructure:"userAgent"`
}

// ValidateAliasOptions validates the alias options based on the mode of operation
func (o *AliasOptions) ValidateAliasOptions() error {
missingRequiredOptions := []string{}

// Default Dir to current working directory if not provided
if o.Dir == "" {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("could not get current working directory: %w", err)
}
o.Dir = cwd
}

// If FlagKey is provided (Local Mode), AccessToken and ProjKey/Projects are not required
if o.FlagKey != "" {
// Local mode - dir is defaulted above if needed
return nil
}

// If FlagKey is not provided (API-Connected Mode), AccessToken and ProjKey/Projects are required
if o.AccessToken == "" {
missingRequiredOptions = append(missingRequiredOptions, "accessToken")
}
if o.ProjKey == "" && len(o.Projects) == 0 {
missingRequiredOptions = append(missingRequiredOptions, "projKey/projects")
}

if len(missingRequiredOptions) > 0 {
return fmt.Errorf("missing required option(s): %v", missingRequiredOptions)
}

return nil
}
74 changes: 74 additions & 0 deletions options/alias_options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package options

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_AliasOptions_ValidateAliasOptions(t *testing.T) {
t.Run("local mode validation - only dir required", func(t *testing.T) {
opts := &AliasOptions{
Dir: "/some/dir",
FlagKey: "test-flag",
}

err := opts.ValidateAliasOptions()
require.NoError(t, err)
})

t.Run("local mode validation - missing dir defaults to cwd", func(t *testing.T) {
opts := &AliasOptions{
FlagKey: "test-flag",
}

err := opts.ValidateAliasOptions()
require.NoError(t, err)
assert.NotEmpty(t, opts.Dir) // Dir should be set to current working directory
})

t.Run("API mode validation - all required fields present", func(t *testing.T) {
opts := &AliasOptions{
Dir: "/some/dir",
AccessToken: "token",
ProjKey: "project",
}

err := opts.ValidateAliasOptions()
require.NoError(t, err)
})

t.Run("API mode validation - missing access token", func(t *testing.T) {
opts := &AliasOptions{
Dir: "/some/dir",
ProjKey: "project",
}

err := opts.ValidateAliasOptions()
require.Error(t, err)
assert.Contains(t, err.Error(), "accessToken")
})

t.Run("API mode validation - missing project key", func(t *testing.T) {
opts := &AliasOptions{
Dir: "/some/dir",
AccessToken: "token",
}

err := opts.ValidateAliasOptions()
require.Error(t, err)
assert.Contains(t, err.Error(), "projKey/projects")
})

t.Run("API mode validation - with projects instead of projKey", func(t *testing.T) {
opts := &AliasOptions{
Dir: "/some/dir",
AccessToken: "token",
Projects: []Project{{Key: "project1"}},
}

err := opts.ValidateAliasOptions()
require.NoError(t, err)
})
}
28 changes: 28 additions & 0 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,34 @@ func InitYAML() error {
return nil
}

// InitYAMLForAlias initializes YAML configuration without requiring accessToken and dir preconditions
func InitYAMLForAlias() error {
dir := viper.GetString("dir")
if dir == "" {
// Default to current working directory
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("could not get current working directory: %w", err)
}
dir = cwd
}

absPath, err := validation.NormalizeAndValidatePath(dir)
if err != nil {
return err
}
subdirectoryPath := viper.GetString("subdirectory")
viper.SetConfigName("coderefs")
viper.SetConfigType("yaml")
configPath := filepath.Join(absPath, subdirectoryPath, ".launchdarkly")
viper.AddConfigPath(configPath)
err = viper.ReadInConfig()
if err != nil && !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return err
}
return nil
}

// validatePreconditions ensures required flags have been set
func validateYAMLPreconditions() error {
token := viper.GetString("accessToken")
Expand Down
Loading