Skip to content

Commit d37dfe7

Browse files
committed
feat(cli): add explicit version metadata output
1 parent 2c54060 commit d37dfe7

6 files changed

Lines changed: 146 additions & 1 deletion

File tree

.github/workflows/release.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,20 @@ jobs:
3232
env:
3333
GOOS: ${{ matrix.goos }}
3434
GOARCH: ${{ matrix.goarch }}
35+
VERSION: ${{ github.ref_name }}
36+
COMMIT: ${{ github.sha }}
3537
run: |
3638
mkdir -p dist
3739
suffix=""
3840
if [ "${GOOS}" = "windows" ]; then suffix=".exe"; fi
3941
output="dist/kcal-${GOOS}-${GOARCH}${suffix}"
40-
CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o "${output}" .
42+
build_date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
43+
if [ -z "${VERSION}" ]; then VERSION="v1.1.0"; fi
44+
ldflags="-s -w \
45+
-X github.com/saad/kcal-cli/cmd/kcal.version=${VERSION} \
46+
-X github.com/saad/kcal-cli/cmd/kcal.commit=${COMMIT} \
47+
-X github.com/saad/kcal-cli/cmd/kcal.date=${build_date}"
48+
CGO_ENABLED=0 go build -trimpath -ldflags "${ldflags}" -o "${output}" .
4149
4250
- name: Smoke test release binary (linux/amd64)
4351
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
2020
- Saved template database schema and model support for saved foods, saved meals, and saved meal components.
2121
- Saved template command families: `kcal saved-food ...` and `kcal saved-meal ...` (including create from entry/barcode, component management, archive/restore, and logging).
2222
- Saved templates included in JSON portability workflows (`kcal export --format json`, `kcal import --format json`), including coverage in portability tests.
23+
- Version metadata output via `kcal version`, `kcal -v`, and `kcal --version` (version tag, commit SHA, and build date).
2324

2425
### Changed
2526
- Consolidated GitHub Pages docs into a single-page experience at `docs/index.md` and removed legacy multi-page duplicates from publish paths.

cmd/kcal/root.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,25 @@ import (
88
)
99

1010
var dbPath string
11+
var showVersion bool
12+
13+
var (
14+
version = "v1.1.0"
15+
commit = "unknown"
16+
date = "unknown"
17+
)
1118

1219
var rootCmd = &cobra.Command{
1320
Use: "kcal",
1421
Short: "kcal tracks calories and macros from your terminal",
1522
Long: "kcal is a local-first calorie and macro tracking CLI with categories, recipes, goals, and analytics.",
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
if showVersion {
25+
printVersion(cmd)
26+
return nil
27+
}
28+
return cmd.Help()
29+
},
1630
}
1731

1832
func Execute() {
@@ -24,4 +38,9 @@ func Execute() {
2438

2539
func init() {
2640
rootCmd.PersistentFlags().StringVar(&dbPath, "db", "", "Path to SQLite database")
41+
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "Show version/build metadata")
42+
}
43+
44+
func printVersion(cmd *cobra.Command) {
45+
fmt.Fprintf(cmd.OutOrStdout(), "kcal %s\ncommit: %s\ndate: %s\n", version, commit, date)
2746
}

cmd/kcal/root_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,29 @@ package kcal
33
import (
44
"bytes"
55
"path/filepath"
6+
"strings"
67
"testing"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/spf13/pflag"
711
)
812

13+
func resetCommandFlags(cmd *cobra.Command) {
14+
cmd.Flags().VisitAll(func(f *pflag.Flag) {
15+
_ = f.Value.Set(f.DefValue)
16+
f.Changed = false
17+
})
18+
cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) {
19+
_ = f.Value.Set(f.DefValue)
20+
f.Changed = false
21+
})
22+
for _, child := range cmd.Commands() {
23+
resetCommandFlags(child)
24+
}
25+
}
26+
927
func TestRootHelp(t *testing.T) {
28+
resetCommandFlags(rootCmd)
1029
buf := &bytes.Buffer{}
1130
rootCmd.SetOut(buf)
1231
rootCmd.SetErr(buf)
@@ -21,6 +40,7 @@ func TestRootHelp(t *testing.T) {
2140
}
2241

2342
func TestInitCommandIdempotent(t *testing.T) {
43+
resetCommandFlags(rootCmd)
2444
path := filepath.Join(t.TempDir(), "kcal.db")
2545
for i := 0; i < 2; i++ {
2646
buf := &bytes.Buffer{}
@@ -32,3 +52,44 @@ func TestInitCommandIdempotent(t *testing.T) {
3252
}
3353
}
3454
}
55+
56+
func TestRootVersionFlagAndCommand(t *testing.T) {
57+
oldVersion, oldCommit, oldDate := version, commit, date
58+
version = "v1.1.0"
59+
commit = "abc123"
60+
date = "2026-02-20T00:00:00Z"
61+
t.Cleanup(func() {
62+
version = oldVersion
63+
commit = oldCommit
64+
date = oldDate
65+
showVersion = false
66+
})
67+
68+
testCases := [][]string{
69+
{"--version"},
70+
{"-v"},
71+
{"version"},
72+
}
73+
74+
for _, args := range testCases {
75+
resetCommandFlags(rootCmd)
76+
buf := &bytes.Buffer{}
77+
rootCmd.SetOut(buf)
78+
rootCmd.SetErr(buf)
79+
rootCmd.SetArgs(args)
80+
if err := rootCmd.Execute(); err != nil {
81+
t.Fatalf("execute %v: %v", args, err)
82+
}
83+
out := buf.String()
84+
if !strings.Contains(out, "kcal v1.1.0") {
85+
t.Fatalf("expected version in output for %v, got: %s", args, out)
86+
}
87+
if !strings.Contains(out, "commit: abc123") {
88+
t.Fatalf("expected commit in output for %v, got: %s", args, out)
89+
}
90+
if !strings.Contains(out, "date: 2026-02-20T00:00:00Z") {
91+
t.Fatalf("expected date in output for %v, got: %s", args, out)
92+
}
93+
showVersion = false
94+
}
95+
}

cmd/kcal/version.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package kcal
2+
3+
import "github.com/spf13/cobra"
4+
5+
var versionCmd = &cobra.Command{
6+
Use: "version",
7+
Short: "Show version/build metadata",
8+
Run: func(cmd *cobra.Command, args []string) {
9+
printVersion(cmd)
10+
},
11+
}
12+
13+
func init() {
14+
rootCmd.AddCommand(versionCmd)
15+
}

tests/cli_edge_cases_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,24 @@ func runKcal(t *testing.T, binPath, dbPath string, args ...string) (string, stri
4444
return stdout.String(), stderr.String(), exitErr.ExitCode()
4545
}
4646

47+
func runKcalRaw(t *testing.T, binPath string, args ...string) (string, string, int) {
48+
t.Helper()
49+
cmd := exec.Command(binPath, args...)
50+
var stdout bytes.Buffer
51+
var stderr bytes.Buffer
52+
cmd.Stdout = &stdout
53+
cmd.Stderr = &stderr
54+
err := cmd.Run()
55+
if err == nil {
56+
return stdout.String(), stderr.String(), 0
57+
}
58+
exitErr, ok := err.(*exec.ExitError)
59+
if !ok {
60+
t.Fatalf("run kcal command: %v", err)
61+
}
62+
return stdout.String(), stderr.String(), exitErr.ExitCode()
63+
}
64+
4765
func initDB(t *testing.T, binPath, dbPath string) {
4866
t.Helper()
4967
_, stderr, exit := runKcal(t, binPath, dbPath, "init")
@@ -254,6 +272,29 @@ func TestCLIExerciseAddAllowsNoDistance(t *testing.T) {
254272
}
255273
}
256274

275+
func TestCLIVersionCommandAndFlags(t *testing.T) {
276+
binPath := buildKcalBinary(t)
277+
278+
for _, args := range [][]string{{"--version"}, {"-v"}, {"version"}} {
279+
stdout, stderr, exit := runKcalRaw(t, binPath, args...)
280+
if exit != 0 {
281+
t.Fatalf("expected version call %v to succeed: exit=%d stderr=%s", args, exit, stderr)
282+
}
283+
if stderr != "" {
284+
t.Fatalf("expected empty stderr for %v, got: %s", args, stderr)
285+
}
286+
if !strings.Contains(stdout, "kcal v1.1.0") {
287+
t.Fatalf("expected version output for %v, got: %s", args, stdout)
288+
}
289+
if !strings.Contains(stdout, "commit:") {
290+
t.Fatalf("expected commit field for %v, got: %s", args, stdout)
291+
}
292+
if !strings.Contains(stdout, "date:") {
293+
t.Fatalf("expected date field for %v, got: %s", args, stdout)
294+
}
295+
}
296+
}
297+
257298
func TestMain(m *testing.M) {
258299
os.Exit(m.Run())
259300
}

0 commit comments

Comments
 (0)