Skip to content

Commit

Permalink
SUP-2582 | watch command (#356)
Browse files Browse the repository at this point in the history
* Rudamentary watch command & expand picker

* Add job output
  • Loading branch information
mcncl authored Sep 13, 2024
1 parent de618c9 commit 873f047
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 5 deletions.
20 changes: 17 additions & 3 deletions internal/io/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,29 @@ package io

import "github.com/charmbracelet/huh"

const (
typeOrganizationMessage = "Pick an organization"
typePipelineMessage = "Select a pipeline"
)

// PromptForOne will show the list of options to the user, allowing them to select one to return.
// It's possible for them to choose none or cancel the selection, resulting in an error.
func PromptForOne(options []string) (string, error) {
func PromptForOne(resource string, options []string) (string, error) {
var message string
switch resource {
case "pipeline":
message = typePipelineMessage
case "organization":
message = typeOrganizationMessage
default:
message = "Please select one of the options below"
}
selected := new(string)
err := huh.NewForm(huh.NewGroup(
huh.NewSelect[string]().
Title("Pick an organization").
Title(message).
Options(
huh.NewOptions[string](options...)...,
huh.NewOptions(options...)...,
).Value(selected),
),
).Run()
Expand Down
2 changes: 1 addition & 1 deletion internal/pipeline/resolver/picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func PickOne(pipelines []pipeline.Pipeline) *pipeline.Pipeline {
names[i] = p.Name
}

chosen, err := io.PromptForOne(names)
chosen, err := io.PromptForOne("pipeline", names)
if err != nil {
return nil
}
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func NewCmdBuild(f *factory.Factory) *cobra.Command {
cmd.AddCommand(NewCmdBuildNew(f))
cmd.AddCommand(NewCmdBuildRebuild(f))
cmd.AddCommand(NewCmdBuildView(f))
cmd.AddCommand(NewCmdBuildWatch(f))

return &cmd
}
Expand Down
120 changes: 120 additions & 0 deletions pkg/cmd/build/watch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package build

import (
"fmt"
"time"

"github.com/MakeNowJust/heredoc"
"github.com/buildkite/cli/v3/internal/build"
buildResolver "github.com/buildkite/cli/v3/internal/build/resolver"
"github.com/buildkite/cli/v3/internal/build/resolver/options"
"github.com/buildkite/cli/v3/internal/job"
pipelineResolver "github.com/buildkite/cli/v3/internal/pipeline/resolver"
"github.com/buildkite/cli/v3/pkg/cmd/factory"
"github.com/buildkite/go-buildkite/v3/buildkite"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)

func NewCmdBuildWatch(f *factory.Factory) *cobra.Command {
var pipeline, branch string
var intervalSeconds int

cmd := cobra.Command{
Use: "watch [number] [flags]",
Short: "Watch a build's progress in real-time",
Args: cobra.MaximumNArgs(1),
Long: heredoc.Doc(`
Watch a build's progress in real-time.
You can pass an optional build number to watch. If omitted, the most recent build on the current branch will be watched.
`),
Example: heredoc.Doc(`
# Watch the most recent build for the current branch
$ bk build watch
# Watch a specific build
$ bk build watch 429
# Watch the most recent build on a specific branch
$ bk build watch -b feature-x
# Watch a build on a specific pipeline
$ bk build watch -p my-pipeline
# Set a custom polling interval (in seconds)
$ bk build watch --interval 5
`),
RunE: func(cmd *cobra.Command, args []string) error {
pipelineRes := pipelineResolver.NewAggregateResolver(
pipelineResolver.ResolveFromFlag(pipeline, f.Config),
pipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOne),
pipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOne)),
)

optionsResolver := options.AggregateResolver{
options.ResolveBranchFromFlag(branch),
options.ResolveBranchFromRepository(f.GitRepository),
}

buildRes := buildResolver.NewAggregateResolver(
buildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),
buildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...),
)

bld, err := buildRes.Resolve(cmd.Context())
if err != nil {
return err
}
if bld == nil {
return fmt.Errorf("no running builds found")
}

fmt.Fprintf(cmd.OutOrStdout(), "Watching build %d on %s/%s\n", bld.BuildNumber, bld.Organization, bld.Pipeline)

ticker := time.NewTicker(time.Duration(intervalSeconds) * time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
b, _, err := f.RestAPIClient.Builds.Get(bld.Organization, bld.Pipeline, fmt.Sprint(bld.BuildNumber), &buildkite.BuildsListOptions{})
if err != nil {
return err
}

summary := buildSummaryWithJobs(b)
fmt.Fprintf(cmd.OutOrStdout(), "\033[2J\033[H%s\n", summary) // Clear screen and move cursor to top-left

if b.FinishedAt != nil {
return nil
}
case <-cmd.Context().Done():
return nil
}
}
},
}

cmd.Flags().StringVarP(&pipeline, "pipeline", "p", "", "The pipeline to watch. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.")
cmd.Flags().StringVarP(&branch, "branch", "b", "", "The branch to watch builds for.")
cmd.Flags().IntVar(&intervalSeconds, "interval", 1, "Polling interval in seconds")

return &cmd
}

func buildSummaryWithJobs(b *buildkite.Build) string {
summary := build.BuildSummary(b)

if len(b.Jobs) > 0 {
summary += lipgloss.NewStyle().Bold(true).Padding(0, 1).Underline(true).Render("\nJobs")
for _, j := range b.Jobs {
bkJob := *j
if *bkJob.Type == "script" {
summary += job.JobSummary(job.Job(bkJob))
}
}
}

return summary
}
2 changes: 1 addition & 1 deletion pkg/cmd/use/use.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func useRun(org *string, conf *config.Config) error {
// prompt to choose from configured orgs if one is not already selected
if org == nil {
var err error
selected, err = io.PromptForOne(conf.ConfiguredOrganizations())
selected, err = io.PromptForOne("organization", conf.ConfiguredOrganizations())
if err != nil {
return err
}
Expand Down

0 comments on commit 873f047

Please sign in to comment.