feat: support loading a dotenv '.env' file for your app#436
feat: support loading a dotenv '.env' file for your app#436
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #436 +/- ##
==========================================
+ Coverage 70.32% 70.44% +0.11%
==========================================
Files 220 221 +1
Lines 18506 18533 +27
==========================================
+ Hits 13015 13055 +40
+ Misses 4313 4306 -7
+ Partials 1178 1172 -6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
zimeg
left a comment
There was a problem hiding this comment.
👾 A few thoughts for wonderful reviewers-
There was a problem hiding this comment.
🗣️ note: The run command uses a separate hook process to handle automatic restarts with file watching so we duplicate some logic here!
| // so we instantiate the default here. | ||
| shell := hooks.HookExecutorDefaultProtocol{ | ||
| IO: clients.IO, | ||
| Fs: clients.Fs, |
There was a problem hiding this comment.
🔭 note: Standalone protocol setups must define fs to access .env but we don't error otherwise. AFAICT this is required for just the deploy and run commands.
mwbrooks
left a comment
There was a problem hiding this comment.
🙌🏻 Woohoo, this is so awesome to see landing!
🥾 I think we have a few more steps to tighten things up. I've left a few suggestions in-line around consolidating code and testing edge-cases that users will hit. Below are a few more suggestions.
suggestion(non-blocking): We have some existing dotenv logic in internal/config/dotenv.go. This seems like a reasonable place to put our new parsing logic, so that everything is in one place. Or, rename it to something that feels better to us. This could be a follow-up PR but we should make sure that we don't fragment our dotenv logic.
suggestion: I think we should update the PR title and CHANGELOG description to focus a little more on the use-facing feature. For example: "feat: support loading a dotenv '.env' file for your app"
internal/hooks/hooks.go
Outdated
| // Order of precedence from lowest to highest: | ||
| // 1. Provided "opts.Env" variables | ||
| // 2. Saved ".env" file | ||
| // 3. Existing shell environment | ||
| // | ||
| // > Each entry is of the form "key=value". | ||
| // > ... | ||
| // > If Env contains duplicate environment keys, only the last value in the slice for each duplicate key is used. | ||
| // | ||
| // https://pkg.go.dev/os/exec#Cmd.Env |
There was a problem hiding this comment.
praise: ❤️ 🖊️ Love the detailed comment for future readers!
internal/pkg/platform/localserver.go
Outdated
| // Order of precedence from lowest to highest: | ||
| // 1. Provided "opts.Env" variables | ||
| // 2. Saved ".env" file | ||
| // 3. Existing shell environment | ||
| // | ||
| // > Each entry is of the form "key=value". | ||
| // > ... | ||
| // > If Env contains duplicate environment keys, only the last value in the slice for each duplicate key is used. | ||
| // | ||
| // https://pkg.go.dev/os/exec#Cmd.Env |
There was a problem hiding this comment.
suggestion: This logic feels important and since it's used in 2 places, I think we should consider DRY'ing up the code and putting the logic into 1 place. For example, internal/config/dotenv.go (existing) or rename the file to internal/slackdotenv if we want something that doesn't name collide with the dotenv dependency.
Additionally, a single function would make future improvements easier. For example, we may want to introduce a --dotenv-overwrite flag and { "dotenv-overwrite": true config value that allow the .env to overwrite session variables. This seems to be a common use-case because most dotenv libraries support it, including our package with godotenv.Overload().
Note: This would also allow us to unit test the scenarios in internal/config/dotenv_test.go instead of in the localserver_test.go and hooks_test.go.
There was a problem hiding this comment.
@mwbrooks Super appreciate that you noticed this! I agree it's meaningful logic that felt fragile in my initial implementation 👁️🗨️
We refactored our environment variable management into a few places:
internal/slackdotenv: This reads the ".env" file.internal/hooks/shell.go#HookExecOpts:ShellEnv(): This orders variables for hook commands with precedence.
This change takes place in two commits f63d0ff and 9160bcc with some adjacent tests.
There was a problem hiding this comment.
@mwbrooks Not meaning to send before addressing the Overload options 👾
These refactors make this and similar changes to loading from environment variable files much more safe I feel. I look forward to these changes!
zimeg
left a comment
There was a problem hiding this comment.
💡 Some thoughts I had during recent changes were left in these comments!
| // GetDotEnvFileVariables collects only the variables in the .env file | ||
| func (c *Config) GetDotEnvFileVariables() (map[string]string, error) { | ||
| variables := map[string]string{} | ||
| file, err := afero.ReadFile(c.fs, ".env") | ||
| if err != nil && !c.os.IsNotExist(err) { | ||
| return variables, err | ||
| } | ||
| return godotenv.UnmarshalBytes(file) | ||
| } |
There was a problem hiding this comment.
📦 note: This is moved to our own slackdotenv package to avoid odd dependencies of config and to avoid duplicate logic!
| // Read parses a .env file from the working directory using the provided | ||
| // filesystem. It returns nil if the filesystem is nil or the file does not | ||
| // exist. | ||
| func Read(fs afero.Fs) (map[string]string, error) { | ||
| if fs == nil { | ||
| return nil, nil | ||
| } | ||
| file, err := afero.ReadFile(fs, ".env") | ||
| if err != nil { | ||
| if os.IsNotExist(err) { | ||
| return nil, nil | ||
| } | ||
| return nil, err | ||
| } | ||
| return godotenv.UnmarshalBytes(file) | ||
| } |
There was a problem hiding this comment.
🪬 note: I'm not confident on the Read vs Load term but for now this is perhaps clear. We might want to revisit this in changes to the actual ".env" file ongoing?
zimeg
left a comment
There was a problem hiding this comment.
@mwbrooks Super appreciate the review and thoughts to improved code health! 🏥 💌
A handful of changes landed since and I'm rerequesting review with hopes that comments have kind resolution, but please let me know if I can follow up with more changes or of other thoughts.
I did make a change to error handling with warnings but nothing that should change current code adjacent. The change is within the other files being updated 📠
|
|
||
| ### Error Handling | ||
|
|
||
| - Wrap errors returned across package boundaries with `slackerror.Wrap(err, slackerror.ErrCode)` so they carry a structured error code |
There was a problem hiding this comment.
📚 note: This is motivated in ongoing changes with encouragement and optimism for checks from the wonderful wrapcheck lint:
Checks that errors returned from external packages are wrapped.
🔗 https://golangci-lint.run/docs/linters/configuration/#wrapcheck
Changelog
Summary
This PR loads environment variables from the
.envfile for hook commands. These variables are loaded before each hook command executes to ensure correctness 🌲 ✨We also fix orderings of environment variable precedence within hooks to be:
SLACK_CLI_XOXPtoken provided with theruncommand..env. file.EXPORTor set variables otherwise. This is most important!Preview
demo.mov
Reviewers
A few test cases might be interesting to saved variables:
Notes
We might follow up with similar
.envpatterns and I'm hoping these changes guide decent direction to placement of loading environment variables for hooks!internal/config/dotenvpackage is for internal configurations instead of developer application variables.clients.Config.ManifestEnvattribute is used formanifestandtriggercommands but notrunor thedeploycommands for Bolt apps. I'm unsure that we should continue to support this as we bring enhancement to a project ".env" file itself? Regardless, it wasn't the right rabbit hole...Requirements