Skip to content

Commit

Permalink
Add "konk exec" (#7)
Browse files Browse the repository at this point in the history
This adds "konk exec" which executes a simple konkfile.
  • Loading branch information
jclem committed Mar 20, 2024
1 parent 7673197 commit f5de180
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 34 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: '1.19'
go-version: '1.22'
cache: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with: {version: v1.49}
uses: golangci/golangci-lint-action@v4
with: {version: v1.56}
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: '1.19'
go-version: '1.22'
cache: true
- run: script/test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
tmp/
dist/
integration/bin/
/konkfile
/konkfile.*
99 changes: 99 additions & 0 deletions cmd/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/BurntSushi/toml"
"github.com/jclem/konk/konk/debugger"
"github.com/jclem/konk/konk/konkfile"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

var konkfilePath string

var execCommand = cobra.Command{
Use: "exec <command>",
Aliases: []string{"e"},
Short: "Execute a command from a konkfile (alias: e)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dbg := debugger.Get(cmd.Context())
dbg.Flags(cmd)

if workingDirectory != "" {
if err := os.Chdir(workingDirectory); err != nil {
return fmt.Errorf("changing working directory: %w", err)
}
}

kfsearch := []string{"konkfile", "konkfile.json", "konkfile.toml", "konkfile.yaml", "konkfile.yml"}
if konkfilePath != "" {
kfsearch = []string{konkfilePath}
}

var kf []byte
var kfpath string

for _, kfp := range kfsearch {
b, err := os.ReadFile(kfp)
if err != nil {
if os.IsNotExist(err) {
continue
}

return fmt.Errorf("reading konkfile: %w", err)
}

kf = b
kfpath = kfp
}

ext := filepath.Ext(kfpath)
var file konkfile.File

if ext == "" {
if err := json.Unmarshal(kf, &file); err != nil {
if err := yaml.Unmarshal(kf, &file); err != nil {
if err := toml.Unmarshal(kf, &file); err != nil {
return fmt.Errorf("unmarshalling konkfile: %w", err)
}
}
}
} else if ext == ".yaml" || ext == ".yml" {
if err := yaml.Unmarshal(kf, &file); err != nil {
return fmt.Errorf("unmarshalling konkfile: %w", err)
}
} else if ext == ".toml" {
if err := toml.Unmarshal(kf, &file); err != nil {
return fmt.Errorf("unmarshalling konkfile: %w", err)
}
} else {
if err := json.Unmarshal(kf, &file); err != nil {
return fmt.Errorf("unmarshalling konkfile: %w", err)
}
}

if err := konkfile.Execute(cmd.Context(), file, args[0], konkfile.ExecuteConfig{
AggregateOutput: aggregateOutput,
ContinueOnError: continueOnError,
NoColor: noColor,
NoShell: noShell,
}); err != nil {
return fmt.Errorf("executing command: %w", err)
}

return nil
},
}

func init() {
execCommand.Flags().StringVarP(&workingDirectory, "working-directory", "w", "", "set the working directory for all commands")
execCommand.Flags().BoolVarP(&aggregateOutput, "aggregate-output", "g", false, "aggregate command output")
execCommand.Flags().BoolVarP(&continueOnError, "continue-on-error", "c", false, "continue running commands after a failure")
execCommand.Flags().StringVarP(&konkfilePath, "konkfile", "k", "", "path to konkfile")
rootCmd.AddCommand(&execCommand)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ require (
)

require (
github.com/BurntSushi/toml v1.3.2
github.com/charmbracelet/lipgloss v0.5.0
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kr/pretty v0.3.0
github.com/mattn/go-shellwords v1.0.12
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.0
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
Expand Down Expand Up @@ -44,6 +46,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc=
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
Expand Down
31 changes: 31 additions & 0 deletions konk/internal/env/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package env

import (
"fmt"

"github.com/mattn/go-shellwords"
)

func Parse(env []string) ([]string, error) {
parsedEnv := make([]string, 0, len(env))

// Unquote any quoted .env vars.
for _, line := range env {
parsed, err := shellwords.Parse(line)
if err != nil {
return nil, fmt.Errorf("parsing .env line: %w", err)
}

if len(parsed) == 0 {
continue
}

if len(parsed) != 1 {
return nil, fmt.Errorf("invalid .env line: %s", line)
}

parsedEnv = append(parsedEnv, parsed[0])
}

return parsedEnv, nil
}
134 changes: 134 additions & 0 deletions konk/konkfile/execute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package konkfile

import (
"context"
"fmt"
"sync"

"github.com/jclem/konk/konk"
"github.com/jclem/konk/konk/konkfile/internal/dag"
"github.com/mattn/go-shellwords"
"golang.org/x/sync/errgroup"
)

type ExecuteConfig struct {
AggregateOutput bool
ContinueOnError bool
NoColor bool
NoShell bool
}

func Execute(ctx context.Context, file File, command string, cfg ExecuteConfig) error {
g := dag.New[string]()

for name := range file.Commands {
g.AddNode(name)
}

for name, cmd := range file.Commands {
for _, dep := range cmd.Needs {
if err := g.AddEdge(name, dep); err != nil {
return fmt.Errorf("adding edge: %w", err)
}
}
}

s := &scheduler{wgs: make(map[string]*sync.WaitGroup, 0)}

for _, n := range g.Nodes() {
s.wgs[n] = new(sync.WaitGroup)
s.wgs[n].Add(1)
}

mut := new(sync.Mutex)
wg := new(sync.WaitGroup)

ctx, cancel := context.WithCancel(ctx)
defer cancel()

onNode := func(n string) error {
mut.Lock()

cmd, ok := file.Commands[n]
if !ok {
return fmt.Errorf("command not found: %s", n)
}

if cmd.Exclusive {
defer mut.Unlock()
wg.Wait()
} else {
wg.Add(1)
defer wg.Done()
mut.Unlock()
}

var c *konk.Command
if cfg.NoShell {
parts, err := shellwords.Parse(cmd.Run)
if err != nil {
return fmt.Errorf("parsing command: %w", err)
}

c = konk.NewCommand(konk.CommandConfig{
Name: parts[0],
Args: parts[1:],
Label: n,
NoColor: cfg.NoColor,
})
} else {
c = konk.NewShellCommand(konk.ShellCommandConfig{
Command: cmd.Run,
Label: n,
NoColor: false,
})
}

if err := c.Run(ctx, cancel, konk.RunCommandConfig{
AggregateOutput: cfg.AggregateOutput,
KillOnCancel: !cfg.ContinueOnError,
}); err != nil {
return fmt.Errorf("running command: %w", err)
}

return nil
}

path, err := g.Visit(command)
if err != nil {
return fmt.Errorf("visiting node: %w", err)
}

var eg errgroup.Group
for _, n := range path {
n := n
eg.Go(func() error {
from := g.From(n)
return s.run(n, from, onNode)
})
}

if err := eg.Wait(); err != nil {
return fmt.Errorf("running commands: %w", err)
}

return nil
}

type scheduler struct {
wgs map[string]*sync.WaitGroup
}

func (s *scheduler) run(n string, deps []string, onNode func(string) error) error {
defer s.wgs[n].Done()

for _, dep := range deps {
s.wgs[dep].Wait()
}

if err := onNode(n); err != nil {
return fmt.Errorf("running node: %w", err)
}

return nil
}
11 changes: 11 additions & 0 deletions konk/konkfile/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package konkfile

type File struct {
Commands map[string]Command `json:"commands" toml:"commands" yaml:"commands"`
}

type Command struct {
Run string `json:"run" toml:"run" yaml:"run"`
Needs []string `json:"needs" toml:"needs" yaml:"needs"`
Exclusive bool `json:"exclusive" toml:"exclusive" yaml:"exclusive"`
}
Loading

0 comments on commit f5de180

Please sign in to comment.