diff --git a/cli b/cli new file mode 100755 index 00000000..0a5a0081 Binary files /dev/null and b/cli differ diff --git a/internal/build/resolver/cli_test.go b/internal/build/resolver/cli_test.go index a1267261..ae5af078 100644 --- a/internal/build/resolver/cli_test.go +++ b/internal/build/resolver/cli_test.go @@ -43,7 +43,7 @@ func TestParseBuildArg(t *testing.T) { t.Parallel() conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("testing") + conf.SelectOrganization("testing", true) res := func(context.Context) (*pipeline.Pipeline, error) { return &pipeline.Pipeline{ Name: testcase.pipeline, @@ -71,7 +71,7 @@ func TestParseBuildArg(t *testing.T) { t.Parallel() conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("testing") + conf.SelectOrganization("testing", true) f := resolver.ResolveFromPositionalArgument([]string{"https://buildkite.com/"}, 0, nil, conf) build, err := f(context.Background()) if err == nil { diff --git a/internal/config/config.go b/internal/config/config.go index 4d3ff894..a518e568 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -96,8 +96,13 @@ func (conf *Config) OrganizationSlug() string { ) } -// SelectOrganization sets the selected organization in the local configuration file -func (conf *Config) SelectOrganization(org string) error { +// SelectOrganization sets the selected organization in the configuration file +func (conf *Config) SelectOrganization(org string, inGitRepo bool) error { + if !inGitRepo { + conf.userConfig.Set("selected_org", org) + return conf.userConfig.WriteConfig() + } + conf.localConfig.Set("selected_org", org) return conf.localConfig.WriteConfig() } @@ -176,12 +181,16 @@ func (conf *Config) PreferredPipelines() []pipeline.Pipeline { } // SetPreferredPipelines will write the provided list of pipelines to local configuration -func (conf *Config) SetPreferredPipelines(pipelines []pipeline.Pipeline) error { +func (conf *Config) SetPreferredPipelines(pipelines []pipeline.Pipeline, inGitRepo bool) error { // only save pipelines if they are present if len(pipelines) == 0 { return nil } + if !inGitRepo { + return fmt.Errorf("cannot save preferred pipelines: not in a git repository") + } + names := make([]string, len(pipelines)) for i, p := range pipelines { names[i] = p.Name diff --git a/internal/pipeline/resolver/cli_test.go b/internal/pipeline/resolver/cli_test.go index e9c1bb52..22e2875a 100644 --- a/internal/pipeline/resolver/cli_test.go +++ b/internal/pipeline/resolver/cli_test.go @@ -38,7 +38,7 @@ func TestParsePipelineArg(t *testing.T) { t.Parallel() conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("testing") + conf.SelectOrganization("testing", true) f := resolver.ResolveFromPositionalArgument([]string{testcase.url}, 0, conf) pipeline, err := f(context.Background()) if err != nil { @@ -57,7 +57,7 @@ func TestParsePipelineArg(t *testing.T) { t.Parallel() conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("testing") + conf.SelectOrganization("testing", true) f := resolver.ResolveFromPositionalArgument([]string{"https://buildkite.com/"}, 0, conf) pipeline, err := f(context.Background()) if err == nil { diff --git a/internal/pipeline/resolver/config_test.go b/internal/pipeline/resolver/config_test.go index bb49875e..862f2d55 100644 --- a/internal/pipeline/resolver/config_test.go +++ b/internal/pipeline/resolver/config_test.go @@ -32,7 +32,7 @@ func TestResolvePipelineFromConfig(t *testing.T) { pipelines := []pipeline.Pipeline{{Name: "pipeline1"}} conf := config.New(afero.NewMemMapFs(), nil) - conf.SetPreferredPipelines(pipelines) + conf.SetPreferredPipelines(pipelines, true) resolve := ResolveFromConfig(conf, PassthruPicker) selected, err := resolve(context.Background()) if err != nil { @@ -53,7 +53,7 @@ func TestResolvePipelineFromConfig(t *testing.T) { pipelines := []pipeline.Pipeline{{Name: "pipeline1"}, {Name: "pipeline2"}, {Name: "pipeline3"}} conf := config.New(afero.NewMemMapFs(), nil) - conf.SetPreferredPipelines(pipelines) + conf.SetPreferredPipelines(pipelines, true) resolve := ResolveFromConfig(conf, PassthruPicker) selected, err := resolve(context.Background()) if err != nil { diff --git a/internal/pipeline/resolver/picker.go b/internal/pipeline/resolver/picker.go index e125fda7..cfcd33d4 100644 --- a/internal/pipeline/resolver/picker.go +++ b/internal/pipeline/resolver/picker.go @@ -47,7 +47,7 @@ func PickOne(pipelines []pipeline.Pipeline) *pipeline.Pipeline { // CachedPicker returns a PipelinePicker that saves the given pipelines to local config as well as running the provider // picker. -func CachedPicker(conf *config.Config, picker PipelinePicker) PipelinePicker { +func CachedPicker(conf *config.Config, picker PipelinePicker, inGitRepo bool) PipelinePicker { return func(pipelines []pipeline.Pipeline) *pipeline.Pipeline { // run the picker first because we want to put the chosen on at the top of the saved list chosen := picker(pipelines) @@ -67,7 +67,7 @@ func CachedPicker(conf *config.Config, picker PipelinePicker) PipelinePicker { pipelines[0], pipelines[index] = tmp, pipelines[0] // save the pipelines to local config before passing to the picker - err := conf.SetPreferredPipelines(pipelines) + err := conf.SetPreferredPipelines(pipelines, inGitRepo) if err != nil { return nil } diff --git a/internal/pipeline/resolver/picker_test.go b/internal/pipeline/resolver/picker_test.go index 6a583e82..28c38677 100644 --- a/internal/pipeline/resolver/picker_test.go +++ b/internal/pipeline/resolver/picker_test.go @@ -21,7 +21,7 @@ func TestPickers(t *testing.T) { pipelines := []pipeline.Pipeline{ {Name: "pipeline", Org: "org"}, } - picked := resolver.CachedPicker(conf, resolver.PassthruPicker)(pipelines) + picked := resolver.CachedPicker(conf, resolver.PassthruPicker, true)(pipelines) if picked == nil { t.Fatal("Should not have received nil from picker") @@ -41,7 +41,7 @@ func TestPickers(t *testing.T) { conf := config.New(fs, nil) pipelines := []pipeline.Pipeline{} - resolver.CachedPicker(conf, func(p []pipeline.Pipeline) *pipeline.Pipeline { return nil })(pipelines) + resolver.CachedPicker(conf, func(p []pipeline.Pipeline) *pipeline.Pipeline { return nil }, true)(pipelines) b, _ := afero.ReadFile(fs, ".bk.yaml") expected := "" @@ -61,7 +61,7 @@ func TestPickers(t *testing.T) { {Name: "second"}, {Name: "third"}, } - resolver.CachedPicker(conf, func(p []pipeline.Pipeline) *pipeline.Pipeline { return &p[1] })(pipelines) + resolver.CachedPicker(conf, func(p []pipeline.Pipeline) *pipeline.Pipeline { return &p[1] }, true)(pipelines) b, _ := afero.ReadFile(fs, ".bk.yaml") expected := "pipelines:\n - second\n - first\n - third\n" diff --git a/internal/pipeline/resolver/repository_test.go b/internal/pipeline/resolver/repository_test.go index c541c114..412f5581 100644 --- a/internal/pipeline/resolver/repository_test.go +++ b/internal/pipeline/resolver/repository_test.go @@ -108,7 +108,7 @@ func testFactory(t *testing.T, serverURL string, org string, repo *git.Repositor } conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization(org) + conf.SelectOrganization(org, true) return &factory.Factory{ Config: conf, RestAPIClient: bkClient, diff --git a/pkg/cmd/agent/agent_test.go b/pkg/cmd/agent/agent_test.go index 68da03cc..c61ea75f 100644 --- a/pkg/cmd/agent/agent_test.go +++ b/pkg/cmd/agent/agent_test.go @@ -41,7 +41,7 @@ func TestParseAgentArg(t *testing.T) { t.Parallel() conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("testing") + conf.SelectOrganization("testing", true) org, agent := parseAgentArg(testcase.url, conf) if org != testcase.org { diff --git a/pkg/cmd/agent/stop_test.go b/pkg/cmd/agent/stop_test.go index ae8a4e2d..1e8bbef1 100644 --- a/pkg/cmd/agent/stop_test.go +++ b/pkg/cmd/agent/stop_test.go @@ -45,7 +45,7 @@ func TestCmdAgentStop(t *testing.T) { } conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("test") + conf.SelectOrganization("test", true) factory := &factory.Factory{ Config: conf, @@ -84,7 +84,7 @@ func TestCmdAgentStop(t *testing.T) { } conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("test") + conf.SelectOrganization("test", true) factory := &factory.Factory{ Config: conf, @@ -124,7 +124,7 @@ func TestCmdAgentStop(t *testing.T) { } conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("test") + conf.SelectOrganization("test", true) factory := &factory.Factory{ Config: conf, @@ -167,7 +167,7 @@ func TestCmdAgentStop(t *testing.T) { } conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("test") + conf.SelectOrganization("test", true) factory := &factory.Factory{ Config: conf, diff --git a/pkg/cmd/artifacts/list.go b/pkg/cmd/artifacts/list.go index 6a127190..708319e4 100644 --- a/pkg/cmd/artifacts/list.go +++ b/pkg/cmd/artifacts/list.go @@ -46,7 +46,7 @@ func NewCmdArtifactsList(f *factory.Factory) *cobra.Command { pipelineRes := pipelineResolver.NewAggregateResolver( pipelineResolver.ResolveFromFlag(pipeline, f.Config), pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)), + pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne, f.GitRepository != nil)), ) // we resolve a build an optional argument or positional argument diff --git a/pkg/cmd/build/cancel.go b/pkg/cmd/build/cancel.go index f22d9b25..a6b7f5ef 100644 --- a/pkg/cmd/build/cancel.go +++ b/pkg/cmd/build/cancel.go @@ -50,7 +50,7 @@ func NewCmdBuildCancel(f *factory.Factory) *cobra.Command { pipelineRes := pipelineResolver.NewAggregateResolver( pipelineResolver.ResolveFromFlag(pipeline, f.Config), pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)), + pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne, f.GitRepository != nil)), ) buildRes := buildResolver.NewAggregateResolver( diff --git a/pkg/cmd/build/download.go b/pkg/cmd/build/download.go index b592fcb5..854746de 100644 --- a/pkg/cmd/build/download.go +++ b/pkg/cmd/build/download.go @@ -52,7 +52,7 @@ func NewCmdBuildDownload(f *factory.Factory) *cobra.Command { pipelineRes := pipelineResolver.NewAggregateResolver( pipelineResolver.ResolveFromFlag(pipeline, f.Config), pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)), + pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne, f.GitRepository != nil)), ) // we resolve a build based on the following rules: diff --git a/pkg/cmd/build/new.go b/pkg/cmd/build/new.go index 4f3e020b..5245da02 100644 --- a/pkg/cmd/build/new.go +++ b/pkg/cmd/build/new.go @@ -83,7 +83,7 @@ func NewCmdBuildNew(f *factory.Factory) *cobra.Command { resolvers := resolver.NewAggregateResolver( resolver.ResolveFromFlag(pipeline, f.Config), resolver.ResolveFromConfig(f.Config, resolver.PickOne), - resolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOne)), + resolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOne, f.GitRepository != nil)), ) resolvedPipeline, err := resolvers.Resolve(cmd.Context()) diff --git a/pkg/cmd/build/rebuild.go b/pkg/cmd/build/rebuild.go index e28231d2..513fb59a 100644 --- a/pkg/cmd/build/rebuild.go +++ b/pkg/cmd/build/rebuild.go @@ -54,7 +54,7 @@ func NewCmdBuildRebuild(f *factory.Factory) *cobra.Command { pipelineRes := pipelineResolver.NewAggregateResolver( pipelineResolver.ResolveFromFlag(pipeline, f.Config), pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)), + pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne, f.GitRepository != nil)), ) // we resolve a build based on the following rules: diff --git a/pkg/cmd/build/view.go b/pkg/cmd/build/view.go index 24770c52..676958c7 100644 --- a/pkg/cmd/build/view.go +++ b/pkg/cmd/build/view.go @@ -61,7 +61,7 @@ func NewCmdBuildView(f *factory.Factory) *cobra.Command { pipelineRes := pipelineResolver.NewAggregateResolver( pipelineResolver.ResolveFromFlag(opts.Pipeline, f.Config), pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)), + pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne, f.GitRepository != nil)), ) // Resolve build options diff --git a/pkg/cmd/build/watch.go b/pkg/cmd/build/watch.go index 7c8accf7..3439c4fe 100644 --- a/pkg/cmd/build/watch.go +++ b/pkg/cmd/build/watch.go @@ -78,7 +78,7 @@ func NewCmdBuildWatch(f *factory.Factory) *cobra.Command { pipelineRes := pipelineResolver.NewAggregateResolver( pipelineResolver.ResolveFromFlag(opts.Pipeline, f.Config), pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne), - pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)), + pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne, f.GitRepository != nil)), ) optionsResolver := options.AggregateResolver{ diff --git a/pkg/cmd/configure/add/add.go b/pkg/cmd/configure/add/add.go index a7126ef2..a84000b9 100644 --- a/pkg/cmd/configure/add/add.go +++ b/pkg/cmd/configure/add/add.go @@ -28,13 +28,19 @@ func NewCmdAdd(f *factory.Factory) *cobra.Command { } func ConfigureWithCredentials(f *factory.Factory, org, token string) error { - if err := f.Config.SelectOrganization(org); err != nil { + if err := f.Config.SelectOrganization(org, f.GitRepository != nil); err != nil { return err } + return f.Config.SetTokenForOrg(org, token) } func ConfigureRun(f *factory.Factory) error { + // Check if we're in a Git repository + if f.GitRepository == nil { + return errors.New("not in a Git repository - bk should be configured at the root of a Git repository") + } + // Get organization slug org, err := promptForInput("Organization slug: ", false) if err != nil { @@ -48,7 +54,7 @@ func ConfigureRun(f *factory.Factory) error { existingToken := getTokenForOrg(f, org) if existingToken != "" { fmt.Printf("Using existing API token for organization: %s\n", org) - return f.Config.SelectOrganization(org) + return f.Config.SelectOrganization(org, f.GitRepository != nil) } // Get API token with password input (no echo) diff --git a/pkg/cmd/configure/add/add_test.go b/pkg/cmd/configure/add/add_test.go index 6e1f7210..26e37eb1 100644 --- a/pkg/cmd/configure/add/add_test.go +++ b/pkg/cmd/configure/add/add_test.go @@ -124,3 +124,34 @@ func TestConfigureTokenReuse(t *testing.T) { } }) } + +func TestConfigureRequiresGitRepository(t *testing.T) { + t.Parallel() + + t.Run("fails when not in a git repository", func(t *testing.T) { + t.Parallel() + fs := afero.NewMemMapFs() + conf := config.New(fs, nil) + + // Create a factory with nil GitRepository (simulating not being in a git repo) + f := &factory.Factory{Config: conf, GitRepository: nil} + + err := ConfigureRun(f) + + if err == nil { + t.Error("expected error when not in a git repository, got nil") + } + + expectedErr := "not in a Git repository - bk should be configured at the root of a Git repository" + if err.Error() != expectedErr { + t.Errorf("expected error message %q, got %q", expectedErr, err.Error()) + } + }) + + t.Run("succeeds when in a git repository", func(t *testing.T) { + // Skip this test because we can't easily mock the interactive prompts + // In a real implementation, we would need to mock the promptForInput function + // or restructure the code to allow for testing without interactive input + t.Skip("skipping test that requires interactive input") + }) +} diff --git a/pkg/cmd/factory/factory.go b/pkg/cmd/factory/factory.go index 8c8ac966..ec321d34 100644 --- a/pkg/cmd/factory/factory.go +++ b/pkg/cmd/factory/factory.go @@ -37,7 +37,13 @@ func (a *gqlHTTPClient) Do(req *http.Request) (*http.Response, error) { } func New(version string) (*Factory, error) { - repo, _ := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true, EnableDotGitCommonDir: true}) + repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true, EnableDotGitCommonDir: true}) + if err != nil { + if err == git.ErrRepositoryNotExists { + repo = nil + } + } + conf := config.New(nil, repo) buildkiteClient, err := buildkite.NewOpts( buildkite.WithBaseURL(conf.RESTAPIEndpoint()), diff --git a/pkg/cmd/pipeline/view.go b/pkg/cmd/pipeline/view.go index d9dc9331..7ea5034e 100644 --- a/pkg/cmd/pipeline/view.go +++ b/pkg/cmd/pipeline/view.go @@ -25,7 +25,7 @@ func NewCmdPipelineView(f *factory.Factory) *cobra.Command { pipelineRes := resolver.NewAggregateResolver( resolver.ResolveFromPositionalArgument(args, 0, f.Config), resolver.ResolveFromConfig(f.Config, resolver.PickOne), - resolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOne)), + resolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOne, f.GitRepository != nil)), ) pipeline, err := pipelineRes.Resolve(cmd.Context()) diff --git a/pkg/cmd/pkg/push_test.go b/pkg/cmd/pkg/push_test.go index 56d9c0c1..f7777fbc 100644 --- a/pkg/cmd/pkg/push_test.go +++ b/pkg/cmd/pkg/push_test.go @@ -202,7 +202,7 @@ func createCommand(t *testing.T, cci createCommandInput) (*cobra.Command, error) } conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("test") + conf.SelectOrganization("test", true) f := &factory.Factory{Config: conf, RestAPIClient: client} diff --git a/pkg/cmd/use/use.go b/pkg/cmd/use/use.go index 002227c8..23fb281e 100644 --- a/pkg/cmd/use/use.go +++ b/pkg/cmd/use/use.go @@ -21,14 +21,14 @@ func NewCmdUse(f *factory.Factory) *cobra.Command { if len(args) > 0 { org = &args[0] } - return useRun(org, f.Config) + return useRun(org, f.Config, f.GitRepository != nil) }, } return cmd } -func useRun(org *string, conf *config.Config) error { +func useRun(org *string, conf *config.Config, inGitRepo bool) error { var selected string // prompt to choose from configured orgs if one is not already selected @@ -51,7 +51,7 @@ func useRun(org *string, conf *config.Config) error { // if the selected org exists, use it if conf.HasConfiguredOrganization(selected) { fmt.Printf("Using configuration for `%s`\n", selected) - return conf.SelectOrganization(selected) + return conf.SelectOrganization(selected, inGitRepo) } // if the selected org doesnt exist, recommend configuring it and error out diff --git a/pkg/cmd/use/use_test.go b/pkg/cmd/use/use_test.go index f604c788..28c32ee6 100644 --- a/pkg/cmd/use/use_test.go +++ b/pkg/cmd/use/use_test.go @@ -13,9 +13,9 @@ func TestCmdUse(t *testing.T) { t.Run("uses already selected org", func(t *testing.T) { t.Parallel() conf := config.New(afero.NewMemMapFs(), nil) - conf.SelectOrganization("testing") + conf.SelectOrganization("testing", true) selected := "testing" - err := useRun(&selected, conf) + err := useRun(&selected, conf, true) if err != nil { t.Error("expected no error") } @@ -30,13 +30,13 @@ func TestCmdUse(t *testing.T) { // add some configurations fs := afero.NewMemMapFs() conf := config.New(fs, nil) - conf.SelectOrganization("testing") + conf.SelectOrganization("testing", true) conf.SetTokenForOrg("testing", "token") conf.SetTokenForOrg("default", "token") // now get a new empty config conf = config.New(fs, nil) selected := "testing" - err := useRun(&selected, conf) + err := useRun(&selected, conf, true) if err != nil { t.Errorf("expected no error: %s", err) } @@ -49,7 +49,7 @@ func TestCmdUse(t *testing.T) { t.Parallel() selected := "testing" conf := config.New(afero.NewMemMapFs(), nil) - err := useRun(&selected, conf) + err := useRun(&selected, conf, true) if err == nil { t.Error("expected an error") }