feat: add --env-file flag to develop.sh (#25621)

Adds `--env-file` to `scripts/develop.sh` to allow reading environment 
from a given file. This makes it easier to configure things like external 
auth providers, access URLs, and other dev-time settings without 
exporting a wall of environment variables in every shell session.

> Generated with [Coder Agents](https://coder.com/agents)
This commit is contained in:
Cian Johnston
2026-05-25 11:54:57 +01:00
committed by GitHub
parent ffc51ec8b3
commit a4afb9dfc6
4 changed files with 203 additions and 3 deletions
+69 -3
View File
@@ -27,6 +27,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/joho/godotenv"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
@@ -69,6 +70,24 @@ const (
)
func main() {
// Pre-parse --env-file before serpent runs so that variables from
// the file are visible to serpent's Env-tag resolution for other
// options. The flag is also registered in the serpent OptionSet
// below for --help discoverability.
envFile, err := parseEnvFileFlag()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "develop: %v\n", err)
os.Exit(1)
}
if envFile != "" {
n, err := loadEnvFile(envFile)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "develop: error loading env file %s: %v\n", envFile, err)
os.Exit(1)
}
_, _ = fmt.Fprintf(os.Stderr, "develop: loaded %d variable(s) from %s\n", n, envFile)
}
var cfg devConfig
cmd := &serpent.Command{
@@ -182,6 +201,12 @@ func main() {
Description: "Accept changed migration files and update tracking. Use when you've manually fixed the DB to match the new migrations.",
Value: serpent.BoolOf(&cfg.dbContinue),
},
{
Flag: "env-file",
Env: "CODER_DEV_ENV_FILE",
Description: "Path to a .env file to load before starting. Variables in the file do not override existing environment variables. Note: unquoted and double-quoted values undergo $VAR expansion against other entries in the same file (not the process environment); use single quotes for literal dollar signs.",
Value: serpent.StringOf(&cfg.envFile),
},
},
Handler: func(inv *serpent.Invocation) error {
cfg.serverExtraArgs = inv.Args
@@ -198,7 +223,7 @@ func main() {
},
}
err := cmd.Invoke(os.Args[1:]...).WithOS().Run()
err = cmd.Invoke(os.Args[1:]...).WithOS().Run()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
@@ -223,6 +248,9 @@ type devConfig struct {
dbRollback bool
dbReset bool
dbContinue bool
// envFile is populated by serpent for --help output; actual loading
// uses parseEnvFileFlag() before serpent runs.
envFile string
projectRoot string
binaryPath string
configDir string
@@ -435,8 +463,46 @@ func (c *devConfig) cmd(ctx context.Context, bin string, args ...string) *exec.C
return cmd
}
// filterEnv returns env with any variables whose key matches
// exclude removed.
// parseEnvFileFlag extracts the --env-file value from os.Args and
// CODER_DEV_ENV_FILE before serpent runs, so that loaded variables
// are visible to serpent's Env-tag resolution for other options.
func parseEnvFileFlag() (string, error) {
for i, arg := range os.Args[1:] {
if arg == "--env-file" {
if i+2 >= len(os.Args) {
return "", xerrors.New("--env-file requires a value")
}
return os.Args[i+2], nil
}
if v, ok := strings.CutPrefix(arg, "--env-file="); ok {
return v, nil
}
}
return os.Getenv("CODER_DEV_ENV_FILE"), nil
}
// loadEnvFile reads the file at path using godotenv and sets any variables
// not already present in the process environment. It returns the number of
// variables set.
func loadEnvFile(path string) (int, error) {
vars, err := godotenv.Read(path)
if err != nil {
return 0, err
}
var n int
for key, val := range vars {
if _, exists := os.LookupEnv(key); exists {
continue
}
if err := os.Setenv(key, val); err != nil {
return n, err
}
n++
}
return n, nil
}
// filterEnv returns env with any variables whose key matches exclude removed.
func filterEnv(env []string, exclude ...string) []string {
out := make([]string, 0, len(env))
for _, e := range env {