Skip to content

Commit d87917e

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

File tree

8 files changed

+350
-16
lines changed

8 files changed

+350
-16
lines changed

cmd/version.go

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

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)