fix(push): warn when git has no tags and version falls back to 'latest'
- commit
- d35750bd3d09b3d2600b05bc1bd689ebc8b0e675
- author
- bnema · · 2026-02-24 21:53 UTC
- parent
- c357053
- signature
- unsigned
2 files · +52 -1
M internal/adapters/in/cli/push.go +4 -1
| @@ -712,9 +712,12 @@ func parseImageRef(image string) (registry, name, tag string) { | |||
| 712 | 712 | } | |
| 713 | 713 | ||
| 714 | 714 | // getGitVersion returns git describe output, or empty string if unavailable. | |
| 715 | + | // When it falls back it prints a warning to stderr so the user knows the | |
| 716 | + | // image will be tagged "latest" rather than a real version. | |
| 715 | 717 | func getGitVersion(ctx context.Context) string { | |
| 716 | - | out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--dirty").Output() | |
| 718 | + | out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--dirty").Output() // #nosec G204 | |
| 717 | 719 | if err != nil { | |
| 720 | + | fmt.Fprintln(os.Stderr, "Warning: no git tags found — image version will be 'latest'. Tag your repo to get versioned images.") | |
| 718 | 721 | return "" | |
| 719 | 722 | } | |
| 720 | 723 | return strings.TrimSpace(string(out)) | |
| @@ -712,9 +712,12 @@ func parseImageRef(image string) (registry, name, tag string) { | |||||
| 712 | } | 712 | } | ||
| 713 | 713 | ||||
| 714 | // getGitVersion returns git describe output, or empty string if unavailable. | 714 | // getGitVersion returns git describe output, or empty string if unavailable. | ||
| 715 | + | // When it falls back it prints a warning to stderr so the user knows the | |||
| 716 | + | // image will be tagged "latest" rather than a real version. | |||
| 715 | func getGitVersion(ctx context.Context) string { | 717 | func getGitVersion(ctx context.Context) string { | ||
| 716 | - | out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--dirty").Output() | 718 | + | out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--dirty").Output() // #nosec G204 |
| 717 | if err != nil { | 719 | if err != nil { | ||
| 720 | + | fmt.Fprintln(os.Stderr, "Warning: no git tags found — image version will be 'latest'. Tag your repo to get versioned images.") | |||
| 718 | return "" | 721 | return "" | ||
| 719 | } | 722 | } | ||
| 720 | return strings.TrimSpace(string(out)) | 723 | return strings.TrimSpace(string(out)) | ||
M internal/adapters/in/cli/push_test.go +48 -0
| @@ -1,7 +1,10 @@ | |||
| 1 | 1 | package cli | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 5 | + | "io" | |
| 4 | 6 | "os" | |
| 7 | + | "os/exec" | |
| 5 | 8 | "path/filepath" | |
| 6 | 9 | "strings" | |
| 7 | 10 | "testing" | |
| @@ -336,6 +339,51 @@ func TestSplitLabelPairs(t *testing.T) { | |||
| 336 | 339 | } | |
| 337 | 340 | } | |
| 338 | 341 | ||
| 342 | + | func TestGetGitVersionNoTagsReturnsFallbackAndWarns(t *testing.T) { | |
| 343 | + | ctx := context.Background() | |
| 344 | + | ||
| 345 | + | // Create a temp dir with a git repo that has no tags | |
| 346 | + | tmpDir := t.TempDir() | |
| 347 | + | if err := exec.Command("git", "-C", tmpDir, "init").Run(); err != nil { // #nosec G204 | |
| 348 | + | t.Skipf("git init failed: %v", err) | |
| 349 | + | } | |
| 350 | + | // Need at least one commit so git describe has something to describe | |
| 351 | + | if err := exec.Command("git", "-C", tmpDir, "-c", "user.email=test@test.com", "-c", "user.name=Test", "commit", "--allow-empty", "-m", "init").Run(); err != nil { // #nosec G204 | |
| 352 | + | t.Skipf("git commit failed: %v", err) | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | // Change to the tmpDir for this test so getGitVersion uses the tag-less repo | |
| 356 | + | origDir, err := os.Getwd() | |
| 357 | + | if err != nil { | |
| 358 | + | t.Fatal(err) | |
| 359 | + | } | |
| 360 | + | if err := os.Chdir(tmpDir); err != nil { // #nosec G204 | |
| 361 | + | t.Fatal(err) | |
| 362 | + | } | |
| 363 | + | defer os.Chdir(origDir) // #nosec G204 | |
| 364 | + | ||
| 365 | + | // Redirect stderr to capture the warning | |
| 366 | + | origStderr := os.Stderr | |
| 367 | + | r, w, err := os.Pipe() | |
| 368 | + | if err != nil { | |
| 369 | + | t.Fatal(err) | |
| 370 | + | } | |
| 371 | + | os.Stderr = w | |
| 372 | + | ||
| 373 | + | v := getGitVersion(ctx) | |
| 374 | + | ||
| 375 | + | w.Close() | |
| 376 | + | os.Stderr = origStderr | |
| 377 | + | stderrOutput, _ := io.ReadAll(r) | |
| 378 | + | ||
| 379 | + | // Should return "" (fallback to "latest" handled by determineVersion) | |
| 380 | + | assert.Equal(t, "", v, "expected empty string fallback when no git tags exist") | |
| 381 | + | ||
| 382 | + | // Should have printed a warning to stderr | |
| 383 | + | assert.Contains(t, string(stderrOutput), "latest", | |
| 384 | + | "expected a warning about 'latest' fallback on stderr") | |
| 385 | + | } | |
| 386 | + | ||
| 339 | 387 | func TestParseLabelPair(t *testing.T) { | |
| 340 | 388 | tests := []struct { | |
| 341 | 389 | name string | |
| @@ -1,7 +1,10 @@ | |||||
| 1 | package cli | 1 | package cli | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 5 | + | "io" | |||
| 4 | "os" | 6 | "os" | ||
| 7 | + | "os/exec" | |||
| 5 | "path/filepath" | 8 | "path/filepath" | ||
| 6 | "strings" | 9 | "strings" | ||
| 7 | "testing" | 10 | "testing" | ||
| @@ -336,6 +339,51 @@ func TestSplitLabelPairs(t *testing.T) { | |||||
| 336 | } | 339 | } | ||
| 337 | } | 340 | } | ||
| 338 | 341 | ||||
| 342 | + | func TestGetGitVersionNoTagsReturnsFallbackAndWarns(t *testing.T) { | |||
| 343 | + | ctx := context.Background() | |||
| 344 | + | ||||
| 345 | + | // Create a temp dir with a git repo that has no tags | |||
| 346 | + | tmpDir := t.TempDir() | |||
| 347 | + | if err := exec.Command("git", "-C", tmpDir, "init").Run(); err != nil { // #nosec G204 | |||
| 348 | + | t.Skipf("git init failed: %v", err) | |||
| 349 | + | } | |||
| 350 | + | // Need at least one commit so git describe has something to describe | |||
| 351 | + | if err := exec.Command("git", "-C", tmpDir, "-c", "user.email=test@test.com", "-c", "user.name=Test", "commit", "--allow-empty", "-m", "init").Run(); err != nil { // #nosec G204 | |||
| 352 | + | t.Skipf("git commit failed: %v", err) | |||
| 353 | + | } | |||
| 354 | + | ||||
| 355 | + | // Change to the tmpDir for this test so getGitVersion uses the tag-less repo | |||
| 356 | + | origDir, err := os.Getwd() | |||
| 357 | + | if err != nil { | |||
| 358 | + | t.Fatal(err) | |||
| 359 | + | } | |||
| 360 | + | if err := os.Chdir(tmpDir); err != nil { // #nosec G204 | |||
| 361 | + | t.Fatal(err) | |||
| 362 | + | } | |||
| 363 | + | defer os.Chdir(origDir) // #nosec G204 | |||
| 364 | + | ||||
| 365 | + | // Redirect stderr to capture the warning | |||
| 366 | + | origStderr := os.Stderr | |||
| 367 | + | r, w, err := os.Pipe() | |||
| 368 | + | if err != nil { | |||
| 369 | + | t.Fatal(err) | |||
| 370 | + | } | |||
| 371 | + | os.Stderr = w | |||
| 372 | + | ||||
| 373 | + | v := getGitVersion(ctx) | |||
| 374 | + | ||||
| 375 | + | w.Close() | |||
| 376 | + | os.Stderr = origStderr | |||
| 377 | + | stderrOutput, _ := io.ReadAll(r) | |||
| 378 | + | ||||
| 379 | + | // Should return "" (fallback to "latest" handled by determineVersion) | |||
| 380 | + | assert.Equal(t, "", v, "expected empty string fallback when no git tags exist") | |||
| 381 | + | ||||
| 382 | + | // Should have printed a warning to stderr | |||
| 383 | + | assert.Contains(t, string(stderrOutput), "latest", | |||
| 384 | + | "expected a warning about 'latest' fallback on stderr") | |||
| 385 | + | } | |||
| 386 | + | ||||
| 339 | func TestParseLabelPair(t *testing.T) { | 387 | func TestParseLabelPair(t *testing.T) { | ||
| 340 | tests := []struct { | 388 | tests := []struct { | ||
| 341 | name string | 389 | name string | ||