Skip to content

Add normalized LETS_OS and LETS_ARCH environment vars#297

Merged
kindermax merged 1 commit into
masterfrom
codex/add-letsos-and-letsarch-env
Mar 14, 2026
Merged

Add normalized LETS_OS and LETS_ARCH environment vars#297
kindermax merged 1 commit into
masterfrom
codex/add-letsos-and-letsarch-env

Conversation

@kindermax

@kindermax kindermax commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • expose normalized LETS_OS and LETS_ARCH variables during command runtime and document them in the environment guide and changelog
  • normalize incoming runtime values via new helpers and cover them with unit tests plus a Bats check that prints the vars
    Testing
  • Not run (not requested)

Summary by Sourcery

Expose normalized OS and architecture information as environment variables available to commands and document their usage.

New Features:

  • Provide LETS_OS and LETS_ARCH environment variables populated with normalized platform values based on the current runtime.

Enhancements:

  • Normalize various OS and architecture name variants into consistent Go-style values via new helper functions.

Documentation:

  • Document the LETS_OS and LETS_ARCH environment variables in the environment guide and changelog.

Tests:

  • Add unit tests for OS and architecture normalization helpers and a Bats test command to verify LETS_OS and LETS_ARCH values at runtime.

@sourcery-ai

sourcery-ai Bot commented Mar 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR introduces normalized LETS_OS and LETS_ARCH environment variables exposed at command runtime, implements platform normalization helpers with tests, wires them into the executor, and documents the new behavior in the environment guide and changelog.

Class diagram for Executor env setup and platform normalization helpers

classDiagram
    class Executor {
        +setupEnv(osCmd exec.Cmd, command config.Command, shell string) error
    }

    class PlatformHelpers {
        +normalizeOS(os string) string
        +normalizeArch(arch string) string
    }

    Executor ..> PlatformHelpers : uses
Loading

Flow diagram for setting normalized LETS_OS and LETS_ARCH in command environment

flowchart TD
    A[Start setupEnv] --> B[Receive osCmd, command, shell]
    B --> C[Set LETS_COMMAND_NAME, LETS_COMMAND_DIR, LETS_COMMAND_WORK_DIR]
    C --> D[Set LETS_CONFIG, LETS_CONFIG_DIR]
    D --> E[Read runtime.GOOS]
    E --> F[Call normalizeOS]
    F --> G[Set LETS_OS in osCmd.Env]
    G --> H[Read runtime.GOARCH]
    H --> I[Call normalizeArch]
    I --> J[Set LETS_ARCH in osCmd.Env]
    J --> K[Set LETS_SHELL and other env vars]
    K --> L[Return configured osCmd]
Loading

File-Level Changes

Change Details Files
Expose normalized LETS_OS and LETS_ARCH environment variables at command runtime via the executor.
  • Import the runtime package in the executor to access GOOS and GOARCH.
  • Use helper functions to normalize runtime.GOOS and runtime.GOARCH before injecting them into the command environment.
  • Add LETS_OS and LETS_ARCH entries to the environment map built in setupEnv.
internal/executor/executor.go
Add platform normalization helpers for OS and architecture with unit tests.
  • Implement normalizeOS to map common OS aliases (e.g., macOS/OSX/win) to Go-style identifiers and lowercase unknown values.
  • Implement normalizeArch to map common architecture aliases (e.g., x86_64/x64/x86/i386/i686/aarch64) to Go-style identifiers and lowercase unknown values.
  • Add table-driven tests for normalizeOS and normalizeArch with subtests run in parallel.
internal/executor/platform.go
internal/executor/platform_test.go
Extend default environment tests and fixtures to validate the new LETS_OS and LETS_ARCH variables.
  • Add a new Bats test that runs the print-os-arch command and asserts that LETS_OS and LETS_ARCH match go env GOOS/GOARCH.
  • Extend the test lets.yaml to define a print-os-arch command that echoes LETS_OS (and relies on the default environment for LETS_ARCH).
tests/default_env.bats
tests/default_env/lets.yaml
Document the new normalized platform environment variables in user-facing docs and changelog.
  • Describe LETS_OS and LETS_ARCH in the environment documentation with examples of possible values.
  • Add a changelog entry under the latest version noting that normalized LETS_OS and LETS_ARCH are now exposed at command runtime.
docs/docs/env.md
docs/docs/changelog.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The print-os-arch command in tests/default_env/lets.yaml only echoes LETS_OS, but default_env.bats asserts both LETS_OS and LETS_ARCH, so the test will currently fail; update the command to also echo LETS_ARCH in the expected second line.
  • In platform_test.go, the table-driven tests use a map with t.Run and t.Parallel, which can lead to non-deterministic ordering and depends on Go’s range variable semantics; consider switching to a slice of structs for the test cases to make the test behavior explicit and stable across Go versions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `print-os-arch` command in `tests/default_env/lets.yaml` only echoes `LETS_OS`, but `default_env.bats` asserts both `LETS_OS` and `LETS_ARCH`, so the test will currently fail; update the command to also echo `LETS_ARCH` in the expected second line.
- In `platform_test.go`, the table-driven tests use a `map` with `t.Run` and `t.Parallel`, which can lead to non-deterministic ordering and depends on Go’s range variable semantics; consider switching to a slice of structs for the test cases to make the test behavior explicit and stable across Go versions.

## Individual Comments

### Comment 1
<location path="internal/executor/platform_test.go" line_range="8-17" />
<code_context>
+func TestNormalizeOS(t *testing.T) {
+	t.Parallel()
+
+	tests := map[string]string{
+		"linux":   "linux",
+		"darwin":  "darwin",
+		"macOS":   "darwin",
+		"OSX":     "darwin",
+		"windows": "windows",
+		"win":     "windows",
+	}
+
+	for input, expected := range tests {
+		t.Run(input, func(t *testing.T) {
+			t.Parallel()
</code_context>
<issue_to_address>
**suggestion (testing):** Subtests use loop variables directly with t.Parallel, which can be fragile across Go versions

In Go <1.22, capturing loop variables directly in subtest closures can cause flakiness because the closure may observe updated loop values. To avoid this and make the tests robust, rebind the loop variables inside the loop before calling `t.Run`:

```go
for input, expected := range tests {
    input, expected := input, expected
    t.Run(input, func(t *testing.T) {
        t.Parallel()
        got := normalizeOS(input)
        if got != expected {
            t.Fatalf("normalizeOS(%q) = %q, want %q", input, got, expected)
        }
    })
}
```

The same pattern should be applied to the `TestNormalizeArch` loop below.

Suggested implementation:

```golang
	for input, expected := range tests {
		input, expected := input, expected

		t.Run(input, func(t *testing.T) {
			t.Parallel()

			got := normalizeOS(input)
			if got != expected {
				t.Fatalf("normalizeOS(%q) = %q, want %q", input, got, expected)
			}
		})
	}

```

` section.

Here are the edits:

<file_operations>
<file_operation operation="edit" file_path="internal/executor/platform_test.go">
<<<<<<< SEARCH
	for input, expected := range tests {
		t.Run(input, func(t *testing.T) {
			t.Parallel()

			got := normalizeOS(input)
			if got != expected {
				t.Fatalf("normalizeOS(%q) = %q, want %q", input, got, expected)
			}
		})
	}
=======
	for input, expected := range tests {
		input, expected := input, expected

		t.Run(input, func(t *testing.T) {
			t.Parallel()

			got := normalizeOS(input)
			if got != expected {
				t.Fatalf("normalizeOS(%q) = %q, want %q", input, got, expected)
			}
		})
	}
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
Apply the same pattern in `TestNormalizeArch` once its body is implemented. Specifically, for the loop that ranges over its test cases:

1. Inside `for arch, expected := range tests { ... }`, add `arch, expected := arch, expected` as the first statement in the loop body.
2. Ensure `t.Run` uses the rebinding (e.g., `t.Run(arch, func(t *testing.T) { ... })`) so each subtest closure captures its own copy of the loop variables.
</issue_to_address>

### Comment 2
<location path="docs/docs/env.md" line_range="21" />
<code_context>
 * `LETS_COMMAND_WORK_DIR` - absolute path to `work_dir` specified in command.
 * `LETS_CONFIG` - absolute path to lets config file.
 * `LETS_CONFIG_DIR` - absolute path to lets config file firectory.
+* `LETS_OS` - normalized current operating system name in Go format, for example `linux`, `darwin`, `windows`
+* `LETS_ARCH` - normalized current architecture name in Go format, for example `amd64`, `arm64`, `386`
</code_context>
<issue_to_address>
**issue (typo):** Fix typo in `LETS_CONFIG_DIR` description ('firectory' -> 'directory').

```suggestion
* `LETS_CONFIG_DIR` - absolute path to lets config file directory.
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/executor/platform_test.go Outdated
Comment on lines +8 to +17
tests := map[string]string{
"linux": "linux",
"darwin": "darwin",
"macOS": "darwin",
"OSX": "darwin",
"windows": "windows",
"win": "windows",
}

for input, expected := range tests {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Subtests use loop variables directly with t.Parallel, which can be fragile across Go versions

In Go <1.22, capturing loop variables directly in subtest closures can cause flakiness because the closure may observe updated loop values. To avoid this and make the tests robust, rebind the loop variables inside the loop before calling t.Run:

for input, expected := range tests {
    input, expected := input, expected
    t.Run(input, func(t *testing.T) {
        t.Parallel()
        got := normalizeOS(input)
        if got != expected {
            t.Fatalf("normalizeOS(%q) = %q, want %q", input, got, expected)
        }
    })
}

The same pattern should be applied to the TestNormalizeArch loop below.

Suggested implementation:

	for input, expected := range tests {
		input, expected := input, expected

		t.Run(input, func(t *testing.T) {
			t.Parallel()

			got := normalizeOS(input)
			if got != expected {
				t.Fatalf("normalizeOS(%q) = %q, want %q", input, got, expected)
			}
		})
	}

` section.

Here are the edits:

<file_operations>
<file_operation operation="edit" file_path="internal/executor/platform_test.go">
<<<<<<< SEARCH
for input, expected := range tests {
t.Run(input, func(t *testing.T) {
t.Parallel()

		got := normalizeOS(input)
		if got != expected {
			t.Fatalf("normalizeOS(%q) = %q, want %q", input, got, expected)
		}
	})
}

=======
for input, expected := range tests {
input, expected := input, expected

	t.Run(input, func(t *testing.T) {
		t.Parallel()

		got := normalizeOS(input)
		if got != expected {
			t.Fatalf("normalizeOS(%q) = %q, want %q", input, got, expected)
		}
	})
}

REPLACE
</file_operation>
</file_operations>

<additional_changes>
Apply the same pattern in TestNormalizeArch once its body is implemented. Specifically, for the loop that ranges over its test cases:

  1. Inside for arch, expected := range tests { ... }, add arch, expected := arch, expected as the first statement in the loop body.
  2. Ensure t.Run uses the rebinding (e.g., t.Run(arch, func(t *testing.T) { ... })) so each subtest closure captures its own copy of the loop variables.

Comment thread docs/docs/env.md
@@ -19,6 +19,8 @@ title: Environment
* `LETS_COMMAND_WORK_DIR` - absolute path to `work_dir` specified in command.
* `LETS_CONFIG` - absolute path to lets config file.
* `LETS_CONFIG_DIR` - absolute path to lets config file firectory.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (typo): Fix typo in LETS_CONFIG_DIR description ('firectory' -> 'directory').

Suggested change
* `LETS_CONFIG_DIR` - absolute path to lets config file firectory.
* `LETS_CONFIG_DIR` - absolute path to lets config file directory.

@greptile-apps

greptile-apps Bot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two new built-in environment variables — LETS_OS and LETS_ARCH — that are injected into every command's runtime environment, exposing the current operating system and CPU architecture in Go's canonical format (e.g. linux, darwin, amd64, arm64). The change is correctly integrated into setupEnv, documented in both the env guide and changelog, and covered by both unit tests and a bats integration test.

Key observations:

  • The implementation is functionally correct: LETS_OS and LETS_ARCH will always hold the expected Go-format values.
  • The normalizeOS and normalizeArch helper functions contain switch cases for user-facing alias strings ("macos", "osx", "win", "x86_64", "aarch64", etc.), but since they are only ever called with runtime.GOOS / runtime.GOARCH — which already produce canonical lowercase Go values — those branches are unreachable dead code. The accompanying unit-test cases for those branches therefore test paths that cannot be triggered from production code.
  • The bats integration test relies on go env GOOS / go env GOARCH being available in the test environment — a reasonable assumption for a Go project's CI.

Confidence Score: 4/5

  • Safe to merge; the feature works correctly, but the normalization helpers contain unreachable switch cases that could mislead future maintainers.
  • The core change (exposing LETS_OS/LETS_ARCH) is correct and non-breaking. The only concern is a design inconsistency: the normalization functions are built to handle user-supplied strings but are only called with Go runtime constants that are already in canonical form, making several code paths permanently dead. This is a style/maintainability issue, not a runtime bug.
  • internal/executor/platform.go and internal/executor/platform_test.go — the normalization switch cases are dead code for the actual call site.

Important Files Changed

Filename Overview
internal/executor/platform.go New file adding normalizeOS/normalizeArch helpers; the normalization cases are dead code because the functions are only called with runtime.GOOS/runtime.GOARCH, which already produce canonical Go format values.
internal/executor/platform_test.go Unit tests for normalizeOS/normalizeArch; tests cover normalization cases (e.g., "macOS"→"darwin", "aarch64"→"arm64") that are unreachable from the actual production call site in executor.go.
internal/executor/executor.go Adds LETS_OS and LETS_ARCH to the default environment map using runtime.GOOS/runtime.GOARCH; integration is clean and correct.
tests/default_env.bats Adds a bats integration test comparing LETS_OS/LETS_ARCH against go env output; requires the go binary to be present in the test environment PATH.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Executor.setupEnv()"] --> B["runtime.GOOS"]
    A --> C["runtime.GOARCH"]
    B --> D["normalizeOS(goos)"]
    C --> E["normalizeArch(goarch)"]
    D -->|"'linux'→'linux'\n'darwin'→'darwin'\n'windows'→'windows'"| F["LETS_OS = result"]
    E -->|"'amd64'→'amd64'\n'arm64'→'arm64'\n'386'→'386'"| G["LETS_ARCH = result"]
    F --> H["defaultEnv map"]
    G --> H
    H --> I["osCmd.Env (appended to os.Environ())"]

    style D fill:#ffe0b2,stroke:#ef6c00
    style E fill:#ffe0b2,stroke:#ef6c00
    note1["⚠️ runtime.GOOS/GOARCH already\nreturn canonical Go-format strings.\nNormalization switch cases are dead code."]
    D -.-> note1
    E -.-> note1
Loading

Last reviewed commit: c882aed

Comment thread internal/executor/platform.go Outdated
Comment on lines +5 to +27
func normalizeOS(os string) string {
switch strings.ToLower(os) {
case "macos", "osx":
return "darwin"
case "win":
return "windows"
default:
return strings.ToLower(os)
}
}

func normalizeArch(arch string) string {
switch strings.ToLower(arch) {
case "x86_64", "x64":
return "amd64"
case "x86", "i386", "i686":
return "386"
case "aarch64":
return "arm64"
default:
return strings.ToLower(arch)
}
}

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.

Normalization cases are dead code for the actual call site

Both normalizeOS and normalizeArch are only ever called with runtime.GOOS and runtime.GOARCH (see executor.go lines 214-215), which are compile-time constants set by the Go toolchain. These constants are already in the canonical Go format — lowercase, e.g. "linux", "darwin", "windows", "amd64", "arm64", "386".

This means the alternative-name switch cases ("macos", "osx", "win", "x86_64", "x64", "aarch64", etc.) can never be reached from production code. The function will always fall through to the default branch, which just returns strings.ToLower(input) — effectively an identity function for already-lowercase runtime values.

The unit tests in platform_test.go also test these unreachable paths (e.g., "macOS" → "darwin", "aarch64" → "arm64"), giving a false sense of coverage. These normalization functions would make sense if they were normalizing user-supplied input (e.g., a LETS_OS override from an env file), but not for Go runtime constants.

Consider either:

  • Simply using runtime.GOOS / runtime.GOARCH directly without the wrapper functions, or
  • Documenting that these functions are intended for future use cases where user-provided strings need normalizing

@kindermax kindermax force-pushed the codex/add-letsos-and-letsarch-env branch from c882aed to 0e21735 Compare March 14, 2026 16:47
@kindermax kindermax merged commit 7e0d935 into master Mar 14, 2026
5 checks passed
@kindermax kindermax deleted the codex/add-letsos-and-letsarch-env branch March 14, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant