Skip to content

feat: env add command supports the ".env" file for Bolt Framework apps#451

Open
zimeg wants to merge 9 commits intomainfrom
zimeg-feat-dotenv-add
Open

feat: env add command supports the ".env" file for Bolt Framework apps#451
zimeg wants to merge 9 commits intomainfrom
zimeg-feat-dotenv-add

Conversation

@zimeg
Copy link
Copy Markdown
Member

@zimeg zimeg commented Mar 30, 2026

Changelog

The slack env add command can now add and update environment variables in the .env file for apps that aren't built or run on Slack infrastructure. This makes setting up and iterating on a Bolt app from the command line faster.

Summary

This PR uses the env add command to add variables to the .env file for non-hosted apps. Follows #437 🌲

Preview

demo

Reviewers

Attempt to write or update variables from this example and branch:

$ vim .env
# Database configuration
DB_HOST=localhost
DB_PORT=5432 # Pretend default
export DB_KEY="---START---
password
---END---"

# App secrets
SECRET_KEY=original_secret
export API_TOKEN="xoxb-new-token" # https://api.slack.com/apps/example


# Feature flags
DEBUG=true
$ slack env list
$ slack env add API_TOKEN
$ slack env add LOG_FORMAT
$ slack env list

Notes

  • We require app selection to determine if an app is hosted or not. Perhaps we can workaround that for apps with the remote function runtime but this app selection might remain useful in later enhancements:
.env
.env.A0123456789

Requirements

@zimeg zimeg added this to the Next Release milestone Mar 30, 2026
@zimeg zimeg self-assigned this Mar 30, 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 area:bolt-js Related to github.com/slackapi/bolt-js area:bolt-python Related to github.com/slackapi/bolt-python labels Mar 30, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

❌ Patch coverage is 76.23762% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.43%. Comparing base (f164165) to head (774e2b1).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
internal/slackdotenv/slackdotenv.go 64.91% 13 Missing and 7 partials ⚠️
cmd/env/add.go 89.47% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #451   +/-   ##
=======================================
  Coverage   70.42%   70.43%           
=======================================
  Files         221      221           
  Lines       18539    18606   +67     
=======================================
+ Hits        13057    13105   +48     
- Misses       4308     4320   +12     
- Partials     1174     1181    +7     

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

🗣️ Open ideas I still have that I'd like to share with kind reviewers-

clients.IO.PrintTrace(ctx, slacktrace.EnvAddSuccess)
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "evergreen_tree",
Text: "App Environment",
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: We might follow up to change the section text for these env commands for outputs that match:

  • 🌲 Environment Add
  • 🌲 Environment List
  • 🌲 Environment Remove

👾 ramble: I avoided this change for now because this PR is so large!

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: Good call! I like that this PR is keeping ROSI untouched, but we should make a note to follow-up on it adding parity between the two experiences.

Emoji: "evergreen_tree",
Text: "App Environment",
Secondary: []string{
fmt.Sprintf("Successfully added \"%s\" as a project environment variable", variableName),
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.

📚 thought: This might behave in unexpected fashion for now since we prompt to select an app but save variables to the project, where all apps have access. I understand we have plans for more nuanced environment variable files and also that these .env file behaviors are understood when developing so I don't know if this requires more of a callout!

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.

Interesting, this is something that I overlooked when we first proposed the command changes.

suggestion(non-blocking): I'm happy with the current approach as phase 1, if you'd like to land the PR, avoid it becoming too large, and iterate.

However, I think we need to improve this experience. One of the main value props of the env add command is to set your app's third-party API Tokens. However, these tokens need to be set before you can run the app.

But... if you can't env add until you've created an app, then you're in a bit of a pickle. 🐣 You'd need to use the (currently) awkward install command before env add.

Would checking for the Deno runtime be a way to determine if we need to prompt for an app? If it's Deno then we prompt (local = .env, deploy = API), otherwise we skip the prompt and just use .env.

Comment on lines +78 to +81
// If the file does not exist, create it with the new entry.
if existing == nil {
return writeFile(fs, []byte(newEntry+"\n"))
}
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.

🔬 question: From the command logic, would it be meaningful to output a note that the .env file shouldn't be checked into version control? Should we check if it's ignored already?

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.

Good question. It feels like the CLI is overstepping to add the .env to the .gitignore but perhaps that's just my own feeling. Others may consider that a delightful feature.

I think outputting a message is a very good start and allows us to improve upon it later (e.g. editing the .gitignore).

This approach has worked well for us in the past:

  1. Output the manual steps
  2. Let it bake and gauge how it feels
  3. Automate the manual steps

@zimeg zimeg marked this pull request as ready for review March 30, 2026 23:50
@zimeg zimeg requested a review from a team as a code owner March 30, 2026 23:50
@mwbrooks mwbrooks changed the title feat: add variables to ".env" for non-hosted apps feat: "env" command supports reading/writing to ".env" for Bolt Framework apps Mar 31, 2026
@mwbrooks mwbrooks changed the title feat: "env" command supports reading/writing to ".env" for Bolt Framework apps feat: env command supports reading/writing to ".env" for Bolt Framework apps Mar 31, 2026
@mwbrooks mwbrooks changed the title feat: env command supports reading/writing to ".env" for Bolt Framework apps feat: env add command supports adding/updating the ".env" for Bolt Framework apps Mar 31, 2026
@mwbrooks mwbrooks changed the title feat: env add command supports adding/updating the ".env" for Bolt Framework apps feat: env add command supports the ".env" file for Bolt Framework apps Mar 31, 2026
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 updating the env add command to support .env files for Bolt apps! Woo! 🎉 I'm throwing an approval on this, but I had 2 important asks for improvement that can happen in this PR or as a follow-up.

🧪 The basics work, but I found a few edge-cases that we will want to fix. See my comments below.

💬 I understand this is a complex PR and thankful that you're keeping it scoped small! 🙇🏻 I found a few edge-cases that I don't think we want to bring into production. However, it's your call if you'd like to merge this PR and iterate on the edge-cases or do it inside this PR.

  1. Skip App Prompt for Non-Deno Projects: We have a chicken-and-egg 🐣 scenario where the env add command fails when there are no apps. But, the happy path experience is to set your third-party API Tokens as environment variables before creating your first app with run. We'll want to explore loosening up this command for non-ROSI (or non-Deno, which may be the clue) apps. 🧩
  2. Flexible .env File Format: Right now, the .env update logic is quite strict and results in duplicating existing entries. Since the .env file is a user-edited file, we know there will be quirky formatting. It looks like other dotenv packages have a more flexible parsing, so we will want to try to match it. We may need to break out some string match foo. 🥷

Short: "Add an environment variable to the app",
Long: strings.Join([]string{
"Add an environment variable to an app deployed to Slack managed infrastructure.",
"Add an environment variable to the app.",
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: Perhaps we should say "project" instead of "app"? For ROSI apps, it was actually the app (server-side) but for all other apps including locally run Deno apps, it's actually the "project's" .env file.

Suggested change
"Add an environment variable to the app.",
"Add an environment variable to the project.",

@@ -33,12 +34,15 @@ func NewEnvAddCommand(clients *shared.ClientFactory) *cobra.Command {
Use: "add <name> <value> [flags]",
Short: "Add an environment variable to the app",
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(non-blocking): See the suggestion below. You may want to merge this change if you accept the one below.

Suggested change
Short: "Add an environment variable to the app",
Short: "Add an environment variable to the project",

@@ -33,12 +34,15 @@ func NewEnvAddCommand(clients *shared.ClientFactory) *cobra.Command {
Use: "add <name> <value> [flags]",
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: Seems like a nice time to update the Usage to be more accurate?

Suggested change
Use: "add <name> <value> [flags]",
Use: "add <name> [value] [flags]",

Comment on lines +42 to +44
"the \".env\" file. This includes the \"run\" command.",
"",
"The \"deploy\" command gathers environment variables from the \".env\" file as well",
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(non-blocking): Please ignore if you want, but thought this may be a chance to keep the code simpler.

Suggested change
"the \".env\" file. This includes the \"run\" command.",
"",
"The \"deploy\" command gathers environment variables from the \".env\" file as well",
`the ".env" file. This includes the "run" command.`,
"",
`The "deploy" command gathers environment variables from the ".env" file as well`,

if clients.Config.ForceFlag {
return nil
}
return cmdutil.IsSlackHostedProject(ctx, clients)
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: Bolt is coming for you env! ⚡

)
},
},
"add a variable to the .env file for non-hosted app": {
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 seeing the new test use-case!

Comment on lines +54 to +58
// Set sets a single environment variable in the .env file, preserving
// comments, blank lines, and other formatting. If the key already exists its
// value is replaced in-place. Otherwise the entry is appended. The file is
// created if it does not exist.
func Set(fs afero.Fs, name string, value string) error {
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: 🧑‍🍳 💋 👌🏻 What a great Godoc description!

Comment on lines +78 to +81
// If the file does not exist, create it with the new entry.
if existing == nil {
return writeFile(fs, []byte(newEntry+"\n"))
}
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.

Good question. It feels like the CLI is overstepping to add the .env to the .gitignore but perhaps that's just my own feeling. Others may consider that a delightful feature.

I think outputting a message is a very good start and allows us to improve upon it later (e.g. editing the .gitignore).

This approach has worked well for us in the past:

  1. Output the manual steps
  2. Let it bake and gauge how it feels
  3. Automate the manual steps

return err
}

// If the file does not exist, create it with the new entry.
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.

question(non-blocking): This is out-of-scope of this PR, but our original discussion included a env init that would copy a .env.sample or .env.example to be .env.

If we eventually add this ability, I can see the init being called here instead of creating a blank .env, since the app may include a .env.sample or .env.example.

Comment on lines +108 to +120
// Try each possible form of the old entry, longest (most specific) first.
// The file can store multiline values with actual newlines, so also try
// the double-quoted raw form.
entries := []string{
"export " + name + "=" + oldMarshaledValue,
"export " + name + "=" + `"` + oldValue + `"`,
"export " + name + "=" + oldValue,
"export " + name + "=" + "'" + oldValue + "'",
name + "=" + oldMarshaledValue,
name + "=" + `"` + oldValue + `"`,
name + "=" + oldValue,
name + "=" + "'" + oldValue + "'",
}
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: This feels very brittle to me. 🤔

For example, the following format fails as a test case, but I'd expect it to be a valid entry - spaces around the equals sign, which works with Python's dotenv package:

FOO = "bar"

Here is the test case:

		"some poorly formatted .env file": {
			name:         "FOO",
			value:        "bar",
			expectedFile: "FOO = \"bar\"\n",
		},

In production, if I use the above .env file and execute the env add then it duplicates the entries:

$ lack env add FOO banana

$ cat .env
FOO = "bar"
FOO="banana"

I feel like there's a hesitation to use regex or string matches, but it may be the more flexible approach here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:bolt-js Related to github.com/slackapi/bolt-js area:bolt-python Related to github.com/slackapi/bolt-python 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