Skip to content

Commit a41c0fb

Browse files
authored
allow for stale branch cleanup (& dry run) (#772)
1 parent 26aa812 commit a41c0fb

8 files changed

Lines changed: 842 additions & 78 deletions

File tree

cmd/preflight/cleanup_cmd.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package preflight
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"time"
9+
10+
"github.com/alecthomas/kong"
11+
"github.com/google/uuid"
12+
13+
"github.com/buildkite/cli/v3/internal/cli"
14+
bkErrors "github.com/buildkite/cli/v3/internal/errors"
15+
"github.com/buildkite/cli/v3/internal/preflight"
16+
)
17+
18+
// CleanupCmd deletes remote bk/preflight/* branches whose builds have completed.
19+
type CleanupCmd struct {
20+
Pipeline string `help:"The pipeline to check builds against. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}." short:"p"`
21+
PreflightUUID string `help:"Target a single preflight branch by its UUID (bk/preflight/<uuid>)." name:"preflight-uuid"`
22+
DryRun bool `help:"Show which branches would be deleted without actually deleting them." name:"dry-run"`
23+
Text bool `help:"Use plain text output instead of interactive terminal UI." xor:"output"`
24+
JSON bool `help:"Emit one JSON object per event (JSONL)." xor:"output"`
25+
}
26+
27+
func (c *CleanupCmd) Help() string {
28+
return `Deletes remote bk/preflight/* branches whose builds have completed (passed, failed, canceled). Branches with in-progress builds are left untouched to avoid interrupting concurrent preflight runs. Pass --preflight-uuid to target a single preflight branch.`
29+
}
30+
31+
func (c *CleanupCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
32+
if c.PreflightUUID != "" {
33+
if _, err := uuid.Parse(c.PreflightUUID); err != nil {
34+
return bkErrors.NewValidationError(err, fmt.Sprintf("invalid preflight UUID %q", c.PreflightUUID))
35+
}
36+
}
37+
38+
pCtx, err := setup(c.Pipeline, globals)
39+
if err != nil {
40+
return err
41+
}
42+
defer pCtx.Stop()
43+
44+
ctx := pCtx.Ctx
45+
repoRoot := pCtx.RepoRoot
46+
resolvedPipeline := pCtx.Pipeline
47+
48+
var branches []preflight.BranchBuild
49+
if c.PreflightUUID != "" {
50+
branch, err := preflight.LookupRemotePreflightBranch(repoRoot, c.PreflightUUID, globals.EnableDebug())
51+
if err != nil {
52+
return bkErrors.NewInternalError(err, "failed to look up preflight branch")
53+
}
54+
if branch != nil {
55+
branches = []preflight.BranchBuild{*branch}
56+
}
57+
} else {
58+
branches, err = preflight.ListRemotePreflightBranches(repoRoot, globals.EnableDebug())
59+
if err != nil {
60+
return bkErrors.NewInternalError(err, "failed to list remote preflight branches")
61+
}
62+
}
63+
64+
if len(branches) == 0 {
65+
if c.PreflightUUID != "" {
66+
fmt.Fprintf(os.Stdout, "No preflight branch found for UUID %s\n", c.PreflightUUID)
67+
} else {
68+
fmt.Fprintln(os.Stdout, "No preflight branches found")
69+
}
70+
return nil
71+
}
72+
73+
renderer := newRenderer(os.Stdout, c.JSON, c.Text, pCtx.Stop)
74+
defer renderer.Close()
75+
76+
_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf("Found %d preflight branch(es), checking build status...", len(branches))})
77+
78+
if err := preflight.ResolveBuilds(ctx, pCtx.Factory.RestAPIClient, resolvedPipeline.Org, resolvedPipeline.Name, branches); err != nil {
79+
if errors.Is(err, context.Canceled) {
80+
_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: "Cleanup interrupted"})
81+
return nil
82+
}
83+
return bkErrors.NewInternalError(err, "failed to check build status for preflight branches")
84+
}
85+
86+
var toDelete []string
87+
var deleted, skipped int
88+
for i := range branches {
89+
bb := branches[i]
90+
if !bb.IsCompleted() {
91+
_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf("Skipping %s (build state: %s)", bb.Branch, bb.Build.State)})
92+
skipped++
93+
continue
94+
}
95+
96+
state := "no build found"
97+
if bb.Build != nil {
98+
state = bb.Build.State
99+
}
100+
101+
if c.DryRun {
102+
_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf("Would delete %s (%s)", bb.Branch, state)})
103+
} else {
104+
_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf("Deleting %s (%s)", bb.Branch, state)})
105+
toDelete = append(toDelete, bb.Ref)
106+
}
107+
deleted++
108+
}
109+
110+
if !c.DryRun && len(toDelete) > 0 {
111+
if err := preflight.CleanupRefs(repoRoot, toDelete, globals.EnableDebug()); err != nil {
112+
return bkErrors.NewInternalError(err, "failed to delete preflight branches from remote")
113+
}
114+
}
115+
116+
verb := "deleted"
117+
if c.DryRun {
118+
verb = "would delete"
119+
}
120+
_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf("Cleanup complete: %d %s, %d skipped", deleted, verb, skipped)})
121+
return nil
122+
}

0 commit comments

Comments
 (0)