diff --git a/AGENT.md b/AGENT.md index 440aee4e..e3caad05 100644 --- a/AGENT.md +++ b/AGENT.md @@ -2,7 +2,7 @@ This project is the Buildkite CLI (`bk`) ## Commands - Test: `go test ./...` -- Lint: `docker-compose -f .buildkite/docker-compose.yaml run lint` +- Lint: `docker-compose -f .buildkite/docker-compose.yaml run golangci-lint golangci-lint run` - Generate: `go generate` (required after GraphQL changes) - Run: `go run cmd/bk/main.go` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ddab0d9..5cf7847f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ To get started with contributing, please follow these steps: 1. Fork the repository 2. Create a feature branch with a nice name (`git checkout -b cli-new-feature`) for your changes 3. Write your code - * We use `golangci-lint` and would be good to use the same in order to pass a PR merge. You can use `docker-compose -f .buildkite/docker-compose.yaml run lint` for that. + * We use `golangci-lint` and would be good to use the same in order to pass a PR merge. You can use `docker-compose -f .buildkite/docker-compose.yaml run golangci-lint golangci-lint run` for that. * Make sure the tests are passing by running go test ./... 5. Commit your changes and push them to your forked repository. 7. Submit a pull request with a detailed description of your changes and linked to any relevant issues. diff --git a/internal/config/config.go b/internal/config/config.go index 6a23d13b..4d3ff894 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -120,6 +120,12 @@ func (conf *Config) SetTokenForOrg(org, token string) error { return conf.userConfig.WriteConfig() } +// GetTokenForOrg gets the API token for a specific organization from the user configuration +func (conf *Config) GetTokenForOrg(org string) string { + key := fmt.Sprintf("organizations.%s.api_token", org) + return conf.userConfig.GetString(key) +} + func (conf *Config) ConfiguredOrganizations() []string { m := conf.userConfig.GetStringMap("organizations") orgs := slices.Collect(maps.Keys(m)) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4e5c9c36..ce802cff 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -60,4 +60,27 @@ func TestConfig(t *testing.T) { t.Errorf("PreferredPipelines() does not match: %d", len(conf.PreferredPipelines())) } }) + + t.Run("GetTokenForOrg returns token for specific organization", func(t *testing.T) { + t.Parallel() + + fs := afero.NewMemMapFs() + conf := New(fs, nil) + + // Set tokens for different organizations + token1 := "token-org1" + token2 := "token-org2" + conf.SetTokenForOrg("org1", token1) + conf.SetTokenForOrg("org2", token2) + + if conf.GetTokenForOrg("org1") != token1 { + t.Errorf("expected token for org1 to be %s, got %s", token1, conf.GetTokenForOrg("org1")) + } + if conf.GetTokenForOrg("org2") != token2 { + t.Errorf("expected token for org2 to be %s, got %s", token2, conf.GetTokenForOrg("org2")) + } + if conf.GetTokenForOrg("nonexistent") != "" { + t.Errorf("expected empty token for nonexistent org, got %s", conf.GetTokenForOrg("nonexistent")) + } + }) } diff --git a/main b/main new file mode 100755 index 00000000..be089ecb Binary files /dev/null and b/main differ diff --git a/pkg/cmd/configure/add/add.go b/pkg/cmd/configure/add/add.go index 7a01e10d..a7126ef2 100644 --- a/pkg/cmd/configure/add/add.go +++ b/pkg/cmd/configure/add/add.go @@ -44,6 +44,13 @@ func ConfigureRun(f *factory.Factory) error { return errors.New("organization slug cannot be empty") } + // Check if token already exists for this organization + existingToken := getTokenForOrg(f, org) + if existingToken != "" { + fmt.Printf("Using existing API token for organization: %s\n", org) + return f.Config.SelectOrganization(org) + } + // Get API token with password input (no echo) token, err := promptForInput("API Token: ", true) if err != nil { @@ -54,10 +61,14 @@ func ConfigureRun(f *factory.Factory) error { } fmt.Println("API token set for organization:", org) - return ConfigureWithCredentials(f, org, token) } +// getTokenForOrg retrieves the token for a specific organization from the user config +func getTokenForOrg(f *factory.Factory, org string) string { + return f.Config.GetTokenForOrg(org) +} + // promptForInput handles terminal input with optional password masking func promptForInput(prompt string, isPassword bool) (string, error) { fmt.Print(prompt) diff --git a/pkg/cmd/configure/add/add_test.go b/pkg/cmd/configure/add/add_test.go new file mode 100644 index 00000000..6e1f7210 --- /dev/null +++ b/pkg/cmd/configure/add/add_test.go @@ -0,0 +1,126 @@ +package add + +import ( + "testing" + + "github.com/buildkite/cli/v3/internal/config" + "github.com/buildkite/cli/v3/pkg/cmd/factory" + "github.com/spf13/afero" +) + +func TestGetTokenForOrg(t *testing.T) { + t.Parallel() + + t.Run("returns empty string when no token exists", func(t *testing.T) { + t.Parallel() + fs := afero.NewMemMapFs() + conf := config.New(fs, nil) + f := &factory.Factory{Config: conf} + + token := getTokenForOrg(f, "nonexistent") + if token != "" { + t.Errorf("expected empty string, got %s", token) + } + }) + + t.Run("returns token when it exists for organization", func(t *testing.T) { + t.Parallel() + fs := afero.NewMemMapFs() + conf := config.New(fs, nil) + f := &factory.Factory{Config: conf} + + // Set up a token for an organization + expectedToken := "bk_test_token_12345" + conf.SetTokenForOrg("test-org", expectedToken) + + token := getTokenForOrg(f, "test-org") + if token != expectedToken { + t.Errorf("expected %s, got %s", expectedToken, token) + } + }) + + t.Run("returns different tokens for different organizations", func(t *testing.T) { + t.Parallel() + fs := afero.NewMemMapFs() + conf := config.New(fs, nil) + f := &factory.Factory{Config: conf} + + // Set up tokens for different organizations + token1 := "bk_test_token_org1" + token2 := "bk_test_token_org2" + conf.SetTokenForOrg("org1", token1) + conf.SetTokenForOrg("org2", token2) + + if getTokenForOrg(f, "org1") != token1 { + t.Errorf("expected %s for org1", token1) + } + if getTokenForOrg(f, "org2") != token2 { + t.Errorf("expected %s for org2", token2) + } + }) +} + +func TestConfigureWithCredentials(t *testing.T) { + t.Parallel() + + t.Run("configures organization and token", func(t *testing.T) { + t.Parallel() + fs := afero.NewMemMapFs() + conf := config.New(fs, nil) + f := &factory.Factory{Config: conf} + + org := "test-org" + token := "bk_test_token_12345" + + err := ConfigureWithCredentials(f, org, token) + if err != nil { + t.Errorf("expected no error, got %s", err) + } + + if conf.OrganizationSlug() != org { + t.Errorf("expected organization to be %s, got %s", org, conf.OrganizationSlug()) + } + + if conf.GetTokenForOrg(org) != token { + t.Errorf("expected token to be %s, got %s", token, conf.GetTokenForOrg(org)) + } + }) +} + +func TestConfigureTokenReuse(t *testing.T) { + t.Parallel() + + t.Run("reuses existing token when available", func(t *testing.T) { + t.Parallel() + fs := afero.NewMemMapFs() + conf := config.New(fs, nil) + f := &factory.Factory{Config: conf} + + org := "test-org" + existingToken := "bk_existing_token_12345" + + // Pre-configure a token for the organization + conf.SetTokenForOrg(org, existingToken) + + // Verify the token can be retrieved + retrievedToken := getTokenForOrg(f, org) + if retrievedToken != existingToken { + t.Errorf("expected to retrieve existing token %s, got %s", existingToken, retrievedToken) + } + + // Configure with the existing token (simulating the logic in ConfigureRun) + err := ConfigureWithCredentials(f, org, retrievedToken) + if err != nil { + t.Errorf("expected no error, got %s", err) + } + + // Verify the configuration still works + if conf.OrganizationSlug() != org { + t.Errorf("expected organization to be %s, got %s", org, conf.OrganizationSlug()) + } + + if conf.GetTokenForOrg(org) != existingToken { + t.Errorf("expected token to be %s, got %s", existingToken, conf.GetTokenForOrg(org)) + } + }) +}