Skip to content

Fix sequential env evaluation and global context for command env#295

Merged
kindermax merged 2 commits intomasterfrom
feature/fix/env-sequential-eval
Mar 14, 2026
Merged

Fix sequential env evaluation and global context for command env#295
kindermax merged 2 commits intomasterfrom
feature/fix/env-sequential-eval

Conversation

@kindermax
Copy link
Copy Markdown
Collaborator

@kindermax kindermax commented Mar 14, 2026

Summary

  • Evaluate env entries in declaration order so sh values can reference earlier keys in the same block.
  • Pass global resolved env into command-level env evaluation, allowing command sh env values to use global env keys.
  • Ensure resolved lets env values override process env during sh evaluation.
  • Add unit coverage for sequential resolution, base env usage, process env override behavior, and execution caching.
  • Add Bats integration coverage for global dependencies, command dependencies, global-to-command usage, and forward-reference behavior.
  • Update config docs and changelog to document sequential env evaluation semantics.

Testing

  • Added unit tests in internal/config/config/env_execute_test.go:
  • resolves env entries sequentially
  • uses base env for sh evaluation
  • resolved lets env overrides process env
  • keeps cached values after first execution
  • Added integration tests in tests/env_dependency.bats with fixture tests/env_dependency/lets.yaml:
  • Global env sh depends on previously resolved global env
  • Command env sh depends on previously resolved command env
  • Command env sh reads global env
  • Forward reference remains unresolved under sequential evaluation
  • Execution status in this PR write-up: Not run

Summary by Sourcery

Ensure environment variables defined in config are evaluated sequentially and can reference previously resolved values, including global env from commands, and document the new semantics.

Bug Fixes:

  • Fix env evaluation so entries are resolved in declaration order and allow sh-based values to reference earlier keys in the same env block.
  • Ensure command-level env evaluation has access to resolved global env values and that config env overrides process environment when executing sh scripts.

Enhancements:

  • Preserve cached env execution results across multiple Execute calls to avoid re-running scripts.
  • Introduce a conversion helper to pass merged process and config env maps into shell execution.

Documentation:

  • Document sequential env evaluation semantics for global and command env blocks in the config docs.
  • Add a design spec and implementation plan documents for a future dependency failure tree feature.
  • Update the changelog to record the sequential env evaluation fix.

Tests:

  • Add unit tests covering sequential env resolution, base env usage, process env override behavior, and caching of evaluated env values.
  • Add Bats integration tests to verify global and command env dependencies, global-to-command env usage, and forward-reference behavior in env resolution.

Chores:

  • Add a future implementation plan and design spec docs for a dependency failure tree feature.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 14, 2026

Reviewer's Guide

Implements sequential evaluation of env entries with an overridable base environment, wires global env into command-level env resolution, and adds tests and docs to cover the new semantics, plus some unrelated planning/spec docs for a future dependency failure tree feature.

Sequence diagram for global and command env execution with baseEnv

sequenceDiagram
    participant Main
    participant Config
    participant GlobalEnvs as Envs_global
    participant Command
    participant CommandEnvs as Envs_command

    Main->>Config: SetupEnv()
    activate Config
    Config->>GlobalEnvs: Execute(cfg, baseEnv=nil)
    activate GlobalEnvs
    GlobalEnvs->>GlobalEnvs: resolvedEnv = cloneMap(nil) => {}
    loop for each key in Envs.Keys
        alt env.Sh is not empty
            GlobalEnvs->>GlobalEnvs: executeScript(cfg.Shell, env.Sh, resolvedEnv)
            GlobalEnvs-->>GlobalEnvs: result
            GlobalEnvs->>GlobalEnvs: env.Value = result
        else env.Value already set
        end
        GlobalEnvs->>GlobalEnvs: resolvedEnv[key] = env.Value
    end
    GlobalEnvs-->>Config: ready=true
    deactivate GlobalEnvs
    Config-->>Main: global env resolved
    deactivate Config

    Main->>Command: GetEnv(cfg)
    activate Command
    Command->>Config: GetEnv()
    Config-->>Command: baseEnv (resolved global env)
    Command->>CommandEnvs: Execute(cfg, baseEnv)
    activate CommandEnvs
    CommandEnvs->>CommandEnvs: resolvedEnv = cloneMap(baseEnv)
    loop for each key in Envs.Keys
        alt env.Sh is not empty
            CommandEnvs->>CommandEnvs: executeScript(cfg.Shell, env.Sh, resolvedEnv)
            CommandEnvs-->>CommandEnvs: result
            CommandEnvs->>CommandEnvs: env.Value = result
        else env.Value already set
        end
        CommandEnvs->>CommandEnvs: resolvedEnv[key] = env.Value
    end
    CommandEnvs-->>Command: command env map
    deactivate CommandEnvs
    Command-->>Main: merged env for command
    deactivate Command
Loading

Class diagram for Config, Command, Envs, and env execution helpers

classDiagram
    class Env {
        +string Value
        +string Sh
    }

    class Envs {
        +map~string,Env~ Mapping
        +[]string Keys
        +bool ready
        +void Set(key string, value Env)
        +error Execute(cfg Config, baseEnv map~string,string~)
    }

    class Config {
        +string Shell
        +Envs Env
        +map~string,string~ GetEnv()
        +error SetupEnv()
    }

    class Command {
        +string Name
        +Envs Env
        +map~string,string~ GetEnv(cfg Config)
    }

    class Helpers {
        +[]string convertEnvMapToList(envMap map~string,string~)
        +string executeScript(shell string, script string, envMap map~string,string~)
    }

    Config "1" o-- "1" Envs : globalEnv
    Command "1" o-- "1" Envs : commandEnv
    Envs "*" o-- Env : Mapping

    Config ..> Helpers : uses
    Command ..> Helpers : uses
    Envs ..> Helpers : calls

    Config --> Envs : GetEnv() reads resolved Mapping
    Config --> Envs : SetupEnv() Execute(cfg, nil)
    Command --> Envs : GetEnv(cfg) Execute(cfg, cfg.GetEnv())
Loading

File-Level Changes

Change Details Files
Support executing env shell scripts with a provided base environment and sequential resolution of env entries.
  • Introduce a helper to convert a string map to KEY=VALUE list suitable for os/exec.Env.
  • Extend executeScript to accept an env map, merge it with the process environment, and pass it to the command.
  • Change Envs.Execute to accept a baseEnv map, clone it, and resolve env entries in key order while updating a cumulative resolvedEnv map after each entry.
  • Ensure Envs.Execute is lazy and respects existing caching via the ready flag.
internal/config/config/env.go
Wire global config env into command-level env execution and adjust config setup accordingly.
  • Update Command.GetEnv to pass cfg.GetEnv() as the base env when executing command-level envs so command sh envs can see global env values.
  • Update Config.SetupEnv to call Envs.Execute with a nil base env for global env resolution.
internal/config/config/command.go
internal/config/config/config.go
Document sequential env evaluation semantics and changelog entry for the fix.
  • Clarify in global env docs that env entries are evaluated sequentially and sh can reference earlier entries in the same block.
  • Clarify in command env docs that command env entries are evaluated sequentially and have access to global env values during evaluation.
  • Add a changelog entry describing the sequential env evaluation and global-to-command env visibility fix.
docs/docs/config.md
docs/docs/changelog.md
Add unit and integration tests to validate sequential env evaluation, base env usage, override semantics, and env dependencies.
  • Add Go unit tests for Envs.Execute covering sequential resolution, use of base env, lets env overriding process env, and caching of computed values.
  • Add a Bats test suite verifying global env sh depending on prior global env, command env sh depending on prior command env, command env sh reading global env, and that forward references remain unresolved under sequential evaluation.
  • Introduce a Bats fixture lets.yaml describing global and command env dependencies and a forward-reference case.
internal/config/config/env_execute_test.go
tests/env_dependency.bats
tests/env_dependency/lets.yaml
Add internal design/plan docs for a future dependency failure tree feature (no runtime changes yet).
  • Add an implementation plan markdown under docs/plans describing steps and tests for a dependency failure tree feature.
  • Add a detailed design spec under docs/specs outlining DependencyError, error chaining, printing behavior, and testing for the dependency failure tree.
docs/plans/2026-03-14-dependency-failure-tree.md
docs/specs/2026-03-13-dependency-failure-tree-design.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

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="internal/config/config/env_execute_test.go" line_range="11-20" />
<code_context>
+		WorkDir: ".",
+	}
+
+	t.Run("resolves env entries sequentially", func(t *testing.T) {
+		envs := &Envs{}
+		envs.Set("ENGINE", Env{Name: "ENGINE", Value: "docker"})
+		envs.Set("COMPOSE", Env{Name: "COMPOSE", Sh: `echo "${ENGINE}-compose"`})
+
+		err := envs.Execute(cfg, nil)
+		if err != nil {
+			t.Fatalf("unexpected execute error: %s", err)
+		}
+
+		if got := envs.Mapping["COMPOSE"].Value; got != "docker-compose" {
+			t.Fatalf("expected COMPOSE=docker-compose, got %q", got)
+		}
+	})
+
+	t.Run("uses base env for sh evaluation", func(t *testing.T) {
</code_context>
<issue_to_address>
**issue (testing):** Add a unit test covering error propagation when `sh` execution fails

Current tests only cover successful `sh` executions. Please add a subtest where the script exits non‑zero (e.g. `Sh:
</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/config/config/env_execute_test.go
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 14, 2026

Greptile Summary

This PR fixes two related gaps in lets' env evaluation: (1) env entries are now evaluated in YAML declaration order so that a sh value can reference keys defined earlier in the same block, and (2) the fully-resolved global env is passed as a base context when evaluating command-level env, enabling command sh entries to reference global keys. Both behaviours are implemented in Envs.Execute by accumulating a resolvedEnv map as each entry is processed and by threading a baseEnv parameter through the call chain.

  • Core logic (internal/config/config/env.go): Execute now accepts a baseEnv map[string]string, initialises a local resolvedEnv from it, and passes resolvedEnv to every executeScript call; after each entry is evaluated its value is added to resolvedEnv so subsequent entries see it.
  • Command integration (internal/config/config/command.go): GetEnv now calls c.Env.Execute(cfg, cfg.GetEnv()), injecting the already-resolved global env as the base.
  • Global env setup (internal/config/config/config.go): SetupEnv passes nil as baseEnv (no predecessor context for top-level env).
  • Tests: Four new unit tests cover sequential resolution, base-env injection, process-env override, and result caching. Four new Bats integration tests exercise all scenarios end-to-end including the intentional "forward reference stays unresolved" edge case.
  • Docs/Changelog: Clear, accurate additions to both config docs and the changelog.
  • Unrelated documents: docs/plans/2026-03-14-dependency-failure-tree.md and docs/specs/2026-03-13-dependency-failure-tree-design.md are 700-line planning/design artefacts for a completely separate "Dependency Failure Tree" feature. They appear to have been accidentally committed onto this branch and should be removed or moved to their own PR.

Confidence Score: 4/5

  • Safe to merge once the two unrelated planning/spec documents are removed from the branch.
  • The code changes are small, well-targeted, and covered by both unit and integration tests. The implementation correctly handles all edge cases (static vs sh vs checksum entries, nil base env, process-env override, caching). The only meaningful concern is the accidental inclusion of two large planning documents for an entirely different feature, which does not affect runtime correctness but does pollute the commit history. A minor implicit ordering contract between SetupEnv and command.GetEnv exists but is already enforced by the executor's initialization flow.
  • docs/plans/2026-03-14-dependency-failure-tree.md and docs/specs/2026-03-13-dependency-failure-tree-design.md — unrelated to this PR and should be removed.

Important Files Changed

Filename Overview
internal/config/config/env.go Core change: adds sequential resolvedEnv accumulation in Execute and passes it to executeScript so each sh entry can reference earlier keys. Logic is sound; minor concern around reliance on Go 1.21+ dedup semantics for process-env override (documented implicitly by tests).
internal/config/config/command.go Passes resolved global env as baseEnv to command-level Execute. Works correctly when SetupEnv is called first; implicit ordering contract between SetupEnv and GetEnv is not enforced in code.
internal/config/config/env_execute_test.go New unit tests covering sequential resolution, base-env injection, process-env override, and caching. All four cases are meaningful and well-structured.
tests/env_dependency.bats New Bats integration tests for all four scenarios (global dependency, command dependency, global-to-command, forward reference). Follows existing project conventions for setup() and cd ./tests/….
docs/plans/2026-03-14-dependency-failure-tree.md 516-line implementation plan for a completely different, unimplemented "Dependency Failure Tree" feature. Appears to be accidentally included in this PR — no corresponding code changes exist.
docs/specs/2026-03-13-dependency-failure-tree-design.md Design spec for "Dependency Failure Tree" — a separate, unimplemented feature. Like the plan file, this appears accidentally included in a PR about env evaluation.

Sequence Diagram

sequenceDiagram
    participant Executor
    participant Config
    participant GlobalEnvs as Config.Env (Global Envs)
    participant Command
    participant CmdEnvs as Command.Env (Command Envs)
    participant Shell as Shell (bash -c)

    Executor->>Config: SetupEnv()
    Config->>GlobalEnvs: Execute(cfg, nil)
    loop For each key in declaration order
        alt sh entry
            GlobalEnvs->>Shell: executeScript(shell, script, resolvedEnv)
            Shell-->>GlobalEnvs: result
            GlobalEnvs->>GlobalEnvs: resolvedEnv[key] = result
        else static / checksum entry
            GlobalEnvs->>GlobalEnvs: resolvedEnv[key] = value
        end
    end
    GlobalEnvs-->>Config: ready=true

    Executor->>Command: GetEnv(cfg)
    Command->>Config: cfg.GetEnv() → Dump() of resolved global env
    Config-->>Command: baseEnv (map[string]string)
    Command->>CmdEnvs: Execute(cfg, baseEnv)
    Note over CmdEnvs: resolvedEnv = clone(baseEnv)<br/>(global env as starting context)
    loop For each key in declaration order
        alt sh entry
            CmdEnvs->>Shell: executeScript(shell, script, resolvedEnv)
            Note over Shell: env = os.Environ() + resolvedEnv<br/>(resolvedEnv appended last → overrides)
            Shell-->>CmdEnvs: result
            CmdEnvs->>CmdEnvs: resolvedEnv[key] = result
        else static entry
            CmdEnvs->>CmdEnvs: resolvedEnv[key] = static value
        end
    end
    CmdEnvs-->>Command: ready=true
    Command-->>Executor: Dump() → map[string]string
Loading

Comments Outside Diff (1)

  1. internal/config/config/command.go, line 129-135 (link)

    cfg.GetEnv() returns empty strings for unresolved sh global env

    cfg.GetEnv() delegates to Envs.Dump(), which returns whatever is currently in e.Mapping[key].Value. For global env entries that use sh:, the Value field is only populated after Config.SetupEnv() has been called. If command.GetEnv() were ever invoked before SetupEnv() (e.g. in a test or a future call-site), the global sh-type env keys passed as baseEnv would be empty strings, silently giving wrong results.

    This is an implicit ordering contract. It would be more robust to assert (or document) this precondition, or to pass the already-executed Envs object directly instead of calling Dump() again:

    // Option: pass the already-resolved global Envs directly
    if err := c.Env.Execute(cfg, cfg.Env.Dump()); err != nil {

    As written this is functionally equivalent when SetupEnv has been called, but makes the dependency more obvious. At minimum a comment noting the ordering requirement would help future readers.

Last reviewed commit: eddf099

Comment on lines +1 to +5
# Dependency Failure Tree Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** When a `lets` command (or any of its `depends`) fails, print an indented tree to stderr showing the full dependency chain with the failing node highlighted in red.
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.

Unrelated planning document included

This file — along with docs/specs/2026-03-13-dependency-failure-tree-design.md — describes a different, unimplemented feature ("Dependency Failure Tree") that has no relationship to the sequential env-evaluation fix this PR delivers. Neither file contains code that is part of this PR's changeset.

Including future-feature plans/specs in a feature PR for an unrelated change muddies the history and could confuse reviewers. These two documents should either be removed from this branch or landed in their own dedicated PR/branch once the dependency-failure-tree work actually begins.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread internal/config/config/env.go
@kindermax kindermax force-pushed the feature/fix/env-sequential-eval branch from eddf099 to 333991d Compare March 14, 2026 15:09
- pass resolved env into `sh` evaluation so later keys can use earlier ones
- allow command-level `env` scripts to read already-resolved global env
- document order-dependent behavior and add unit + bats coverage
@kindermax kindermax force-pushed the feature/fix/env-sequential-eval branch from 66108fb to 709dccb Compare March 14, 2026 15:34
@kindermax kindermax merged commit cccbc25 into master Mar 14, 2026
5 checks passed
@kindermax kindermax deleted the feature/fix/env-sequential-eval branch March 14, 2026 15:37
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