Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0758ded
feat(secret): managed secrets with env and file delivery
skevetter Jul 24, 2026
f71b5ca
fix(secret): suppress G301 for the traversable /run/secrets dir
skevetter Jul 24, 2026
b416783
feat(secret): scaffold git-token and build-secret resolution
skevetter Jul 24, 2026
6355e73
feat(secret): expose build secrets to BuildKit builds
skevetter Jul 24, 2026
08954e1
feat(secret): clone private HTTP repos with a managed access token
skevetter Jul 24, 2026
aa25358
feat(env): managed environment variables over the shared value store
skevetter Jul 24, 2026
c3a8b62
fix(up): resolve build-secret field collision and lint
skevetter Jul 24, 2026
f66d989
fix(secret): address code-review findings
skevetter Jul 24, 2026
34ec5b5
fix(desktop): call renamed 'secret' command, not removed 'secrets'
skevetter Jul 24, 2026
b150974
refactor(secret): typed Kind, cmd/env parity, desktop env-var UI
skevetter Jul 24, 2026
a7617b1
refactor: remove dead code (desktop attach/detach wiring, unused NewS…
skevetter Jul 24, 2026
23ed11d
fix(secret): address re-review findings
skevetter Jul 24, 2026
b2b3215
fix(secret): address third-pass review findings
skevetter Jul 24, 2026
9ba327a
fix(secret): address fourth-pass review findings
skevetter Jul 24, 2026
374c7b4
style(secret): trim non-essential comments
skevetter Jul 24, 2026
b250ea2
fix(secret): preserve multiline values + idiom cleanups (5th review)
skevetter Jul 24, 2026
755b116
fix(desktop): deliver stdin to secret set (was hanging on Saving...)
skevetter Jul 24, 2026
70e8186
test(e2e): managed env var injects into workspace via --env
skevetter Jul 24, 2026
fcb5882
test: cover secret set --stdin (CLI) and desktop runRawStdin
skevetter Jul 24, 2026
788bc44
feat(desktop): warn before overwriting an existing secret/env var
skevetter Jul 24, 2026
e16160a
feat(desktop): surface secrets/env vars in command palette and breadc…
skevetter Jul 24, 2026
120906b
feat(desktop): add nav hotkeys for Secrets (6) and Env Vars (7)
skevetter Jul 24, 2026
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
22 changes: 22 additions & 0 deletions cmd/context/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"github.com/devsy-org/devsy/cmd/flags"
"github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/log"
"github.com/devsy-org/devsy/pkg/secrets"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -59,6 +61,8 @@ func (cmd *DeleteCmd) Run(ctx context.Context, context string) error {
return fmt.Errorf("cannot delete 'default' context")
}

deleteContextSecrets(devsyConfig, context)

delete(devsyConfig.Contexts, context)
if devsyConfig.DefaultContext == context {
devsyConfig.DefaultContext = "default"
Expand All @@ -74,3 +78,21 @@ func (cmd *DeleteCmd) Run(ctx context.Context, context string) error {

return nil
}

func deleteContextSecrets(devsyConfig *config.Config, contextName string) {
ctxConfig := devsyConfig.Contexts[contextName]
if ctxConfig == nil || len(ctxConfig.Secrets) == 0 {
return
}

store, err := secrets.NewStoreForConfig(devsyConfig)
if err != nil {
log.Warnf("skip deleting secrets for context %q: %v", contextName, err)
return
}
for _, name := range ctxConfig.Secrets {
if err := store.Delete(contextName, name); err != nil {
log.Warnf("delete secret %q for context %q: %v", name, contextName, err)
}
}
}
47 changes: 47 additions & 0 deletions cmd/env/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package env

import (
"context"
"fmt"

"github.com/devsy-org/devsy/cmd/flags"
"github.com/devsy-org/devsy/pkg/log"
"github.com/spf13/cobra"
)

type DeleteCmd struct {
*flags.GlobalFlags
}

func NewDeleteCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &DeleteCmd{GlobalFlags: flags}
deleteCmd := &cobra.Command{
Use: "delete NAME",
Aliases: []string{"rm"},
Short: "Delete an environment variable from the active context",
Args: cobra.ExactArgs(1),
RunE: func(cobraCmd *cobra.Command, args []string) error {
return cmd.Run(cobraCmd.Context(), args[0])
},
}
return deleteCmd
}

func (cmd *DeleteCmd) Run(_ context.Context, name string) error {
contextName, store, err := resolveContext(cmd.GlobalFlags)
if err != nil {
return err
}
meta, err := store.Meta(contextName, name)
if err != nil {
return err
}
if meta.Sensitive() {
return fmt.Errorf("%q is a secret; use \"devsy secret delete\"", name)
}
if err := store.Delete(contextName, name); err != nil {
return err
}
log.Infof("env var %q deleted from context %q", name, contextName)
return nil
}
38 changes: 38 additions & 0 deletions cmd/env/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package env

import (
"github.com/devsy-org/devsy/cmd/flags"
"github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/secrets"
"github.com/spf13/cobra"
)

// NewEnvCmd returns the `devsy env` command group: managed, non-sensitive
// environment variables stored per context and injected into workspaces.
func NewEnvCmd(globalFlags *flags.GlobalFlags) *cobra.Command {
envCmd := &cobra.Command{
Use: "env",
Short: "Devsy managed environment variables",
Long: `Manage named environment variables stored per context and injected
into workspaces. Values are stored in plaintext in the Devsy config directory;
use "devsy secret" for sensitive values.`,
}
envCmd.AddCommand(NewSetCmd(globalFlags))
envCmd.AddCommand(NewListCmd(globalFlags))
envCmd.AddCommand(NewGetCmd(globalFlags))
envCmd.AddCommand(NewDeleteCmd(globalFlags))
return envCmd
}

// resolveContext returns the active context name and a Store for it.
func resolveContext(globalFlags *flags.GlobalFlags) (string, secrets.Store, error) {
devsyConfig, err := config.LoadConfig(globalFlags.Context, globalFlags.Provider)
if err != nil {
return "", nil, err
}
store, err := secrets.NewStoreForConfig(devsyConfig)
if err != nil {
return "", nil, err
}
return devsyConfig.DefaultContext, store, nil
}
47 changes: 47 additions & 0 deletions cmd/env/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package env

import (
"context"
"fmt"

"github.com/devsy-org/devsy/cmd/flags"
"github.com/spf13/cobra"
)

type GetCmd struct {
*flags.GlobalFlags
}

func NewGetCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &GetCmd{GlobalFlags: flags}
getCmd := &cobra.Command{
Use: "get NAME",
Short: "Print an environment variable's value",
Args: cobra.ExactArgs(1),
RunE: func(cobraCmd *cobra.Command, args []string) error {
return cmd.Run(cobraCmd.Context(), args[0])
},
}
return getCmd
}

func (cmd *GetCmd) Run(_ context.Context, name string) error {
contextName, store, err := resolveContext(cmd.GlobalFlags)
if err != nil {
return err
}
meta, err := store.Meta(contextName, name)
if err != nil {
return err
}
if meta.Sensitive() {
return fmt.Errorf("%q is a secret; use \"devsy secret get\"", name)
}
value, err := store.Get(contextName, name)
if err != nil {
return err
}
//nolint:forbidigo // get prints the raw value to stdout for scripting.
fmt.Println(value)
return nil
}
82 changes: 82 additions & 0 deletions cmd/env/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package env

import (
"context"
"encoding/json"
"fmt"

"github.com/devsy-org/devsy/cmd/flags"
"github.com/devsy-org/devsy/pkg/output"
"github.com/devsy-org/devsy/pkg/table"
"github.com/spf13/cobra"
)

type ListCmd struct {
*flags.GlobalFlags
}

func NewListCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &ListCmd{GlobalFlags: flags}
listCmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List environment variables in the active context",
Args: cobra.NoArgs,
RunE: func(cobraCmd *cobra.Command, args []string) error {
return cmd.Run(cobraCmd.Context())
},
}
return listCmd
}

type envEntry struct {
Name string `json:"name"`
Value string `json:"value"`
}

func (cmd *ListCmd) Run(_ context.Context) error {
contextName, store, err := resolveContext(cmd.GlobalFlags)
if err != nil {
return err
}
metas, err := store.List(contextName)
if err != nil {
return err
}

entries := make([]envEntry, 0, len(metas))
for _, m := range metas {
if m.Sensitive() {
continue
}
entries = append(entries, envEntry{Name: m.Name, Value: m.Value})
}

mode, err := output.ResolveMode(cmd.ResultFormat)
if err != nil {
return err
}
if mode == output.ModeJSON {
return renderJSON(entries)
}
renderPlain(entries)
return nil
}

func renderJSON(entries []envEntry) error {
out, err := json.MarshalIndent(entries, "", " ")
if err != nil {
return err
}
//nolint:forbidigo // list --result-format json prints structured data to stdout.
fmt.Print(string(out))
return nil
}

func renderPlain(entries []envEntry) {
rows := make([][]string, 0, len(entries))
for _, e := range entries {
rows = append(rows, []string{e.Name, e.Value})
}
table.Print([]string{"Name", "Value"}, rows)
}
57 changes: 57 additions & 0 deletions cmd/env/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package env

import (
"context"
"strings"

"github.com/devsy-org/devsy/cmd/flags"
cliflags "github.com/devsy-org/devsy/pkg/flags"
"github.com/devsy-org/devsy/pkg/flags/names"
"github.com/devsy-org/devsy/pkg/log"
"github.com/devsy-org/devsy/pkg/secrets"
"github.com/spf13/cobra"
)

type SetCmd struct {
*flags.GlobalFlags

Value string
}

func NewSetCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &SetCmd{GlobalFlags: flags}
setCmd := &cobra.Command{
Use: "set NAME=VALUE | set NAME --value VALUE",
Short: "Create or update an environment variable",
Args: cobra.ExactArgs(1),
RunE: func(cobraCmd *cobra.Command, args []string) error {
return cmd.Run(cobraCmd.Context(), args[0])
},
}
cliflags.Add(
setCmd,
cliflags.String(&cmd.Value, names.Value, "", "The value (alternative to NAME=VALUE)"),
)
return setCmd
}

func (cmd *SetCmd) Run(_ context.Context, arg string) error {
name, value := arg, cmd.Value
if k, v, ok := strings.Cut(arg, "="); ok {
name, value = k, v
}
if err := secrets.ValidateName(name); err != nil {
return err
}

contextName, store, err := resolveContext(cmd.GlobalFlags)
if err != nil {
return err
}
if err := store.Set(contextName, name, value, secrets.KindEnv); err != nil {
return err
}

log.Infof("env var %q set in context %q", name, contextName)
return nil
}
44 changes: 36 additions & 8 deletions cmd/internal/agentcontainer/deferred_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ package agentcontainer

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"

"github.com/devsy-org/devsy/cmd/flags"
"github.com/devsy-org/devsy/pkg/compress"
config2 "github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/devcontainer/setup"
cliflags "github.com/devsy-org/devsy/pkg/flags"
Expand All @@ -17,14 +21,43 @@ import (
"github.com/spf13/cobra"
)

// encodeSecretsEnv serializes KEY=VALUE secrets for the DEVSY_SECRETS_ENV var
// used to pass them to re-exec'd hook processes. Each entry is base64-encoded so
// values may contain newlines or any bytes (e.g. PEM keys) without corruption.
func encodeSecretsEnv(secretsEnv []string) string {
encoded := make([]string, len(secretsEnv))
for i, entry := range secretsEnv {
encoded[i] = base64.StdEncoding.EncodeToString([]byte(entry))
}
return strings.Join(encoded, "\n")
}

func secretsEnvFromEnvironment() []string {
raw := os.Getenv(config2.EnvSecretsEnv)
if raw == "" {
return nil
}

parts := strings.Split(raw, "\n")
entries := make([]string, 0, len(parts))
for _, p := range parts {
decoded, err := base64.StdEncoding.DecodeString(p)
if err != nil {
continue
}
entries = append(entries, string(decoded))
}

return entries
}

// DeferredHooksCmd runs deferred lifecycle hooks as a detached background process.
type DeferredHooksCmd struct {
*flags.GlobalFlags
SetupInfo string
Prebuild bool
DotfilesRepo string
DotfilesScript string
SecretsEnv []string
}

// NewDeferredHooksCmd creates a new command.
Expand All @@ -51,12 +84,6 @@ func NewDeferredHooksCmd(flags *flags.GlobalFlags) *cobra.Command {
"",
"Dotfiles install script path",
),
cliflags.StringSlice(
&cmd.SecretsEnv,
names.SecretsEnv,
[]string{},
"Secrets to inject into lifecycle commands (KEY=VALUE)",
),
)
_ = deferredCmd.MarkFlagRequired(names.SetupInfo)
return deferredCmd
Expand All @@ -79,7 +106,8 @@ func (cmd *DeferredHooksCmd) Run(ctx context.Context) error {
Repository: cmd.DotfilesRepo,
InstallScript: cmd.DotfilesScript,
RemoteUser: config.GetRemoteUser(setupInfo),
}, cmd.SecretsEnv, setup.SkipPhases{})
// Mount values aren't in this process's env; read them back for redaction.
}, secretsEnvFromEnvironment(), setup.MountSecretsForRedaction(), setup.SkipPhases{})
if err != nil {
return fmt.Errorf("deferred hooks setup: %w", err)
}
Expand Down
Loading
Loading