Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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]
Expand Down
28 changes: 14 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion cmd/lets/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
)

var Version = "0.0.0-dev"
var BuildDate = ""

func main() {
ctx := getContext()
Expand All @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
}
45 changes: 41 additions & 4 deletions internal/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions lets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand All @@ -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=$?
Expand Down
2 changes: 1 addition & 1 deletion tests/config_version.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
13 changes: 13 additions & 0 deletions tests/version.bats
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Comment on lines +24 to +28
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded /tmp path may fail in parallel CI runs

The test binary is written to /tmp/lets-version-test, a shared path across all processes. If this test suite runs concurrently (e.g., multiple CI jobs on the same runner), two jobs could race on the same file — one deleting it while the other is trying to use it, or one overwriting the binary during another's execution.

Consider using a unique temp path, such as one derived from $BATS_TMPDIR (provided by the BATS framework), which is isolated per test run:

Suggested change
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
go build -ldflags="-X main.Version=${TEST_VERSION} -X main.BuildDate=${TEST_BUILD_DATE}" -o "${BATS_TMPDIR}/lets-version-test" cmd/lets/main.go
run "${BATS_TMPDIR}/lets-version-test" --version
assert_success
assert_line --index 0 "lets version ${TEST_VERSION} (${TEST_BUILD_DATE})"
rm -f "${BATS_TMPDIR}/lets-version-test"

}
Loading