Skip to content

Commit 69a4cf0

Browse files
✨ (alpha update): easier commits, safer defaults, more control (#5013)
chore: improve alpha update with clearer commits, safer defaults, and more control - clearer commit messages (e.g. "(chore) scaffold from release vX.Y.Z") - better error reporting with actionable context - automatic cleanup of temporary branches - predictable output always on the output branch - simpler history by default (squash), with --show-commits for full history - explicit push control (nothing pushed unless --push) - refactored code for maintainability and updated docs Assisted-by: ChatGPT (OpenAI) Co-authored-by: Vitor Floriano <[email protected]>
1 parent ec50522 commit 69a4cf0

File tree

13 files changed

+967
-554
lines changed

13 files changed

+967
-554
lines changed

docs/book/src/reference/commands/alpha_update.md

Lines changed: 93 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,12 @@
22

33
## Overview
44

5-
`kubebuilder alpha update` upgrades your project’s scaffold to a newer scaffold version.
5+
`kubebuilder alpha update` upgrades your project’s scaffold to a newer Kubebuilder release using a **3-way Git merge**. It rebuilds clean scaffolds for the old and new versions, merges your current code into the new scaffold, and gives you a reviewable output branch.
6+
It takes care of the heavy lifting so you can focus on reviewing and resolving conflicts,
7+
not re-applying your code.
68

7-
It uses a **3-way merge** so you do less manual work. The command creates these branches:
8-
9-
- **Ancestor**: clean scaffold from the **old** version
10-
- **Original**: your current project (from your base branch)
11-
- **Upgrade**: clean scaffold from the **new** version
12-
- **Merge**: result of merging **Original** into **Upgrade** (this is where conflicts appear)
13-
14-
You can review and test the merge result before applying it to your main branch.
15-
Optionally, use **`--squash`** to put the merge result into **one commit** on a stable output branch (great for PRs).
16-
17-
<aside class="note warning">
18-
<h1>Creates branches and deletes files</h1>
19-
20-
This command creates branches like `tmp-kb-update-*` and removes files during the process.
21-
Make sure your work is committed before you run it.
22-
23-
</aside>
9+
By default, the final result is **squashed into a single commit** on a dedicated output branch.
10+
If you prefer to keep the full history (no squash), use `--show-commits`.
2411

2512
## When to Use It
2613

@@ -33,26 +20,45 @@ Use this command when you:
3320

3421
## How It Works
3522

36-
1. **Detect versions**
37-
Reads `--from-version` (or the `PROJECT` file) and `--to-version` (or uses the latest).
38-
39-
2. **Create branches & re-scaffold**
40-
- `tmp-ancestor-*`: clean scaffold from **from-version**
41-
- `tmp-original-*`: snapshot of your **from-branch** (e.g., `main`)
42-
- `tmp-upgrade-*`: clean scaffold from **to-version**
43-
44-
3. **3-way merge**
45-
Creates `tmp-merge-*` from **Upgrade** and merges **Original** into it.
46-
Runs `make manifests generate fmt vet lint-fix` to normalise outputs.
47-
Runs `make manifests generate fmt vet lint-fix` to normalize outputs.
48-
49-
4. **(Optional) Squash**
50-
With `--squash`, copies the merge result to a stable output branch and commits **once**:
51-
- Default output branch: `kubebuilder-alpha-update-to-<to-version>`
52-
- Or set your own with `--output-branch`
53-
If there are conflicts, the single commit will include conflict markers.
54-
55-
## How to Use It
23+
You tell the tool the **new version**, and which branch has your project.
24+
It rebuilds both scaffolds, merges your code into the new one with a **3-way merge**,
25+
and gives you an output branch you can review and merge safely.
26+
You decide if you want one clean commit, the full history, or an auto-push to remote.
27+
28+
### Step 1: Detect versions
29+
- It looks at your `PROJECT` file or the flags you pass.
30+
- Decides which **old version** you are coming from by reading the `cliVersion` field in the `PROJECT` file (if available).
31+
- Figures out which **new version** you want (defaults to the latest release).
32+
- Chooses which branch has your current code (defaults to `main`).
33+
34+
### Step 2: Create scaffolds
35+
The command creates three temporary branches:
36+
- **Ancestor**: a clean project scaffold from the **old version**.
37+
- **Original**: a snapshot of your **current code**.
38+
- **Upgrade**: a clean scaffold from the **new version**.
39+
40+
### Step 3: Do a 3-way merge
41+
- Merges **Original** (your code) into **Upgrade** (the new scaffold) using Git’s **3-way merge**.
42+
- This keeps your customizations while pulling in upstream changes.
43+
- If conflicts happen:
44+
- **Default** → stop and let you resolve them manually.
45+
- **With `--force`** → continue and commit even with conflict markers. **(ideal for automation)**
46+
- Runs `make manifests generate fmt vet lint-fix` to tidy things up.
47+
48+
### Step 4: Write the output branch
49+
- By default, everything is **squashed into one commit** on a safe output branch:
50+
`kubebuilder-update-from-<from-version>-to-<to-version>`.
51+
- You can change the behavior:
52+
- `--show-commits`: keep the full history.
53+
- `--preserve-path`: in squash mode, restore specific files (like CI configs) from your base branch.
54+
- `--output-branch`: pick a custom branch name.
55+
- `--push`: push the result to `origin` automatically.
56+
57+
### Step 5: Cleanup
58+
- Once the output branch is ready, all the temporary working branches are deleted.
59+
- You are left with one clean branch you can test, review, and merge back into your main branch.
60+
61+
## How to Use It (commands)
5662

5763
Run from your project root:
5864

@@ -64,38 +70,43 @@ Pin versions and base branch:
6470

6571
```shell
6672
kubebuilder alpha update \
67-
--from-version v4.5.2 \
68-
--to-version v4.6.0 \
69-
--from-branch main
73+
--from-version v4.5.2 \
74+
--to-version v4.6.0 \
75+
--from-branch main
7076
```
7177
Automation-friendly (proceed even with conflicts):
7278

7379
```shell
7480
kubebuilder alpha update --force
7581
```
7682

77-
Create a **single squashed commit** on a stable PR branch:
83+
Keep full history instead of squashing:
84+
```
85+
kubebuilder alpha update --from-version v4.5.0 --to-version v4.7.0 --force --show-commits
86+
```
87+
88+
Default squash but **preserve** CI/workflows from the base branch:
7889

7990
```shell
80-
kubebuilder alpha update --force --squash
91+
kubebuilder alpha update --force \
92+
--preserve-path .github/workflows \
93+
--preserve-path docs
8194
```
8295

83-
Squash while **preserving** paths from your base branch (keep CI/workflows, docs, etc.):
96+
Use a custom output branch name:
8497

8598
```shell
86-
kubebuilder alpha update --force --squash \
87-
--preserve-path .github/workflows \
88-
--preserve-path docs
99+
kubebuilder alpha update --force \
100+
--output-branch upgrade/kb-to-v4.7.0
89101
```
90102

91-
Use a **custom output branch** name:
103+
Run update and push the result to origin:
92104

93105
```shell
94-
kubebuilder alpha update --force --squash \
95-
-output-branch upgrade/kb-to-v4.7.0
106+
kubebuilder alpha update --from-version v4.6.0 --to-version v4.7.0 --force --push
96107
```
97108

98-
## Merge Conflicts with `--force`
109+
## Handling Conflicts (`--force` vs default)
99110

100111
When you use `--force`, Git finishes the merge even if there are conflicts.
101112
The commit will include markers like:
@@ -105,15 +116,35 @@ The commit will include markers like:
105116
Your changes
106117
=======
107118
Incoming changes
108-
>>>>>>> tmp-original-…
119+
>>>>>>> (original)
109120
```
110121

111-
- **Without `--force`**: the command stops on `tmp-merge-*` and prints guidance; no commit is created.
112-
- **With `--force`**: the merge is committed (on `tmp-merge-*`, or on the output branch if using `--squash`) and contains the markers.
122+
This allows you to run the command in CI or cron jobs without manual intervention.
123+
124+
- Without `--force`: the command stops on the merge branch and prints guidance; no commit is created.
125+
- With `--force`: the merge is committed (merge or output branch) and contains the markers.
126+
127+
After you fix conflicts, always run:
128+
129+
```shell
130+
make manifests generate fmt vet lint-fix
131+
# or
132+
make all
133+
```
113134

114-
## Commit message used in `--squash` mode
135+
## Flags
115136

116-
> [kubebuilder-automated-update]: update scaffold from <from> to <to>; (squashed 3-way merge)
137+
| Flag | Description |
138+
|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
139+
| `--from-version` | Kubebuilder release to update **from** (e.g., `v4.6.0`). If unset, read from the `PROJECT` file when possible. |
140+
| `--to-version` | Kubebuilder release to update **to** (e.g., `v4.7.0`). If unset, defaults to the latest available release. |
141+
| `--from-branch` | Git branch that holds your current project code. Defaults to `main`. |
142+
| `--force` | Continue even if merge conflicts happen. Conflicted files are committed with conflict markers (CI/cron friendly). |
143+
| `--show-commits` | Keep full history (do not squash). **Not compatible** with `--preserve-path`. |
144+
| `--preserve-path` | Repeatable. **Squash mode only.** After copying the merge tree to the output branch, restore these paths from the base branch (e.g., `.github/workflows`). |
145+
| `--output-branch` | Name of the output branch. Default: `kubebuilder-update-from-<from-version>-to-<to-version>`. |
146+
| `--push` | Push the output branch to the `origin` remote after the update completes. |
147+
| `-h, --help` | Show help for this command. |
117148

118149
<aside class="note warning">
119150
<h1>You might need to upgrade your project first</h1>
@@ -133,51 +164,19 @@ We use that value to pick the correct CLI for re-scaffolding.
133164

134165
</aside>
135166

136-
<aside class="note warning">
137-
You must resolve these conflicts before merging into `main` (or your base branch).
138-
<strong>After resolving conflicts, always run:</strong>
139-
140-
```shell
141-
make manifests generate fmt vet lint-fix
142-
# or
143-
make all
144-
```
145-
146-
</aside>
147-
148-
## Flags
167+
## Demonstration
149168

150-
| Flag | Description |
151-
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
152-
| `--from-version` | Kubebuilder version your project was created with. If unset, taken from the `PROJECT` file. |
153-
| `--to-version` | Version to upgrade to. Defaults to the latest release. |
154-
| `--from-branch` | Git branch that has your current project code. Defaults to `main`. |
155-
| `--force` | Continue even if merge conflicts happen. Conflicted files are committed with conflict markers (useful for CI/cron). |
156-
| `--squash` | Write the merge result as **one commit** on a stable output branch. |
157-
| `--preserve-path` | Repeatable. With `--squash`, restore these paths from the base branch (e.g., `--preserve-path .github/workflows`). |
158-
| `--output-branch` | Branch name to use for the squashed commit (default: `kubebuilder-alpha-update-to-<to-version>`). |
159-
| `-h, --help` | Show help for this command. |
169+
<iframe width="560" height="315" src="https://www.youtube.com/embed/J8zonID__8k?si=WC-FXOHX0mCjph71" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
160170

161171
<aside class="note">
162-
<h1>CLI Version Tracking</h1>
163-
164-
Projects created with **Kubebuilder v4.6.0+** include `cliVersion` in the `PROJECT` file.
165-
We use that value to pick the correct CLI for re-scaffolding.
172+
<h1>About this demo</h1>
166173

167-
If your project was created with an older version,
168-
you can set `--from-version` to the version you used.
169-
170-
However, this command uses `kubebuilder alpha generate` under the hood.
171-
We tested projects created with <strong>v4.5.0+</strong>.
172-
If yours is older, first run `kubebuilder alpha generate` once to modernize the scaffold.
173-
After that, you can use `kubebuilder alpha update` for future upgrades.
174+
This video was recorded with Kubebuilder release `v7.0.1`.
175+
Since then, the command has been improved,
176+
so the current behavior may differ slightly from what is shown in the demo.
174177

175178
</aside>
176179

177-
## Demonstration
178-
179-
<iframe width="560" height="315" src="https://www.youtube.com/embed/J8zonID__8k?si=WC-FXOHX0mCjph71" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
180-
181180
## Further Resources
182181

183182
- WIP: Design proposal for update automation — https://github.com/kubernetes-sigs/kubebuilder/pull/4302

pkg/cli/alpha/generate_test.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,10 @@ limitations under the License.
1616
package alpha
1717

1818
import (
19-
"testing"
20-
2119
. "github.com/onsi/ginkgo/v2"
2220
. "github.com/onsi/gomega"
2321
)
2422

25-
// Figuring out ways to test run these tests similar to existing.
26-
// Currently unable to run without this on VSCode. Will remove once done
27-
func TestCommand(t *testing.T) {
28-
RegisterFailHandler(Fail)
29-
RunSpecs(t, "alpha")
30-
}
31-
3223
var _ = Describe("NewScaffoldCommand", func() {
3324
When("NewScaffoldCommand", func() {
3425
It("Testing the NewScaffoldCommand", func() {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 helpers
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"io"
23+
log "log/slog"
24+
"net/http"
25+
"os"
26+
"path/filepath"
27+
"runtime"
28+
"time"
29+
30+
"github.com/spf13/afero"
31+
)
32+
33+
const KubebuilderReleaseURL = "https://github.com/kubernetes-sigs/kubebuilder/releases/download/%s/kubebuilder_%s_%s"
34+
35+
func BuildReleaseURL(version string) string {
36+
return fmt.Sprintf(KubebuilderReleaseURL, version, runtime.GOOS, runtime.GOARCH)
37+
}
38+
39+
// DownloadReleaseVersionWith downloads the specified released version from GitHub releases and saves it
40+
// to a temporary directory with executable permissions.
41+
// Returns the temporary directory path containing the binary.
42+
func DownloadReleaseVersionWith(version string) (string, error) {
43+
url := BuildReleaseURL(version)
44+
45+
// Create temp directory
46+
fs := afero.NewOsFs()
47+
tempDir, err := afero.TempDir(fs, "", "kubebuilder"+version+"-")
48+
if err != nil {
49+
return "", fmt.Errorf("failed to create temporary directory: %w", err)
50+
}
51+
52+
// Ensure cleanup on any error after this point
53+
cleanupOnErr := func() {
54+
if rmErr := os.RemoveAll(tempDir); rmErr != nil {
55+
log.Error("failed to remove temporary directory", "dir", tempDir, "error", rmErr)
56+
}
57+
}
58+
59+
binaryPath := filepath.Join(tempDir, "kubebuilder")
60+
f, err := fs.Create(binaryPath)
61+
if err != nil {
62+
cleanupOnErr()
63+
return "", fmt.Errorf("failed to create the binary file: %w", err)
64+
}
65+
defer func() {
66+
if closeErr := f.Close(); closeErr != nil {
67+
log.Error("failed to close the binary file", "error", closeErr)
68+
}
69+
}()
70+
71+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
72+
defer cancel()
73+
74+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
75+
if err != nil {
76+
cleanupOnErr()
77+
return "", fmt.Errorf("failed to build download request: %w", err)
78+
}
79+
req.Header.Set("User-Agent", "kubebuilder-updater/1.0 (+https://github.com/kubernetes-sigs/kubebuilder)")
80+
81+
resp, err := http.DefaultClient.Do(req)
82+
if err != nil {
83+
cleanupOnErr()
84+
return "", fmt.Errorf("failed to download the binary: %w", err)
85+
}
86+
defer func() {
87+
if closeErr := resp.Body.Close(); closeErr != nil {
88+
log.Error("failed to close HTTP response body", "error", closeErr)
89+
}
90+
}()
91+
92+
if resp.StatusCode != http.StatusOK {
93+
cleanupOnErr()
94+
return "", fmt.Errorf("failed to download the binary: HTTP %d", resp.StatusCode)
95+
}
96+
97+
if _, err := io.Copy(f, resp.Body); err != nil {
98+
cleanupOnErr()
99+
return "", fmt.Errorf("failed to write the binary content to file: %w", err)
100+
}
101+
102+
// Flush to disk before changing mode (best effort)
103+
if syncErr := f.Sync(); syncErr != nil {
104+
log.Warn("failed to sync binary to disk (continuing)", "error", syncErr)
105+
}
106+
107+
if err := os.Chmod(binaryPath, 0o755); err != nil {
108+
cleanupOnErr()
109+
return "", fmt.Errorf("failed to make binary executable: %w", err)
110+
}
111+
112+
return tempDir, nil
113+
}

0 commit comments

Comments
 (0)