fix(push): inject GIT_TAG, GIT_SHA, BUILD_TIME as explicit build args in --build mode
- commit
- 56a8a7bc9f0fd2b93178cb07c71dcb424388d1c7
- author
- bnema · · 2026-02-24 21:48 UTC
- parent
- fc00677
- signature
- unsigned
2 files · +69 -4
M internal/adapters/in/cli/push.go +32 -2
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "path/filepath" | |
| 10 | 10 | "regexp" | |
| 11 | 11 | "strings" | |
| 12 | + | "time" | |
| 12 | 13 | ||
| 13 | 14 | tea "github.com/charmbracelet/bubbletea" | |
| 14 | 15 | "github.com/spf13/cobra" | |
| @@ -464,7 +465,7 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui | |||
| 464 | 465 | // using docker push gives us chunked uploads (~5MB per request). | |
| 465 | 466 | fmt.Println("\nBuilding image...") | |
| 466 | 467 | buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204 | |
| 467 | - | buildCmd.Env = append(os.Environ(), "VERSION="+version) | |
| 468 | + | buildCmd.Env = os.Environ() // VERSION is now passed as --build-arg VERSION=<value> | |
| 468 | 469 | buildCmd.Stdout = os.Stdout | |
| 469 | 470 | buildCmd.Stderr = os.Stderr | |
| 470 | 471 | if err := buildCmd.Run(); err != nil { | |
| @@ -484,6 +485,29 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui | |||
| 484 | 485 | return nil | |
| 485 | 486 | } | |
| 486 | 487 | ||
| 488 | + | // standardBuildArgs returns the standard set of git-related build args as | |
| 489 | + | // explicit KEY=VALUE pairs. User-supplied args are appended after and take | |
| 490 | + | // precedence (Docker uses the last occurrence of a duplicate key). | |
| 491 | + | func standardBuildArgs(version string) []string { | |
| 492 | + | gitSHA := resolveGitSHA() | |
| 493 | + | buildTime := time.Now().UTC().Format(time.RFC3339) | |
| 494 | + | return []string{ | |
| 495 | + | "VERSION=" + version, | |
| 496 | + | "GIT_TAG=" + version, | |
| 497 | + | "GIT_SHA=" + gitSHA, | |
| 498 | + | "BUILD_TIME=" + buildTime, | |
| 499 | + | } | |
| 500 | + | } | |
| 501 | + | ||
| 502 | + | // resolveGitSHA returns the short git SHA of HEAD, or "unknown" if unavailable. | |
| 503 | + | func resolveGitSHA() string { | |
| 504 | + | out, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() // #nosec G204 | |
| 505 | + | if err != nil { | |
| 506 | + | return "unknown" | |
| 507 | + | } | |
| 508 | + | return strings.TrimSpace(string(out)) | |
| 509 | + | } | |
| 510 | + | ||
| 487 | 511 | // buildImageArgs constructs the docker buildx build arguments. | |
| 488 | 512 | // Uses --load instead of --push so the image is loaded into the local | |
| 489 | 513 | // daemon, allowing docker push to handle the upload with chunked requests. | |
| @@ -493,14 +517,20 @@ func buildImageArgs(version, platform, dockerfile string, buildArgs []string, ve | |||
| 493 | 517 | "--platform", platform, | |
| 494 | 518 | "-f", dockerfile, | |
| 495 | 519 | "-t", latestRef, | |
| 496 | - | "--build-arg", "VERSION", | |
| 497 | 520 | } | |
| 498 | 521 | if version != "latest" { | |
| 499 | 522 | args = append(args, "-t", versionRef) | |
| 500 | 523 | } | |
| 524 | + | ||
| 525 | + | // Inject standard git build args as explicit KEY=VALUE pairs. | |
| 526 | + | // User-supplied --build-arg flags are appended AFTER so they override defaults. | |
| 527 | + | for _, ba := range standardBuildArgs(version) { | |
| 528 | + | args = append(args, "--build-arg", ba) | |
| 529 | + | } | |
| 501 | 530 | for _, ba := range buildArgs { | |
| 502 | 531 | args = append(args, "--build-arg", ba) | |
| 503 | 532 | } | |
| 533 | + | ||
| 504 | 534 | args = append(args, "--load", ".") | |
| 505 | 535 | return args | |
| 506 | 536 | } | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | "path/filepath" | 9 | "path/filepath" | ||
| 10 | "regexp" | 10 | "regexp" | ||
| 11 | "strings" | 11 | "strings" | ||
| 12 | + | "time" | |||
| 12 | 13 | ||||
| 13 | tea "github.com/charmbracelet/bubbletea" | 14 | tea "github.com/charmbracelet/bubbletea" | ||
| 14 | "github.com/spf13/cobra" | 15 | "github.com/spf13/cobra" | ||
| @@ -464,7 +465,7 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui | |||||
| 464 | // using docker push gives us chunked uploads (~5MB per request). | 465 | // using docker push gives us chunked uploads (~5MB per request). | ||
| 465 | fmt.Println("\nBuilding image...") | 466 | fmt.Println("\nBuilding image...") | ||
| 466 | buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204 | 467 | buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204 | ||
| 467 | - | buildCmd.Env = append(os.Environ(), "VERSION="+version) | 468 | + | buildCmd.Env = os.Environ() // VERSION is now passed as --build-arg VERSION=<value> |
| 468 | buildCmd.Stdout = os.Stdout | 469 | buildCmd.Stdout = os.Stdout | ||
| 469 | buildCmd.Stderr = os.Stderr | 470 | buildCmd.Stderr = os.Stderr | ||
| 470 | if err := buildCmd.Run(); err != nil { | 471 | if err := buildCmd.Run(); err != nil { | ||
| @@ -484,6 +485,29 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui | |||||
| 484 | return nil | 485 | return nil | ||
| 485 | } | 486 | } | ||
| 486 | 487 | ||||
| 488 | + | // standardBuildArgs returns the standard set of git-related build args as | |||
| 489 | + | // explicit KEY=VALUE pairs. User-supplied args are appended after and take | |||
| 490 | + | // precedence (Docker uses the last occurrence of a duplicate key). | |||
| 491 | + | func standardBuildArgs(version string) []string { | |||
| 492 | + | gitSHA := resolveGitSHA() | |||
| 493 | + | buildTime := time.Now().UTC().Format(time.RFC3339) | |||
| 494 | + | return []string{ | |||
| 495 | + | "VERSION=" + version, | |||
| 496 | + | "GIT_TAG=" + version, | |||
| 497 | + | "GIT_SHA=" + gitSHA, | |||
| 498 | + | "BUILD_TIME=" + buildTime, | |||
| 499 | + | } | |||
| 500 | + | } | |||
| 501 | + | ||||
| 502 | + | // resolveGitSHA returns the short git SHA of HEAD, or "unknown" if unavailable. | |||
| 503 | + | func resolveGitSHA() string { | |||
| 504 | + | out, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() // #nosec G204 | |||
| 505 | + | if err != nil { | |||
| 506 | + | return "unknown" | |||
| 507 | + | } | |||
| 508 | + | return strings.TrimSpace(string(out)) | |||
| 509 | + | } | |||
| 510 | + | ||||
| 487 | // buildImageArgs constructs the docker buildx build arguments. | 511 | // buildImageArgs constructs the docker buildx build arguments. | ||
| 488 | // Uses --load instead of --push so the image is loaded into the local | 512 | // Uses --load instead of --push so the image is loaded into the local | ||
| 489 | // daemon, allowing docker push to handle the upload with chunked requests. | 513 | // daemon, allowing docker push to handle the upload with chunked requests. | ||
| @@ -493,14 +517,20 @@ func buildImageArgs(version, platform, dockerfile string, buildArgs []string, ve | |||||
| 493 | "--platform", platform, | 517 | "--platform", platform, | ||
| 494 | "-f", dockerfile, | 518 | "-f", dockerfile, | ||
| 495 | "-t", latestRef, | 519 | "-t", latestRef, | ||
| 496 | - | "--build-arg", "VERSION", | |||
| 497 | } | 520 | } | ||
| 498 | if version != "latest" { | 521 | if version != "latest" { | ||
| 499 | args = append(args, "-t", versionRef) | 522 | args = append(args, "-t", versionRef) | ||
| 500 | } | 523 | } | ||
| 524 | + | ||||
| 525 | + | // Inject standard git build args as explicit KEY=VALUE pairs. | |||
| 526 | + | // User-supplied --build-arg flags are appended AFTER so they override defaults. | |||
| 527 | + | for _, ba := range standardBuildArgs(version) { | |||
| 528 | + | args = append(args, "--build-arg", ba) | |||
| 529 | + | } | |||
| 501 | for _, ba := range buildArgs { | 530 | for _, ba := range buildArgs { | ||
| 502 | args = append(args, "--build-arg", ba) | 531 | args = append(args, "--build-arg", ba) | ||
| 503 | } | 532 | } | ||
| 533 | + | ||||
| 504 | args = append(args, "--load", ".") | 534 | args = append(args, "--load", ".") | ||
| 505 | return args | 535 | return args | ||
| 506 | } | 536 | } | ||
M internal/adapters/in/cli/push_test.go +37 -2
| @@ -3,6 +3,7 @@ package cli | |||
| 3 | 3 | import ( | |
| 4 | 4 | "os" | |
| 5 | 5 | "path/filepath" | |
| 6 | + | "strings" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/stretchr/testify/assert" | |
| @@ -72,8 +73,42 @@ func TestBuildAndPush_BuildArgs(t *testing.T) { | |||
| 72 | 73 | assert.Contains(t, args, "--platform") | |
| 73 | 74 | assert.Contains(t, args, "-f") | |
| 74 | 75 | assert.Contains(t, args, "Dockerfile") | |
| 75 | - | assert.Contains(t, args, "VERSION") | |
| 76 | - | assert.NotContains(t, args, "VERSION=v1.0.0") | |
| 76 | + | assert.Contains(t, args, "VERSION=v1.0.0") | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func TestBuildImageArgsInjectsGitBuildArgs(t *testing.T) { | |
| 80 | + | args := buildImageArgs("v1.2.3", "linux/amd64", "Dockerfile", nil, "registry/img:v1.2.3", "registry/img:latest") | |
| 81 | + | ||
| 82 | + | // Must contain explicit KEY=VALUE for all standard git build args | |
| 83 | + | argStr := strings.Join(args, " ") | |
| 84 | + | for _, key := range []string{"VERSION=v1.2.3", "GIT_TAG=v1.2.3", "GIT_SHA=", "BUILD_TIME="} { | |
| 85 | + | if !strings.Contains(argStr, key) { | |
| 86 | + | t.Errorf("expected args to contain %q, got: %s", key, argStr) | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | // Must NOT contain bare "--build-arg VERSION" (without =value) | |
| 91 | + | for i, a := range args { | |
| 92 | + | if a == "--build-arg" && i+1 < len(args) && args[i+1] == "VERSION" { | |
| 93 | + | t.Error("found bare '--build-arg VERSION' (without =value); should be '--build-arg VERSION=v1.2.3'") | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func TestBuildImageArgsUserArgsOverrideDefaults(t *testing.T) { | |
| 99 | + | userArgs := []string{"GIT_TAG=custom-override"} | |
| 100 | + | args := buildImageArgs("v1.2.3", "linux/amd64", "Dockerfile", userArgs, "r/i:v1.2.3", "r/i:latest") | |
| 101 | + | ||
| 102 | + | // Count how many times GIT_TAG appears — user override should come last (Docker uses last value) | |
| 103 | + | count := 0 | |
| 104 | + | for i, a := range args { | |
| 105 | + | if a == "--build-arg" && i+1 < len(args) && strings.HasPrefix(args[i+1], "GIT_TAG=") { | |
| 106 | + | count++ | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | if count < 2 { | |
| 110 | + | t.Errorf("expected GIT_TAG to appear twice (default + override), got %d", count) | |
| 111 | + | } | |
| 77 | 112 | } | |
| 78 | 113 | ||
| 79 | 114 | func TestParseTagRef(t *testing.T) { | |
| @@ -3,6 +3,7 @@ package cli | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "os" | 4 | "os" | ||
| 5 | "path/filepath" | 5 | "path/filepath" | ||
| 6 | + | "strings" | |||
| 6 | "testing" | 7 | "testing" | ||
| 7 | 8 | ||||
| 8 | "github.com/stretchr/testify/assert" | 9 | "github.com/stretchr/testify/assert" | ||
| @@ -72,8 +73,42 @@ func TestBuildAndPush_BuildArgs(t *testing.T) { | |||||
| 72 | assert.Contains(t, args, "--platform") | 73 | assert.Contains(t, args, "--platform") | ||
| 73 | assert.Contains(t, args, "-f") | 74 | assert.Contains(t, args, "-f") | ||
| 74 | assert.Contains(t, args, "Dockerfile") | 75 | assert.Contains(t, args, "Dockerfile") | ||
| 75 | - | assert.Contains(t, args, "VERSION") | 76 | + | assert.Contains(t, args, "VERSION=v1.0.0") |
| 76 | - | assert.NotContains(t, args, "VERSION=v1.0.0") | 77 | + | } |
| 78 | + | ||||
| 79 | + | func TestBuildImageArgsInjectsGitBuildArgs(t *testing.T) { | |||
| 80 | + | args := buildImageArgs("v1.2.3", "linux/amd64", "Dockerfile", nil, "registry/img:v1.2.3", "registry/img:latest") | |||
| 81 | + | ||||
| 82 | + | // Must contain explicit KEY=VALUE for all standard git build args | |||
| 83 | + | argStr := strings.Join(args, " ") | |||
| 84 | + | for _, key := range []string{"VERSION=v1.2.3", "GIT_TAG=v1.2.3", "GIT_SHA=", "BUILD_TIME="} { | |||
| 85 | + | if !strings.Contains(argStr, key) { | |||
| 86 | + | t.Errorf("expected args to contain %q, got: %s", key, argStr) | |||
| 87 | + | } | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | // Must NOT contain bare "--build-arg VERSION" (without =value) | |||
| 91 | + | for i, a := range args { | |||
| 92 | + | if a == "--build-arg" && i+1 < len(args) && args[i+1] == "VERSION" { | |||
| 93 | + | t.Error("found bare '--build-arg VERSION' (without =value); should be '--build-arg VERSION=v1.2.3'") | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | func TestBuildImageArgsUserArgsOverrideDefaults(t *testing.T) { | |||
| 99 | + | userArgs := []string{"GIT_TAG=custom-override"} | |||
| 100 | + | args := buildImageArgs("v1.2.3", "linux/amd64", "Dockerfile", userArgs, "r/i:v1.2.3", "r/i:latest") | |||
| 101 | + | ||||
| 102 | + | // Count how many times GIT_TAG appears — user override should come last (Docker uses last value) | |||
| 103 | + | count := 0 | |||
| 104 | + | for i, a := range args { | |||
| 105 | + | if a == "--build-arg" && i+1 < len(args) && strings.HasPrefix(args[i+1], "GIT_TAG=") { | |||
| 106 | + | count++ | |||
| 107 | + | } | |||
| 108 | + | } | |||
| 109 | + | if count < 2 { | |||
| 110 | + | t.Errorf("expected GIT_TAG to appear twice (default + override), got %d", count) | |||
| 111 | + | } | |||
| 77 | } | 112 | } | ||
| 78 | 113 | ||||
| 79 | func TestParseTagRef(t *testing.T) { | 114 | func TestParseTagRef(t *testing.T) { | ||