Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/commands/auth_provider_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func AuthProviderAddGitHub(ctx *Context, opts struct {
Name string `position:"0" usage:"Name for this identity provider" required:"true"`
ClientID string `long:"client-id" description:"GitHub OAuth app client ID" required:"true"`
ClientSecret string `long:"client-secret" description:"GitHub OAuth app client secret" required:"true"`
Orgs []string `long:"org" description:"GitHub org restriction (repeatable). Use \"name\" for any-member, or \"name:team1,team2\" to require team membership and populate X-User-Groups."`
Orgs []string `long:"org" split:"false" description:"GitHub org restriction (repeatable). Use \"name\" for any-member, or \"name:team1,team2\" to require team membership and populate X-User-Groups."`
Update bool `long:"update" description:"Overwrite an existing provider with the same name (rotates client secret)"`
ConfigCentric
}) error {
Expand Down
6 changes: 5 additions & 1 deletion cli/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,10 @@ miren deploy --analyze
Name: "Set an environment variable",
Body: "miren env set -e DATABASE_URL=postgres://localhost/mydb",
}),
WithExample(mflags.Example{
Name: "Set several variables without flags",
Body: "miren env set LOG_LEVEL=debug ALLOWED_HOSTS=a.example.com,b.example.com",
}),
WithExample(mflags.Example{
Name: "Set a sensitive variable (prompted with masking)",
Body: "miren env set -s SECRET_KEY",
Expand Down Expand Up @@ -1417,7 +1421,7 @@ Prefer ` + "`" + `miren secret disable` + "`" + ` when revoking a leaked credent

const envSetDescription = `Setting an environment variable creates a new app version and rolls it out automatically — you do not need to run ` + "`" + `miren deploy` + "`" + ` or ` + "`" + `miren app restart` + "`" + ` afterward. The new version reuses your existing container image (no rebuild); Miren boots new sandboxes with the updated environment and drains the old ones. The command waits for the new version to become healthy before returning.

Use ` + "`" + `-e` + "`" + ` for plain values and ` + "`" + `-s` + "`" + ` for sensitive values (masked in output and logs). Note that ` + "`" + `-s` + "`" + ` affects display only — the value itself is stored in the clear. For a credential that should be encrypted at rest, store it with ` + "`" + `miren secret set` + "`" + ` instead. Pass ` + "`" + `--service` + "`" + ` to scope the change to a single service instead of all services.
Use ` + "`" + `-e` + "`" + ` for plain values and ` + "`" + `-s` + "`" + ` for sensitive values (masked in output and logs). Bare ` + "`" + `KEY=VALUE` + "`" + ` arguments without a flag are treated as plain values, the same as ` + "`" + `-e` + "`" + `. Values may contain commas. Note that ` + "`" + `-s` + "`" + ` affects display only — the value itself is stored in the clear. For a credential that should be encrypted at rest, store it with ` + "`" + `miren secret set` + "`" + ` instead. Pass ` + "`" + `--service` + "`" + ` to scope the change to a single service instead of all services.

:::note[No restart needed]
Environment variable changes take effect on their own. Running ` + "`" + `miren app restart` + "`" + ` afterward only triggers a redundant second rollout.
Expand Down
4 changes: 2 additions & 2 deletions cli/commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ func Deploy(ctx *Context, opts struct {
Explain bool `short:"x" long:"explain" description:"Explain the build process"`
ExplainFormat string `long:"explain-format" description:"Explain format" choice:"auto" choice:"plain" choice:"tty" choice:"rawjson" default:"auto"` //nolint
Force bool `short:"f" long:"force" description:"Skip confirmation prompt"`
Env []string `short:"e" long:"env" description:"Set environment variable (KEY=VALUE, KEY=@file, or KEY to prompt)"`
Sensitive []string `short:"s" long:"sensitive" description:"Set sensitive environment variable (masked in output)"`
Env []string `short:"e" long:"env" split:"false" description:"Set environment variable (KEY=VALUE, KEY=@file, or KEY to prompt)"`
Sensitive []string `short:"s" long:"sensitive" split:"false" description:"Set sensitive environment variable (masked in output)"`
Ephemeral string `long:"ephemeral" description:"Deploy as ephemeral preview with this label (e.g. feat-login)"`
TTL string `long:"ttl" description:"TTL for ephemeral version (e.g. 48h)" default:"24h"`
SummaryJSON string `long:"summary-json" description:"Write a JSON summary of the deploy result (deploy id, version, and route URLs) to this path"`
Expand Down
16 changes: 11 additions & 5 deletions cli/commands/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"regexp"
"slices"
"sort"
"strings"

Expand Down Expand Up @@ -196,22 +197,27 @@ func parseEnvVarSpec(spec string, sensitive bool) (EnvVarSpec, error) {

func EnvSet(ctx *Context, opts struct {
AppCentric
Args []string `rest:"true" usage:"KEY=VALUE pairs to set, the same as passing each with -e"`
Service string `short:"S" long:"service" description:"Set env var for specific service only (if not specified, sets for all services)"`
Env []string `short:"e" long:"env" description:"Set environment variables (use KEY to prompt, KEY=VALUE to set directly, KEY=@file to read from file)"`
Sensitive []string `short:"s" long:"sensitive" description:"Set sensitive environment variables (use KEY to prompt with masking, KEY=VALUE to set directly, KEY=@file to read from file)"`
Env []string `short:"e" long:"env" split:"false" description:"Set environment variables (use KEY to prompt, KEY=VALUE to set directly, KEY=@file to read from file)"`
Sensitive []string `short:"s" long:"sensitive" split:"false" description:"Set sensitive environment variables (use KEY to prompt with masking, KEY=VALUE to set directly, KEY=@file to read from file)"`
Backend string `short:"b" long:"backend" description:"Source the value from a secret backend instead of setting it literally (default: cluster, with --ref)"`
Ref string `long:"ref" description:"Backend-relative reference to the secret, e.g. payments/stripe-key"`
}) error {
// Bare KEY=VALUE arguments count as plain -e entries.
env := slices.Concat(opts.Env, opts.Args)
sensitive := opts.Sensitive

if opts.Ref != "" || opts.Backend != "" {
return envSetReference(ctx, opts.App, opts.Service, opts.Env, opts.Sensitive, opts.Backend, opts.Ref)
return envSetReference(ctx, opts.App, opts.Service, env, sensitive, opts.Backend, opts.Ref)
}

if len(opts.Env) == 0 && len(opts.Sensitive) == 0 {
if len(env) == 0 && len(sensitive) == 0 {
return fmt.Errorf("no environment variables specified")
}

// Parse all env var specs
specs, err := ParseEnvVarSpecs(opts.Env, opts.Sensitive)
specs, err := ParseEnvVarSpecs(env, sensitive)
if err != nil {
return err
}
Expand Down
63 changes: 63 additions & 0 deletions cli/commands/env_set_flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package commands

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

// stringSliceField reads a []string option off a parsed command by field name.
// The option structs are anonymous, so the fields cannot be type-asserted.
func stringSliceField(t *testing.T, cmd *Cmd, name string) []string {
t.Helper()
f := cmd.opts.Elem().FieldByName(name)
require.True(t, f.IsValid(), "field %s", name)
return f.Interface().([]string)
}

func TestEnvSetKeepsCommasAndAcceptsBareArgs(t *testing.T) {
cmd := Infer("env set", "test", EnvSet)
require.NoError(t, cmd.fs.Parse([]string{
"-e", "A=1,2", "B=3,4", "-s", "SECRET=x,y", "--env=C=5", "D=6", "-eE=7,8",
}))

require.Equal(t, []string{"A=1,2", "C=5", "E=7,8"}, stringSliceField(t, cmd, "Env"))
require.Equal(t, []string{"SECRET=x,y"}, stringSliceField(t, cmd, "Sensitive"))
require.Equal(t, []string{"B=3,4", "D=6"}, stringSliceField(t, cmd, "Args"))
}

func TestDeployKeepsCommasInEnvFlags(t *testing.T) {
cmd := Infer("deploy", "test", Deploy)
require.NoError(t, cmd.fs.Parse([]string{"-e", "A=1,2", "-s", "SECRET=x,y", "-e", "B=3"}))

require.Equal(t, []string{"A=1,2", "B=3"}, stringSliceField(t, cmd, "Env"))
require.Equal(t, []string{"SECRET=x,y"}, stringSliceField(t, cmd, "Sensitive"))
}

func TestAuthProviderGitHubOrgTeamsSurviveParsing(t *testing.T) {
cmd := Infer("auth provider add github", "test", AuthProviderAddGitHub)
require.NoError(t, cmd.fs.Parse([]string{
"gh", "--client-id", "id", "--client-secret", "secret",
"--org", "mirendev:platform,eng", "--org", "other",
}))

orgs := stringSliceField(t, cmd, "Orgs")
require.Equal(t, []string{"mirendev:platform,eng", "other"}, orgs)

raw, err := buildGitHubConfigJSON(orgs)
require.NoError(t, err)

var cfg struct {
Orgs []struct {
Name string `json:"name"`
Teams []string `json:"teams"`
} `json:"orgs"`
}
require.NoError(t, json.Unmarshal([]byte(raw), &cfg))
require.Len(t, cfg.Orgs, 2)
require.Equal(t, "mirendev", cfg.Orgs[0].Name)
require.Equal(t, []string{"platform", "eng"}, cfg.Orgs[0].Teams)
require.Equal(t, "other", cfg.Orgs[1].Name)
require.Empty(t, cfg.Orgs[1].Teams)
}
10 changes: 8 additions & 2 deletions docs/docs/command/env-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Set environment variables for an application

Setting an environment variable creates a new app version and rolls it out automatically — you do not need to run `miren deploy` or `miren app restart` afterward. The new version reuses your existing container image (no rebuild); Miren boots new sandboxes with the updated environment and drains the old ones. The command waits for the new version to become healthy before returning.

Use `-e` for plain values and `-s` for sensitive values (masked in output and logs). Note that `-s` affects display only — the value itself is stored in the clear. For a credential that should be encrypted at rest, store it with `miren secret set` instead. Pass `--service` to scope the change to a single service instead of all services.
Use `-e` for plain values and `-s` for sensitive values (masked in output and logs). Bare `KEY=VALUE` arguments without a flag are treated as plain values, the same as `-e`. Values may contain commas. Note that `-s` affects display only — the value itself is stored in the clear. For a credential that should be encrypted at rest, store it with `miren secret set` instead. Pass `--service` to scope the change to a single service instead of all services.

:::note[No restart needed]
Environment variable changes take effect on their own. Running `miren app restart` afterward only triggers a redundant second rollout.
Expand All @@ -19,7 +19,7 @@ Environment variable changes take effect on their own. Running `miren app restar
## Usage

```bash
miren env set [flags]
miren env set [args...] [flags]
```

## Flags
Expand Down Expand Up @@ -54,6 +54,12 @@ miren env set [flags]
miren env set -e DATABASE_URL=postgres://localhost/mydb
```

**Set several variables without flags:**

```bash
miren env set LOG_LEVEL=debug ALLOWED_HOSTS=a.example.com,b.example.com
```

**Set a sensitive variable (prompted with masking):**

```bash
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ require (
gopkg.in/yaml.v3 v3.0.1
k8s.io/klog/v2 v2.130.1
miren.dev/lbd v0.0.0-20260224020427-8914d8db2233
miren.dev/mflags v0.0.0-20260709231109-a397dcbc98df
miren.dev/mflags v0.0.0-20260910225849-7704913d5c9c
modernc.org/sqlite v1.45.0
sigs.k8s.io/knftables v0.0.21
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1955,8 +1955,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
miren.dev/lbd v0.0.0-20260224020427-8914d8db2233 h1:9DxH7Dhnmu7hn1OA2JC5fHpLuVgAqBySws9GLNssLl4=
miren.dev/lbd v0.0.0-20260224020427-8914d8db2233/go.mod h1:+x9fy2p45csBnGUJdqxCUmzlUTCipoVDbv6zIapTgDA=
miren.dev/mflags v0.0.0-20260709231109-a397dcbc98df h1:4fo71OveODliBgU9sv2M/uepiRNAwQPJ7YTPuNffQNc=
miren.dev/mflags v0.0.0-20260709231109-a397dcbc98df/go.mod h1:G1eQ/upWVdO6BGT6dlh5Yqjt+9ncH5RUAKX6UKi1F9Q=
miren.dev/mflags v0.0.0-20260910225849-7704913d5c9c h1:x4XxiRPIEh64GV1DzDaxGoiMqj+9qYCOnFBW2h+L3Vs=
miren.dev/mflags v0.0.0-20260910225849-7704913d5c9c/go.mod h1:G1eQ/upWVdO6BGT6dlh5Yqjt+9ncH5RUAKX6UKi1F9Q=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
Expand Down
Loading