Skip to content

Commit 700ba6d

Browse files
authored
Migrating all build x commands to Kong (#571)
1 parent 454f9d6 commit 700ba6d

File tree

21 files changed

+1345
-1514
lines changed

21 files changed

+1345
-1514
lines changed

cmd/build/cancel.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package build
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/alecthomas/kong"
8+
buildResolver "github.com/buildkite/cli/v3/internal/build/resolver"
9+
"github.com/buildkite/cli/v3/internal/cli"
10+
bk_io "github.com/buildkite/cli/v3/internal/io"
11+
pipelineResolver "github.com/buildkite/cli/v3/internal/pipeline/resolver"
12+
"github.com/buildkite/cli/v3/internal/util"
13+
"github.com/buildkite/cli/v3/internal/version"
14+
"github.com/buildkite/cli/v3/pkg/cmd/factory"
15+
"github.com/buildkite/cli/v3/pkg/cmd/validation"
16+
buildkite "github.com/buildkite/go-buildkite/v4"
17+
)
18+
19+
type CancelCmd struct {
20+
BuildNumber string `arg:"" help:"Build number to cancel"`
21+
Pipeline string `help:"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}." short:"p"`
22+
Web bool `help:"Open the build in a web browser after it has been cancelled." short:"w"`
23+
}
24+
25+
func (c *CancelCmd) Help() string {
26+
return `
27+
Examples:
28+
# Cancel a build by number
29+
$ bk build cancel 123 --pipeline my-pipeline
30+
31+
# Cancel a build and open in browser
32+
$ bk build cancel 123 -pipeline my-pipeline --web`
33+
}
34+
35+
func (c *CancelCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
36+
f, err := factory.New(version.Version)
37+
if err != nil {
38+
return err
39+
}
40+
41+
f.SkipConfirm = globals.SkipConfirmation()
42+
f.NoInput = globals.DisableInput()
43+
44+
if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {
45+
return err
46+
}
47+
48+
ctx := context.Background()
49+
50+
pipelineRes := pipelineResolver.NewAggregateResolver(
51+
pipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),
52+
pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne),
53+
pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne, f.GitRepository != nil)),
54+
)
55+
56+
args := []string{c.BuildNumber}
57+
buildRes := buildResolver.NewAggregateResolver(
58+
buildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),
59+
)
60+
61+
bld, err := buildRes.Resolve(ctx)
62+
if err != nil {
63+
return err
64+
}
65+
66+
confirmed, err := bk_io.Confirm(f, fmt.Sprintf("Cancel build #%d on %s", bld.BuildNumber, bld.Pipeline))
67+
if err != nil {
68+
return err
69+
}
70+
71+
if !confirmed {
72+
return nil
73+
}
74+
75+
return cancelBuild(ctx, bld.Organization, bld.Pipeline, fmt.Sprint(bld.BuildNumber), c.Web, f)
76+
}
77+
78+
func cancelBuild(ctx context.Context, org string, pipeline string, buildId string, web bool, f *factory.Factory) error {
79+
var err error
80+
var build buildkite.Build
81+
spinErr := bk_io.SpinWhile(fmt.Sprintf("Cancelling build #%s from pipeline %s", buildId, pipeline), func() {
82+
build, err = f.RestAPIClient.Builds.Cancel(ctx, org, pipeline, buildId)
83+
})
84+
if spinErr != nil {
85+
return spinErr
86+
}
87+
if err != nil {
88+
return err
89+
}
90+
91+
fmt.Printf("%s\n", renderResult(fmt.Sprintf("Build canceled: %s", build.WebURL)))
92+
93+
return util.OpenInWebBrowser(web, build.WebURL)
94+
}

cmd/build/create.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package build
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"github.com/alecthomas/kong"
11+
"github.com/buildkite/cli/v3/internal/cli"
12+
bkErrors "github.com/buildkite/cli/v3/internal/errors"
13+
bk_io "github.com/buildkite/cli/v3/internal/io"
14+
"github.com/buildkite/cli/v3/internal/pipeline/resolver"
15+
"github.com/buildkite/cli/v3/internal/util"
16+
"github.com/buildkite/cli/v3/internal/version"
17+
"github.com/buildkite/cli/v3/pkg/cmd/factory"
18+
"github.com/buildkite/cli/v3/pkg/cmd/validation"
19+
buildkite "github.com/buildkite/go-buildkite/v4"
20+
"github.com/charmbracelet/lipgloss"
21+
)
22+
23+
type CreateCmd struct {
24+
Message string `help:"Description of the build. If left blank, the commit message will be used once the build starts." short:"m"`
25+
Commit string `help:"The commit to build." short:"c" default:"HEAD"`
26+
Branch string `help:"The branch to build. Defaults to the default branch of the pipeline." short:"b"`
27+
Author string `help:"Author of the build. Supports: \"Name <email>\", \"[email protected]\", \"Full Name\", or \"username\"" short:"a"`
28+
Web bool `help:"Open the build in a web browser after it has been created." short:"w"`
29+
Pipeline string `help:"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}." short:"p"`
30+
Env []string `help:"Set environment variables for the build (KEY=VALUE)" short:"e"`
31+
Metadata []string `help:"Set metadata for the build (KEY=VALUE)" short:"M"`
32+
IgnoreBranchFilters bool `help:"Ignore branch filters for the pipeline" short:"i"`
33+
EnvFile string `help:"Set the environment variables for the build via an environment file" short:"f"`
34+
}
35+
36+
func (c *CreateCmd) Help() string {
37+
return `The web URL to the build will be printed to stdout.
38+
39+
Examples:
40+
# Create a new build
41+
$ bk build create
42+
43+
# Create a new build with environment variables set
44+
$ bk build create -e "FOO=BAR" -e "BAR=BAZ"
45+
46+
# Create a new build with metadata
47+
$ bk build create -M "key=value" -M "foo=bar"`
48+
}
49+
50+
func (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
51+
// Initialize factory
52+
f, err := factory.New(version.Version)
53+
if err != nil {
54+
return bkErrors.NewInternalError(err, "failed to initialize CLI", "This is likely a bug", "Report to Buildkite")
55+
}
56+
57+
f.SkipConfirm = globals.SkipConfirmation()
58+
f.NoInput = globals.DisableInput()
59+
60+
if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {
61+
return err
62+
}
63+
64+
ctx := context.Background()
65+
66+
resolvers := resolver.NewAggregateResolver(
67+
resolver.ResolveFromFlag(c.Pipeline, f.Config),
68+
resolver.ResolveFromConfig(f.Config, resolver.PickOne),
69+
resolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOne, f.GitRepository != nil)),
70+
)
71+
72+
resolvedPipeline, err := resolvers.Resolve(ctx)
73+
if err != nil {
74+
return err // Already wrapped by resolver
75+
}
76+
if resolvedPipeline == nil {
77+
return bkErrors.NewResourceNotFoundError(
78+
nil,
79+
"could not resolve a pipeline",
80+
"Specify a pipeline with --pipeline (-p)",
81+
"Run 'bk pipeline list' to see available pipelines",
82+
)
83+
}
84+
85+
confirmed, err := bk_io.Confirm(f, fmt.Sprintf("Create new build on %s?", resolvedPipeline.Name))
86+
if err != nil {
87+
return bkErrors.NewUserAbortedError(err, "confirmation canceled")
88+
}
89+
90+
if !confirmed {
91+
fmt.Println("Build creation canceled")
92+
return nil
93+
}
94+
95+
// Process environment variables
96+
envMap := make(map[string]string)
97+
for _, e := range c.Env {
98+
key, value, _ := strings.Cut(e, "=")
99+
envMap[key] = value
100+
}
101+
102+
// Process metadata variables
103+
metaDataMap := make(map[string]string)
104+
for _, m := range c.Metadata {
105+
key, value, _ := strings.Cut(m, "=")
106+
metaDataMap[key] = value
107+
}
108+
109+
// Process environment file if specified
110+
if c.EnvFile != "" {
111+
file, err := os.Open(c.EnvFile)
112+
if err != nil {
113+
return bkErrors.NewValidationError(
114+
err,
115+
fmt.Sprintf("could not open environment file: %s", c.EnvFile),
116+
"Check that the file exists and is readable",
117+
)
118+
}
119+
defer file.Close()
120+
121+
content := bufio.NewScanner(file)
122+
for content.Scan() {
123+
key, value, _ := strings.Cut(content.Text(), "=")
124+
envMap[key] = value
125+
}
126+
127+
if err := content.Err(); err != nil {
128+
return bkErrors.NewValidationError(
129+
err,
130+
"error reading environment file",
131+
"Ensure the file contains valid environment variables in KEY=VALUE format",
132+
)
133+
}
134+
}
135+
136+
return createBuild(ctx, resolvedPipeline.Org, resolvedPipeline.Name, f, c.Message, c.Commit, c.Branch, c.Web, envMap, metaDataMap, c.IgnoreBranchFilters, c.Author)
137+
}
138+
139+
func parseAuthor(author string) buildkite.Author {
140+
if author == "" {
141+
return buildkite.Author{}
142+
}
143+
144+
// Check for Git-style format: "Name <[email protected]>"
145+
if strings.Contains(author, "<") && strings.Contains(author, ">") {
146+
parts := strings.Split(author, "<")
147+
if len(parts) == 2 {
148+
name := strings.TrimSpace(parts[0])
149+
email := strings.TrimSpace(strings.Trim(parts[1], ">"))
150+
if name != "" && email != "" {
151+
return buildkite.Author{Name: name, Email: email}
152+
}
153+
}
154+
}
155+
156+
// Check for email-only format
157+
if strings.Contains(author, "@") && strings.Contains(author, ".") && !strings.Contains(author, " ") {
158+
return buildkite.Author{Email: author}
159+
}
160+
161+
// Check for name format (contains spaces but no email)
162+
if strings.Contains(author, " ") {
163+
return buildkite.Author{Name: author}
164+
}
165+
166+
// Default to username
167+
return buildkite.Author{Username: author}
168+
}
169+
170+
func createBuild(ctx context.Context, org string, pipeline string, f *factory.Factory, message string, commit string, branch string, web bool, env map[string]string, metaData map[string]string, ignoreBranchFilters bool, author string) error {
171+
var actionErr error
172+
var build buildkite.Build
173+
spinErr := bk_io.SpinWhile(fmt.Sprintf("Starting new build for %s", pipeline), func() {
174+
branch = strings.TrimSpace(branch)
175+
if len(branch) == 0 {
176+
p, _, err := f.RestAPIClient.Pipelines.Get(ctx, org, pipeline)
177+
if err != nil {
178+
actionErr = bkErrors.WrapAPIError(err, "fetching pipeline information")
179+
return
180+
}
181+
182+
// Check if the pipeline has a default branch set
183+
if p.DefaultBranch == "" {
184+
actionErr = bkErrors.NewValidationError(
185+
nil,
186+
fmt.Sprintf("No default branch set for pipeline %s", pipeline),
187+
"Please specify a branch using --branch (-b)",
188+
"Set a default branch in your pipeline settings on Buildkite",
189+
)
190+
return
191+
}
192+
branch = p.DefaultBranch
193+
}
194+
195+
newBuild := buildkite.CreateBuild{
196+
Message: message,
197+
Commit: commit,
198+
Branch: branch,
199+
Author: parseAuthor(author),
200+
Env: env,
201+
MetaData: metaData,
202+
IgnorePipelineBranchFilters: ignoreBranchFilters,
203+
}
204+
205+
var err error
206+
build, _, err = f.RestAPIClient.Builds.Create(ctx, org, pipeline, newBuild)
207+
if err != nil {
208+
actionErr = bkErrors.WrapAPIError(err, "creating build")
209+
return
210+
}
211+
})
212+
if spinErr != nil {
213+
return bkErrors.NewInternalError(spinErr, "error in spinner UI")
214+
}
215+
216+
if actionErr != nil {
217+
return actionErr
218+
}
219+
220+
if build.WebURL == "" {
221+
return bkErrors.NewAPIError(
222+
nil,
223+
"build was created but no URL was returned",
224+
"This may be due to an API version mismatch",
225+
)
226+
}
227+
228+
fmt.Printf("%s\n", renderResult(fmt.Sprintf("Build created: %s", build.WebURL)))
229+
230+
if err := util.OpenInWebBrowser(web, build.WebURL); err != nil {
231+
return bkErrors.NewInternalError(err, "failed to open web browser")
232+
}
233+
234+
return nil
235+
}
236+
237+
func renderResult(result string) string {
238+
return lipgloss.JoinVertical(lipgloss.Top,
239+
lipgloss.NewStyle().Padding(0, 0).Render(result))
240+
}

0 commit comments

Comments
 (0)