github.com/buildkite/cli
Advanced tools
| package cli | ||
| import ( | ||
| "fmt" | ||
| "github.com/buildkite/cli/git" | ||
| "github.com/buildkite/cli/graphql" | ||
| "github.com/fatih/color" | ||
| ) | ||
| type BuildCreateCommandContext struct { | ||
| TerminalContext | ||
| KeyringContext | ||
| Debug bool | ||
| DebugHTTP bool | ||
| Dir string | ||
| PipelineSlug string | ||
| Branch string | ||
| Commit string | ||
| Message string | ||
| } | ||
| func BuildCreateCommand(ctx BuildCreateCommandContext) error { | ||
| params := buildkiteBuildParams{ | ||
| Branch: ctx.Branch, | ||
| Commit: ctx.Commit, | ||
| Message: ctx.Message, | ||
| } | ||
| bk, err := ctx.BuildkiteGraphQLClient() | ||
| if err != nil { | ||
| ctx.Failure(err.Error()) | ||
| return NewExitError(err, 1) | ||
| } | ||
| pipelineSlug := ctx.PipelineSlug | ||
| if pipelineSlug == "" { | ||
| gitRemote, err := git.Remote(ctx.Dir) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| allPipelines, err := listPipelines(bk) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| ps := pipelineSelect{ | ||
| Pipelines: allPipelines, | ||
| Filter: func(p pipeline) bool { | ||
| return git.MatchRemotes(p.RepositoryURL, gitRemote) | ||
| }, | ||
| } | ||
| pipeline, err := ps.Run() | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| params.PipelineID = pipeline.ID | ||
| pipelineSlug = fmt.Sprintf("%s/%s", pipeline.Org, pipeline.Slug) | ||
| if params.Branch == "" { | ||
| params.Branch, err = git.Branch(ctx.Dir) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| } | ||
| if params.Commit == "" { | ||
| params.Commit, err = git.Commit(ctx.Dir) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| } | ||
| if params.Message == "" { | ||
| params.Message, err = git.Message(ctx.Dir) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| } | ||
| } else { | ||
| params.PipelineID, err = getBuildkitePipelineID(bk, ctx.PipelineSlug) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| } | ||
| if params.Branch == "" { | ||
| params.Branch = `master` | ||
| } | ||
| if params.Commit == "" { | ||
| params.Commit = `HEAD` | ||
| } | ||
| if params.Message == "" { | ||
| params.Message = `Build triggered with bk cli :rocket:` | ||
| } | ||
| debugf("Build will use branch=%q, commit=%q, message=%q", | ||
| params.Branch, params.Commit, params.Message) | ||
| buildTry := ctx.Try() | ||
| buildTry.Start(fmt.Sprintf("Triggering a build on pipeline %s", pipelineSlug)) | ||
| build, err := createBuildkiteBuild(bk, params) | ||
| if err != nil { | ||
| buildTry.Failure(err.Error()) | ||
| return NewExitError(err, 1) | ||
| } | ||
| buildTry.Success(fmt.Sprintf("Created #%d", build.Number)) | ||
| ctx.Printf(color.GreenString("\nCheck out your build at %s 🚀\n"), build.URL) | ||
| return nil | ||
| } | ||
| func getBuildkitePipelineID(client *graphql.Client, slug string) (string, error) { | ||
| resp, err := client.Do(` | ||
| query($slug:ID!) { | ||
| pipeline(slug: $slug) { | ||
| id | ||
| } | ||
| } | ||
| `, map[string]interface{}{ | ||
| "slug": slug, | ||
| }) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| var parsedResp struct { | ||
| Data struct { | ||
| Pipeline struct { | ||
| ID string `json:"id"` | ||
| } `json:"pipeline"` | ||
| } `json:"data"` | ||
| } | ||
| if err = resp.DecodeInto(&parsedResp); err != nil { | ||
| return "", fmt.Errorf("Failed to parse GraphQL response: %v", err) | ||
| } | ||
| if parsedResp.Data.Pipeline.ID == "" { | ||
| return "", fmt.Errorf("Failed to find pipeline id for %s", slug) | ||
| } | ||
| return parsedResp.Data.Pipeline.ID, nil | ||
| } | ||
| type buildkiteBuildDetails struct { | ||
| URL string | ||
| Number int | ||
| } | ||
| type buildkiteBuildParams struct { | ||
| PipelineID string | ||
| Commit string | ||
| Branch string | ||
| Message string | ||
| } | ||
| func createBuildkiteBuild(client *graphql.Client, params buildkiteBuildParams) (buildkiteBuildDetails, error) { | ||
| resp, err := client.Do(` | ||
| mutation($input: BuildCreateInput!) { | ||
| buildCreate(input: $input) { | ||
| build { | ||
| url | ||
| number | ||
| } | ||
| } | ||
| } | ||
| `, map[string]interface{}{ | ||
| "input": map[string]interface{}{ | ||
| "pipelineID": params.PipelineID, | ||
| "message": params.Message, | ||
| "commit": params.Commit, | ||
| "branch": params.Branch, | ||
| }}) | ||
| if err != nil { | ||
| return buildkiteBuildDetails{}, err | ||
| } | ||
| var parsedResp struct { | ||
| Data struct { | ||
| BuildCreate struct { | ||
| Build struct { | ||
| URL string `json:"url"` | ||
| Number int `json:"number"` | ||
| } `json:"build"` | ||
| } `json:"buildCreate"` | ||
| } `json:"data"` | ||
| } | ||
| if err = resp.DecodeInto(&parsedResp); err != nil { | ||
| return buildkiteBuildDetails{}, | ||
| fmt.Errorf("Failed to parse GraphQL response: %v", err) | ||
| } | ||
| return buildkiteBuildDetails{ | ||
| URL: parsedResp.Data.BuildCreate.Build.URL, | ||
| Number: parsedResp.Data.BuildCreate.Build.Number, | ||
| }, nil | ||
| } |
+106
| package cli | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log" | ||
| "os" | ||
| "os/exec" | ||
| "os/signal" | ||
| "path/filepath" | ||
| "regexp" | ||
| "strings" | ||
| "github.com/buildkite/cli/local" | ||
| ) | ||
| type LocalRunCommandContext struct { | ||
| TerminalContext | ||
| KeyringContext | ||
| Debug bool | ||
| DebugHTTP bool | ||
| File *os.File | ||
| Env []string | ||
| Command string | ||
| StepFilterRegex *regexp.Regexp | ||
| Prompt bool | ||
| DryRun bool | ||
| } | ||
| func LocalRunCommand(ctx LocalRunCommandContext) error { | ||
| quit := make(chan os.Signal, 1) | ||
| signal.Notify(quit, os.Interrupt) | ||
| if ctx.Debug { | ||
| local.Debug = true | ||
| } | ||
| cancelCtx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
| go func() { | ||
| <-quit | ||
| fmt.Printf("\n>>> Gracefully shutting down...\n") | ||
| cancel() | ||
| }() | ||
| wd, err := os.Getwd() | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| commit, err := gitCommit() | ||
| if err != nil { | ||
| log.Printf("Error getting git commit: %v", err) | ||
| commit = "no_commit_found" | ||
| } | ||
| branch, err := gitBranch() | ||
| if err != nil { | ||
| log.Printf("Error getting git branch: %v", err) | ||
| branch = "master" | ||
| } | ||
| command := ctx.Command | ||
| if ctx.File != nil { | ||
| command = fmt.Sprintf("buildkite-agent pipeline upload %q", ctx.File.Name()) | ||
| } | ||
| if err := local.Run(cancelCtx, local.RunParams{ | ||
| Env: ctx.Env, | ||
| DryRun: ctx.DryRun, | ||
| Command: command, | ||
| Dir: wd, | ||
| Prompt: ctx.Prompt, | ||
| StepFilter: ctx.StepFilterRegex, | ||
| JobTemplate: local.Job{ | ||
| Commit: commit, | ||
| Branch: branch, | ||
| Repository: wd, | ||
| OrganizationSlug: "local", | ||
| PipelineSlug: filepath.Base(wd), | ||
| }, | ||
| }); err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| return nil | ||
| } | ||
| func gitBranch() (string, error) { | ||
| out, err := exec.Command(`git`, `rev-parse`, `--abbrev-ref`, `HEAD`).Output() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return strings.TrimSpace(string(out)), nil | ||
| } | ||
| func gitCommit() (string, error) { | ||
| out, err := exec.Command(`git`, `rev-parse`, `HEAD`).Output() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return strings.TrimSpace(string(out)), nil | ||
| } |
| package cli | ||
| import ( | ||
| "fmt" | ||
| "github.com/sahilm/fuzzy" | ||
| ) | ||
| type PipelineListCommandContext struct { | ||
| TerminalContext | ||
| KeyringContext | ||
| Debug bool | ||
| DebugHTTP bool | ||
| Fuzzy string | ||
| Limit int | ||
| ShowURL bool | ||
| } | ||
| func PipelineListCommand(ctx PipelineListCommandContext) error { | ||
| bk, err := ctx.BuildkiteGraphQLClient() | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| pipelines, err := listPipelines(bk) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| var counter int | ||
| var pipelineStrings []string | ||
| for _, pipeline := range pipelines { | ||
| pipelineStrings = append(pipelineStrings, | ||
| fmt.Sprintf("%s/%s", pipeline.Org, pipeline.Slug)) | ||
| } | ||
| if ctx.Fuzzy != "" { | ||
| const bold = "\033[1m%s\033[0m" | ||
| matches := fuzzy.Find(ctx.Fuzzy, pipelineStrings) | ||
| for _, match := range matches { | ||
| counter++ | ||
| if ctx.Limit > 0 && counter > ctx.Limit { | ||
| break | ||
| } | ||
| if ctx.ShowURL { | ||
| ctx.Printf(`https://buildkite.com/`) | ||
| } | ||
| for i := 0; i < len(match.Str); i++ { | ||
| if contains(i, match.MatchedIndexes) { | ||
| fmt.Print(fmt.Sprintf(bold, string(match.Str[i]))) | ||
| } else { | ||
| fmt.Print(string(match.Str[i])) | ||
| } | ||
| } | ||
| fmt.Println() | ||
| } | ||
| return nil | ||
| } | ||
| for _, p := range pipelineStrings { | ||
| counter++ | ||
| if ctx.Limit > 0 && counter > ctx.Limit { | ||
| break | ||
| } | ||
| if ctx.ShowURL { | ||
| ctx.Printf(`https://buildkite.com/`) | ||
| } | ||
| ctx.Println(p) | ||
| } | ||
| return nil | ||
| } | ||
| func contains(needle int, haystack []int) bool { | ||
| for _, i := range haystack { | ||
| if needle == i { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } |
@@ -6,3 +6,3 @@ # Config for https://github.com/apps/tap-release | ||
| template: | | ||
| # Generated by https://github.com/buildkite/bk/blob/master/.github/tap-release.yml | ||
| # Generated by https://github.com/buildkite/cli/blob/master/.github/tap-release.yml | ||
@@ -17,3 +17,3 @@ class Bk < Formula | ||
| def install | ||
| mv prefix/"bk-darwin-amd64-$STABLE_VERSION_NUMBER", "bk" | ||
| mv "bk-darwin-amd64-$STABLE_VERSION_NUMBER", "bk" | ||
| bin.install "bk" | ||
@@ -26,4 +26,1 @@ end | ||
| end | ||
| branches: | ||
| - master | ||
| - add-homebrew-tap |
+169
-73
@@ -10,2 +10,3 @@ package main | ||
| "github.com/fatih/color" | ||
| "golang.org/x/crypto/ssh/terminal" | ||
@@ -43,6 +44,11 @@ "github.com/99designs/keyring" | ||
| var ( | ||
| debug bool | ||
| debugGraphQL bool | ||
| keyringBackend string | ||
| keyringImpl keyring.Keyring | ||
| debug bool | ||
| debugGraphQL bool | ||
| keyringBackend string | ||
| keyringFileDir string | ||
| keyringKeychain string | ||
| keyringPassDir string | ||
| keyringPassCmd string | ||
| keyringPassPrefix string | ||
| keyringImpl keyring.Keyring | ||
| ) | ||
@@ -60,2 +66,23 @@ | ||
| app.Flag("keyring-file-dir", "Use the file keyring-backend with a specific directory"). | ||
| OverrideDefaultFromEnvar("BUILDKITE_CLI_KEYRING_FILE_DIR"). | ||
| Default("~/.buildkite/keyring/"). | ||
| StringVar(&keyringFileDir) | ||
| app.Flag("keyring-keychain", "Use the macOS keychain keyring-backend with a specific keychain (defaults to login)"). | ||
| OverrideDefaultFromEnvar("BUILDKITE_CLI_KEYRING_KEYCHAIN"). | ||
| StringVar(&keyringKeychain) | ||
| app.Flag("keyring-pass-dir", "Use the pass password manager with a specific directory"). | ||
| OverrideDefaultFromEnvar("BUILDKITE_CLI_KEYRING_PASS_DIR"). | ||
| StringVar(&keyringPassDir) | ||
| app.Flag("keyring-pass-cmd", "Name of the pass executable"). | ||
| OverrideDefaultFromEnvar("BUILDKITE_CLI_KEYRING_PASS_CMD"). | ||
| StringVar(&keyringPassCmd) | ||
| app.Flag("keyring-pass-prefix", "Prefix to prepend to the item path stored in pass"). | ||
| OverrideDefaultFromEnvar("BUILDKITE_CLI_KEYRING_PASS_PREFIX"). | ||
| StringVar(&keyringPassPrefix) | ||
| app.PreAction(func(c *kingpin.ParseContext) (err error) { | ||
@@ -69,4 +96,48 @@ if debug { | ||
| } | ||
| // infer keyring backend types if we get certain config | ||
| if keyringFileDir != "" { | ||
| keyringBackend = `file` | ||
| } else if keyringKeychain != "" { | ||
| keyringBackend = `keychain` | ||
| } else if keyringPassDir != "" { | ||
| keyringBackend = `pass` | ||
| } | ||
| var allowedBackends []keyring.BackendType | ||
| if keyringBackend != `` { | ||
| allowedBackends = append(allowedBackends, keyring.BackendType(keyringBackend)) | ||
| } | ||
| // otherwise use one of the defaults | ||
| if keyringBackend == `` { | ||
| for _, k := range backendsAvailable { | ||
| switch keyring.BackendType(k) { | ||
| // secret-service and kwallet are kind of the worst | ||
| case keyring.KWalletBackend, keyring.SecretServiceBackend: | ||
| continue | ||
| default: | ||
| allowedBackends = append(allowedBackends, keyring.BackendType(k)) | ||
| } | ||
| } | ||
| } | ||
| // default keychain to the login keychain | ||
| if keyringBackend == `keyring` && keyringKeychain == `` { | ||
| keyringKeychain = `login` | ||
| } | ||
| keyringImpl, err = keyring.Open(keyring.Config{ | ||
| ServiceName: "buildkite", | ||
| ServiceName: "buildkite", | ||
| AllowedBackends: allowedBackends, | ||
| KeychainName: keyringKeychain, | ||
| FileDir: keyringFileDir, | ||
| FilePasswordFunc: terminalPrompt, | ||
| PassDir: keyringPassDir, | ||
| PassCmd: keyringPassCmd, | ||
| PassPrefix: keyringPassPrefix, | ||
| LibSecretCollectionName: "buildkite", | ||
| KWalletAppID: "buildkite", | ||
| KWalletFolder: "buildkite", | ||
| KeychainTrustApplication: true, | ||
| }) | ||
@@ -137,41 +208,41 @@ if err != nil { | ||
| // -------------------------- | ||
| // create commands | ||
| // build commands | ||
| createCmd := app.Command("create", "Create various things") | ||
| buildCmd := app.Command("build", "Operate on builds") | ||
| createBuildCtx := cli.CreateBuildCommandContext{} | ||
| createBuildCmd := createCmd. | ||
| Command("build", "Create a new build in a pipeline"). | ||
| buildCreateCtx := cli.BuildCreateCommandContext{} | ||
| buildCreateCmd := buildCmd. | ||
| Command("create", "Create a new build in a pipeline"). | ||
| Action(func(c *kingpin.ParseContext) error { | ||
| createBuildCtx.Debug = debug | ||
| createBuildCtx.Keyring = keyringImpl | ||
| createBuildCtx.TerminalContext = &cli.Terminal{} | ||
| buildCreateCtx.Debug = debug | ||
| buildCreateCtx.Keyring = keyringImpl | ||
| buildCreateCtx.TerminalContext = &cli.Terminal{} | ||
| // Default to the current directory | ||
| if createBuildCtx.PipelineSlug == "" && createBuildCtx.Dir == "" { | ||
| createBuildCtx.Dir = "." | ||
| if buildCreateCtx.PipelineSlug == "" && buildCreateCtx.Dir == "" { | ||
| buildCreateCtx.Dir = "." | ||
| } | ||
| return cli.CreateBuildCommand(createBuildCtx) | ||
| return cli.BuildCreateCommand(buildCreateCtx) | ||
| }) | ||
| createBuildCmd. | ||
| buildCreateCmd. | ||
| Flag("dir", "Build a specific directory, defaults to the current"). | ||
| ExistingDirVar(&createBuildCtx.Dir) | ||
| ExistingDirVar(&buildCreateCtx.Dir) | ||
| createBuildCmd. | ||
| buildCreateCmd. | ||
| Flag("pipeline", "Build a specific pipeline rather than a directory"). | ||
| StringVar(&createBuildCtx.PipelineSlug) | ||
| StringVar(&buildCreateCtx.PipelineSlug) | ||
| createBuildCmd. | ||
| buildCreateCmd. | ||
| Flag("message", "The message to use for the build"). | ||
| StringVar(&createBuildCtx.Message) | ||
| StringVar(&buildCreateCtx.Message) | ||
| createBuildCmd. | ||
| buildCreateCmd. | ||
| Flag("commit", "The commit to use for the build"). | ||
| StringVar(&createBuildCtx.Commit) | ||
| StringVar(&buildCreateCtx.Commit) | ||
| createBuildCmd. | ||
| buildCreateCmd. | ||
| Flag("branch", "The branch to use for the build"). | ||
| StringVar(&createBuildCtx.Branch) | ||
| StringVar(&buildCreateCtx.Branch) | ||
@@ -201,72 +272,88 @@ // -------------------------- | ||
| // -------------------------- | ||
| // list command | ||
| // pipeline commands | ||
| listCmd := app.Command("list", "List various things") | ||
| pipelineCmd := app.Command("pipeline", "Operate on pipeline") | ||
| listPipelinesCtx := cli.ListPipelinesCommandContext{} | ||
| listPipelinesCmd := listCmd. | ||
| Command("pipelines", "List buildkite pipelines"). | ||
| pipelineListCtx := cli.PipelineListCommandContext{} | ||
| pipelineListCmd := pipelineCmd. | ||
| Command("list", "List buildkite pipelines"). | ||
| Default(). | ||
| Action(func(c *kingpin.ParseContext) error { | ||
| listPipelinesCtx.Debug = debug | ||
| listPipelinesCtx.Keyring = keyringImpl | ||
| listPipelinesCtx.TerminalContext = &cli.Terminal{} | ||
| return cli.ListPipelinesCommand(listPipelinesCtx) | ||
| pipelineListCtx.Debug = debug | ||
| pipelineListCtx.Keyring = keyringImpl | ||
| pipelineListCtx.TerminalContext = &cli.Terminal{} | ||
| return cli.PipelineListCommand(pipelineListCtx) | ||
| }) | ||
| listPipelinesCmd. | ||
| pipelineListCmd. | ||
| Flag("fuzzy", "Fuzzy filter pipelines based on org and slug"). | ||
| StringVar(&listPipelinesCtx.Fuzzy) | ||
| StringVar(&pipelineListCtx.Fuzzy) | ||
| listPipelinesCmd. | ||
| pipelineListCmd. | ||
| Flag("url", "Show buildkite.com urls for pipelines"). | ||
| BoolVar(&listPipelinesCtx.ShowURL) | ||
| BoolVar(&pipelineListCtx.ShowURL) | ||
| listPipelinesCmd. | ||
| pipelineListCmd. | ||
| Flag("limit", "How many pipelines to output"). | ||
| IntVar(&listPipelinesCtx.Limit) | ||
| IntVar(&pipelineListCtx.Limit) | ||
| // -------------------------- | ||
| // run command | ||
| // local command | ||
| runCmd := app.Command("run", "Run builds") | ||
| localCmd := app.Command("local", "Operate on your local repositories") | ||
| runLocalCmdCtx := cli.RunLocalCommandContext{} | ||
| runLocalCmd := runCmd. | ||
| Command("local", "Run a pipeline locally"). | ||
| Default(). | ||
| Action(func(c *kingpin.ParseContext) error { | ||
| runLocalCmdCtx.Debug = debug | ||
| runLocalCmdCtx.Keyring = keyringImpl | ||
| runLocalCmdCtx.TerminalContext = &cli.Terminal{} | ||
| return cli.RunLocalCommand(runLocalCmdCtx) | ||
| }) | ||
| var setupRunCmd = func(cmd *kingpin.CmdClause, runCmdCtx *cli.LocalRunCommandContext) { | ||
| cmd. | ||
| Flag("command", "The initial command to execute"). | ||
| Default("buildkite-agent pipeline upload"). | ||
| StringVar(&runCmdCtx.Command) | ||
| runLocalCmd. | ||
| Flag("command", "The initial command to execute"). | ||
| Default("buildkite-agent pipeline upload"). | ||
| StringVar(&runLocalCmdCtx.Command) | ||
| cmd. | ||
| Flag("filter", "A regex to filter step labels with"). | ||
| RegexpVar(&runCmdCtx.StepFilterRegex) | ||
| runLocalCmd. | ||
| Flag("filter", "A regex to filter step labels with"). | ||
| RegexpVar(&runLocalCmdCtx.StepFilterRegex) | ||
| cmd. | ||
| Flag("dry-run", "Show what steps will be executed"). | ||
| BoolVar(&runCmdCtx.DryRun) | ||
| runLocalCmd. | ||
| Flag("dry-run", "Show what steps will be executed"). | ||
| BoolVar(&runLocalCmdCtx.DryRun) | ||
| cmd. | ||
| Flag("prompt", "Prompt for each step before executing"). | ||
| BoolVar(&runCmdCtx.Prompt) | ||
| runLocalCmd. | ||
| Flag("prompt", "Prompt for each step before executing"). | ||
| BoolVar(&runLocalCmdCtx.Prompt) | ||
| cmd. | ||
| Flag("env", "Environment to pass to the agent"). | ||
| Short('E'). | ||
| StringsVar(&runCmdCtx.Env) | ||
| runLocalCmd. | ||
| Flag("env", "Environment to pass to the agent"). | ||
| Short('E'). | ||
| StringsVar(&runLocalCmdCtx.Env) | ||
| cmd. | ||
| Arg("file", "A specific pipeline file to upload"). | ||
| FileVar(&runCmdCtx.File) | ||
| } | ||
| runLocalCmd. | ||
| Arg("file", "A specific pipeline file to upload"). | ||
| FileVar(&runLocalCmdCtx.File) | ||
| localRunCmdCtx := cli.LocalRunCommandContext{} | ||
| localRunCmd := localCmd. | ||
| Command("run", "Run a pipeline locally"). | ||
| Default(). | ||
| Action(func(c *kingpin.ParseContext) error { | ||
| localRunCmdCtx.Debug = debug | ||
| localRunCmdCtx.Keyring = keyringImpl | ||
| localRunCmdCtx.TerminalContext = &cli.Terminal{} | ||
| return cli.LocalRunCommand(localRunCmdCtx) | ||
| }) | ||
| setupRunCmd(localRunCmd, &localRunCmdCtx) | ||
| runCmdCtx := cli.LocalRunCommandContext{} | ||
| runCmd := app. | ||
| Command("run", "Run a pipeline locally (alias for local run)"). | ||
| Default(). | ||
| Action(func(c *kingpin.ParseContext) error { | ||
| runCmdCtx.Debug = debug | ||
| runCmdCtx.Keyring = keyringImpl | ||
| runCmdCtx.TerminalContext = &cli.Terminal{} | ||
| return cli.LocalRunCommand(runCmdCtx) | ||
| }) | ||
| setupRunCmd(runCmd, &runCmdCtx) | ||
| // -------------------------- | ||
@@ -284,3 +371,12 @@ // run the app, parse args | ||
| } | ||
| } | ||
| func terminalPrompt(prompt string) (string, error) { | ||
| fmt.Printf("%s: ", prompt) | ||
| b, err := terminal.ReadPassword(int(os.Stdin.Fd())) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| fmt.Println() | ||
| return string(b), nil | ||
| } |
+3
-3
| module github.com/buildkite/cli | ||
| require ( | ||
| github.com/99designs/keyring v0.0.0-20180523072454-ccd0779e6f10 | ||
| github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f | ||
| github.com/ahmetb/go-cursor v0.0.0-20131010032410-8136607ea412 | ||
@@ -15,3 +15,3 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect | ||
| github.com/danieljoos/wincred v1.0.1 // indirect | ||
| github.com/davecgh/go-spew v1.1.1 | ||
| github.com/davecgh/go-spew v1.1.1 // indirect | ||
| github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae // indirect | ||
@@ -39,3 +39,3 @@ github.com/fatih/color v1.7.0 | ||
| github.com/stretchr/testify v1.2.2 // indirect | ||
| golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 | ||
| golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 | ||
| golang.org/x/net v0.0.0-20180530034148-89e543239a64 // indirect | ||
@@ -42,0 +42,0 @@ golang.org/x/oauth2 v0.0.0-20180529203656-ec22f46f877b |
+4
-2
@@ -1,3 +0,3 @@ | ||
| github.com/99designs/keyring v0.0.0-20180523072454-ccd0779e6f10 h1:sQgghNA6oD/MyxmpmgbHX5nNIIPjI3SEJJExf+bsjK0= | ||
| github.com/99designs/keyring v0.0.0-20180523072454-ccd0779e6f10/go.mod h1:aKt8W/yd91/xHY6ixZAJZ2vYbhr3pP8DcrvuGSGNPJk= | ||
| github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f h1:WXiWWJrYCaOaYimBAXlRdRJ7qOisrYyMLYnCvvhHVms= | ||
| github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f/go.mod h1:aKt8W/yd91/xHY6ixZAJZ2vYbhr3pP8DcrvuGSGNPJk= | ||
| github.com/ahmetb/go-cursor v0.0.0-20131010032410-8136607ea412 h1:mjEdk5IWaOUyDfmIScVahVtW56YQ1gBv8RMyHl69Z30= | ||
@@ -75,2 +75,4 @@ github.com/ahmetb/go-cursor v0.0.0-20131010032410-8136607ea412/go.mod h1:6/fH+MoHXlGOc3iy8TSNB4eM1oaBDMs1oxPVN40M3h0= | ||
| golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||
| golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc= | ||
| golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||
| golang.org/x/net v0.0.0-20180530034148-89e543239a64 h1:otRUcJnCzmletog6NvNtimZZStU31VhmAuuno53i0Nk= | ||
@@ -77,0 +79,0 @@ golang.org/x/net v0.0.0-20180530034148-89e543239a64/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
+0
-2
@@ -5,4 +5,2 @@ package local | ||
| // http://iterm2.com/images.html | ||
| // | ||
| // Be sure to install iterm2 nightly. | ||
@@ -9,0 +7,0 @@ import ( |
+10
-0
@@ -292,2 +292,7 @@ package local | ||
| buildDir, err := ioutil.TempFile(os.TempDir(), "buildkite-build-") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| cmd.Env = append(a.Env, | ||
@@ -300,2 +305,3 @@ `HOME=`+os.Getenv(`HOME`), | ||
| `BUILDKITE_BOOTSTRAP_SCRIPT_PATH=`+bootstrap.Name(), | ||
| `BUILDKITE_BUILD_PATH=`+buildDir.Name(), | ||
| ) | ||
@@ -355,2 +361,6 @@ | ||
| if err := tmpFile.Close(); err != nil { | ||
| return nil, err | ||
| } | ||
| if err = os.Chmod(tmpFile.Name(), 0700); err != nil { | ||
@@ -357,0 +367,0 @@ return nil, err |
+1
-1
@@ -820,3 +820,3 @@ package local | ||
| defer r.Body.Close() | ||
| return json.Unmarshal(body, &into) | ||
@@ -823,0 +823,0 @@ } |
+26
-23
| # bk - The Buildkite CLI | ||
| [](https://github.com/buildkite/cli/releases/latest) | ||
| A command line interface for [Buildkite](https://buildkite.com/). | ||
| ## Status | ||
| ## 💬 Pre-Release Feedback | ||
| This is experimental! 🦄🦑 | ||
| This is currently a pre-release, and we'd love to hear any feedback and questions you might have. Please [file an issue on GitHub](https://github.com/buildkite/cli/issues) and let us know 💖 | ||
| For any questions, issues of feedback please [file an issue](https://github.com/buildkite/cli/issues) 💖 | ||
| ## ⬇️ Installation | ||
| ## Usage | ||
| On macOS, you can install with [Homebrew](https://brew.sh): | ||
| ``` | ||
| brew install buildkite/cli/bk | ||
| ```` | ||
| On all other platforms, download a binary from the [latest GitHub releases](https://github.com/buildkite/cli/releases/latest). | ||
| ## 📄 Usage | ||
| ```bash | ||
| # Sets up your credentials | ||
| # Sets up your credentials (stored in your operating system's native secure storage, using https://github.com/99designs/keyring) | ||
| bk configure | ||
| # Creates a .buildkite/pipeline.yml with queue=default and no-op step | ||
| # Also creates a bk pipeline for the current project, and sets up webhooks in GitHub/Bitbucket | ||
| bk init | ||
| # Opens the current pipeline in your browser | ||
| bk browse | ||
| # Triggers a build via the cli | ||
| bk create build | ||
| # List the pipelines that you have access to | ||
| bk pipelines list | ||
| # Opens the current pipeline in your browse | ||
| bk browse | ||
| # Triggers a build for the current directory's commit and branch | ||
| bk build create | ||
| # Lists pipelines that you have access to | ||
| bk list pipelines | ||
| # Runs the current directory's pipeline steps locally (requires the buildkite-agent to be installed) | ||
| bk local run | ||
| # Runs a build entirely locally for development | ||
| bk run local | ||
| # Sets up your current git project directory for Buildkite, creating a .buildkite/pipeline.yml file, a pipeline in Buildkite, and setting up the webhooks on GitHub or Bitbucket | ||
| bk init | ||
| ``` | ||
| ## Development | ||
| ## 🔨 Development | ||
@@ -44,7 +53,1 @@ Developed using Golang 1.11+ with modules. | ||
| ``` | ||
| ## Design | ||
| ### Secret Storage | ||
| `bk` needs several sets of credentials to operate (aws, buildkite, and github/gitlab/bithucket), all of which need to be stored securely on your local machine. We use 99design's [keyring](https://github.com/99designs/keyring) to store the credentials in your operating system's native secure store. On macOS this is Keychain. |
+1
-1
@@ -5,3 +5,3 @@ package cli | ||
| // Version is the version of the CLI tool | ||
| Version = "0.2.0" | ||
| Version = "0.3.0" | ||
| ) | ||
@@ -8,0 +8,0 @@ |
| package cli | ||
| import ( | ||
| "fmt" | ||
| "github.com/buildkite/cli/git" | ||
| "github.com/buildkite/cli/graphql" | ||
| "github.com/fatih/color" | ||
| ) | ||
| type CreateBuildCommandContext struct { | ||
| TerminalContext | ||
| KeyringContext | ||
| Debug bool | ||
| DebugHTTP bool | ||
| Dir string | ||
| PipelineSlug string | ||
| Branch string | ||
| Commit string | ||
| Message string | ||
| } | ||
| func CreateBuildCommand(ctx CreateBuildCommandContext) error { | ||
| params := buildkiteBuildParams{ | ||
| Branch: ctx.Branch, | ||
| Commit: ctx.Commit, | ||
| Message: ctx.Message, | ||
| } | ||
| bk, err := ctx.BuildkiteGraphQLClient() | ||
| if err != nil { | ||
| ctx.Failure(err.Error()) | ||
| return NewExitError(err, 1) | ||
| } | ||
| pipelineSlug := ctx.PipelineSlug | ||
| if pipelineSlug == "" { | ||
| gitRemote, err := git.Remote(ctx.Dir) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| allPipelines, err := listPipelines(bk) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| ps := pipelineSelect{ | ||
| Pipelines: allPipelines, | ||
| Filter: func(p pipeline) bool { | ||
| return git.MatchRemotes(p.RepositoryURL, gitRemote) | ||
| }, | ||
| } | ||
| pipeline, err := ps.Run() | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| params.PipelineID = pipeline.ID | ||
| pipelineSlug = fmt.Sprintf("%s/%s", pipeline.Org, pipeline.Slug) | ||
| if params.Branch == "" { | ||
| params.Branch, err = git.Branch(ctx.Dir) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| } | ||
| if params.Commit == "" { | ||
| params.Commit, err = git.Commit(ctx.Dir) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| } | ||
| if params.Message == "" { | ||
| params.Message, err = git.Message(ctx.Dir) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| } | ||
| } else { | ||
| params.PipelineID, err = getBuildkitePipelineID(bk, ctx.PipelineSlug) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| } | ||
| if params.Branch == "" { | ||
| params.Branch = `master` | ||
| } | ||
| if params.Commit == "" { | ||
| params.Commit = `HEAD` | ||
| } | ||
| if params.Message == "" { | ||
| params.Message = `Build triggered with bk cli :rocket:` | ||
| } | ||
| debugf("Build will use branch=%q, commit=%q, message=%q", | ||
| params.Branch, params.Commit, params.Message) | ||
| buildTry := ctx.Try() | ||
| buildTry.Start(fmt.Sprintf("Triggering a build on pipeline %s", pipelineSlug)) | ||
| build, err := createBuildkiteBuild(bk, params) | ||
| if err != nil { | ||
| buildTry.Failure(err.Error()) | ||
| return NewExitError(err, 1) | ||
| } | ||
| buildTry.Success(fmt.Sprintf("Created #%d", build.Number)) | ||
| ctx.Printf(color.GreenString("\nCheck out your build at %s 🚀\n"), build.URL) | ||
| return nil | ||
| } | ||
| func getBuildkitePipelineID(client *graphql.Client, slug string) (string, error) { | ||
| resp, err := client.Do(` | ||
| query($slug:ID!) { | ||
| pipeline(slug: $slug) { | ||
| id | ||
| } | ||
| } | ||
| `, map[string]interface{}{ | ||
| "slug": slug, | ||
| }) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| var parsedResp struct { | ||
| Data struct { | ||
| Pipeline struct { | ||
| ID string `json:"id"` | ||
| } `json:"pipeline"` | ||
| } `json:"data"` | ||
| } | ||
| if err = resp.DecodeInto(&parsedResp); err != nil { | ||
| return "", fmt.Errorf("Failed to parse GraphQL response: %v", err) | ||
| } | ||
| if parsedResp.Data.Pipeline.ID == "" { | ||
| return "", fmt.Errorf("Failed to find pipeline id for %s", slug) | ||
| } | ||
| return parsedResp.Data.Pipeline.ID, nil | ||
| } | ||
| type buildkiteBuildDetails struct { | ||
| URL string | ||
| Number int | ||
| } | ||
| type buildkiteBuildParams struct { | ||
| PipelineID string | ||
| Commit string | ||
| Branch string | ||
| Message string | ||
| } | ||
| func createBuildkiteBuild(client *graphql.Client, params buildkiteBuildParams) (buildkiteBuildDetails, error) { | ||
| resp, err := client.Do(` | ||
| mutation($input: BuildCreateInput!) { | ||
| buildCreate(input: $input) { | ||
| build { | ||
| url | ||
| number | ||
| } | ||
| } | ||
| } | ||
| `, map[string]interface{}{ | ||
| "input": map[string]interface{}{ | ||
| "pipelineID": params.PipelineID, | ||
| "message": params.Message, | ||
| "commit": params.Commit, | ||
| "branch": params.Branch, | ||
| }}) | ||
| if err != nil { | ||
| return buildkiteBuildDetails{}, err | ||
| } | ||
| var parsedResp struct { | ||
| Data struct { | ||
| BuildCreate struct { | ||
| Build struct { | ||
| URL string `json:"url"` | ||
| Number int `json:"number"` | ||
| } `json:"build"` | ||
| } `json:"buildCreate"` | ||
| } `json:"data"` | ||
| } | ||
| if err = resp.DecodeInto(&parsedResp); err != nil { | ||
| return buildkiteBuildDetails{}, | ||
| fmt.Errorf("Failed to parse GraphQL response: %v", err) | ||
| } | ||
| return buildkiteBuildDetails{ | ||
| URL: parsedResp.Data.BuildCreate.Build.URL, | ||
| Number: parsedResp.Data.BuildCreate.Build.Number, | ||
| }, nil | ||
| } |
| package cli | ||
| import ( | ||
| "fmt" | ||
| "github.com/sahilm/fuzzy" | ||
| ) | ||
| type ListPipelinesCommandContext struct { | ||
| TerminalContext | ||
| KeyringContext | ||
| Debug bool | ||
| DebugHTTP bool | ||
| Fuzzy string | ||
| Limit int | ||
| ShowURL bool | ||
| } | ||
| func ListPipelinesCommand(ctx ListPipelinesCommandContext) error { | ||
| bk, err := ctx.BuildkiteGraphQLClient() | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| pipelines, err := listPipelines(bk) | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| var counter int | ||
| var pipelineStrings []string | ||
| for _, pipeline := range pipelines { | ||
| pipelineStrings = append(pipelineStrings, | ||
| fmt.Sprintf("%s/%s", pipeline.Org, pipeline.Slug)) | ||
| } | ||
| if ctx.Fuzzy != "" { | ||
| const bold = "\033[1m%s\033[0m" | ||
| matches := fuzzy.Find(ctx.Fuzzy, pipelineStrings) | ||
| for _, match := range matches { | ||
| counter++ | ||
| if ctx.Limit > 0 && counter > ctx.Limit { | ||
| break | ||
| } | ||
| if ctx.ShowURL { | ||
| ctx.Printf(`https://buildkite.com/`) | ||
| } | ||
| for i := 0; i < len(match.Str); i++ { | ||
| if contains(i, match.MatchedIndexes) { | ||
| fmt.Print(fmt.Sprintf(bold, string(match.Str[i]))) | ||
| } else { | ||
| fmt.Print(string(match.Str[i])) | ||
| } | ||
| } | ||
| fmt.Println() | ||
| } | ||
| return nil | ||
| } | ||
| for _, p := range pipelineStrings { | ||
| counter++ | ||
| if ctx.Limit > 0 && counter > ctx.Limit { | ||
| break | ||
| } | ||
| if ctx.ShowURL { | ||
| ctx.Printf(`https://buildkite.com/`) | ||
| } | ||
| ctx.Println(p) | ||
| } | ||
| return nil | ||
| } | ||
| func contains(needle int, haystack []int) bool { | ||
| for _, i := range haystack { | ||
| if needle == i { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } |
-106
| package cli | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log" | ||
| "os" | ||
| "os/exec" | ||
| "os/signal" | ||
| "path/filepath" | ||
| "regexp" | ||
| "strings" | ||
| "github.com/buildkite/cli/local" | ||
| ) | ||
| type RunLocalCommandContext struct { | ||
| TerminalContext | ||
| KeyringContext | ||
| Debug bool | ||
| DebugHTTP bool | ||
| File *os.File | ||
| Env []string | ||
| Command string | ||
| StepFilterRegex *regexp.Regexp | ||
| Prompt bool | ||
| DryRun bool | ||
| } | ||
| func RunLocalCommand(ctx RunLocalCommandContext) error { | ||
| quit := make(chan os.Signal, 1) | ||
| signal.Notify(quit, os.Interrupt) | ||
| if ctx.Debug { | ||
| local.Debug = true | ||
| } | ||
| cancelCtx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
| go func() { | ||
| <-quit | ||
| fmt.Printf("\n>>> Gracefully shutting down...\n") | ||
| cancel() | ||
| }() | ||
| wd, err := os.Getwd() | ||
| if err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| commit, err := gitCommit() | ||
| if err != nil { | ||
| log.Printf("Error getting git commit: %v", err) | ||
| commit = "no_commit_found" | ||
| } | ||
| branch, err := gitBranch() | ||
| if err != nil { | ||
| log.Printf("Error getting git branch: %v", err) | ||
| branch = "master" | ||
| } | ||
| command := ctx.Command | ||
| if ctx.File != nil { | ||
| command = fmt.Sprintf("buildkite-agent pipeline upload %q", ctx.File.Name()) | ||
| } | ||
| if err := local.Run(cancelCtx, local.RunParams{ | ||
| Env: ctx.Env, | ||
| DryRun: ctx.DryRun, | ||
| Command: command, | ||
| Dir: wd, | ||
| Prompt: ctx.Prompt, | ||
| StepFilter: ctx.StepFilterRegex, | ||
| JobTemplate: local.Job{ | ||
| Commit: commit, | ||
| Branch: branch, | ||
| Repository: wd, | ||
| OrganizationSlug: "local", | ||
| PipelineSlug: filepath.Base(wd), | ||
| }, | ||
| }); err != nil { | ||
| return NewExitError(err, 1) | ||
| } | ||
| return nil | ||
| } | ||
| func gitBranch() (string, error) { | ||
| out, err := exec.Command(`git`, `rev-parse`, `--abbrev-ref`, `HEAD`).Output() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return strings.TrimSpace(string(out)), nil | ||
| } | ||
| func gitCommit() (string, error) { | ||
| out, err := exec.Command(`git`, `rev-parse`, `HEAD`).Output() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return strings.TrimSpace(string(out)), nil | ||
| } |