Skip to content

Commit 598d178

Browse files
Change cmd/version to ensure that for dev env and test we still having cliVersion: (devel) instead of dirty tag
1 parent 2c7cea6 commit 598d178

File tree

8 files changed

+347
-16
lines changed

8 files changed

+347
-16
lines changed

cmd/version.go

Lines changed: 193 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,23 @@ package cmd
1818

1919
import (
2020
"fmt"
21+
"os"
22+
"os/exec"
23+
"path/filepath"
24+
"runtime"
2125
"runtime/debug"
26+
"strconv"
27+
"strings"
28+
"time"
29+
30+
"golang.org/x/mod/semver"
2231
)
2332

24-
const unknown = "unknown"
33+
const (
34+
unknown = "unknown"
35+
develVersion = "(devel)"
36+
pseudoVersionTimestampLayout = "20060102150405"
37+
)
2538

2639
// var needs to be used instead of const as ldflags is used to fill this
2740
// information in the release process
@@ -47,11 +60,7 @@ type version struct {
4760

4861
// versionString returns the Full CLI version
4962
func versionString() string {
50-
if kubeBuilderVersion == unknown {
51-
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
52-
kubeBuilderVersion = info.Main.Version
53-
}
54-
}
63+
kubeBuilderVersion = getKubebuilderVersion()
5564

5665
return fmt.Sprintf("Version: %#v", version{
5766
kubeBuilderVersion,
@@ -65,10 +74,184 @@ func versionString() string {
6574

6675
// getKubebuilderVersion returns only the CLI version string
6776
func getKubebuilderVersion() string {
68-
if kubeBuilderVersion == unknown {
69-
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
70-
kubeBuilderVersion = info.Main.Version
71-
}
77+
if strings.Contains(kubeBuilderVersion, "dirty") {
78+
return develVersion
79+
}
80+
if shouldResolveVersion(kubeBuilderVersion) {
81+
kubeBuilderVersion = resolveKubebuilderVersion()
7282
}
7383
return kubeBuilderVersion
7484
}
85+
86+
func shouldResolveVersion(v string) bool {
87+
return v == "" || v == unknown || v == develVersion
88+
}
89+
90+
func resolveKubebuilderVersion() string {
91+
if info, ok := debug.ReadBuildInfo(); ok {
92+
mainVersion := strings.TrimSpace(info.Main.Version)
93+
if mainVersion != "" && mainVersion != develVersion {
94+
return mainVersion
95+
}
96+
97+
if v := pseudoVersionFromGit(info.Main.Path); v != "" {
98+
return v
99+
}
100+
}
101+
102+
if v := pseudoVersionFromGit(""); v != "" {
103+
return v
104+
}
105+
106+
return unknown
107+
}
108+
109+
func pseudoVersionFromGit(modulePath string) string {
110+
repoRoot, err := findRepoRoot()
111+
if err != nil {
112+
return ""
113+
}
114+
return pseudoVersionFromGitDir(modulePath, repoRoot)
115+
}
116+
117+
func pseudoVersionFromGitDir(modulePath, repoRoot string) string {
118+
dirty, err := repoDirty(repoRoot)
119+
if err != nil {
120+
return ""
121+
}
122+
if dirty {
123+
return develVersion
124+
}
125+
126+
commitHash, err := runGitCommand(repoRoot, "rev-parse", "--short=12", "HEAD")
127+
if err != nil || commitHash == "" {
128+
return ""
129+
}
130+
131+
commitTimestamp, err := runGitCommand(repoRoot, "show", "-s", "--format=%ct", "HEAD")
132+
if err != nil || commitTimestamp == "" {
133+
return ""
134+
}
135+
seconds, err := strconv.ParseInt(commitTimestamp, 10, 64)
136+
if err != nil {
137+
return ""
138+
}
139+
timestamp := time.Unix(seconds, 0).UTC().Format(pseudoVersionTimestampLayout)
140+
141+
if tag, err := runGitCommand(repoRoot, "describe", "--tags", "--exact-match"); err == nil {
142+
tag = strings.TrimSpace(tag)
143+
if tag != "" {
144+
return tag
145+
}
146+
}
147+
148+
if baseTag, err := runGitCommand(repoRoot, "describe", "--tags", "--abbrev=0"); err == nil {
149+
baseTag = strings.TrimSpace(baseTag)
150+
if semver.IsValid(baseTag) {
151+
if next := incrementPatch(baseTag); next != "" {
152+
return fmt.Sprintf("%s-0.%s-%s", next, timestamp, commitHash)
153+
}
154+
}
155+
if baseTag != "" {
156+
return baseTag
157+
}
158+
}
159+
160+
major := moduleMajorVersion(modulePath)
161+
return buildDefaultPseudoVersion(major, timestamp, commitHash)
162+
}
163+
164+
func repoDirty(repoRoot string) (bool, error) {
165+
status, err := runGitCommand(repoRoot, "status", "--porcelain", "--untracked-files=no")
166+
if err != nil {
167+
return false, err
168+
}
169+
return status != "", nil
170+
}
171+
172+
func incrementPatch(tag string) string {
173+
trimmed := strings.TrimPrefix(tag, "v")
174+
trimmed = strings.SplitN(trimmed, "-", 2)[0]
175+
parts := strings.Split(trimmed, ".")
176+
if len(parts) < 3 {
177+
return ""
178+
}
179+
major, err := strconv.Atoi(parts[0])
180+
if err != nil {
181+
return ""
182+
}
183+
minor, err := strconv.Atoi(parts[1])
184+
if err != nil {
185+
return ""
186+
}
187+
patch, err := strconv.Atoi(parts[2])
188+
if err != nil {
189+
return ""
190+
}
191+
patch++
192+
return fmt.Sprintf("v%d.%d.%d", major, minor, patch)
193+
}
194+
195+
func buildDefaultPseudoVersion(major int, timestamp, commitHash string) string {
196+
if major < 0 {
197+
major = 0
198+
}
199+
return fmt.Sprintf("v%d.0.0-%s-%s", major, timestamp, commitHash)
200+
}
201+
202+
func moduleMajorVersion(modulePath string) int {
203+
if modulePath == "" {
204+
return 0
205+
}
206+
lastSlash := strings.LastIndex(modulePath, "/v")
207+
if lastSlash == -1 || lastSlash == len(modulePath)-2 {
208+
return 0
209+
}
210+
majorStr := modulePath[lastSlash+2:]
211+
if strings.Contains(majorStr, "/") {
212+
majorStr = majorStr[:strings.Index(majorStr, "/")]
213+
}
214+
major, err := strconv.Atoi(majorStr)
215+
if err != nil {
216+
return 0
217+
}
218+
return major
219+
}
220+
221+
func findRepoRoot() (string, error) {
222+
_, currentFile, _, ok := runtime.Caller(0)
223+
if !ok {
224+
return "", fmt.Errorf("failed to determine caller")
225+
}
226+
227+
if !filepath.IsAbs(currentFile) {
228+
abs, err := filepath.Abs(currentFile)
229+
if err != nil {
230+
return "", fmt.Errorf("getting absolute path: %w", err)
231+
}
232+
currentFile = abs
233+
}
234+
235+
dir := filepath.Dir(currentFile)
236+
for {
237+
if dir == "" || dir == filepath.Dir(dir) {
238+
return "", fmt.Errorf("git repository root not found from %s", currentFile)
239+
}
240+
241+
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
242+
return dir, nil
243+
}
244+
dir = filepath.Dir(dir)
245+
}
246+
}
247+
248+
func runGitCommand(dir string, args ...string) (string, error) {
249+
cmd := exec.Command("git", args...)
250+
cmd.Dir = dir
251+
cmd.Env = append(os.Environ(), "LC_ALL=C", "LANG=C")
252+
output, err := cmd.CombinedOutput()
253+
if err != nil {
254+
return "", fmt.Errorf("running git %v: %w", args, err)
255+
}
256+
return strings.TrimSpace(string(output)), nil
257+
}

cmd/version_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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 cmd
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"os/exec"
23+
"path/filepath"
24+
"strconv"
25+
"strings"
26+
"testing"
27+
"time"
28+
)
29+
30+
func TestPseudoVersionFromGitDirExactTag(t *testing.T) {
31+
repo := initGitRepo(t)
32+
33+
if _, err := runGitCommand(repo, "tag", "v1.2.3"); err != nil {
34+
t.Fatalf("tagging repo: %v", err)
35+
}
36+
37+
version := pseudoVersionFromGitDir("example.com/module/v1", repo)
38+
if version != "v1.2.3" {
39+
t.Fatalf("expected tag version, got %q", version)
40+
}
41+
}
42+
43+
func TestPseudoVersionFromGitDirAfterTag(t *testing.T) {
44+
repo := initGitRepo(t)
45+
46+
if _, err := runGitCommand(repo, "tag", "v1.2.3"); err != nil {
47+
t.Fatalf("tagging repo: %v", err)
48+
}
49+
createCommit(t, repo, "second file", "second change")
50+
51+
version := pseudoVersionFromGitDir("example.com/module/v1", repo)
52+
if version == "" {
53+
t.Fatalf("expected pseudo version, got empty string")
54+
}
55+
56+
hash, err := runGitCommand(repo, "rev-parse", "--short=12", "HEAD")
57+
if err != nil {
58+
t.Fatalf("retrieving hash: %v", err)
59+
}
60+
timestampStr, err := runGitCommand(repo, "show", "-s", "--format=%ct", "HEAD")
61+
if err != nil {
62+
t.Fatalf("retrieving timestamp: %v", err)
63+
}
64+
seconds, err := strconv.ParseInt(timestampStr, 10, 64)
65+
if err != nil {
66+
t.Fatalf("parsing timestamp: %v", err)
67+
}
68+
expected := fmt.Sprintf("v1.2.4-0.%s-%s", time.Unix(seconds, 0).UTC().Format(pseudoVersionTimestampLayout), hash)
69+
if version != expected {
70+
t.Fatalf("expected %q, got %q", expected, version)
71+
}
72+
}
73+
74+
func TestPseudoVersionFromGitDirDirty(t *testing.T) {
75+
repo := initGitRepo(t)
76+
77+
if _, err := runGitCommand(repo, "tag", "v1.2.3"); err != nil {
78+
t.Fatalf("tagging repo: %v", err)
79+
}
80+
createCommit(t, repo, "second file", "second change")
81+
82+
targetFile := filepath.Join(repo, "tracked.txt")
83+
if err := os.WriteFile(targetFile, []byte("dirty change\n"), 0o644); err != nil {
84+
t.Fatalf("creating dirty file: %v", err)
85+
}
86+
87+
version := pseudoVersionFromGitDir("example.com/module/v1", repo)
88+
if version != develVersion {
89+
t.Fatalf("expected %q for dirty repo, got %q", develVersion, version)
90+
}
91+
}
92+
93+
func TestPseudoVersionFromGitDirWithoutTag(t *testing.T) {
94+
repo := initGitRepo(t)
95+
version := pseudoVersionFromGitDir("example.com/module/v4", repo)
96+
if !strings.HasPrefix(version, "v4.0.0-") {
97+
t.Fatalf("expected prefix v4.0.0-, got %q", version)
98+
}
99+
}
100+
101+
func TestGetKubebuilderVersionDirtyString(t *testing.T) {
102+
t.Cleanup(func() { kubeBuilderVersion = unknown })
103+
kubeBuilderVersion = "v1.2.3+dirty"
104+
if got := getKubebuilderVersion(); got != develVersion {
105+
t.Fatalf("expected %q, got %q", develVersion, got)
106+
}
107+
}
108+
109+
func initGitRepo(t *testing.T) string {
110+
t.Helper()
111+
112+
dir := t.TempDir()
113+
114+
commands := [][]string{
115+
{"init"},
116+
{"config", "user.email", "[email protected]"},
117+
{"config", "user.name", "Kubebuilder Dev"},
118+
}
119+
for _, args := range commands {
120+
if _, err := runGitCommand(dir, args...); err != nil {
121+
t.Fatalf("initializing repo (%v): %v", args, err)
122+
}
123+
}
124+
125+
createCommit(t, dir, "tracked.txt", "initial")
126+
return dir
127+
}
128+
129+
func createCommit(t *testing.T, repo, file, content string) {
130+
t.Helper()
131+
132+
if err := os.WriteFile(filepath.Join(repo, file), []byte(content+"\n"), 0o644); err != nil {
133+
t.Fatalf("writing file: %v", err)
134+
}
135+
if _, err := runGitCommand(repo, "add", file); err != nil {
136+
t.Fatalf("git add: %v", err)
137+
}
138+
commitEnv := append(os.Environ(),
139+
"GIT_COMMITTER_DATE=2006-01-02T15:04:05Z",
140+
"GIT_AUTHOR_DATE=2006-01-02T15:04:05Z",
141+
)
142+
cmd := exec.Command("git", "commit", "-m", fmt.Sprintf("commit %s", file))
143+
cmd.Dir = repo
144+
cmd.Env = append(commitEnv, "LC_ALL=C", "LANG=C")
145+
if output, err := cmd.CombinedOutput(); err != nil {
146+
t.Fatalf("git commit: %v: %s", err, output)
147+
}
148+
}

docs/book/src/cronjob-tutorial/testdata/project/PROJECT

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is used to track the info used to scaffold your project
33
# and allow the plugins properly work.
44
# More info: https://book.kubebuilder.io/reference/project-config.html
5-
cliVersion: v4.7.1-0.20251013232032-b89dd722e54a+dirty
5+
cliVersion: (devel)
66
domain: tutorial.kubebuilder.io
77
layout:
88
- go.kubebuilder.io/v4

docs/book/src/getting-started/testdata/project/PROJECT

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is used to track the info used to scaffold your project
33
# and allow the plugins properly work.
44
# More info: https://book.kubebuilder.io/reference/project-config.html
5-
cliVersion: v4.7.1-0.20251013232032-b89dd722e54a+dirty
5+
cliVersion: (devel)
66
domain: example.com
77
layout:
88
- go.kubebuilder.io/v4

0 commit comments

Comments
 (0)