Skip to content

Commit f5de180

Browse files
authored
Add "konk exec" (#7)
This adds "konk exec" which executes a simple konkfile.
1 parent 7673197 commit f5de180

File tree

10 files changed

+405
-34
lines changed

10 files changed

+405
-34
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ jobs:
1414
name: Lint
1515
runs-on: ubuntu-latest
1616
steps:
17-
- uses: actions/checkout@v3
18-
- uses: actions/setup-go@v3
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-go@v4
1919
with:
20-
go-version: '1.19'
20+
go-version: '1.22'
2121
cache: true
2222
- name: golangci-lint
23-
uses: golangci/golangci-lint-action@v3
24-
with: {version: v1.49}
23+
uses: golangci/golangci-lint-action@v4
24+
with: {version: v1.56}
2525
test:
2626
name: Test
2727
runs-on: ubuntu-latest
2828
steps:
29-
- uses: actions/checkout@v3
30-
- uses: actions/setup-go@v3
29+
- uses: actions/checkout@v4
30+
- uses: actions/setup-go@v4
3131
with:
32-
go-version: '1.19'
32+
go-version: '1.22'
3333
cache: true
3434
- run: script/test

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
tmp/
22
dist/
33
integration/bin/
4+
/konkfile
5+
/konkfile.*

cmd/exec.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/BurntSushi/toml"
10+
"github.com/jclem/konk/konk/debugger"
11+
"github.com/jclem/konk/konk/konkfile"
12+
"github.com/spf13/cobra"
13+
"gopkg.in/yaml.v3"
14+
)
15+
16+
var konkfilePath string
17+
18+
var execCommand = cobra.Command{
19+
Use: "exec <command>",
20+
Aliases: []string{"e"},
21+
Short: "Execute a command from a konkfile (alias: e)",
22+
Args: cobra.ExactArgs(1),
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
dbg := debugger.Get(cmd.Context())
25+
dbg.Flags(cmd)
26+
27+
if workingDirectory != "" {
28+
if err := os.Chdir(workingDirectory); err != nil {
29+
return fmt.Errorf("changing working directory: %w", err)
30+
}
31+
}
32+
33+
kfsearch := []string{"konkfile", "konkfile.json", "konkfile.toml", "konkfile.yaml", "konkfile.yml"}
34+
if konkfilePath != "" {
35+
kfsearch = []string{konkfilePath}
36+
}
37+
38+
var kf []byte
39+
var kfpath string
40+
41+
for _, kfp := range kfsearch {
42+
b, err := os.ReadFile(kfp)
43+
if err != nil {
44+
if os.IsNotExist(err) {
45+
continue
46+
}
47+
48+
return fmt.Errorf("reading konkfile: %w", err)
49+
}
50+
51+
kf = b
52+
kfpath = kfp
53+
}
54+
55+
ext := filepath.Ext(kfpath)
56+
var file konkfile.File
57+
58+
if ext == "" {
59+
if err := json.Unmarshal(kf, &file); err != nil {
60+
if err := yaml.Unmarshal(kf, &file); err != nil {
61+
if err := toml.Unmarshal(kf, &file); err != nil {
62+
return fmt.Errorf("unmarshalling konkfile: %w", err)
63+
}
64+
}
65+
}
66+
} else if ext == ".yaml" || ext == ".yml" {
67+
if err := yaml.Unmarshal(kf, &file); err != nil {
68+
return fmt.Errorf("unmarshalling konkfile: %w", err)
69+
}
70+
} else if ext == ".toml" {
71+
if err := toml.Unmarshal(kf, &file); err != nil {
72+
return fmt.Errorf("unmarshalling konkfile: %w", err)
73+
}
74+
} else {
75+
if err := json.Unmarshal(kf, &file); err != nil {
76+
return fmt.Errorf("unmarshalling konkfile: %w", err)
77+
}
78+
}
79+
80+
if err := konkfile.Execute(cmd.Context(), file, args[0], konkfile.ExecuteConfig{
81+
AggregateOutput: aggregateOutput,
82+
ContinueOnError: continueOnError,
83+
NoColor: noColor,
84+
NoShell: noShell,
85+
}); err != nil {
86+
return fmt.Errorf("executing command: %w", err)
87+
}
88+
89+
return nil
90+
},
91+
}
92+
93+
func init() {
94+
execCommand.Flags().StringVarP(&workingDirectory, "working-directory", "w", "", "set the working directory for all commands")
95+
execCommand.Flags().BoolVarP(&aggregateOutput, "aggregate-output", "g", false, "aggregate command output")
96+
execCommand.Flags().BoolVarP(&continueOnError, "continue-on-error", "c", false, "continue running commands after a failure")
97+
execCommand.Flags().StringVarP(&konkfilePath, "konkfile", "k", "", "path to konkfile")
98+
rootCmd.AddCommand(&execCommand)
99+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ require (
2020
)
2121

2222
require (
23+
github.com/BurntSushi/toml v1.3.2
2324
github.com/charmbracelet/lipgloss v0.5.0
2425
github.com/inconshreveable/mousetrap v1.0.0 // indirect
2526
github.com/kr/pretty v0.3.0
2627
github.com/mattn/go-shellwords v1.0.12
2728
github.com/spf13/pflag v1.0.5 // indirect
2829
github.com/stretchr/testify v1.8.0
30+
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81
2931
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
3032
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
2+
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
13
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
24
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
35
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -44,6 +46,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
4446
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
4547
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
4648
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
49+
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc=
50+
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
4751
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
4852
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
4953
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=

konk/internal/env/env.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package env
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/mattn/go-shellwords"
7+
)
8+
9+
func Parse(env []string) ([]string, error) {
10+
parsedEnv := make([]string, 0, len(env))
11+
12+
// Unquote any quoted .env vars.
13+
for _, line := range env {
14+
parsed, err := shellwords.Parse(line)
15+
if err != nil {
16+
return nil, fmt.Errorf("parsing .env line: %w", err)
17+
}
18+
19+
if len(parsed) == 0 {
20+
continue
21+
}
22+
23+
if len(parsed) != 1 {
24+
return nil, fmt.Errorf("invalid .env line: %s", line)
25+
}
26+
27+
parsedEnv = append(parsedEnv, parsed[0])
28+
}
29+
30+
return parsedEnv, nil
31+
}

konk/konkfile/execute.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package konkfile
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
8+
"github.com/jclem/konk/konk"
9+
"github.com/jclem/konk/konk/konkfile/internal/dag"
10+
"github.com/mattn/go-shellwords"
11+
"golang.org/x/sync/errgroup"
12+
)
13+
14+
type ExecuteConfig struct {
15+
AggregateOutput bool
16+
ContinueOnError bool
17+
NoColor bool
18+
NoShell bool
19+
}
20+
21+
func Execute(ctx context.Context, file File, command string, cfg ExecuteConfig) error {
22+
g := dag.New[string]()
23+
24+
for name := range file.Commands {
25+
g.AddNode(name)
26+
}
27+
28+
for name, cmd := range file.Commands {
29+
for _, dep := range cmd.Needs {
30+
if err := g.AddEdge(name, dep); err != nil {
31+
return fmt.Errorf("adding edge: %w", err)
32+
}
33+
}
34+
}
35+
36+
s := &scheduler{wgs: make(map[string]*sync.WaitGroup, 0)}
37+
38+
for _, n := range g.Nodes() {
39+
s.wgs[n] = new(sync.WaitGroup)
40+
s.wgs[n].Add(1)
41+
}
42+
43+
mut := new(sync.Mutex)
44+
wg := new(sync.WaitGroup)
45+
46+
ctx, cancel := context.WithCancel(ctx)
47+
defer cancel()
48+
49+
onNode := func(n string) error {
50+
mut.Lock()
51+
52+
cmd, ok := file.Commands[n]
53+
if !ok {
54+
return fmt.Errorf("command not found: %s", n)
55+
}
56+
57+
if cmd.Exclusive {
58+
defer mut.Unlock()
59+
wg.Wait()
60+
} else {
61+
wg.Add(1)
62+
defer wg.Done()
63+
mut.Unlock()
64+
}
65+
66+
var c *konk.Command
67+
if cfg.NoShell {
68+
parts, err := shellwords.Parse(cmd.Run)
69+
if err != nil {
70+
return fmt.Errorf("parsing command: %w", err)
71+
}
72+
73+
c = konk.NewCommand(konk.CommandConfig{
74+
Name: parts[0],
75+
Args: parts[1:],
76+
Label: n,
77+
NoColor: cfg.NoColor,
78+
})
79+
} else {
80+
c = konk.NewShellCommand(konk.ShellCommandConfig{
81+
Command: cmd.Run,
82+
Label: n,
83+
NoColor: false,
84+
})
85+
}
86+
87+
if err := c.Run(ctx, cancel, konk.RunCommandConfig{
88+
AggregateOutput: cfg.AggregateOutput,
89+
KillOnCancel: !cfg.ContinueOnError,
90+
}); err != nil {
91+
return fmt.Errorf("running command: %w", err)
92+
}
93+
94+
return nil
95+
}
96+
97+
path, err := g.Visit(command)
98+
if err != nil {
99+
return fmt.Errorf("visiting node: %w", err)
100+
}
101+
102+
var eg errgroup.Group
103+
for _, n := range path {
104+
n := n
105+
eg.Go(func() error {
106+
from := g.From(n)
107+
return s.run(n, from, onNode)
108+
})
109+
}
110+
111+
if err := eg.Wait(); err != nil {
112+
return fmt.Errorf("running commands: %w", err)
113+
}
114+
115+
return nil
116+
}
117+
118+
type scheduler struct {
119+
wgs map[string]*sync.WaitGroup
120+
}
121+
122+
func (s *scheduler) run(n string, deps []string, onNode func(string) error) error {
123+
defer s.wgs[n].Done()
124+
125+
for _, dep := range deps {
126+
s.wgs[dep].Wait()
127+
}
128+
129+
if err := onNode(n); err != nil {
130+
return fmt.Errorf("running node: %w", err)
131+
}
132+
133+
return nil
134+
}

konk/konkfile/file.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package konkfile
2+
3+
type File struct {
4+
Commands map[string]Command `json:"commands" toml:"commands" yaml:"commands"`
5+
}
6+
7+
type Command struct {
8+
Run string `json:"run" toml:"run" yaml:"run"`
9+
Needs []string `json:"needs" toml:"needs" yaml:"needs"`
10+
Exclusive bool `json:"exclusive" toml:"exclusive" yaml:"exclusive"`
11+
}

0 commit comments

Comments
 (0)