Skip to content
Open
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
8 changes: 8 additions & 0 deletions cmd/environment_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ func (c *runmeOwlStoreClient) ProjectSpec(ctx context.Context, req owlcmd.Projec
return owlcmd.NewLocalStoreClient(owlcmd.LocalStoreOptions{}).ProjectSpec(ctx, req)
}

func (c *runmeOwlStoreClient) Resolve(ctx context.Context, req owlcmd.ResolveRequest) (*owlcmd.ResolveResult, error) {
return owlcmd.NewLocalStoreClient(owlcmd.LocalStoreOptions{}).Resolve(ctx, req)
}

func (c *runmeOwlStoreClient) ApplyPromptAnswers(ctx context.Context, answers []owlcmd.PromptAnswer) (*owlcmd.ResolveResult, error) {
return owlcmd.NewLocalStoreClient(owlcmd.LocalStoreOptions{}).ApplyPromptAnswers(ctx, answers)
}

func includeSnapshotTypeProposal(req owlcmd.TypeRequest, suggested bool) bool {
return req.All || suggested
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ require (
github.com/redpanda-data/protoc-gen-go-mcp v0.0.0-20260729122341-6334690a62b6
github.com/rogpeppe/go-internal v1.16.0
github.com/rs/cors v1.11.1
github.com/runmedev/owl v1.0.2-0.20260731050242-dc8e804fafc4
github.com/runmedev/owl v1.0.2-0.20260807195527-13e696e86cd4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ github.com/runmedev/owl v1.0.2-0.20260727163318-bc2a2f08918e h1:UWl6S93tMgLqjIpw
github.com/runmedev/owl v1.0.2-0.20260727163318-bc2a2f08918e/go.mod h1:j9dHba3PZdEasw/CqycYvRyysO/gwfoWOq4p2jMfvNY=
github.com/runmedev/owl v1.0.2-0.20260731050242-dc8e804fafc4 h1:/fBCxqqRZndOSyihGABGDh5XrWn4Hhn92sg+nXzQ/R8=
github.com/runmedev/owl v1.0.2-0.20260731050242-dc8e804fafc4/go.mod h1:/QP9vSsqqcREuqy0kRzRdBE2SCgN/OYkcEOgGV4jFWw=
github.com/runmedev/owl v1.0.2-0.20260807170509-32ba631d72fe h1:XvyOR4z94yD8a/n+bW5iVYXL9aZ/HseKRTrHqGZ6xSE=
github.com/runmedev/owl v1.0.2-0.20260807170509-32ba631d72fe/go.mod h1:sgo4HQXy6y/IqbS+c7y6XmY6RpHVYN21O6xQI7jbfxQ=
github.com/runmedev/owl v1.0.2-0.20260807195527-13e696e86cd4 h1:7nkLDx/Re9k9MZTZApC6Wv/5DVA0Xp87dnATx+wS7Y0=
github.com/runmedev/owl v1.0.2-0.20260807195527-13e696e86cd4/go.mod h1:sgo4HQXy6y/IqbS+c7y6XmY6RpHVYN21O6xQI7jbfxQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
Expand Down
64 changes: 14 additions & 50 deletions runner/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ package runner
import (
"context"
"fmt"
"sort"
"strings"
"sync"

"github.com/runmedev/owl/pkg/owl"
"github.com/runmedev/owl/pkg/owl/seed"
"go.uber.org/zap"

"github.com/runmedev/runme/v3/internal/lru"
Expand Down Expand Up @@ -70,7 +69,7 @@ func NewSessionWithStore(envs []string, proj *project.Project, owlStore bool, lo
logger: logger,
}

if proj != nil {
if proj != nil && !owlStore {
msg, err := s.loadDirEnv(context.Background(), proj)
if err != nil {
logger.Info("failed to load direnv", zap.Error(err))
Expand Down Expand Up @@ -188,55 +187,27 @@ type owlEnvStorer struct {
}

func newOwlStorer(envs []string, proj *project.Project, logger *zap.Logger) (*owlEnvStorer, error) {
opts := []owl.StoreOption{
owl.WithDotenv("[process]", strings.NewReader(dotenvLines(envs))),
direnvPolicy := seed.DirenvDisabled
if proj.EnvDirEnvEnabled() {
direnvPolicy = seed.DirenvEnabledWarn
}

envSpecFiles := []string{}
// envFilesOrder := []string{}
if proj != nil {
// todo(sebastian): specs loading should be independent of project
envSpecFiles = []string{".env.sample", ".env.example", ".env.spec"}
}

for _, specFile := range envSpecFiles {
raw, _ := proj.LoadRawFile(specFile)
if raw == nil {
continue
}
opts = append(opts, owl.WithEnvSpec(specFile, strings.NewReader(string(raw))))
}

envWithSource, err := proj.LoadEnvWithSource()
if err != nil {
return nil, err
}

for envSource, envMap := range envWithSource {
envs := []string{}
for k, v := range envMap {
env := fmt.Sprintf("%s=%s", k, v)
envs = append(envs, env)
}
sort.Strings(envs)
opts = append(opts, owl.WithDotenv(envSource, strings.NewReader(dotenvLines(envs))))
}

owlYAML, err := proj.LoadRawFile(".runme/owl.yaml")
result, err := seed.NewStore(context.Background(), seed.Options{
EnvFiles: proj.EnvFilesReadOrder(),
Observed: []seed.ObservedSource{{Source: owl.Source{Name: "[process]", Kind: "process"}, Environ: envs}},
WorkDir: proj.Root(),
Direnv: direnvPolicy,
})
if err != nil {
return nil, err
} else if owlYAML != nil {
logger.Warn("ignoring .runme/owl.yaml because Owl v2 resolver/CRD support is not part of this cutover")
}

owlStore, err := owl.NewStore(opts...)
if err != nil {
return nil, err
for _, diagnostic := range result.Diagnostics {
logger.Warn("owl seed diagnostic", zap.String("diagnostic", diagnosticMessage(diagnostic)))
}

return &owlEnvStorer{
logger: logger,
owlStore: owlStore,
owlStore: result.Store,
}, nil
}

Expand Down Expand Up @@ -360,13 +331,6 @@ func (es *owlEnvStorer) envs() ([]string, error) {
return vals, nil
}

func dotenvLines(envs []string) string {
if len(envs) == 0 {
return ""
}
return strings.Join(envs, "\n") + "\n"
}

type sessionList = lru.Cache[*Session]

// sessionListCapacity is a maximum number of entries
Expand Down
115 changes: 115 additions & 0 deletions runner/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/runmedev/owl/pkg/owl"
Expand Down Expand Up @@ -186,6 +187,120 @@ func TestOwlSessionRevealsSnapshotWithInsecurePolicy(t *testing.T) {
assert.Equal(t, "[process]", byName["TERMINFO"].Source.Name)
}

func TestOwlSessionSeedsDotenvSource(t *testing.T) {
t.Parallel()

dir := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(dir, ".env.example"),
[]byte("FILE_ONLY=\"File only\" # Plain\n"),
0o600,
))
require.NoError(t, os.WriteFile(
filepath.Join(dir, ".env"),
[]byte("FILE_ONLY=from-dotenv\n"),
0o600,
))
proj, err := project.NewDirProject(dir)
require.NoError(t, err)

snapshot := owlSessionSnapshot(t, nil, proj, owl.SnapshotPolicy{})
env := snapshotItemsByName(snapshot)["FILE_ONLY"]

assert.Equal(t, "from-dotenv", env.Value)
assert.Equal(t, owl.VisibilityLiteral, env.Visibility)
assert.Equal(t, "dotenv", env.Source.Kind)
assert.Equal(t, ".env", filepath.Base(env.Source.Name))
}

func TestOwlSessionAttributesDirenvMatchingObservedValue(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fake direnv shell script is POSIX-only")
}

dir := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(dir, ".env.example"),
[]byte("DIRENV_ONLY=\"Direnv only\" # Plain\n"),
0o600,
))
require.NoError(t, os.WriteFile(
filepath.Join(dir, ".envrc"),
[]byte("export DIRENV_ONLY=from-direnv\n"),
0o600,
))

binDir := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(binDir, "direnv"),
[]byte("#!/bin/sh\necho '{\"DIRENV_ONLY\":\"from-direnv\"}'\n"),
0o700,
))
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))

proj, err := project.NewDirProject(dir, project.WithEnvDirEnv(true))
require.NoError(t, err)

snapshot := owlSessionSnapshot(t, []string{"DIRENV_ONLY=from-direnv"}, proj, owl.SnapshotPolicy{})
env := snapshotItemsByName(snapshot)["DIRENV_ONLY"]

assert.Equal(t, "from-direnv", env.Value)
assert.Equal(t, owl.VisibilityLiteral, env.Visibility)
assert.Equal(t, ".envrc", env.Source.Name)
assert.Equal(t, "direnv", env.Source.Kind)
}

func TestOwlSessionKeepsProcessSourceWhenDirenvDiffers(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fake direnv shell script is POSIX-only")
}

dir := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(dir, ".env.example"),
[]byte("DIRENV_ONLY=\"Direnv only\" # Plain\n"),
0o600,
))
require.NoError(t, os.WriteFile(
filepath.Join(dir, ".envrc"),
[]byte("export DIRENV_ONLY=from-direnv\n"),
0o600,
))

binDir := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(binDir, "direnv"),
[]byte("#!/bin/sh\necho '{\"DIRENV_ONLY\":\"from-direnv\"}'\n"),
0o700,
))
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))

proj, err := project.NewDirProject(dir, project.WithEnvDirEnv(true))
require.NoError(t, err)

snapshot := owlSessionSnapshot(t, []string{"DIRENV_ONLY=from-process"}, proj, owl.SnapshotPolicy{})
env := snapshotItemsByName(snapshot)["DIRENV_ONLY"]

assert.Equal(t, "from-process", env.Value)
assert.Equal(t, owl.VisibilityLiteral, env.Visibility)
assert.Equal(t, "[process]", env.Source.Name)
assert.Equal(t, "process", env.Source.Kind)
}

func owlSessionSnapshot(t *testing.T, envs []string, proj *project.Project, policy owl.SnapshotPolicy) []owl.SnapshotItem {
t.Helper()

sess, err := NewSessionWithStore(envs, proj, true, zap.NewNop())
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
snapshotc := make(chan []owl.SnapshotItem)
require.NoError(t, sess.SubscribeWithPolicy(ctx, snapshotc, policy))

return <-snapshotc
}

func snapshotItemsByName(items []owl.SnapshotItem) map[string]owl.SnapshotItem {
result := make(map[string]owl.SnapshotItem, len(items))
for _, item := range items {
Expand Down
Loading