Skip to content

feat: support loading a dotenv '.env' file for your app#436

Open
zimeg wants to merge 12 commits intomainfrom
zimeg-feat-load-hook-dotenv
Open

feat: support loading a dotenv '.env' file for your app#436
zimeg wants to merge 12 commits intomainfrom
zimeg-feat-load-hook-dotenv

Conversation

@zimeg
Copy link
Member

@zimeg zimeg commented Mar 24, 2026

Changelog

Environment variables saved to a .env file are now loaded into the process before hook scripts run. This brings meaningful improvement to the slack run command foremost as additional dependencies are no longer required for loading environment variables into the runtime context.

Other commands, such as the slack deploy command and the slack manifest command, also benefit from this since the underlying hooks can be more expressive in customizations and configured to custom environments.

Summary

This PR loads environment variables from the .env file 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:

  1. Provided variables to hooks - Ex: The SLACK_CLI_XOXP token provided with the run command.
  2. Saved ".env" file - Ex: The developer's project keeps a .env. file.
  3. Existing shell environment - Ex: Prior EXPORT or set variables otherwise. This is most important!

Preview

demo.mov

Reviewers

A few test cases might be interesting to saved variables:

$ slack create asdf -t slack-samples/bolt-js-assistant-template
$ cd asdf
$ vim .env
OPENAI_API_KEY=sk-proj-example-123  # Replace please!
$ npm uninstall dotenv
$ diff app.js
- import 'dotenv/config';           # Delete this line
$ slack run                         # File is read
$ OPENAI_API_KEY=oopsies slack run  # Shell overrides

Notes

We might follow up with similar .env patterns and I'm hoping these changes guide decent direction to placement of loading environment variables for hooks!

  • Earlier exploration attempted to read environment variables during the CLI setup but that required configuration changes that seemed incorrect to make. I understand now the internal/config/dotenv package is for internal configurations instead of developer application variables.
  • The existing clients.Config.ManifestEnv attribute is used for manifest and trigger commands but not run or the deploy commands 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

@zimeg zimeg added this to the Next Release milestone Mar 24, 2026
@zimeg zimeg self-assigned this Mar 24, 2026
@zimeg zimeg added enhancement M-T: A feature request for new functionality semver:minor Use on pull requests to describe the release version increment labels Mar 24, 2026
@codecov
Copy link

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 86.66667% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.44%. Comparing base (0c37128) to head (6cadb2d).

Files with missing lines Patch % Lines
internal/pkg/platform/run.go 0.00% 3 Missing ⚠️
internal/slackdotenv/slackdotenv.go 85.71% 2 Missing ⚠️
internal/pkg/platform/localserver.go 0.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member Author

@zimeg zimeg left a comment

Choose a reason for hiding this comment

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

👾 A few thoughts for wonderful reviewers-

Copy link
Member Author

Choose a reason for hiding this comment

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

🗣️ 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,
Copy link
Member Author

Choose a reason for hiding this comment

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

🔭 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.

@zimeg zimeg marked this pull request as ready for review March 24, 2026 22:43
@zimeg zimeg requested a review from a team as a code owner March 24, 2026 22:43
Copy link
Member

@mwbrooks mwbrooks left a comment

Choose a reason for hiding this comment

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

🙌🏻 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"

Comment on lines +91 to +100
// 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
Copy link
Member

Choose a reason for hiding this comment

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

praise: ❤️ 🖊️ Love the detailed comment for future readers!

Comment on lines +326 to +335
// 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
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

@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.

Copy link
Member Author

Choose a reason for hiding this comment

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

@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 zimeg changed the title feat: load environment variables from '.env' file for hook commands feat: support loading a dotenv '.env' file for your app Mar 26, 2026
Copy link
Member Author

@zimeg zimeg left a comment

Choose a reason for hiding this comment

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

💡 Some thoughts I had during recent changes were left in these comments!

Comment on lines -25 to -33
// 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)
}
Copy link
Member Author

Choose a reason for hiding this comment

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

📦 note: This is moved to our own slackdotenv package to avoid odd dependencies of config and to avoid duplicate logic!

Comment on lines +29 to +44
// 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)
}
Copy link
Member Author

Choose a reason for hiding this comment

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

🪬 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?

Copy link
Member Author

@zimeg zimeg left a comment

Choose a reason for hiding this comment

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

@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
Copy link
Member Author

Choose a reason for hiding this comment

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

📚 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

@zimeg zimeg requested a review from mwbrooks March 27, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement M-T: A feature request for new functionality semver:minor Use on pull requests to describe the release version increment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants