detached 56a8a7b

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 (
99 "path/filepath"
1010 "regexp"
1111 "strings"
12+ "time"
1213
1314 tea "github.com/charmbracelet/bubbletea"
1415 "github.com/spf13/cobra"
@@ -464,7 +465,7 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui
464465 // using docker push gives us chunked uploads (~5MB per request).
465466 fmt.Println("\nBuilding image...")
466467 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>
468469 buildCmd.Stdout = os.Stdout
469470 buildCmd.Stderr = os.Stderr
470471 if err := buildCmd.Run(); err != nil {
@@ -484,6 +485,29 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui
484485 return nil
485486 }
486487
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+
487511 // buildImageArgs constructs the docker buildx build arguments.
488512 // Uses --load instead of --push so the image is loaded into the local
489513 // daemon, allowing docker push to handle the upload with chunked requests.
@@ -493,14 +517,20 @@ func buildImageArgs(version, platform, dockerfile string, buildArgs []string, ve
493517 "--platform", platform,
494518 "-f", dockerfile,
495519 "-t", latestRef,
496- "--build-arg", "VERSION",
497520 }
498521 if version != "latest" {
499522 args = append(args, "-t", versionRef)
500523 }
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+ }
501530 for _, ba := range buildArgs {
502531 args = append(args, "--build-arg", ba)
503532 }
533+
504534 args = append(args, "--load", ".")
505535 return args
506536 }
@@ -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 G204467 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.Stdout469 buildCmd.Stdout = os.Stdout
469 buildCmd.Stderr = os.Stderr470 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 nil485 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 local512 // 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 args535 return args
506 }536 }
M internal/adapters/in/cli/push_test.go +37 -2
@@ -3,6 +3,7 @@ package cli
33 import (
44 "os"
55 "path/filepath"
6+ "strings"
67 "testing"
78
89 "github.com/stretchr/testify/assert"
@@ -72,8 +73,42 @@ func TestBuildAndPush_BuildArgs(t *testing.T) {
7273 assert.Contains(t, args, "--platform")
7374 assert.Contains(t, args, "-f")
7475 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+ }
77112 }
78113
79114 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) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close