diff --git a/.goreleaser.yml b/.goreleaser.yml index 8c539b41..4b6629ea 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -23,7 +23,7 @@ builds: flags: - -mod=readonly ldflags: - - -s -w -X main.Version={{.Version}} + - -s -w -X main.Version={{.Version}} -X main.BuildDate={{.Date}} - id: darwin-arm64 main: ./cmd/lets goos: @@ -38,7 +38,7 @@ builds: flags: - -mod=readonly ldflags: - - -s -w -X main.Version={{.Version}} + - -s -w -X main.Version={{.Version}} -X main.BuildDate={{.Date}} - id: linux-amd64 main: ./cmd/lets goos: @@ -51,7 +51,7 @@ builds: flags: - -mod=readonly ldflags: - - -s -w -X main.Version={{.Version}} + - -s -w -X main.Version={{.Version}} -X main.BuildDate={{.Date}} archives: - formats: [tar.gz] diff --git a/AGENTS.md b/AGENTS.md index 5cd5c29c..f9bcc27a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,20 +32,20 @@ lets publish-docs # deploy docs site ## Package Structure -- `main.go` — entry point, flag parsing, signal handling -- `cmd/` — Cobra commands (root, subcommands, completion, LSP, self-update) -- `config/` — config file discovery, loading, validation; `config/config/` defines Config/Command/Mixin structs and YAML unmarshaling -- `executor/` — command execution, dependency resolution, env setup, checksum verification -- `env/` — debug level state (`LETS_DEBUG`, levels 0-2) -- `logging/` — logrus-based logging with command chain formatting -- `lsp/` — Language Server Protocol: definition lookup, completion for depends, tree-sitter YAML parsing; `lets lsp` runs stdio-based server for IDE integration -- `checksum/` — SHA1 file checksumming with glob patterns -- `docopt/` — docopt argument parsing, produces `LETSOPT_*` and `LETSCLI_*` env vars -- `upgrade/` — binary self-update from GitHub releases -- `util/` — file/dir/version helpers -- `workdir/` — `--init` scaffolding -- `set/` — generic Set data structure -- `test/` — test utilities (temp files, args helpers) +- `cmd/lets/main.go` — CLI entry point, flag parsing, signal handling +- `internal/cmd/` — Cobra command setup (root, subcommands, completion, LSP, self-update) +- `internal/config/` — config file discovery, loading, validation; `internal/config/config/` defines Config/Command/Mixin structs and YAML unmarshaling; `internal/config/path/` contains config path helpers +- `internal/executor/` — command execution, dependency resolution, env setup, checksum verification +- `internal/env/` — debug level state (`LETS_DEBUG`, levels 0-2) +- `internal/logging/` — logrus-based logging with command chain formatting +- `internal/lsp/` — Language Server Protocol: definition lookup, completion for depends, tree-sitter YAML parsing; `lets lsp` runs stdio-based server for IDE integration +- `internal/checksum/` — SHA1 file checksumming with glob patterns +- `internal/docopt/` — docopt argument parsing, produces `LETSOPT_*` and `LETSCLI_*` env vars +- `internal/upgrade/` — binary self-update from GitHub releases; `internal/upgrade/registry/` contains release registry implementation +- `internal/util/` — file/dir/version helpers +- `internal/workdir/` — `--init` scaffolding +- `internal/set/` — generic Set data structure +- `internal/test/` — test utilities (temp files, args helpers) ## Key lets.yaml Fields diff --git a/cmd/lets/main.go b/cmd/lets/main.go index 7fa766eb..8f0347ec 100644 --- a/cmd/lets/main.go +++ b/cmd/lets/main.go @@ -21,6 +21,7 @@ import ( ) var Version = "0.0.0-dev" +var BuildDate = "" func main() { ctx := getContext() @@ -29,7 +30,7 @@ func main() { logging.InitLogging(os.Stdout, os.Stderr) - rootCmd := cmd.CreateRootCommand(Version) + rootCmd := cmd.CreateRootCommand(Version, BuildDate) rootCmd.InitDefaultHelpFlag() rootCmd.InitDefaultVersionFlag() reinitCompletionCmd := cmd.InitCompletionCmd(rootCmd, nil) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index eaf7ae31..2db163b6 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -80,8 +80,9 @@ func newRootCmd(version string) *cobra.Command { } // CreateRootCommand used to run only root command without config. -func CreateRootCommand(version string) *cobra.Command { +func CreateRootCommand(version string, buildDate string) *cobra.Command { rootCmd := newRootCmd(version) + rootCmd.Annotations = map[string]string{"buildDate": buildDate} initRootFlags(rootCmd) @@ -230,6 +231,10 @@ func PrintRootHelpMessage(cmd *cobra.Command) error { } func PrintVersionMessage(cmd *cobra.Command) error { - _, err := fmt.Fprintf(cmd.OutOrStdout(), "lets version %s\n", cmd.Version) + msg := fmt.Sprintf("lets version %s", cmd.Version) + if buildDate := cmd.Annotations["buildDate"]; buildDate != "" { + msg += fmt.Sprintf(" (%s)", buildDate) + } + _, err := fmt.Fprintln(cmd.OutOrStdout(), msg) return err } diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go index 8b3112d9..1c311bf7 100644 --- a/internal/cmd/root_test.go +++ b/internal/cmd/root_test.go @@ -11,7 +11,7 @@ import ( ) func newTestRootCmd(args []string) (rootCmd *cobra.Command) { - root := CreateRootCommand("v0.0.0-test") + root := CreateRootCommand("v0.0.0-test", "") root.SetArgs(args) InitCompletionCmd(root, nil) @@ -27,7 +27,7 @@ func newTestRootCmdWithConfig(args []string) (rootCmd *cobra.Command, out *bytes cfg.Commands["foo"] = &config.Command{Name: "foo"} cfg.Commands["bar"] = &config.Command{Name: "bar"} - root := CreateRootCommand("v0.0.0-test") + root := CreateRootCommand("v0.0.0-test", "") root.SetArgs(args) root.SetOut(bufOut) root.SetErr(bufOut) @@ -138,11 +138,48 @@ func TestRootCmdWithConfig(t *testing.T) { }) } +func TestPrintVersionMessage(t *testing.T) { + t.Run("should include build date in parentheses when non-empty", func(t *testing.T) { + buf := new(bytes.Buffer) + root := CreateRootCommand("v0.0.0-test", "2024-01-15T10:30:00Z") + root.SetOut(buf) + + err := PrintVersionMessage(root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := buf.String() + if !strings.Contains(out, "v0.0.0-test") { + t.Errorf("expected version in output, got %q", out) + } + if !strings.Contains(out, "(2024-01-15T10:30:00Z)") { + t.Errorf("expected build date in parentheses, got %q", out) + } + }) + + t.Run("should omit parentheses when build date is empty", func(t *testing.T) { + buf := new(bytes.Buffer) + root := CreateRootCommand("v0.0.0-test", "") + root.SetOut(buf) + + err := PrintVersionMessage(root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := buf.String() + if strings.Contains(out, "(") { + t.Errorf("expected no parentheses when build date is empty, got %q", out) + } + }) +} + func TestSelfCmd(t *testing.T) { t.Run("should return exit code 2 for unknown self subcommand", func(t *testing.T) { bufOut := new(bytes.Buffer) - rootCmd := CreateRootCommand("v0.0.0-test") + rootCmd := CreateRootCommand("v0.0.0-test", "") rootCmd.SetArgs([]string{"self", "ls"}) rootCmd.SetOut(bufOut) rootCmd.SetErr(bufOut) @@ -178,7 +215,7 @@ func TestSelfCmd(t *testing.T) { t.Run("should return exit code 2 for unknown self subcommand with no suggestions", func(t *testing.T) { bufOut := new(bytes.Buffer) - rootCmd := CreateRootCommand("v0.0.0-test") + rootCmd := CreateRootCommand("v0.0.0-test", "") rootCmd.SetArgs([]string{"self", "zzzznotacommand"}) rootCmd.SetOut(bufOut) rootCmd.SetErr(bufOut) diff --git a/lets.yaml b/lets.yaml index 9c935992..cefd782d 100644 --- a/lets.yaml +++ b/lets.yaml @@ -109,7 +109,7 @@ commands: PATH2LETSDEV=$LETSOPT_PATH fi - go build -ldflags="-X main.Version=${VERSION:1}-dev" -o "${BIN}" ./cmd/lets && \ + go build -ldflags="-X main.Version=${VERSION:1}-dev -X main.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o "${BIN}" ./cmd/lets && \ sudo mv ./${BIN} $PATH2LETSDEV/${BIN} && \ echo " - binary ${BIN} version $($PATH2LETSDEV/${BIN} --version) successfully installed in ${PATH2LETSDEV}" @@ -122,7 +122,7 @@ commands: BIN=${LETSOPT_BIN:-lets} go build \ - -ldflags="-X main.Version=${VERSION:1}-dev" \ + -ldflags="-X main.Version=${VERSION:1}-dev -X main.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o ${BIN} ./cmd/lets success=$? diff --git a/tests/config_version.bats b/tests/config_version.bats index 1b699148..e64717d3 100644 --- a/tests/config_version.bats +++ b/tests/config_version.bats @@ -6,7 +6,7 @@ setup() { load "${BATS_UTILS_PATH}/bats-support/load.bash" load "${BATS_UTILS_PATH}/bats-assert/load.bash" # NOTICE to test this functionality properly we building lets with specified version ${TEST_VERSION} - go build -ldflags="-X main.Version=${TEST_VERSION}" -o ./tests/config_version/lets cmd/lets/main.go + go build -ldflags="-X main.Version=${TEST_VERSION} -X main.BuildDate=2024-01-01T00:00:00Z" -o ./tests/config_version/lets cmd/lets/main.go cd ./tests/config_version } diff --git a/tests/version.bats b/tests/version.bats index a44656c5..e5ba0c43 100644 --- a/tests/version.bats +++ b/tests/version.bats @@ -1,3 +1,8 @@ +load test_helpers + +TEST_VERSION=1.2.3 +TEST_BUILD_DATE=2024-01-15T10:30:00Z + setup() { load "${BATS_UTILS_PATH}/bats-support/load.bash" load "${BATS_UTILS_PATH}/bats-assert/load.bash" @@ -14,3 +19,11 @@ setup() { assert_success assert_line --index 0 "lets version 0.0.0-dev" } + +@test "version: show build date when set via ldflags" { + go build -ldflags="-X main.Version=${TEST_VERSION} -X main.BuildDate=${TEST_BUILD_DATE}" -o /tmp/lets-version-test cmd/lets/main.go + run /tmp/lets-version-test --version + assert_success + assert_line --index 0 "lets version ${TEST_VERSION} (${TEST_BUILD_DATE})" + rm -f /tmp/lets-version-test +}