• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubernetes-sigs / kubebuilder / 17202442548

25 Aug 2025 07:38AM UTC coverage: 67.097% (+2.6%) from 64.534%
17202442548

Pull #5050

github

camilamacedo86
refactor: (alpha update) optimize diff generation and add unified conflict detection

Consolidate duplicate conflict scanning, improve Kubebuilder file patterns,
and add comprehensive test coverage.

Assisted-by: Cursor
Pull Request #5050: refactor: (alpha update) optimize diff generation and add unified conflict detection

111 of 172 new or added lines in 2 files covered. (64.53%)

8 existing lines in 3 files now uncovered.

3326 of 4957 relevant lines covered (67.1%)

40.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.15
/pkg/cli/alpha/internal/update/update.go
1
/*
2
Copyright 2025 The Kubernetes Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package update
18

19
import (
20
        "bytes"
21
        "errors"
22
        "fmt"
23
        log "log/slog"
24
        "os"
25
        "os/exec"
26
        "strings"
27
        "time"
28

29
        "sigs.k8s.io/kubebuilder/v4/pkg/cli/alpha/internal/update/helpers"
30
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
31
)
32

33
// Update contains configuration for the update operation.
34
type Update struct {
35
        // FromVersion is the release version to update FROM (the base/original scaffold),
36
        // e.g., "v4.5.0". This is used to regenerate the ancestor scaffold.
37
        FromVersion string
38

39
        // ToVersion is the release version to update TO (the target scaffold),
40
        // e.g., "v4.6.0". This is used to regenerate the upgrade scaffold.
41
        ToVersion string
42

43
        // FromBranch is the base Git branch that represents the user's current project state,
44
        // e.g., "main". Its contents are captured into the "original" branch during the update.
45
        FromBranch string
46

47
        // Force, when true, commits the merge result even if there are conflicts.
48
        // In that case, conflict markers are kept in the files.
49
        Force bool
50

51
        // ShowCommits controls whether to keep full history (no squash).
52
        //   - true  => keep history: point the output branch at the merge commit
53
        //              (no squashed commit is created).
54
        //   - false => squash: write the merge tree as a single commit on the output branch.
55
        //
56
        // The output branch name defaults to "kubebuilder-update-from-<FromVersion>-to-<ToVersion>"
57
        // unless OutputBranch is explicitly set.
58
        ShowCommits bool
59

60
        // RestorePath is a list of paths to restore from the base branch (FromBranch)
61
        // when SQUASHING, so things like CI config remain unchanged.
62
        // Example: []string{".github/workflows"}
63
        // NOTE: This is ignored when ShowCommits == true.
64
        RestorePath []string
65

66
        // OutputBranch is the name of the branch that will receive the result:
67
        //   - In squash mode (ShowCommits == false): the single squashed commit.
68
        //   - In keep-history mode (ShowCommits == true): the merge commit.
69
        // If empty, it defaults to "kubebuilder-update-from-<FromVersion>-to-<ToVersion>".
70
        OutputBranch string
71

72
        // Push, when true, pushes the OutputBranch to the "origin" remote after the update completes.
73
        Push bool
74

75
        // OpenGhIssue, when true, automatically creates a GitHub issue after the update
76
        // completes. The issue includes a pre-filled checklist and a compare link from
77
        // the base branch (--from-branch) to the output branch. This requires the GitHub
78
        // CLI (`gh`) to be installed and authenticated in the local environment.
79
        OpenGhIssue bool
80

81
        UseGhModels bool
82

83
        // GitConfig holds per-invocation Git settings applied to every `git` command via
84
        // `git -c key=value`.
85
        //
86
        // Examples:
87
        //   []string{"merge.renameLimit=999999"}         // improve rename detection during merges
88
        //   []string{"diff.renameLimit=999999"}          // improve rename detection during diffs
89
        //   []string{"merge.conflictStyle=diff3"}        // show ancestor in conflict markers
90
        //   []string{"rerere.enabled=true"}              // reuse recorded resolutions
91
        //
92
        // Defaults:
93
        //   When no --git-config flags are provided, the updater adds:
94
        //     []string{"merge.renameLimit=999999", "diff.renameLimit=999999"}
95
        //
96
        // Behavior:
97
        //   • If one or more --git-config flags are supplied, those values are appended on top of the defaults.
98
        //   • To disable the defaults entirely, include a literal "disable", for example:
99
        //       --git-config disable --git-config rerere.enabled=true
100
        GitConfig []string
101

102
        // Temporary branches created during the update process. These are internal to the run
103
        // and are surfaced for transparency/debugging:
104
        //   - AncestorBranch: clean scaffold generated from FromVersion
105
        //   - OriginalBranch: snapshot of the user's current project (FromBranch)
106
        //   - UpgradeBranch:  clean scaffold generated from ToVersion
107
        //   - MergeBranch:    result of merging Original into Upgrade (pre-output)
108
        AncestorBranch string
109
        OriginalBranch string
110
        UpgradeBranch  string
111
        MergeBranch    string
112
}
113

114
// Update a project using a default three-way Git merge.
115
// This helps apply new scaffolding changes while preserving custom code.
116
func (opts *Update) Update() error {
117
        log.Info("Checking out base branch", "branch", opts.FromBranch)
118
        checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch)
119
        if err := checkoutCmd.Run(); err != nil {
3✔
120
                return fmt.Errorf("failed to checkout base branch %s: %w", opts.FromBranch, err)
3✔
121
        }
3✔
122

4✔
123
        suffix := time.Now().Format("02-01-06-15-04")
1✔
124

1✔
125
        opts.AncestorBranch = "tmp-ancestor-" + suffix
126
        opts.OriginalBranch = "tmp-original-" + suffix
2✔
127
        opts.UpgradeBranch = "tmp-upgrade-" + suffix
2✔
128
        opts.MergeBranch = "tmp-merge-" + suffix
2✔
129

2✔
130
        log.Debug("temporary branches",
2✔
131
                "ancestor", opts.AncestorBranch,
2✔
132
                "original", opts.OriginalBranch,
2✔
133
                "upgrade", opts.UpgradeBranch,
2✔
134
                "merge", opts.MergeBranch,
2✔
135
        )
2✔
136

2✔
137
        // 1. Creates an ancestor branch based on base branch
2✔
138
        // 2. Deletes everything except .git and PROJECT
2✔
139
        // 3. Installs old release
2✔
140
        // 4. Runs alpha generate with old release binary
2✔
141
        // 5. Commits the result
2✔
142
        log.Info("Preparing Ancestor branch", "branch_name", opts.AncestorBranch)
2✔
143
        if err := opts.prepareAncestorBranch(); err != nil {
2✔
144
                return fmt.Errorf("failed to prepare ancestor branch: %w", err)
2✔
145
        }
2✔
146
        // 1. Creates original branch
3✔
147
        // 2. Ensure that original branch is == Based on user’s current base branch content with
1✔
148
        // git checkout "main" -- .
1✔
149
        // 3. Commits this state
150
        log.Info("Preparing Original branch", "branch_name", opts.OriginalBranch)
151
        if err := opts.prepareOriginalBranch(); err != nil {
152
                return fmt.Errorf("failed to checkout current off ancestor: %w", err)
153
        }
1✔
154
        // 1. Creates upgrade branch from ancestor
1✔
155
        // 2. Cleans up the branch by removing all files except .git and PROJECT
×
156
        // 2. Regenerates scaffold using alpha generate with new version
×
157
        // 3. Commits the result
158
        log.Info("Preparing Upgrade branch", "branch_name", opts.UpgradeBranch)
159
        if err := opts.prepareUpgradeBranch(); err != nil {
160
                return fmt.Errorf("failed to checkout upgrade off ancestor: %w", err)
161
        }
1✔
162

1✔
163
        // 1. Creates merge branch from upgrade
×
164
        // 2. Merges in original (user code)
×
165
        // 3. If conflicts occur, it will warn the user and leave the merge branch for manual resolution
166
        // 4. If merge is clean, it stages the changes and commits the result
167
        log.Info("Preparing Merge branch and performing merge", "branch_name", opts.MergeBranch)
168
        hasConflicts, err := opts.mergeOriginalToUpgrade()
169
        if err != nil {
170
                return fmt.Errorf("failed to merge upgrade into merge branch: %w", err)
1✔
171
        }
1✔
172

1✔
173
        // Squash or keep commits based on ShowCommits flag
×
174
        if opts.ShowCommits {
×
175
                log.Info("Keeping commits history")
176
                out := opts.getOutputBranchName()
177
                if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", out, opts.MergeBranch).Run(); err != nil {
1✔
178
                        return fmt.Errorf("checkout %s: %w", out, err)
×
179
                }
×
180
        } else {
×
181
                log.Info("Squashing merge result to output branch", "output_branch", opts.getOutputBranchName())
×
182
                if err := opts.squashToOutputBranch(hasConflicts); err != nil {
×
183
                        return fmt.Errorf("failed to squash to output branch: %w", err)
1✔
184
                }
1✔
185
        }
1✔
186

×
187
        // Push the output branch if requested
×
188
        if opts.Push {
189
                if opts.Push {
190
                        out := opts.getOutputBranchName()
191
                        _ = helpers.GitCmd(opts.GitConfig, "checkout", out).Run()
1✔
192
                        if err := helpers.GitCmd(opts.GitConfig, "push", "-u", "origin", out).Run(); err != nil {
×
193
                                return fmt.Errorf("failed to push %s: %w", out, err)
×
194
                        }
×
195
                }
×
196
        }
×
197

×
198
        opts.cleanupTempBranches()
199
        log.Info("Update completed successfully")
200

201
        if opts.OpenGhIssue {
1✔
202
                if err := opts.openGitHubIssue(hasConflicts); err != nil {
1✔
203
                        return fmt.Errorf("failed to open GitHub issue: %w", err)
1✔
204
                }
1✔
205
        }
×
206

×
207
        return nil
×
208
}
209

210
func (opts *Update) openGitHubIssue(hasConflicts bool) error {
1✔
211
        log.Info("Creating GitHub Issue to track the need to update the project")
212
        out := opts.getOutputBranchName()
213

6✔
214
        // Detect repo "owner/name"
6✔
215
        repoCmd := exec.Command("gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner")
6✔
216
        repoBytes, err := repoCmd.Output()
6✔
217
        if err != nil {
6✔
218
                return fmt.Errorf("failed to detect GitHub repository via `gh repo view`: %s", err)
6✔
219
        }
6✔
220
        repo := strings.TrimSpace(string(repoBytes))
9✔
221

3✔
222
        createPRURL := fmt.Sprintf("https://github.com/%s/compare/%s...%s?expand=1", repo, opts.FromBranch, out)
3✔
223
        title := fmt.Sprintf(helpers.IssueTitleTmpl, opts.ToVersion, opts.FromVersion)
3✔
224

3✔
225
        // Skip if an open issue with same title already exists
3✔
226
        checkCmd := exec.Command("gh", "issue", "list",
3✔
227
                "--repo", repo,
3✔
228
                "--state", "open",
3✔
229
                "--search", fmt.Sprintf("in:title \"%s\"", title),
3✔
230
                "--json", "title")
3✔
231
        if checkOut, checkErr := checkCmd.Output(); checkErr == nil && strings.Contains(string(checkOut), title) {
3✔
232
                log.Info("GitHub Issue already exists, skipping creation", "title", title)
3✔
233
                return nil
3✔
234
        }
3✔
235

×
236
        // Base issue body
×
237
        var body string
×
238
        if hasConflicts {
239
                body = fmt.Sprintf(helpers.IssueBodyTmplWithConflicts, opts.ToVersion, createPRURL, opts.FromVersion, out)
240
        } else {
3✔
241
                body = fmt.Sprintf(helpers.IssueBodyTmpl, opts.ToVersion, createPRURL, opts.FromVersion, out)
4✔
242
        }
1✔
243

3✔
244
        log.Info("Creating GitHub Issue")
2✔
245
        createCmd := exec.Command("gh", "issue", "create",
2✔
246
                "--repo", repo,
247
                "--title", title,
3✔
248
                "--body", body,
3✔
249
        )
3✔
250
        createOut, createErr := createCmd.CombinedOutput()
3✔
251
        if createErr != nil {
3✔
252
                return fmt.Errorf("failed to create GitHub issue: %v\n%s", createErr, string(createOut))
3✔
253
        }
3✔
254
        outStr := string(createOut)
4✔
255

1✔
256
        // Try to extract the issue URL from stdout
1✔
257
        issueURL := helpers.FirstURL(outStr)
2✔
258

2✔
259
        // Fallback: query the just-created issue by title
2✔
260
        if issueURL == "" {
2✔
261
                viewCmd := exec.Command("gh", "issue", "list",
2✔
262
                        "--repo", repo,
2✔
263
                        "--state", "open",
4✔
264
                        "--search", fmt.Sprintf("in:title \"%s\"", title),
2✔
265
                        "--json", "url",
2✔
266
                        "--jq", ".[0].url",
2✔
267
                )
2✔
268
                urlBytes, vErr := viewCmd.Output()
2✔
269
                if vErr != nil {
2✔
270
                        log.Warn("could not determine issue URL from gh output", "stdout", outStr, "error", vErr)
2✔
271
                }
2✔
272
                issueURL = strings.TrimSpace(string(urlBytes))
2✔
273
        }
×
274
        log.Info("GitHub Issue created to track the update", "url", issueURL, "compare", createPRURL)
×
275

2✔
276
        if opts.UseGhModels {
277
                log.Info("Generating AI summary with gh models")
2✔
278

2✔
279
                if issueURL == "" {
2✔
280
                        return fmt.Errorf("issue created but URL could not be determined")
×
281
                }
×
282

×
283
                releaseURL := fmt.Sprintf("https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%s",
×
284
                        opts.ToVersion)
×
285

286
                ctx := helpers.BuildFullPrompet(
×
287
                        opts.FromVersion, opts.ToVersion, opts.FromBranch, out,
×
288
                        createPRURL, releaseURL)
×
289

×
290
                var outBuf, errBuf bytes.Buffer
×
291
                cmd := exec.Command(
×
292
                        "gh", "models", "run", "openai/gpt-5",
×
293
                        "--system-prompt", helpers.AiPRPrompt,
×
294
                )
×
295
                cmd.Stdin = strings.NewReader(ctx)
×
296
                cmd.Stdout = &outBuf
×
297
                cmd.Stderr = &errBuf
×
298
                if err := cmd.Run(); err != nil {
×
299
                        return fmt.Errorf("gh models run failed: %w\nstderr:\n%s", err, errBuf.String())
×
300
                }
×
301

×
302
                summary := strings.TrimSpace(outBuf.String())
×
303
                if summary != "" {
×
304
                        num := helpers.IssueNumberFromURL(issueURL)
305
                        target := issueURL
×
306
                        args := []string{"issue", "comment", "--repo", repo}
×
307
                        if num != "" {
×
308
                                target = num
×
309
                        }
×
310
                        args = append(args, target, "--body", summary)
×
311
                        commentCmd := exec.Command("gh", args...)
×
312
                        commentCmd.Stdout = os.Stdout
×
313
                        commentCmd.Stderr = os.Stderr
×
314
                        if err := commentCmd.Run(); err != nil {
×
315
                                return fmt.Errorf("failed to add AI summary comment: %s", err)
×
316
                        }
×
317
                        log.Info("AI summary comment added to the issue")
×
318
                } else {
×
319
                        log.Warn("AI summary was empty, no comment added")
×
320
                }
×
321
        }
×
322
        return nil
×
323
}
×
324

325
func (opts *Update) cleanupTempBranches() {
2✔
326
        _ = helpers.GitCmd(opts.GitConfig, "checkout", opts.getOutputBranchName()).Run()
327

328
        branches := []string{
1✔
329
                opts.AncestorBranch,
1✔
330
                opts.OriginalBranch,
1✔
331
                opts.UpgradeBranch,
1✔
332
                opts.MergeBranch,
1✔
333
        }
1✔
334

1✔
335
        for _, b := range branches {
1✔
336
                b = strings.TrimSpace(b)
1✔
337
                if b == "" {
1✔
338
                        continue
5✔
339
                }
4✔
340
                // Delete only if it's a LOCAL branch.
4✔
341
                if err := helpers.GitCmd(opts.GitConfig,
×
342
                        "show-ref", "--verify", "--quiet", "refs/heads/"+b).Run(); err == nil {
343
                        _ = helpers.GitCmd(opts.GitConfig, "branch", "-D", b).Run()
344
                }
4✔
345
        }
8✔
346
}
4✔
347

4✔
348
// getOutputBranchName returns the output branch name
349
func (opts *Update) getOutputBranchName() string {
350
        if opts.OutputBranch != "" {
351
                return opts.OutputBranch
352
        }
20✔
353
        return fmt.Sprintf("kubebuilder-update-from-%s-to-%s", opts.FromVersion, opts.ToVersion)
23✔
354
}
3✔
355

3✔
356
// preservePaths checks out the paths specified in RestorePath
17✔
357
func (opts *Update) preservePaths() {
358
        for _, p := range opts.RestorePath {
359
                p = strings.TrimSpace(p)
360
                if p == "" {
5✔
361
                        continue
9✔
362
                }
4✔
363
                if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch, "--", p).Run(); err != nil {
5✔
364
                        log.Warn("failed to restore preserved path", "path", p, "branch", opts.FromBranch, "error", err)
1✔
365
                }
366
        }
3✔
367
}
×
368

×
369
// squashToOutputBranch takes the exact tree of the MergeBranch and writes it as ONE commit
370
// on a branch derived from FromBranch (e.g., "main"). If RestorePath is set, those paths
371
// are restored from the base branch after copying the merge tree, so CI config etc. stays put.
372
func (opts *Update) squashToOutputBranch(hasConflicts bool) error {
373
        out := opts.getOutputBranchName()
374

375
        // 1) base -> out
5✔
376
        if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch).Run(); err != nil {
5✔
377
                return fmt.Errorf("checkout %s: %w", opts.FromBranch, err)
5✔
378
        }
5✔
379
        if err := helpers.GitCmd(opts.GitConfig, "checkout", "-B", out, opts.FromBranch).Run(); err != nil {
5✔
380
                return fmt.Errorf("create/reset %s from %s: %w", out, opts.FromBranch, err)
×
381
        }
×
382

5✔
383
        // 2) clean worktree, then copy merge tree
×
384
        if err := helpers.CleanWorktree("output branch"); err != nil {
×
385
                return fmt.Errorf("output branch: %w", err)
386
        }
387
        if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.MergeBranch, "--", ".").Run(); err != nil {
5✔
388
                return fmt.Errorf("checkout %s content: %w", "merge", err)
×
389
        }
×
390

5✔
391
        // 3) optionally restore preserved paths from base (tests assert on 'git restore …')
×
392
        opts.preservePaths()
×
393

394
        // 4) stage and single squashed commit
395
        if err := helpers.GitCmd(opts.GitConfig, "add", "--all").Run(); err != nil {
5✔
396
                return fmt.Errorf("stage output: %w", err)
5✔
397
        }
5✔
398

5✔
399
        if err := helpers.CommitIgnoreEmpty(opts.getMergeMessage(hasConflicts), "final"); err != nil {
×
400
                return fmt.Errorf("failed to commit final branch: %w", err)
×
401
        }
402

5✔
403
        return nil
×
404
}
×
405

406
// regenerateProjectWithVersion downloads the release binary for the specified version,
5✔
407
// and runs the `alpha generate` command to re-scaffold the project
408
func regenerateProjectWithVersion(version string) error {
409
        tempDir, err := helpers.DownloadReleaseVersionWith(version)
410
        if err != nil {
411
                return fmt.Errorf("failed to download release %s binary: %w", version, err)
8✔
412
        }
8✔
413
        if err := runAlphaGenerate(tempDir, version); err != nil {
10✔
414
                return fmt.Errorf("failed to run alpha generate on ancestor branch: %w", err)
2✔
415
        }
2✔
416
        return nil
7✔
417
}
1✔
418

1✔
419
// prepareAncestorBranch prepares the ancestor branch by checking it out,
5✔
420
// cleaning up the project files, and regenerating the project with the specified version.
421
func (opts *Update) prepareAncestorBranch() error {
422
        if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.AncestorBranch, opts.FromBranch).Run(); err != nil {
423
                return fmt.Errorf("failed to create %s from %s: %w", opts.AncestorBranch, opts.FromBranch, err)
424
        }
4✔
425
        if err := cleanupBranch(); err != nil {
5✔
426
                return fmt.Errorf("failed to cleanup the %s : %w", opts.AncestorBranch, err)
1✔
427
        }
1✔
428
        if err := regenerateProjectWithVersion(opts.FromVersion); err != nil {
3✔
429
                return fmt.Errorf("failed to regenerate project with fromVersion %s: %w", opts.FromVersion, err)
×
430
        }
×
431
        gitCmd := helpers.GitCmd(opts.GitConfig, "add", "--all")
4✔
432
        if err := gitCmd.Run(); err != nil {
1✔
433
                return fmt.Errorf("failed to stage changes in %s: %w", opts.AncestorBranch, err)
1✔
434
        }
2✔
435
        commitMessage := "(chore) initial scaffold from release version: " + opts.FromVersion
2✔
436
        if err := helpers.CommitIgnoreEmpty(commitMessage, "ancestor"); err != nil {
×
437
                return fmt.Errorf("failed to commit ancestor branch: %w", err)
×
438
        }
2✔
439
        return nil
2✔
440
}
×
441

×
442
// cleanupBranch removes all files and folders in the current directory
2✔
443
// except for the .git directory and the PROJECT file.
444
// This is necessary to ensure the ancestor branch starts with a clean slate
445
// TODO: Analise if this command is still needed in the future.
446
// It is required because the alpha generate command in versions prior to v4.7.0 do not properly
447
// handle the removal of files in the ancestor branch.
448
func cleanupBranch() error {
449
        cmd := exec.Command("sh", "-c", "find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'PROJECT' -exec rm -rf {} +")
450

451
        if err := cmd.Run(); err != nil {
7✔
452
                return fmt.Errorf("failed to clean up files: %w", err)
7✔
453
        }
7✔
454
        return nil
8✔
455
}
1✔
456

1✔
457
// runMakeTargets runs the make targets needed to keep the tree consistent.
6✔
458
// If skipConflicts is true, it avoids running targets that are guaranteed
459
// to fail noisily when there are unresolved conflicts.
460
func runMakeTargets(skipConflicts bool) {
461
        if !skipConflicts {
462
                for _, t := range []string{"manifests", "generate", "fmt", "vet", "lint-fix"} {
463
                        if err := util.RunCmd(fmt.Sprintf("Running make %s", t), "make", t); err != nil {
10✔
464
                                log.Warn("make target failed", "target", t, "error", err)
17✔
465
                        }
42✔
466
                }
40✔
467
                return
5✔
468
        }
5✔
469

470
        // Conflict-aware path: decide what to run based on repo state.
7✔
471
        cs := helpers.DetectConflicts()
472
        targets := helpers.DecideMakeTargets(cs)
473

474
        if cs.Makefile {
3✔
475
                log.Warn("Skipping all make targets because Makefile has merge conflicts")
3✔
476
                return
3✔
477
        }
3✔
478
        if cs.API {
×
479
                log.Warn("API conflicts detected; skipping make targets: manifests, generate")
×
480
        }
×
481
        if cs.AnyGo {
3✔
482
                log.Warn("Go conflicts detected; skipping make targets: fmt, vet, lint-fix")
×
483
        }
×
484

3✔
UNCOV
485
        if len(targets) == 0 {
×
UNCOV
486
                log.Warn("No make targets will be run due to conflicts")
×
487
                return
488
        }
3✔
489

×
490
        for _, t := range targets {
×
491
                if err := util.RunCmd(fmt.Sprintf("Running make %s", t), "make", t); err != nil {
×
492
                        log.Warn("make target failed", "target", t, "error", err)
493
                }
18✔
494
        }
15✔
495
}
×
496

×
497
// runAlphaGenerate executes the old Kubebuilder version's 'alpha generate' command
498
// to create clean scaffolding in the ancestor branch. This uses the downloaded
499
// binary with the original PROJECT file to recreate the project's initial state.
500
func runAlphaGenerate(tempDir, version string) error {
501
        log.Info("Generating project", "version", version)
502

503
        tempBinaryPath := tempDir + "/kubebuilder"
9✔
504
        cmd := exec.Command(tempBinaryPath, "alpha", "generate")
9✔
505
        cmd.Env = envWithPrefixedPath(tempDir)
9✔
506
        cmd.Stdout = os.Stdout
9✔
507
        cmd.Stderr = os.Stderr
9✔
508

9✔
509
        if err := cmd.Run(); err != nil {
9✔
510
                return fmt.Errorf("failed to run alpha generate: %w", err)
9✔
511
        }
9✔
512

9✔
513
        log.Info("Project scaffold generation complete", "version", version)
×
514
        runMakeTargets(false)
×
515
        return nil
9✔
516
}
9✔
517

×
518
func envWithPrefixedPath(dir string) []string {
×
519
        env := os.Environ()
520
        prefix := "PATH="
9✔
521
        for i, kv := range env {
×
522
                if strings.HasPrefix(kv, prefix) {
×
523
                        env[i] = "PATH=" + dir + string(os.PathListSeparator) + strings.TrimPrefix(kv, prefix)
524
                        return env
525
                }
9✔
526
        }
9✔
527
        return append(env, "PATH="+dir)
9✔
528
}
12✔
529

3✔
530
// prepareOriginalBranch creates the 'original' branch from ancestor and
3✔
531
// populates it with the user's actual project content from the default branch.
532
// This represents the current state of the user's project.
6✔
533
func (opts *Update) prepareOriginalBranch() error {
6✔
534
        gitCmd := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.OriginalBranch)
6✔
535
        if err := gitCmd.Run(); err != nil {
536
                return fmt.Errorf("failed to checkout branch %s: %w", opts.OriginalBranch, err)
537
        }
538

18✔
539
        gitCmd = helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch, "--", ".")
18✔
540
        if err := gitCmd.Run(); err != nil {
18✔
541
                return fmt.Errorf("failed to checkout content from %s branch onto %s: %w", opts.FromBranch, opts.OriginalBranch, err)
18✔
542
        }
18✔
543

18✔
544
        gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all")
18✔
545
        if err := gitCmd.Run(); err != nil {
×
546
                return fmt.Errorf("failed to stage all changes in current: %w", err)
×
547
        }
×
548
        if err := helpers.CommitIgnoreEmpty(
×
549
                fmt.Sprintf("(chore) original code from %s to keep changes", opts.FromBranch),
×
550
                "original",
×
551
        ); err != nil {
×
552
                return fmt.Errorf("failed to commit original branch: %w", err)
×
553
        }
×
554
        return nil
×
555
}
×
556

×
557
// prepareUpgradeBranch creates the 'upgrade' branch from ancestor and
×
558
// generates fresh scaffolding using the current (latest) CLI version.
×
559
// This represents what the project should look like with the new version.
×
560
func (opts *Update) prepareUpgradeBranch() error {
×
561
        gitCmd := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.UpgradeBranch, opts.AncestorBranch)
×
562
        if err := gitCmd.Run(); err != nil {
×
563
                return fmt.Errorf("failed to checkout %s branch off %s: %w",
×
564
                        opts.UpgradeBranch, opts.AncestorBranch, err)
×
565
        }
×
566

×
567
        checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.UpgradeBranch)
×
568
        if err := checkoutCmd.Run(); err != nil {
×
569
                return fmt.Errorf("failed to checkout base branch %s: %w", opts.UpgradeBranch, err)
×
570
        }
571

×
572
        if err := cleanupBranch(); err != nil {
×
573
                return fmt.Errorf("failed to cleanup the %s branch: %w", opts.UpgradeBranch, err)
×
574
        }
×
575
        if err := regenerateProjectWithVersion(opts.ToVersion); err != nil {
×
576
                return fmt.Errorf("failed to regenerate project with version %s: %w", opts.ToVersion, err)
×
577
        }
×
578
        gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all")
579
        if err := gitCmd.Run(); err != nil {
580
                return fmt.Errorf("failed to stage changes in %s: %w", opts.UpgradeBranch, err)
581
        }
582
        if err := helpers.CommitIgnoreEmpty(
9✔
583
                "(chore) initial scaffold from release version: "+opts.ToVersion, "upgrade"); err != nil {
9✔
584
                return fmt.Errorf("failed to commit upgrade branch: %w", err)
9✔
585
        }
990✔
586
        return nil
990✔
587
}
9✔
588

9✔
589
// mergeOriginalToUpgrade attempts to merge the upgrade branch
9✔
590
func (opts *Update) mergeOriginalToUpgrade() (bool, error) {
591
        hasConflicts := false
×
592
        if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.MergeBranch, opts.UpgradeBranch).Run(); err != nil {
593
                return hasConflicts, fmt.Errorf("failed to create merge branch %s from %s: %w",
594
                        opts.MergeBranch, opts.UpgradeBranch, err)
595
        }
596

597
        checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.MergeBranch)
3✔
598
        if err := checkoutCmd.Run(); err != nil {
3✔
599
                return hasConflicts, fmt.Errorf("failed to checkout base branch %s: %w", opts.MergeBranch, err)
4✔
600
        }
1✔
601

1✔
602
        mergeCmd := helpers.GitCmd(opts.GitConfig, "merge", "--no-edit", "--no-commit", opts.OriginalBranch)
603
        err := mergeCmd.Run()
2✔
604
        if err != nil {
2✔
605
                var exitErr *exec.ExitError
×
606
                // If the merge has an error that is not a conflict, return an error 2
×
607
                if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
608
                        hasConflicts = true
2✔
609
                        if !opts.Force {
2✔
610
                                log.Warn("Merge stopped due to conflicts. Manual resolution is required.")
×
611
                                log.Warn("After resolving the conflicts, run the following command:")
×
612
                                log.Warn("    make manifests generate fmt vet lint-fix")
2✔
613
                                log.Warn("This ensures manifests and generated files are up to date, and the project layout remains consistent.")
2✔
614
                                return hasConflicts, fmt.Errorf("merge stopped due to conflicts")
2✔
615
                        }
2✔
616
                        log.Warn("Merge completed with conflicts. Conflict markers will be committed.")
×
617
                } else {
×
618
                        return hasConflicts, fmt.Errorf("merge failed unexpectedly: %w", err)
2✔
619
                }
620
        }
621

622
        if !hasConflicts {
623
                log.Info("Merge happened without conflicts.")
624
        }
3✔
625

3✔
626
        // Best effort to run make targets to ensure the project is in a good state
4✔
627
        runMakeTargets(true)
1✔
628

1✔
629
        // Step 4: Stage and commit
1✔
630
        if err := helpers.GitCmd(opts.GitConfig, "add", "--all").Run(); err != nil {
631
                return hasConflicts, fmt.Errorf("failed to stage merge results: %w", err)
2✔
632
        }
2✔
633

×
634
        if err := helpers.CommitIgnoreEmpty(opts.getMergeMessage(hasConflicts), "merge"); err != nil {
×
635
                return hasConflicts, fmt.Errorf("failed to commit merge branch: %w", err)
636
        }
2✔
637
        log.Info("Merge completed")
×
638
        return hasConflicts, nil
×
639
}
2✔
640

×
641
func (opts *Update) getMergeMessage(hasConflicts bool) string {
×
642
        base := fmt.Sprintf("scaffold update: %s -> %s", opts.FromVersion, opts.ToVersion)
2✔
643
        if hasConflicts {
2✔
644
                return fmt.Sprintf(":warning: (chore) [with conflicts] %s", base)
×
645
        }
×
646
        return fmt.Sprintf("(chore) %s", base)
2✔
647
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc