Skip to content

Commit 9d9701e

Browse files
committed
chore: addresses PR review comments
1 parent 036c17c commit 9d9701e

10 files changed

Lines changed: 58 additions & 74 deletions

File tree

Runfile.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
tasks:
22
build:
3+
env:
4+
CGO_ENABLED: 0
35
cmd:
46
- |+
57
echo "building ..."
@@ -43,5 +45,5 @@ tasks:
4345
test:only-failing:
4446
cmd:
4547
- go test -json ./pkg/runfile/resolver/... | gotestfmt --hide successful-tests
46-
go test -json ./pkg/executor/... $pattern_args | gotestfmt --hide successful-tests
48+
- go test -json ./pkg/executor/... | gotestfmt --hide successful-tests
4749

cmd/run/completions/run.fish

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function __fish_completion_no_subcommand --description 'Test if there has been a
2626
return 0
2727
end
2828

29-
complete -c completion -n '__fish_completion_no_subcommand' -f -l help -s h -d 'show help'
30-
complete -c completion -n '__fish_completion_no_subcommand' -f -l help -s h -d 'show help'
29+
complete -c run -n '__fish_completion_no_subcommand' -f -l help -s h -d 'show help'
30+
complete -c run -n '__fish_completion_no_subcommand' -f -l list -s l -d 'list all tasks'
31+
complete -c run -n '__fish_completion_no_subcommand' -rF -l file -s f -d 'runs targets from this runfile'
3132

cmd/run/main.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -179,16 +179,6 @@ func main() {
179179
// INFO: for supporting flags that have been suffixed post arguments
180180
args := make([]string, 0, len(c.Args().Slice()))
181181
for _, arg := range c.Args().Slice() {
182-
if arg == "-p" || arg == "--parallel" {
183-
parallel = true
184-
continue
185-
}
186-
187-
if arg == "-w" || arg == "--watch" {
188-
watch = true
189-
continue
190-
}
191-
192182
if arg == "--debug" {
193183
debug = true
194184
continue
@@ -260,7 +250,7 @@ func getRunfilePath(dir string) (string, error) {
260250
}
261251

262252
if stat.IsDir() {
263-
return "", fmt.Errorf("Runfile.yml is a directory")
253+
return "", fmt.Errorf("%s is a directory", filepath.Join(dir, f))
264254
}
265255

266256
return filepath.Join(dir, f), nil
@@ -283,10 +273,15 @@ func locateRunfile(c *cli.Command) (string, error) {
283273

284274
for oldDir != dir {
285275
fp, err := getRunfilePath(dir)
286-
if err != nil && !errors.Is(err, ErrRunfileNotFound) {
287-
oldDir = dir
288-
dir = filepath.Dir(dir)
289-
continue
276+
if err != nil {
277+
if errors.Is(err, ErrRunfileNotFound) {
278+
// Not found in this dir, try parent
279+
oldDir = dir
280+
dir = filepath.Dir(dir)
281+
continue
282+
}
283+
// Some other error
284+
return "", err
290285
}
291286

292287
return fp, nil

pkg/errors/constants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,5 @@ func ErrInvalidShellAlias(alias string) *errors.Error {
7171
}
7272

7373
func ErrCircularDependency(taskName string) *errors.Error {
74-
return errors.New("invalid shell alias").KV("task", taskName)
74+
return errors.New("circular dependency detected").KV("task", taskName)
7575
}

pkg/runfile/resolver/env.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,24 @@ import (
1616
)
1717

1818
func parseDotEnvFilesInto(store map[string]string, files []string) error {
19-
for _, f := range files {
20-
if !filepath.IsAbs(f) {
21-
return errors.New("dotenv file must have absolute paths").KV("dotenv.file", f)
19+
for _, file := range files {
20+
if !filepath.IsAbs(file) {
21+
return errors.New("dotenv file must have absolute paths").KV("dotenv.file", file)
2222
}
2323

24-
f, err := os.Open(f)
24+
f, err := os.Open(file)
2525
if err != nil {
26-
return errors.New("failed to open dotenv file").Wrap(err).KV("dotenv.file", f)
26+
return errors.New("failed to open dotenv file").Wrap(err).KV("dotenv.file", file)
2727
}
2828

2929
m, err := godotenv.Parse(f)
3030
if err != nil {
31-
return errors.New("failed to parse dotenv file").Wrap(err).KV("dotenv.file", f)
31+
return errors.New("failed to parse dotenv file").Wrap(err).KV("dotenv.file", file)
32+
}
33+
34+
if err := f.Close(); err != nil {
35+
return errors.New("failed to close dotenv file").Wrap(err).KV("dotenv.file", file)
3236
}
33-
f.Close()
3437

3538
maps.Copy(store, m)
3639
}
@@ -70,7 +73,9 @@ func parseEnvInto(ctx context.Context, envStore map[string]string, envMap map[st
7073
return errors.New("ENV-EXPRESSION: value field `required` must be a boolean").KV("env.key", k, "env.value", value)
7174
}
7275
if isRequired {
73-
return errors.New(fmt.Sprintf("ENV-EXPRESSION: env var '%s' is required, it must be provided", k)).KV("env.key", k, "env.value", value)
76+
if _, exists := lookupEnv(k); !exists {
77+
return errors.New(fmt.Sprintf("ENV-EXPRESSION: env var '%s' is required, it must be provided", k)).KV("env.key", k, "env.value", value)
78+
}
7479
}
7580
}
7681

@@ -81,7 +86,10 @@ func parseEnvInto(ctx context.Context, envStore map[string]string, envMap map[st
8186
return errors.New(fmt.Sprintf("ENV-EXPRESSION: value field `%s`, must have a string value", optKey)).KV("env.key", k, "env.value", value)
8287
}
8388

84-
lazyEvalMap[k] = exec.CommandContext(ctx, shell[0], append(shell[1:], envEvalScript)...)
89+
// #nosec G204 - This is intentional: env vars with sh/bash keys are meant to
90+
// execute shell commands defined in the Runfile. The Runfile is trusted
91+
// user-provided configuration, similar to Makefiles or shell scripts.
92+
lazyEvalMap[k] = exec.CommandContext(ctx, shell[0], append(shell[1:], envEvalScript)...)
8593
break
8694
}
8795
}

pkg/runfile/resolver/env_test.go

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package resolver
22

33
import (
44
"context"
5+
"fmt"
56
"maps"
67
"os"
78
"path/filepath"
@@ -11,8 +12,7 @@ import (
1112
func TestParseDotEnvFilesInto(t *testing.T) {
1213
tests := []struct {
1314
name string
14-
setupFiles map[string]string // filename -> content
15-
files []string // will be converted to absolute paths
15+
fileContents []string // each entry becomes .env0, .env1, etc.
1616
initialStore map[string]string
1717
expected map[string]string
1818
wantErr bool
@@ -21,14 +21,13 @@ func TestParseDotEnvFilesInto(t *testing.T) {
2121
}{
2222
{
2323
name: "when files list is empty, it must pass",
24-
files: []string{},
24+
fileContents: []string{},
2525
initialStore: map[string]string{},
2626
expected: map[string]string{},
2727
wantErr: false,
2828
},
2929
{
3030
name: "when file path is relative, it must fail",
31-
files: []string{"relative/path/.env"},
3231
useRelPath: true,
3332
wantErr: true,
3433
},
@@ -37,15 +36,20 @@ func TestParseDotEnvFilesInto(t *testing.T) {
3736
useNonExistent: true,
3837
wantErr: true,
3938
},
39+
{
40+
name: "when dotenv file has invalid syntax, it must return a parse error",
41+
fileContents: []string{"INVALID LINE WITHOUT EQUALS\nANOTHER BAD LINE"},
42+
initialStore: map[string]string{},
43+
expected: map[string]string{},
44+
wantErr: true,
45+
},
4046
{
4147
name: "when file is valid, it must parse all key-value pairs",
42-
setupFiles: map[string]string{
43-
".env": `KEY1=value1
44-
KEY2=value2
45-
KEY3="quoted value"
46-
`,
48+
fileContents: []string{
49+
`KEY1=value1
50+
KEY2=value2
51+
KEY3="quoted value"`,
4752
},
48-
files: []string{".env"},
4953
initialStore: map[string]string{},
5054
expected: map[string]string{
5155
"KEY1": "value1",
@@ -56,13 +60,10 @@ func TestParseDotEnvFilesInto(t *testing.T) {
5660
},
5761
{
5862
name: "when multiple files have same key, it must use value from last file",
59-
setupFiles: map[string]string{
60-
".env1": `KEY1=value1
61-
KEY2=original`,
62-
".env2": `KEY2=overridden
63-
KEY3=value3`,
63+
fileContents: []string{
64+
"KEY1=value1\nKEY2=original",
65+
"KEY2=overridden\nKEY3=value3",
6466
},
65-
files: []string{".env1", ".env2"},
6667
initialStore: map[string]string{},
6768
expected: map[string]string{
6869
"KEY1": "value1",
@@ -72,11 +73,8 @@ KEY3=value3`,
7273
wantErr: false,
7374
},
7475
{
75-
name: "when store has existing keys, it must preserve non-overlapping values",
76-
setupFiles: map[string]string{
77-
".env": `NEW_KEY=new_value`,
78-
},
79-
files: []string{".env"},
76+
name: "when store has existing keys, it must preserve non-overlapping values",
77+
fileContents: []string{"NEW_KEY=new_value"},
8078
initialStore: map[string]string{"EXISTING": "existing_value"},
8179
expected: map[string]string{
8280
"EXISTING": "existing_value",
@@ -91,19 +89,18 @@ KEY3=value3`,
9189
var files []string
9290

9391
if tt.useRelPath {
94-
files = tt.files
92+
files = []string{"relative/path/.env"}
9593
} else if tt.useNonExistent {
9694
files = []string{"/non/existent/path/.env"}
9795
} else {
9896
tmpDir := t.TempDir()
99-
for filename, content := range tt.setupFiles {
97+
for i, content := range tt.fileContents {
98+
filename := fmt.Sprintf(".env%d", i)
10099
path := filepath.Join(tmpDir, filename)
101100
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
102101
t.Fatalf("failed to create temp file: %v", err)
103102
}
104-
}
105-
for _, f := range tt.files {
106-
files = append(files, filepath.Join(tmpDir, f))
103+
files = append(files, path)
107104
}
108105
}
109106

pkg/runfile/resolver/errors.go

Lines changed: 0 additions & 9 deletions
This file was deleted.

pkg/runfile/resolver/resolver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func Load(ctx context.Context, file string) (*Resolver, error) {
107107

108108
for i := range task.DotEnv {
109109
if !filepath.IsAbs(task.DotEnv[i]) {
110-
task.DotEnv[i] = filepath.Join(runfileDir, task.DotEnv[i])
110+
task.DotEnv[i] = filepath.Join(includedDir, task.DotEnv[i])
111111
}
112112
}
113113

pkg/runfile/resolver/task.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,6 @@ func (r *Resolver) RunTask(ctx context.Context, name string) error {
165165
slog.Debug("3. watcher closed")
166166
}()
167167

168-
pipeline.Start(ctx)
169-
170168
counter := 0
171169
for ev := range watch.GetEvents() {
172170
slog.Debug("received", "event", ev)

pkg/runfile/resolver/task_test.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package resolver
33
import (
44
"testing"
55

6-
"github.com/nxtcoder17/runfile/pkg/executor"
76
"github.com/nxtcoder17/runfile/pkg/runfile/spec"
87
"github.com/nxtcoder17/runfile/pkg/writer"
98
)
@@ -482,10 +481,3 @@ func TestCreateStepsWithEnv(t *testing.T) {
482481
})
483482
}
484483
}
485-
486-
// Helper to check if a step uses the correct executor type
487-
func isInteractiveStep(step executor.Step) bool {
488-
// This is a basic check - in real tests you might want to
489-
// use reflection or add a method to identify command types
490-
return len(step.Commands) > 0
491-
}

0 commit comments

Comments
 (0)