Skip to content
Merged
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
28 changes: 20 additions & 8 deletions cmd/bob/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func newBuildCmd() *cobra.Command {
var pvcName string
var downloadDir string
var skipVerify bool
var force bool

cmd := &cobra.Command{
Use: "build [name]",
Expand All @@ -50,13 +51,15 @@ func newBuildCmd() *cobra.Command {
bob build body-ecu-nucleo # re-trigger existing BuildJob
bob build body-ecu-mpu-hostlike --local # sync . and build
bob build body-ecu-mpu-hostlike --local --source ~/code # sync specific dir
bob build body-ecu-nucleo -d ./out # build and download artifacts
bob build body-ecu-nucleo -d ./out # download artifacts (rebuild only if needed)
bob build body-ecu-nucleo -d ./out --force # force rebuild and download

When --local is used, the BuildJob is temporarily switched to use your local
source. The next build without --local automatically restores git source.

When -d is used, the CLI waits for the build to complete and automatically
downloads the resulting artifacts to the specified directory.`,
When -d is used, the CLI checks if the build already succeeded. If so, it
downloads the existing artifacts without retriggering. Use --force to rebuild
regardless.`,
RunE: func(cmd *cobra.Command, args []string) error {
ns := firstNonEmpty(bobNamespace, os.Getenv("BOB_NAMESPACE"), "bob-builds")
if local {
Expand Down Expand Up @@ -96,9 +99,17 @@ downloads the resulting artifacts to the specified directory.`,
if len(args) == 0 {
return fmt.Errorf("provide a BuildJob name or use -f <file>")
}
if err := autoRestoreIfLocal(ns, args[0]); err != nil {
restored, err := autoRestoreIfLocal(ns, args[0])
if err != nil {
return fmt.Errorf("restoring git source: %w", err)
}
if downloadDir != "" && !force && !restored {
if skipped, err := downloadIfUpToDate(cmd.Context(), args[0], downloadDir, skipVerify); err != nil {
return err
} else if skipped {
return nil
}
}
if err := retrigger(cmd.Context(), args[0]); err != nil {
return err
}
Expand All @@ -116,6 +127,7 @@ downloads the resulting artifacts to the specified directory.`,
cmd.Flags().StringVar(&pvcName, "pvc", "source-code", "PVC name for local source upload")
cmd.Flags().StringVarP(&downloadDir, "download", "d", "", "Wait for build to finish and download artifacts to this directory")
cmd.Flags().BoolVar(&skipVerify, "skip-verify", false, "Skip cosign signature verification on download")
cmd.Flags().BoolVar(&force, "force", false, "Force a new build even if the last one succeeded")
return cmd
}

Expand Down Expand Up @@ -183,17 +195,17 @@ func createFromFile(ctx context.Context, path string, branch string) (string, er
return result.Name, nil
}

func autoRestoreIfLocal(namespace, bjName string) error {
func autoRestoreIfLocal(namespace, bjName string) (restored bool, err error) {
kubecli := detectKubeClient()
if kubecli == "" {
return nil
return false, nil
}
out, _ := exec.Command(kubecli, "get", "buildjob", bjName, "-n", namespace,
"-o", `jsonpath={.metadata.annotations.builder\.sdv\.cloud\.redhat\.com/original-source}`).CombinedOutput()
if len(out) > 0 && string(out) != "" {
return restoreGitSource(kubecli, namespace, bjName)
return true, restoreGitSource(kubecli, namespace, bjName)
}
return nil
return false, nil
}

func retrigger(ctx context.Context, name string) error {
Expand Down
39 changes: 35 additions & 4 deletions cmd/bob/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,39 @@ import (
const (
buildPollInterval = 5 * time.Second
buildStartupTimeout = 2 * time.Minute

phaseSucceeded = "Succeeded"
phaseFailed = "Failed"
phaseRunning = "Running"
phasePending = "Pending"
)

// downloadIfUpToDate checks if the build already succeeded and downloads
// artifacts without triggering a new run. Returns (true, nil) if artifacts
// were downloaded, (false, nil) if a new build is needed.
func downloadIfUpToDate(ctx context.Context, name, downloadDir string, skipVerify bool) (bool, error) {
c := newClient()
build, err := c.Get(ctx, name)
if err != nil {
return false, fmt.Errorf("getting build status: %w", err)
}

if build.Phase != phaseSucceeded {
return false, nil
}

fmt.Printf("Build %q already up-to-date, skipping rebuild.\n", name)
fmt.Printf(" Run: %s\n", build.PipelineRun)
if build.CommitSHA != "" {
fmt.Printf(" Commit: %s\n", build.CommitSHA)
}
if build.Image != "" {
fmt.Printf(" Image: %s\n", build.Image)
}
fmt.Println()
return true, downloadBuildArtifacts(ctx, c, build, downloadDir, skipVerify)
}

func waitAndDownload(ctx context.Context, name, downloadDir string, skipVerify bool) error {
c := newClient()

Expand Down Expand Up @@ -60,10 +91,10 @@ func waitAndDownload(ctx context.Context, name, downloadDir string, skipVerify b
case build.PipelineRun != previousRun:
buildStarted = true
fmt.Printf(" New PipelineRun: %s\n", build.PipelineRun)
case build.Phase == "Running" || build.Phase == "Pending":
case build.Phase == phaseRunning || build.Phase == phasePending:
buildStarted = true
fmt.Printf(" PipelineRun: %s\n", build.PipelineRun)
case build.Phase == "Succeeded" || build.Phase == "Failed":
case build.Phase == phaseSucceeded || build.Phase == phaseFailed:
if time.Since(start) > buildStartupTimeout {
fmt.Printf(" Build already completed (run %s), downloading existing artifacts.\n", build.PipelineRun)
buildStarted = true
Expand All @@ -86,10 +117,10 @@ func waitAndDownload(ctx context.Context, name, downloadDir string, skipVerify b
}

switch build.Phase {
case "Succeeded":
case phaseSucceeded:
fmt.Printf("\nBuild succeeded in %s\n", time.Since(start).Truncate(time.Second))
return downloadBuildArtifacts(ctx, c, build, downloadDir, skipVerify)
case "Failed":
case phaseFailed:
fmt.Fprintf(os.Stderr, "\nBuild failed after %s\n", time.Since(start).Truncate(time.Second))
fmt.Fprintf(os.Stderr, "View logs with: bob logs %s\n", name)
return fmt.Errorf("build %q failed", name)
Expand Down
22 changes: 22 additions & 0 deletions cmd/bob/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"testing"
"time"
)

func TestNewBuildCmd_HasDownloadFlag(t *testing.T) {
Expand All @@ -21,8 +22,29 @@ func TestNewBuildCmd_HasDownloadFlag(t *testing.T) {
}
}

func TestNewBuildCmd_HasForceFlag(t *testing.T) {
cmd := newBuildCmd()

f := cmd.Flags().Lookup("force")
if f == nil {
t.Fatal("expected --force flag")
}
if f.DefValue != "false" {
t.Errorf("expected default false, got %s", f.DefValue)
}
}

func TestBuildPollInterval(t *testing.T) {
if buildPollInterval <= 0 {
t.Fatal("poll interval should be positive")
}
}

func TestBuildStartupTimeout(t *testing.T) {
if buildStartupTimeout < 1*time.Minute {
t.Fatal("startup timeout should be at least 1 minute")
}
if buildStartupTimeout > 5*time.Minute {
t.Fatal("startup timeout should not exceed 5 minutes")
}
}
Loading