Skip to content

Commit 52c48c8

Browse files
committed
Implement env_file support flow
1 parent 072d796 commit 52c48c8

27 files changed

Lines changed: 784 additions & 28 deletions

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CUSTOM_NAME=xxx

cmd/lets/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func main() {
124124
if errors.As(err, &depErr) {
125125
executor.PrintDependencyTree(depErr, os.Stderr)
126126
}
127-
log.Error(err.Error())
127+
log.Errorf("lets: %s", err.Error())
128128
os.Exit(getExitCode(err, 1))
129129
}
130130
}

docs/docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ title: Changelog
1010
* `[Added]` Expose `LETS_OS` and `LETS_ARCH` environment variables at command runtime.
1111
* `[Removed]` Drop deprecated `eval_env` directive. Use `env` with `sh` execution mode instead.
1212
* `[Added]` When a command or its `depends` chain fails, print an indented tree to stderr showing the full chain with the failing command highlighted
13+
* `[Added]` Support `env_file` in global config and commands. File names are expanded after `env` is resolved, and values loaded from env files override values from `env`.
1314

1415
## [0.0.59](https://github.com/lets-cli/lets/releases/tag/v0.0.59)
1516

docs/docs/config.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,58 @@ env:
101101
sh: echo "${ENGINE}-compose"
102102
```
103103
104+
### Global env_file
105+
106+
`key: env_file`
107+
108+
`type: string | map | list`
109+
110+
Load one or more dotenv-style files and expose their values to all commands.
111+
112+
Supported forms:
113+
114+
```yaml
115+
env_file: .env
116+
env_file: -.env.local
117+
env_file:
118+
name: .env.${TARGET}
119+
required: false
120+
env_file:
121+
- .env
122+
- -.env.local
123+
- name: .env.${LETS_OS}
124+
required: true
125+
```
126+
127+
Rules:
128+
129+
- `-filename` is a short form of `required: false`
130+
- files are resolved relative to the config directory
131+
- file names are expanded after global `env` is resolved, so `env_file` can depend on global `env`
132+
- values loaded from `env_file` have higher precedence than values from `env`
133+
- missing files fail by default
134+
- invalid env file syntax reports an error with the file name
135+
136+
Example:
137+
138+
```yaml
139+
shell: bash
140+
141+
env:
142+
TARGET: dev
143+
ENGINE: docker
144+
145+
env_file:
146+
- .env.${TARGET}
147+
- -.env.local
148+
149+
commands:
150+
echo-env:
151+
cmd: |
152+
echo ENGINE=${ENGINE}
153+
echo API_URL=${API_URL}
154+
```
155+
104156
### Global before
105157

106158
`key: before`
@@ -696,6 +748,43 @@ commands:
696748
```
697749
698750
751+
### `env_file`
752+
753+
`key: env_file`
754+
755+
`type: string | map | list`
756+
757+
Load dotenv-style env files for a single command.
758+
759+
Rules:
760+
761+
- command `env` is resolved first
762+
- command `env_file` file names are expanded using builtin lets vars, merged global env, and resolved command `env`
763+
- values loaded from command `env_file` override values from command `env`
764+
- paths are resolved relative to the config directory, not `work_dir`
765+
766+
Example:
767+
768+
```yaml
769+
shell: bash
770+
771+
env:
772+
TARGET: dev
773+
774+
commands:
775+
up:
776+
env:
777+
SUFFIX: local
778+
ENGINE: docker
779+
env_file:
780+
- .env.${TARGET}.${SUFFIX}
781+
- -.env.override
782+
cmd: |
783+
echo ENGINE=${ENGINE}
784+
echo API_URL=${API_URL}
785+
```
786+
787+
699788
### `checksum`
700789

701790
`key: checksum`

docs/docs/env.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,28 @@ title: Environment
2727

2828
### Override command env with -E flag
2929

30+
### `env_file` precedence
31+
32+
`env_file` loads dotenv-style files from config and command definitions.
33+
34+
Precedence order is:
35+
36+
* process env
37+
* builtin lets vars
38+
* global `env`
39+
* global `env_file`
40+
* command `env`
41+
* command `env_file`
42+
* command options, `-E` / `--env`, checksum vars
43+
44+
When the same key is present in both directives, `env_file` wins over `env` at the same scope.
45+
46+
`env_file` file names are expanded after `env` is resolved. This means:
47+
48+
* global `env_file` can depend on global `env`
49+
* command `env_file` can depend on merged global env and command `env`
50+
* `env.sh` still does not read values from `env_file`
51+
3052
You can override environment for command with `-E` flag:
3153

3254
```yaml

docs/static/schema.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
"env": {
4343
"$ref": "#/definitions/env"
4444
},
45+
"env_file": {
46+
"$ref": "#/definitions/env_file"
47+
},
4548
"before": {
4649
"type": "string",
4750
"description": "Commands to run before the main script."
@@ -137,6 +140,9 @@
137140
"env": {
138141
"$ref": "#/definitions/env"
139142
},
143+
"env_file": {
144+
"$ref": "#/definitions/env_file"
145+
},
140146
"after": {
141147
"type": "string",
142148
"description": "A shell sctipt to run after the command."
@@ -192,6 +198,45 @@
192198
}
193199
},
194200
"additionalProperties": false
201+
},
202+
"env_file_entry": {
203+
"oneOf": [
204+
{
205+
"type": "string",
206+
"description": "Path to an env file. Prefix with '-' to make it optional."
207+
},
208+
{
209+
"type": "object",
210+
"properties": {
211+
"name": {
212+
"type": "string",
213+
"description": "Path to an env file."
214+
},
215+
"required": {
216+
"type": "boolean",
217+
"description": "Whether the env file must exist. Defaults to true."
218+
}
219+
},
220+
"required": [
221+
"name"
222+
],
223+
"additionalProperties": false
224+
}
225+
]
226+
},
227+
"env_file": {
228+
"description": "Env file or list of env files to load.",
229+
"oneOf": [
230+
{
231+
"$ref": "#/definitions/env_file_entry"
232+
},
233+
{
234+
"type": "array",
235+
"items": {
236+
"$ref": "#/definitions/env_file_entry"
237+
}
238+
}
239+
]
195240
}
196241
},
197242
"additionalProperties": false

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ require (
4040
require (
4141
github.com/h2non/filetype v1.1.1 // indirect
4242
github.com/inconshreveable/mousetrap v1.1.0 // indirect
43+
github.com/joho/godotenv v1.5.1
4344
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f // indirect
4445
github.com/juju/testing v0.0.0-20201216035041-2be42bba85f3 // indirect
4546
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS
2020
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
2121
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
2222
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
23+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
24+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
2325
github.com/juju/ansiterm v0.0.0-20160907234532-b99631de12cf/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
2426
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
2527
github.com/juju/cmd v0.0.0-20171107070456-e74f39857ca0/go.mod h1:yWJQHl73rdSX4DHVKGqkAip+huBslxRwS8m9CrOLq18=

internal/config/config/command.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ type Command struct {
2525
Description string
2626
// env from command
2727
Env *Envs
28+
// env files from command
29+
EnvFiles *EnvFiles
2830
// store docopts from options directive
2931
Docopts string
3032
SkipDocopts bool // default false
@@ -42,6 +44,8 @@ type Command struct {
4244

4345
// ref is basically a command name to use with predefined args, env
4446
ref *ref
47+
48+
resolvedEnv map[string]string
4549
}
4650

4751
type Commands map[string]*Command
@@ -62,6 +66,7 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error {
6266
Description string
6367
Shell string
6468
Env *Envs
69+
EnvFiles *EnvFiles `yaml:"env_file"`
6570
Options string
6671
Depends *Deps
6772
WorkDir string `yaml:"work_dir"`
@@ -87,6 +92,10 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error {
8792
if c.Env == nil {
8893
c.Env = &Envs{}
8994
}
95+
c.EnvFiles = cmd.EnvFiles
96+
if c.EnvFiles == nil {
97+
c.EnvFiles = &EnvFiles{}
98+
}
9099

91100
c.Shell = cmd.Shell
92101
c.Docopts = cmd.Options
@@ -126,12 +135,39 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error {
126135
return nil
127136
}
128137

129-
func (c *Command) GetEnv(cfg Config) (map[string]string, error) {
130-
if err := c.Env.Execute(cfg, cfg.GetEnv()); err != nil {
138+
func (c *Command) GetEnv(cfg Config, builtinEnv map[string]string) (map[string]string, error) {
139+
if c.resolvedEnv != nil {
140+
return cloneMap(c.resolvedEnv), nil
141+
}
142+
143+
baseEnv := cloneMap(builtinEnv)
144+
if baseEnv == nil {
145+
baseEnv = make(map[string]string)
146+
}
147+
for key, value := range cfg.GetEnv() {
148+
baseEnv[key] = value
149+
}
150+
151+
if err := c.Env.Execute(cfg, baseEnv); err != nil {
131152
return nil, err
132153
}
133154

134-
return c.Env.Dump(), nil
155+
filenameEnv := cloneMap(baseEnv)
156+
for key, value := range c.Env.Dump() {
157+
filenameEnv[key] = value
158+
}
159+
160+
envFileEnv, err := c.EnvFiles.Load(cfg, filenameEnv)
161+
if err != nil {
162+
return nil, fmt.Errorf("failed to resolve env_file for command '%s': %w", c.Name, err)
163+
}
164+
165+
c.resolvedEnv = c.Env.Dump()
166+
for key, value := range envFileEnv {
167+
c.resolvedEnv[key] = value
168+
}
169+
170+
return cloneMap(c.resolvedEnv), nil
135171
}
136172

137173
func (c *Command) Clone() *Command {
@@ -144,6 +180,7 @@ func (c *Command) Clone() *Command {
144180
WorkDir: c.WorkDir,
145181
Description: c.Description,
146182
Env: c.Env.Clone(),
183+
EnvFiles: c.EnvFiles.Clone(),
147184
Docopts: c.Docopts,
148185
SkipDocopts: c.SkipDocopts,
149186
Options: cloneMap(c.Options),

0 commit comments

Comments
 (0)