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
30 changes: 23 additions & 7 deletions cmd/pipeline/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

type CopyCmd struct {
Pipeline string `arg:"" help:"Source pipeline to copy (slug or org/slug). Uses current pipeline if not specified." optional:""`
Org string `help:"Organization slug." name:"org"`
Target string `help:"Name for the new pipeline, or org/name to copy to a different organization" short:"t"`
Cluster string `help:"Cluster name or ID for the new pipeline (required for cross-org copies if target org uses clusters)" short:"c"`
DryRun bool `help:"Show what would be copied without creating the pipeline"`
Expand All @@ -31,6 +32,14 @@ type copyTarget struct {
Name string
}

func (c *CopyCmd) orgSlug(f *factory.Factory) string {
if c.Org != "" {
return c.Org
}

return f.Config.OrganizationSlug()
}

func (c *CopyCmd) Help() string {
// returns the biggest help message ever seen
return `Copy an existing pipeline's configuration to create a new pipeline.
Expand Down Expand Up @@ -72,7 +81,7 @@ Examples:
}

func (c *CopyCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f, err := factory.New(factory.WithDebug(globals.EnableDebug()))
f, err := factory.New(factory.WithDebug(globals.EnableDebug()), factory.WithOrgOverride(c.Org))
if err != nil {
return err
}
Expand All @@ -81,7 +90,7 @@ func (c *CopyCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f.NoInput = globals.DisableInput()
f.Quiet = globals.IsQuiet()

if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {
if err := validation.ValidateConfigurationForOrg(f.Config, kongCtx.Command(), c.Org); err != nil {
return err
}

Expand Down Expand Up @@ -128,10 +137,17 @@ func (c *CopyCmd) resolveSourcePipeline(ctx context.Context, f *factory.Factory)
args = []string{c.Pipeline}
}

picker := resolver.PickOneWithFactory(f)
cachedPicker := resolver.CachedPicker(f.Config, picker)
repositoryResolver := resolver.ResolveFromRepository(f, cachedPicker)
if c.Org != "" {
repositoryResolver = resolver.ResolveFromRepositoryInOrg(f, cachedPicker, c.Org)
}

pipelineRes := resolver.NewAggregateResolver(
resolver.ResolveFromPositionalArgument(args, 0, f.Config),
resolver.ResolveFromConfig(f.Config, resolver.PickOneWithFactory(f)),
resolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOneWithFactory(f))),
resolver.WithOrg(c.Org, resolver.ResolveFromPositionalArgument(args, 0, f.Config)),
resolver.WithOrg(c.Org, resolver.ResolveFromConfig(f.Config, picker)),
repositoryResolver,
)

p, err := pipelineRes.Resolve(ctx)
Expand All @@ -156,7 +172,7 @@ func (c *CopyCmd) resolveTarget(f *factory.Factory, sourceName string) (*copyTar

// Parse target - could be "name" or "org/name"
// we check to see if `/` is present for org name, if not we use the existing org selected
return parseTarget(targetStr, f.Config.OrganizationSlug()), nil
return parseTarget(targetStr, c.orgSlug(f)), nil
}

// parseTarget parses a target string into org and name components.
Expand Down Expand Up @@ -272,7 +288,7 @@ func (c *CopyCmd) runCopy(kongCtx *kong.Context, f *factory.Factory, source *bui

// getClientForOrg creates a Buildkite client authenticated for the specified organization
func (c *CopyCmd) getClientForOrg(f *factory.Factory, org string) (*buildkite.Client, error) {
token := f.Config.GetTokenForOrg(org)
token := f.Config.APITokenForOrg(org)
if token == "" {
return nil, fmt.Errorf("no API token configured for organization %q. Run 'bk configure' to add it", org)
}
Expand Down
43 changes: 26 additions & 17 deletions cmd/pipeline/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/alecthomas/kong"
"github.com/buildkite/cli/v3/internal/cli"
"github.com/buildkite/cli/v3/internal/config"
"github.com/buildkite/cli/v3/internal/graphql"
bkIO "github.com/buildkite/cli/v3/internal/io"
"github.com/buildkite/cli/v3/pkg/cmd/factory"
Expand All @@ -20,6 +21,7 @@ import (

type CreateCmd struct {
Name string `arg:"" help:"Name of the pipeline" required:""`
Org string `help:"Organization slug." name:"org"`
Description string `help:"Description of the pipeline" short:"d"`
Repository string `help:"Repository URL" short:"r"`
ClusterID string `help:"Cluster name or ID to assign the pipeline to" short:"c"`
Expand All @@ -28,6 +30,13 @@ type CreateCmd struct {
output.OutputFlags
}

func (c *CreateCmd) orgSlug(conf *config.Config) string {
if c.Org != "" {
return c.Org
}
return conf.OrganizationSlug()
}

func (c *CreateCmd) Help() string {
return `Creates a new pipeline in the current org and outputs the URL to the pipeline.

Expand Down Expand Up @@ -59,7 +68,7 @@ Examples:
}

func (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f, err := factory.New(factory.WithDebug(globals.EnableDebug()))
f, err := factory.New(factory.WithDebug(globals.EnableDebug()), factory.WithOrgOverride(c.Org))
if err != nil {
return err
}
Expand All @@ -68,7 +77,7 @@ func (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f.NoInput = globals.DisableInput()
f.Quiet = globals.IsQuiet()

if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {
if err := validation.ValidateConfigurationForOrg(f.Config, kongCtx.Command(), c.Org); err != nil {
return err
}

Expand Down Expand Up @@ -124,7 +133,7 @@ func (c *CreateCmd) runPipelineCreate(kongCtx *kong.Context, f *factory.Factory)

func (c *CreateCmd) createPipeline(ctx context.Context, f *factory.Factory) (*buildkite.Pipeline, error) {
// Resolve cluster name to ID if provided
clusterID, err := resolveClusterID(ctx, f, c.ClusterID)
clusterID, err := resolveClusterID(ctx, f, c.orgSlug(f.Config), c.ClusterID)
if err != nil {
return nil, err
}
Expand All @@ -143,7 +152,7 @@ func (c *CreateCmd) createPipeline(ctx context.Context, f *factory.Factory) (*bu
Configuration: "steps:\n - label: \":pipeline:\"\n command: buildkite-agent pipeline upload",
}

pipeline, resp, err = f.RestAPIClient.Pipelines.Create(ctx, f.Config.OrganizationSlug(), createPipeline)
pipeline, resp, err = f.RestAPIClient.Pipelines.Create(ctx, c.orgSlug(f.Config), createPipeline)
})

if spinErr != nil {
Expand Down Expand Up @@ -171,7 +180,7 @@ func (c *CreateCmd) findPipelineByName(ctx context.Context, f *factory.Factory)
},
}

pipelines, _, err := f.RestAPIClient.Pipelines.List(ctx, f.Config.OrganizationSlug(), &opts)
pipelines, _, err := f.RestAPIClient.Pipelines.List(ctx, c.orgSlug(f.Config), &opts)
if err != nil {
return nil
}
Expand Down Expand Up @@ -238,12 +247,12 @@ func initialisePipelineDryRun() PipelineDryRun {
func (c *CreateCmd) createPipelineDryRun(ctx context.Context, f *factory.Factory) (*PipelineDryRun, error) {
pipelineSlug := generateSlug(c.Name)

pipelineSlug, err := getAvailablePipelineSlug(ctx, f, pipelineSlug, c.Name)
pipelineSlug, err := getAvailablePipelineSlug(ctx, f, c.orgSlug(f.Config), pipelineSlug, c.Name)
if err != nil {
return nil, err
}

orgSlug := f.Config.OrganizationSlug()
orgSlug := c.orgSlug(f.Config)
pipeline := initialisePipelineDryRun()

pipeline.ID = "00000000-0000-0000-0000-000000000000"
Expand Down Expand Up @@ -322,8 +331,8 @@ func extractRepoPath(repoURL string) string {
return repoURL
}

func getAvailablePipelineSlug(ctx context.Context, f *factory.Factory, pipelineSlug, pipelineName string) (string, error) {
pipeline, resp, err := f.RestAPIClient.Pipelines.Get(ctx, f.Config.OrganizationSlug(), pipelineSlug)
func getAvailablePipelineSlug(ctx context.Context, f *factory.Factory, org, pipelineSlug, pipelineName string) (string, error) {
pipeline, resp, err := f.RestAPIClient.Pipelines.Get(ctx, org, pipelineSlug)
if err != nil {
if resp != nil && resp.StatusCode == 404 {
return pipelineSlug, nil
Expand All @@ -338,7 +347,7 @@ func getAvailablePipelineSlug(ctx context.Context, f *factory.Factory, pipelineS
counter := 1
for {
newSlug := fmt.Sprintf("%s-%d", pipelineSlug, counter)
pipeline, resp, err := f.RestAPIClient.Pipelines.Get(ctx, f.Config.OrganizationSlug(), newSlug)
pipeline, resp, err := f.RestAPIClient.Pipelines.Get(ctx, org, newSlug)
if err != nil {
if resp != nil && resp.StatusCode == 404 {
return newSlug, nil
Expand All @@ -364,7 +373,7 @@ func getClusterUrl(orgSlug, clusterID string) string {
return fmt.Sprintf("https://api.buildkite.com/v2/organizations/%s/clusters/%s", orgSlug, clusterID)
}

func getClusters(ctx context.Context, f *factory.Factory) (map[string]string, error) {
func getClusters(ctx context.Context, f *factory.Factory, org string) (map[string]string, error) {
clusterMap := make(map[string]string)
page := 1
per_page := 30
Expand All @@ -376,7 +385,7 @@ func getClusters(ctx context.Context, f *factory.Factory) (map[string]string, er
PerPage: per_page,
},
}
clusters, resp, err := f.RestAPIClient.Clusters.List(ctx, f.Config.OrganizationSlug(), &opts)
clusters, resp, err := f.RestAPIClient.Clusters.List(ctx, org, &opts)
if err != nil {
return map[string]string{}, err
}
Expand All @@ -398,8 +407,8 @@ func getClusters(ctx context.Context, f *factory.Factory) (map[string]string, er
return clusterMap, nil
}

func listClusterNames(ctx context.Context, f *factory.Factory) ([]string, error) {
clusterMap, err := getClusters(ctx, f)
func listClusterNames(ctx context.Context, f *factory.Factory, org string) ([]string, error) {
clusterMap, err := getClusters(ctx, f, org)
if err != nil {
return nil, err
}
Expand All @@ -413,13 +422,13 @@ func listClusterNames(ctx context.Context, f *factory.Factory) ([]string, error)
return clusterNames, nil
}

func resolveClusterID(ctx context.Context, f *factory.Factory, clusterNameOrID string) (string, error) {
func resolveClusterID(ctx context.Context, f *factory.Factory, org, clusterNameOrID string) (string, error) {
if clusterNameOrID == "" {
return "", nil
}

// First, try to get clusters map
clusterMap, err := getClusters(ctx, f)
clusterMap, err := getClusters(ctx, f, org)
if err != nil {
return "", fmt.Errorf("failed to fetch clusters: %w", err)
}
Expand All @@ -437,7 +446,7 @@ func resolveClusterID(ctx context.Context, f *factory.Factory, clusterNameOrID s
}

// Not found - provide helpful error with available clusters
clusterNames, _ := listClusterNames(ctx, f)
clusterNames, _ := listClusterNames(ctx, f, org)
if len(clusterNames) > 0 {
return "", fmt.Errorf("cluster '%s' not found. Available clusters: %s", clusterNameOrID, strings.Join(clusterNames, ", "))
}
Expand Down
10 changes: 7 additions & 3 deletions cmd/pipeline/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
)

type ListCmd struct {
Org string `help:"Organization slug." name:"org"`
Name string `help:"Filter pipelines by name (supports partial matches, case insensitive)" short:"n"`
Repository string `help:"Filter pipelines by repository URL (supports partial matches, case insensitive)" short:"r"`
Limit int `help:"Maximum number of pipelines to return (max: 3000)" short:"l" default:"100"`
Expand Down Expand Up @@ -55,7 +56,7 @@ Examples:
}

func (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f, err := factory.New(factory.WithDebug(globals.EnableDebug()))
f, err := factory.New(factory.WithDebug(globals.EnableDebug()), factory.WithOrgOverride(c.Org))
if err != nil {
return err
}
Expand All @@ -65,7 +66,7 @@ func (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f.Quiet = globals.IsQuiet()
f.NoPager = f.NoPager || globals.DisablePager()

if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {
if err := validation.ValidateConfigurationForOrg(f.Config, kongCtx.Command(), c.Org); err != nil {
return err
}

Expand All @@ -78,7 +79,10 @@ func (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
}

func (c *ListCmd) runPipelineList(ctx context.Context, f *factory.Factory) error {
org := f.Config.OrganizationSlug()
org := c.Org
if org == "" {
org = f.Config.OrganizationSlug()
}
if org == "" {
return fmt.Errorf("no organization configured. Use 'bk configure' to set up your organization")
}
Expand Down
36 changes: 27 additions & 9 deletions cmd/pipeline/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ import (
)

type ViewCmd struct {
Pipeline string `arg:"" help:"The pipeline to view. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}." optional:""`
Web bool `help:"Open the pipeline in a web browser." short:"w"`
// Pipeline is the positional arg; PipelineFlag (--pipeline/-p) takes priority when both are provided.
Pipeline string `arg:"" help:"The pipeline to view. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}." optional:""`
PipelineFlag string `help:"The pipeline to view. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}." short:"p" name:"pipeline"`
Org string `help:"Organization slug." name:"org"`
Web bool `help:"Open the pipeline in a web browser." short:"w"`
output.OutputFlags
}

Expand All @@ -32,6 +35,9 @@ Examples:
# View a pipeline
$ bk pipeline view my-pipeline

# View a pipeline using flags
$ bk pipeline view --org my-org --pipeline my-pipeline

# View a pipeline in a specific organization
$ bk pipeline view my-org/my-pipeline

Expand All @@ -44,7 +50,7 @@ Examples:
}

func (c *ViewCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f, err := factory.New(factory.WithDebug(globals.EnableDebug()))
f, err := factory.New(factory.WithDebug(globals.EnableDebug()), factory.WithOrgOverride(c.Org))
if err != nil {
return err
}
Expand All @@ -54,22 +60,34 @@ func (c *ViewCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f.Quiet = globals.IsQuiet()
f.NoPager = f.NoPager || globals.DisablePager()

if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {
if err := validation.ValidateConfigurationForOrg(f.Config, kongCtx.Command(), c.Org); err != nil {
return err
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

pipelineArg := c.PipelineFlag
if pipelineArg == "" {
pipelineArg = c.Pipeline
}

var args []string
if c.Pipeline != "" {
args = []string{c.Pipeline}
if pipelineArg != "" {
args = []string{pipelineArg}
}

picker := resolver.PickOneWithFactory(f)
cachedPicker := resolver.CachedPicker(f.Config, picker)
repositoryResolver := resolver.ResolveFromRepository(f, cachedPicker)
if c.Org != "" {
repositoryResolver = resolver.ResolveFromRepositoryInOrg(f, cachedPicker, c.Org)
}

pipelineRes := resolver.NewAggregateResolver(
resolver.ResolveFromPositionalArgument(args, 0, f.Config),
resolver.ResolveFromConfig(f.Config, resolver.PickOneWithFactory(f)),
resolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOneWithFactory(f))),
resolver.WithOrg(c.Org, resolver.ResolveFromPositionalArgument(args, 0, f.Config)),
resolver.WithOrg(c.Org, resolver.ResolveFromConfig(f.Config, picker)),
repositoryResolver,
)

pipeline, err := pipelineRes.Resolve(ctx)
Expand Down
14 changes: 9 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,27 @@ func (conf *Config) SelectOrganization(org string, inGitRepo bool) error {
// APIToken gets the API token configured for the currently selected organization.
// Precedence: environment variable > keyring > user config > local config
func (conf *Config) APIToken() string {
return conf.APITokenForOrg(conf.OrganizationSlug())
}

// APITokenForOrg gets the API token for a specific organization.
// Precedence: environment variable > keyring > user config > local config
func (conf *Config) APITokenForOrg(org string) string {
if token := os.Getenv("BUILDKITE_API_TOKEN"); token != "" {
return token
}

slug := conf.OrganizationSlug()

// Try keyring first
kr := keyring.New()
if kr.IsAvailable() {
if token, err := kr.Get(slug); err == nil && token != "" {
if token, err := kr.Get(org); err == nil && token != "" {
return token
}
}

return firstNonEmpty(
conf.user.getToken(slug),
conf.local.getToken(slug),
conf.user.getToken(org),
conf.local.getToken(org),
)
}

Expand Down
Loading