Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

github.com/buildkite/cli

Package Overview
Dependencies
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

github.com/buildkite/cli - go Package Compare versions

Comparing version
v0.2.1-0.20181110040431-a4ea119de8dd
to
v0.3.0
+210
cmd_build_create.go
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
}
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
}
+2
-5

@@ -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

@@ -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=

@@ -5,4 +5,2 @@ package local

// http://iterm2.com/images.html
//
// Be sure to install iterm2 nightly.

@@ -9,0 +7,0 @@ import (

@@ -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

@@ -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
[![Latest release](https://img.shields.io/github/release/buildkite/cli.svg)](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.

@@ -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
}
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
}