Skip to content

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

Merged
zimeg merged 14 commits intomainfrom
zimeg-feat-load-hook-dotenv
Mar 30, 2026
Merged

feat: support loading a dotenv '.env' file for your app#436
zimeg merged 14 commits intomainfrom
zimeg-feat-load-hook-dotenv

Conversation

@zimeg
Copy link
Copy Markdown
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
Copy Markdown

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.39%. Comparing base (922813e) to head (64b67f6).
⚠️ Report is 3 commits behind head on main.

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.36%   70.39%   +0.03%     
==========================================
  Files         220      221       +1     
  Lines       18506    18533      +27     
==========================================
+ Hits        13021    13046      +25     
- Misses       4308     4311       +3     
+ Partials     1177     1176       -1     

☔ 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
Copy Markdown
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
Copy Markdown
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!

Comment thread cmd/platform/deploy.go
// so we instantiate the default here.
shell := hooks.HookExecutorDefaultProtocol{
IO: clients.IO,
Fs: clients.Fs,
Copy link
Copy Markdown
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
Copy Markdown
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 thread internal/hooks/hook_executor_default_test.go Outdated
Comment thread internal/hooks/hook_executor_v2_test.go Outdated
Comment thread internal/hooks/hooks.go Outdated
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
Copy Markdown
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 thread internal/pkg/platform/localserver.go Outdated
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Woo woo, thanks @zimeg. The internal/slackdotenv organization makes a lot more sense to me. Glad to see internal/config is gradually becoming an interface for [system|project]/.slack/config.json

Comment thread internal/hooks/hooks.go Outdated
Comment thread internal/hooks/hooks.go Outdated
Comment thread internal/hooks/hooks_test.go Outdated
Comment thread internal/hooks/hooks.go Outdated
@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
Copy Markdown
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 thread internal/config/dotenv.go
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Member

Choose a reason for hiding this comment

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

thought: I like the Read right now. While it's personal, to me Load implies that the content is "loaded into some variable or context" while Read implies it'll read the file and return the contents.

This method reads the file and returns the contents. So, Read make sense to me.

Copy link
Copy Markdown
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 📠

Comment thread .claude/CLAUDE.md

### Error Handling

- Wrap errors returned across package boundaries with `slackerror.Wrap(err, slackerror.ErrCode)` so they carry a structured error code
Copy link
Copy Markdown
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
Copy link
Copy Markdown
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.

✅ Thanks so much for this powerful PR and enhancement @zimeg! 🚀

🧪 Local testing looks good!

📝 Thanks again for addressing the feedback. I like how internal/slackdotenv has shaped up and our parsing order is feeling more consistent and predictable by developers.

@zimeg
Copy link
Copy Markdown
Member Author

zimeg commented Mar 30, 2026

@mwbrooks Thank you too for discussions leading up to this! 🌲 ✨

I'm feeling good too so we should merge this. The preference to reading variables from slackdotenv feels much better to me too.

@zimeg zimeg merged commit 1ddaf8e into main Mar 30, 2026
7 checks passed
@zimeg zimeg deleted the zimeg-feat-load-hook-dotenv branch March 30, 2026 18:53
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