diff --git a/.gitignore b/.gitignore index b33b889..5c0338c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist/ +dog-*/ *tar.gz diff --git a/.travis.yml b/.travis.yml index 2ea2b6d..7a9b54a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,8 @@ go: install: - go get -t ./... - go get github.com/golang/lint/golint + - go get honnef.co/go/staticcheck/cmd/staticcheck + - go get github.com/kisielk/errcheck script: - diff <(echo -n) <(gofmt -s -d .) @@ -15,11 +17,5 @@ script: after_script: - golint ./... - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/87d757f61554eae3d779 - on_success: change - on_failure: always - on_start: never + - staticcheck ./... + - errcheck ./... diff --git a/DOGFILE_SPEC.md b/DOGFILE_SPEC.md index c81cdc1..d0fa791 100644 --- a/DOGFILE_SPEC.md +++ b/DOGFILE_SPEC.md @@ -11,11 +11,11 @@ Dogfiles are [YAML](http://yaml.org/) files that describe the execution of autom ```yml - task: hello description: Say Hello - run: echo hello + code: echo hello - task: bye description: Say Good Bye - run: echo bye + code: echo bye ``` Multiple Dogfiles in the same directory are processed together as a single entity. Although the name `Dogfile.yml` is recommended, any file with a name that starts with `Dogfile` and follows this specification is a valid Dogfile. @@ -40,18 +40,18 @@ Description of the task. Tasks that avoid this directive are not shown in the ta description: This task does some cool stuff ``` -### run +### code The code that will be executed. ```yml - run: echo 'hello' + code: echo 'hello' ``` Multiline scripts are supported. ```yml - run: | + code: | echo "This is the Dogfile in your current directory:" for dogfile in `ls -1 Dogfile*`; do @@ -59,20 +59,20 @@ Multiline scripts are supported. done ``` -### exec +### runner -When this directive is not defined, the default executor is `sh`. Additional executors are supported if they are present in the system. The following example uses the Ruby executor to print 'Hello World'. +When this directive is not defined, the default runner is `sh`. Additional runners are supported if they are present in the system. The following example uses the Ruby runner to print 'Hello World'. ```yml task: hello-ruby description: Hello World from Ruby - exec: ruby - run: | + runner: ruby + code: | hello = "Hello World" puts hello ``` -The following list of executors are known to work: +The following list of runners are supported: - sh - bash @@ -179,7 +179,7 @@ Additional parameters can be provided to the task that will be executed. All par - name: age regex: ^\d+$ - run: echo "Hello, I'm in the city of $1, planet $2. I am a $3 and I'm $4 years old" + code: echo "Hello, I'm in the city of $1, planet $2. I am a $3 and I'm $4 years old" ``` The *regex* option and the *choices* option are mutually exclusive. @@ -190,13 +190,13 @@ Registers store the STDOUT of executed tasks as environment variables so other t ```yml task: get-dog-version - run: dog --version | awk '{print $3}' + code: dog --version | awk '{print $3}' register: DOG_VERSION task: print-dog-version description: Print Dog version pre: get-dog-version - run: echo "I am running Dog $DOG_VERSION" + code: echo "I am running Dog $DOG_VERSION" ``` Dogfiles don't have global variables, use registers instead. @@ -210,7 +210,7 @@ Tools using Dogfiles and having special requirements can define their own direct description: Clear the cache x_path: /task/clear-cache x_tls_required: true - run: ./scripts/cache-clear.sh + code: ./scripts/cache-clear.sh ``` (*) Not implemented yet diff --git a/Dogfile.yml b/Dogfile.yml index e58b997..594f39f 100644 --- a/Dogfile.yml +++ b/Dogfile.yml @@ -1,45 +1,47 @@ - task: clean description: Clean compiled binaries - run: rm -rf dist + code: rm -rf dist dog-* - task: build description: Build dog binary for current platform env: - OUTPUT_PATH=dist/current - - REV=v0.3.0 - run: | + - REV=v0.4.0 + code: | go build \ -ldflags "-X main.Release=$REV -w" \ -o "${OUTPUT_PATH}/dog" \ - . + cmd/dog/*.go - task: install-build-deps description: Installs required dependencies for building dog - run: go get -u github.com/mitchellh/gox + code: go get -u github.com/mitchellh/gox - task: build-all description: Build dog binary for all platforms env: - XC_ARCH=386 amd64 - XC_OS=linux darwin freebsd openbsd solaris - - REV=v0.3.0 + - REV=v0.4.0 pre: - install-build-deps - clean - run: | + code: | gox \ -os="${XC_OS}" \ -arch="${XC_ARCH}" \ -ldflags "-X main.Release=$REV -w" \ -output "dist/{{.OS}}_{{.Arch}}/dog" \ - . + ./cmd/dog/ - task: dist description: Put all dist binaries in a compressed file - env: REV=v0.3.0 + env: REV=v0.4.0 pre: build-all - run: tar zcvf dog-${REV}.tar.gz dist/ + code: | + mv -v dist dog-${REV} + tar zcvf dog-${REV}.tar.gz dog-${REV} - task: run-test-dogfiles description: Run all Tasks in testdata Dogfiles - run: ./scripts/test-dogfiles.sh + code: ./scripts/test-dogfiles.sh diff --git a/README.md b/README.md index ed1e150..f6810d9 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,8 @@ # Dog [![Build Status](https://travis-ci.org/dogtools/dog.svg?branch=master)](https://travis-ci.org/dogtools/dog) -[![Join the chat](https://badges.gitter.im/dogtools/dog.svg)](https://gitter.im/dogtools/dog) -Dog is a command line application that executes automated tasks. It works in a similar way as GNU Make but it is a more generic task runner, not a build tool. Dog's default script syntax is `sh` but most interpreted languages like BASH, Python, Ruby or Perl can also be used. - -## Installing Dog - -If you are on macOS you can install Dog using brew: - -``` -brew tap dogtools/dog -brew install dog -``` - -If you have your golang environment set up, you can use: - -``` -go get github.com/dogtools/dog -``` +Dog is a command line application that executes automated tasks. It works in a similar way as GNU Make but it is a more generic task runner, not a build tool. Dog's default script syntax is `sh` but most interpreted languages like BASH, Python or Ruby can also be used. ## Using Dog @@ -30,7 +14,7 @@ Execute a task dog taskname -Execute a task, printing elapsed time and status code +Execute a task, printing elapsed time and exit status dog -i taskname @@ -41,28 +25,33 @@ Dogfile is a specification that uses YAML to describe the tasks related to a pro - Read Dog's own [Dogfile.yml][1] - Read the [Dogfile Spec][2] +## Installing Dog + +If you are using macOS you can install Dog using brew: + + brew tap dogtools/dog + brew install dog + +If you have your golang environment set up, you can use: + + go get -u github.com/dogtools/dog + ## Other tools -Tools that use Dogfiles are called *dogtools*. Dog is the first dogtool but there are other things that can implemented in the future: web and desktop UIs, chat bot interfaces, plugins for text editors and IDEs, tools to export Dogfiles to other formats, HTTP API interfaces, even implementations of the cli in other languages! To simplify the process of creating dogtools we are implementing parts of Dog as Go packages so they can be used in other projects (see [parser][3], [types][4] and [execute][5]). Let us know if you have any uncovered need on any of these packages. +Tools that use the Dogfile Specification are called *dogtools*. Dog is the first dogtool but there are other things that can be implemented in the future: web and desktop UIs, chat bot interfaces, plugins for text editors and IDEs, tools to export Dogfiles to other formats, HTTP API interfaces, even implementations of the cli in other languages! -## Contributing +The root directory of this repository contains the dog package that can be used to create dogtools in Go. -If you want to help, take a look at: + import "github.com/dogtools/dog" -- Open [bugs][6] -- Lacking features for [v0.4.0][7] -- Our [Code of Conduct][8] +Check the `examples/` directory to see how it works. -In case you are not interested in improving Dog but on building your own tool on top of the Dogfile Spec, please help us discussing it: +## Contributing -- Dogfile Spec [open discussion][9] +If you want to help, take a look at the open [bugs][3], the list of all [issues][4] and our [Code of Conduct][5]. [1]: https://github.com/dogtools/dog/blob/master/Dogfile.yml [2]: https://github.com/dogtools/dog/blob/master/DOGFILE_SPEC.md -[3]: https://github.com/dogtools/dog/tree/master/parser -[4]: https://github.com/dogtools/dog/tree/master/types -[5]: https://github.com/dogtools/dog/tree/master/execute -[6]: https://github.com/dogtools/dog/issues?q=is%3Aissue+is%3Aopen+label%3Abug -[7]: https://github.com/dogtools/dog/milestone/4 -[8]: https://github.com/dogtools/dog/blob/master/CODE_OF_CONDUCT.md -[9]: https://github.com/dogtools/dog/issues?q=is%3Aissue+is%3Aopen+label%3A%22dogfile+spec%22 +[3]: https://github.com/dogtools/dog/issues?q=is%3Aissue+is%3Aopen+label%3Abug +[4]: https://github.com/dogtools/dog/issues +[5]: https://github.com/dogtools/dog/blob/master/CODE_OF_CONDUCT.md diff --git a/chain.go b/chain.go new file mode 100644 index 0000000..b758b03 --- /dev/null +++ b/chain.go @@ -0,0 +1,158 @@ +package dog + +import ( + "errors" + "fmt" + "io" + "os/exec" + "syscall" + "time" + + "github.com/dogtools/dog/run" +) + +// ErrCycleInTaskChain means that there is a loop in the path of tasks execution. +var ErrCycleInTaskChain = errors.New("TaskChain includes a cycle of tasks") + +// TaskChain contains one or more tasks to be executed in order. +type TaskChain struct { + Tasks []*Task +} + +// Generate creates the TaskChain for a specific task. +func (taskChain *TaskChain) Generate(d Dogfile, task string) error { + + t, found := d.Tasks[task] + if !found { + return fmt.Errorf("Task %q does not exist", task) + } + + // Cycle detection + for i := 0; i < len(taskChain.Tasks); i++ { + if taskChain.Tasks[i].Name == task { + if len(taskChain.Tasks[i].Pre) > 0 || len(taskChain.Tasks[i].Post) > 0 { + return ErrCycleInTaskChain + } + } + } + + // Iterate over pre-tasks + if err := addToChain(taskChain, d, t.Pre); err != nil { + return err + } + + // Add current task to chain + taskChain.Tasks = append(taskChain.Tasks, t) + + // Iterate over post-tasks + if err := addToChain(taskChain, d, t.Post); err != nil { + return err + } + return nil +} + +// addToChain iterates over a list of pre or post tasks and adds them to the task chain. +func addToChain(taskChain *TaskChain, d Dogfile, tasks []string) error { + for _, name := range tasks { + + t, found := d.Tasks[name] + if !found { + return fmt.Errorf("Task %q does not exist", name) + } + + if err := taskChain.Generate(d, t.Name); err != nil { + return err + } + } + return nil +} + +// Run handles the execution of all tasks in the TaskChain. +func (taskChain *TaskChain) Run(stdout, stderr io.Writer) error { + var startTime time.Time + + for _, t := range taskChain.Tasks { + var err error + var runner run.Runner + exitStatus := 0 + + switch t.Runner { + case "sh": + runner, err = run.NewShRunner(t.Code, t.Workdir, t.Env) + case "bash": + runner, err = run.NewBashRunner(t.Code, t.Workdir, t.Env) + case "python": + runner, err = run.NewPythonRunner(t.Code, t.Workdir, t.Env) + case "ruby": + runner, err = run.NewRubyRunner(t.Code, t.Workdir, t.Env) + case "perl": + runner, err = run.NewPerlRunner(t.Code, t.Workdir, t.Env) + default: + if t.Runner == "" { + return errors.New("Runner not specified") + } + return fmt.Errorf("%s is not a supported runner", t.Runner) + } + if err != nil { + return err + } + + runOut, runErr, err := run.GetOutputs(runner) + if err != nil { + return err + } + + go io.Copy(stdout, runOut) + go io.Copy(stderr, runErr) + + startTime = time.Now() + err = runner.Start() + if err != nil { + return err + } + + err = runner.Wait() + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); !ok { + exitStatus = 1 // For unknown error exit codes set it to 1 + } else { + exitStatus = waitStatus.ExitStatus() + } + } + if ProvideExtraInfo { + fmt.Printf("-- %s (%s) failed with exit status %d\n", + t.Name, formatDuration(time.Since(startTime)), exitStatus) + } + return err + } + + if ProvideExtraInfo { + fmt.Printf("-- %s (%s) finished with exit status %d\n", + t.Name, formatDuration(time.Since(startTime)), exitStatus) + } + + } + return nil +} + +// formatDuration returns a string representing a time duration in the format +// {x}h{y}m{z}s, for example 3m25s. +func formatDuration(d time.Duration) (timeMsg string) { + + if d.Hours() > 1.0 { + timeMsg = fmt.Sprintf("%1.0fh", d.Hours()) + } + + if d.Minutes() > 1.0 { + timeMsg += fmt.Sprintf("%1.0fm", d.Minutes()) + } + + if d.Seconds() > 1.0 { + timeMsg += fmt.Sprintf("%1.0fs", d.Seconds()) + } else { + timeMsg += fmt.Sprintf("%1.3fs", d.Seconds()) + } + + return timeMsg +} diff --git a/cli.go b/cmd/dog/cli.go similarity index 75% rename from cli.go rename to cmd/dog/cli.go index df0cccd..e6895a4 100644 --- a/cli.go +++ b/cmd/dog/cli.go @@ -1,12 +1,6 @@ package main -import ( - "fmt" - "sort" - "strings" - - "github.com/dogtools/dog/types" -) +import "fmt" type userArgs struct { help bool @@ -36,10 +30,8 @@ func printHelp() { dog [--help] [--version] Dog is a command line application that executes tasks. - Options: - - -i, --info Print execution info (duration, statuscode) after task execution + -i, --info Print execution info (duration, exit status) after task execution -w, --workdir Specify the working directory -d, --directory Specify the dogfiles' directory -h, --help Print usage information and help @@ -52,29 +44,6 @@ Need help? --> dog --help More info --> https://github.com/dogtools/dog`) } -func printTasks(tm types.TaskMap) { - - maxCharSize := 0 - for taskName, task := range tm { - if task.Description != "" && len(taskName) > maxCharSize { - maxCharSize = len(taskName) - } - } - - var tasks []string - for taskName, task := range tm { - if task.Description != "" { - tasks = append(tasks, taskName) - } - } - sort.Strings(tasks) - - for _, taskName := range tasks { - spaces := strings.Repeat(" ", maxCharSize-len(taskName)+2) - fmt.Printf("%s%s%s\n", taskName, spaces, tm[taskName].Description) - } -} - func parseArgs(args []string) (a userArgs, err error) { // default values @@ -102,18 +71,16 @@ func parseArgs(args []string) (a userArgs, err error) { if i == 0 && len(args) == 1 && a.taskName == "" { a.help = true return a, nil - } else { - return a, fmt.Errorf("Error: %s doesn't accept additional parameters", arg) } + return a, fmt.Errorf("Error: %s doesn't accept additional parameters", arg) } if arg == "--version" || arg == "-v" { if i == 0 && len(args) == 1 && a.taskName == "" { a.version = true return a, nil - } else { - return a, fmt.Errorf("Error: %s doesn't accept additional parameters", arg) } + return a, fmt.Errorf("Error: %s doesn't accept additional parameters", arg) } if arg == "--info" || arg == "-i" { diff --git a/cmd/dog/main.go b/cmd/dog/main.go new file mode 100644 index 0000000..73487a3 --- /dev/null +++ b/cmd/dog/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/dogtools/dog" +) + +const version = "v0.4.0" + +func main() { + // parse cli arguments + a, err := parseArgs(os.Args[1:]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if a.help { + printHelp() + os.Exit(0) + } + + if a.version { + printVersion() + os.Exit(0) + } + + // parse dogfile + var d dog.Dogfile + if err = d.ParseFromDisk(a.directory); err != nil { + printNoValidDogfile() + os.Exit(1) + } + dog.DeprecationWarnings(os.Stderr) + + if a.taskName != "" { + if a.info { + dog.ProvideExtraInfo = true + } + + if d.Tasks[a.taskName] != nil { + if a.workdir != "" { + d.Tasks[a.taskName].Workdir = a.workdir + } + if d.Tasks[a.taskName].Workdir == "" { + d.Tasks[a.taskName].Workdir = a.directory + } + } else { + fmt.Println("Unknown task name:", a.taskName) + os.Exit(1) + } + + // generate task chain + var tc dog.TaskChain + if err = tc.Generate(d, a.taskName); err != nil { + fmt.Println(err) + os.Exit(1) + } + + // run task chain + err = tc.Run(os.Stdout, os.Stderr) + if err != nil { + os.Exit(2) + } + + } else { + printTasks(d) + os.Exit(0) + } +} + +// print tasks with description +func printTasks(d dog.Dogfile) { + maxCharSize := 0 + for taskName, task := range d.Tasks { + if task.Description != "" && len(taskName) > maxCharSize { + maxCharSize = len(taskName) + } + } + + var tasks []string + for taskName, task := range d.Tasks { + if task.Description != "" { + tasks = append(tasks, taskName) + } + } + sort.Strings(tasks) + + for _, taskName := range tasks { + spaces := strings.Repeat(" ", maxCharSize-len(taskName)+2) + fmt.Printf("%s%s%s\n", taskName, spaces, d.Tasks[taskName].Description) + } +} diff --git a/examples/hello/hello.go b/examples/hello/hello.go new file mode 100644 index 0000000..a6f7fba --- /dev/null +++ b/examples/hello/hello.go @@ -0,0 +1,49 @@ +package main + +// This example shows how to define a Dogfile, parse it, generate the task +// chain and finally run it. + +import ( + "fmt" + "os" + + "github.com/dogtools/dog" +) + +func main() { + + // Define two tasks in the Dogfile format using YAML + dogfile := ` +- task: hello-dog + description: Say Hello + post: hello-world + code: echo "Hello Dog!" + +- task: hello-world + description: Say Hello Again + code: echo "Hello World!" +` + + // Parse Dogfile + var d dog.Dogfile + err := d.Parse([]byte(dogfile)) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Generate task chain that starts with 'hello-dog' but include both tasks + var tc dog.TaskChain + err = tc.Generate(d, "hello-dog") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Run task chain + err = tc.Run(os.Stdout, os.Stderr) + if err != nil { + fmt.Println(err) + os.Exit(2) + } +} diff --git a/examples/http-api/http-api.go b/examples/http-api/http-api.go new file mode 100644 index 0000000..fa105de --- /dev/null +++ b/examples/http-api/http-api.go @@ -0,0 +1,55 @@ +package main + +// This example shows an application exposing the execution of Dogfile tasks +// through an HTTP endpoint. + +import ( + "fmt" + "net/http" + "os" + + "github.com/dogtools/dog" +) + +// Dogfile object +var d dog.Dogfile + +func main() { + + // Parse Dogfile from current path + err := d.ParseFromDisk(".") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Launch the HTTP server + http.HandleFunc("/", handler) + err = http.ListenAndServe(":8080", nil) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func handler(w http.ResponseWriter, r *http.Request) { + + // Get task name from path + taskName := r.URL.Path[1:] + + // Generate task chain for the task named as the URL path + var tc dog.TaskChain + err := tc.Generate(d, taskName) + if err != nil { + fmt.Fprintf(w, "task chain generation failed: %s\n", err) + os.Exit(1) + } + + // Run task chain, HTTP client receives info about how task finished + err = tc.Run(os.Stdout, os.Stderr) + if err != nil { + fmt.Fprintf(w, "%s failed: %s\n", taskName, err) + os.Exit(2) + } + fmt.Fprintf(w, "%s finished\n", taskName) +} diff --git a/execute/executor.go b/execute/executor.go deleted file mode 100644 index e9373c5..0000000 --- a/execute/executor.go +++ /dev/null @@ -1,115 +0,0 @@ -package execute - -import ( - "bufio" - "io/ioutil" - "os" - "os/exec" - "syscall" - - "github.com/dogtools/dog/types" -) - -func writeTempFile(dir, prefix string, data string) (*os.File, error) { - f, err := ioutil.TempFile(dir, prefix) - if err != nil { - return f, err - } - - _, err = f.WriteString(data) - return f, err -} - -// Executor implements standard shell executor. -type Executor struct { - cmd string -} - -// NewExecutor returns a default executor with a cmd. -func NewExecutor(cmd string) *Executor { - return &Executor{ - cmd, - } -} - -// Exec executes the created tmp script and writes the output to the writer. -func (ex *Executor) Exec(t *types.Task, eventsChan chan *types.Event) error { - f, err := writeTempFile("", "dog", t.Run) - if err != nil { - return err - } - - defer func() { - if err := os.Remove(f.Name()); err != nil { - eventsChan <- types.NewOutputEvent(t.Name, []byte(err.Error())) - } - }() - - binary, err := exec.LookPath(ex.cmd) - if err != nil { - return err - } - - if t.Workdir != "" { - if err := os.Chdir(t.Workdir); err != nil { - return err - } - } - - cmd := exec.Command(binary, f.Name()) - - if err := gatherCmdOutput(t.Name, cmd, eventsChan); err != nil { - return err - } - - cmd.Stdin = os.Stdin - if err := cmd.Start(); err != nil { - return nil - } - - eventsChan <- types.NewStartEvent(t.Name) - - statusCode := 0 - if err := cmd.Wait(); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); !ok { - // For unknown error status codes set it to 1 - statusCode = 1 - } else { - statusCode = waitStatus.ExitStatus() - } - } - } - - eventsChan <- types.NewEndEvent(t.Name, statusCode) - - return nil -} - -func gatherCmdOutput(taskName string, cmd *exec.Cmd, eventsChan chan *types.Event) error { - stdoutReader, err := cmd.StdoutPipe() - if err != nil { - return err - } - - stderrReader, err := cmd.StderrPipe() - if err != nil { - return err - } - - stdoutScanner := bufio.NewScanner(stdoutReader) - stderrScanner := bufio.NewScanner(stderrReader) - go func() { - for stdoutScanner.Scan() { - eventsChan <- types.NewOutputEvent(taskName, stdoutScanner.Bytes()) - } - }() - - go func() { - for stderrScanner.Scan() { - eventsChan <- types.NewOutputEvent(taskName, stderrScanner.Bytes()) - } - }() - - return nil -} diff --git a/execute/runner.go b/execute/runner.go deleted file mode 100644 index af8d007..0000000 --- a/execute/runner.go +++ /dev/null @@ -1,198 +0,0 @@ -package execute - -import ( - "errors" - "fmt" - "os" - "strings" - "time" - - "github.com/dogtools/dog/types" -) - -type runner struct { - taskHierarchy map[string][]*types.Task - eventsChan chan *types.Event - printFooter bool -} - -func isCyclic(chain []*types.Task) bool { - maxLen := len(chain) / 2 - for i := 2; i < maxLen; i++ { - a := chain[:i] - b := chain[i : 2*i] - for x, c := range a { - if c != b[x] { - return false - } - } - return true - } - return false -} - -func generateChainFor(t *types.Task, tm types.TaskMap, chain []*types.Task) ([]*types.Task, error) { - var err error - if isCyclic(chain) { - return nil, errors.New("Task " + t.Name + " has a hook cycle") - } - - for _, preName := range t.Pre { - pre, found := tm[preName] - if !found { - return nil, errors.New( - "Task " + preName + " does not exist", - ) - } - - for _, prePre := range pre.Pre { - if prePre == t.Name { - return nil, errors.New("Task " + preName + " has a hook cycle") - } - } - - chain, err = generateChainFor(pre, tm, chain) - if err != nil { - return nil, err - } - } - - chain = append(chain, t) - - for _, postName := range t.Post { - post, found := tm[postName] - if !found { - return nil, errors.New( - "Task " + postName + " does not exist", - ) - } - chain, err = generateChainFor(post, tm, chain) - if err != nil { - return nil, err - } - } - - return chain, nil -} - -func buildHierarchy(tm types.TaskMap) (map[string][]*types.Task, error) { - th := make(map[string][]*types.Task, len(tm)) - - for n, t := range tm { - chain, err := generateChainFor(t, tm, []*types.Task{}) - if err != nil { - return nil, err - } - th[n] = chain - } - - return th, nil -} - -func formatDuration(d time.Duration) (s string) { - timeMsg := "" - - if d.Hours() > 1.0 { - timeMsg += fmt.Sprintf("%1.0fh", d.Hours()) - } - - if d.Minutes() > 1.0 { - timeMsg += fmt.Sprintf("%1.0fm", d.Minutes()) - } - - if d.Seconds() > 1.0 { - timeMsg += fmt.Sprintf("%1.0fs", d.Seconds()) - } else { - timeMsg += fmt.Sprintf("%1.3fs", d.Seconds()) - } - - return timeMsg -} - -// NewRunner creates a new runner that contains a list of all execution paths. -func NewRunner(tm types.TaskMap, printFooter bool) (*runner, error) { - th, err := buildHierarchy(tm) - if err != nil { - return nil, err - } - - return &runner{ - taskHierarchy: th, - eventsChan: make(chan *types.Event, 2048), - printFooter: printFooter, - }, nil -} - -// Run executes the execution path for a given task. -func (r *runner) Run(taskName string) { - tasks, found := r.taskHierarchy[taskName] - if !found { - fmt.Println("Task " + taskName + " does not exist") - os.Exit(1) - } - executors := map[string]*Executor{} - go func() { - for _, t := range tasks { - var e *Executor - if t.Executor == "" { - e = NewExecutor("sh") - } else { - e, found = executors[t.Executor] - if !found { - e = NewExecutor(t.Executor) - executors[t.Executor] = e - } - } - - modifiedEnvvars := map[string]bool{} - - for _, e := range t.Env { - pair := strings.SplitN(e, "=", 2) - if len(pair) != 2 { - fmt.Println("Error: env var invalid for task", t.Name) - os.Exit(1) - } - - if os.Getenv(pair[0]) == "" { - os.Setenv(pair[0], pair[1]) - modifiedEnvvars[pair[0]] = true - } - } - - if err := e.Exec(t, r.eventsChan); err != nil { - fmt.Println(err) - os.Exit(1) - } - - for k := range modifiedEnvvars { - os.Setenv(k, "") - } - } - }() - r.waitFor((tasks[len(tasks)-1]).Name) -} - -func (r *runner) waitFor(taskName string) { - var startTime time.Time - - for { - select { - case event := <-r.eventsChan: - switch event.Type { - case types.StartEvent: - startTime = event.Time - case types.OutputEvent: - fmt.Println(string(event.Body)) - case types.EndEvent: - if r.printFooter { - fmt.Printf("-- %s took %s and finished with status code %d\n", - event.Task, formatDuration(time.Since(startTime)), event.ExitCode) - } - - if event.ExitCode != 0 || event.Task == taskName { - os.Exit(event.ExitCode) - } - } - } - } -} diff --git a/img/dog_logo.png b/img/dog_logo.png new file mode 100644 index 0000000..e710efe Binary files /dev/null and b/img/dog_logo.png differ diff --git a/init.go b/init.go new file mode 100644 index 0000000..ec36609 --- /dev/null +++ b/init.go @@ -0,0 +1,25 @@ +package dog + +import "runtime" + +// DefaultRunner defines the runner to use in case the task does not specify it. +// +// The value is automatically assigned based on the operating system when the +// package initializes. +var DefaultRunner string + +// ProvideExtraInfo specifies if dog needs to provide execution info (duration, +// exit status) after task execution. +var ProvideExtraInfo bool + +// deprecation warning flags +var deprecationWarningRun bool +var deprecationWarningExec bool + +func init() { + if runtime.GOOS == "windows" { + DefaultRunner = "cmd" // not implemented yet + } else { + DefaultRunner = "sh" + } +} diff --git a/main.go b/main.go deleted file mode 100644 index aa01d25..0000000 --- a/main.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/dogtools/dog/execute" - "github.com/dogtools/dog/parser" - "github.com/joho/godotenv" -) - -const version = "v0.3.0" - -func main() { - // if .env file exists (in same dir as Dogfile), load values into env - if _, err := os.Stat(`./.env`); !os.IsNotExist(err) { - err = godotenv.Load() - if err != nil { - fmt.Println("Error loading .env file") - os.Exit(1) - } - } - - a, err := parseArgs(os.Args[1:]) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if a.help { - printHelp() - os.Exit(0) - } - - if a.version { - printVersion() - os.Exit(0) - } - - tm, err := parser.LoadDogFile(a.directory) - if err != nil { - printNoValidDogfile() - os.Exit(1) - } - - if a.taskName != "" { - if tm[a.taskName] != nil { - if a.workdir != "" { - tm[a.taskName].Workdir = a.workdir - } - if tm[a.taskName].Workdir == "" { - tm[a.taskName].Workdir = a.directory - } - } - - runner, err := execute.NewRunner(tm, a.info) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - runner.Run(a.taskName) - } else { - printTasks(tm) - os.Exit(0) - } -} diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..3544831 --- /dev/null +++ b/parse.go @@ -0,0 +1,260 @@ +package dog + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "regexp" + + "github.com/ghodss/yaml" +) + +// ErrMalformedStringArray means that a task have a value of +// pre, post or env that can't be parsed as an array of strings. +var ErrMalformedStringArray = errors.New("Malformed strings array") + +// ErrNoDogfile means that the application is unable to find +// a Dogfile in the specified directory. +var ErrNoDogfile = errors.New("No dogfile found") + +// Dogfile contains tasks defined in the Dogfile format. +type Dogfile struct { + + // Tasks is used to map task objects by their name. + Tasks map[string]*Task + + // Path is an optional field that stores the directory + // where the Dogfile is found. + Path string + + // Files is an optional field that stores the full path + // of each Dogfile used to define the Dogfile object. + Files []string +} + +// TaskYAML represents a task written in the Dogfile format. +type taskYAML struct { + Name string `json:"task"` + Description string `json:"description,omitempty"` + + Code string `json:"code"` + Run string `json:"run"` // backwards compatibility for 'code' + + Runner string `json:"runner,omitempty"` + Exec string `json:"exec,omitempty"` // backwards compatibility for 'runner' + + Pre interface{} `json:"pre,omitempty"` + Post interface{} `json:"post,omitempty"` + Env interface{} `json:"env,omitempty"` + Workdir string `json:"workdir,omitempty"` +} + +// Parse accepts a slice of bytes and parses it following the Dogfile Spec. +func (d *Dogfile) Parse(p []byte) error { + var tasks []*taskYAML + + err := yaml.Unmarshal(p, &tasks) + if err != nil { + return err + } + + for _, parsedTask := range tasks { + if _, ok := d.Tasks[parsedTask.Name]; ok { + return fmt.Errorf("Duplicated task name %s", parsedTask.Name) + } else if !validTaskName(parsedTask.Name) { + return fmt.Errorf("Invalid name for task %s", parsedTask.Name) + } else { + task := &Task{ + Name: parsedTask.Name, + Description: parsedTask.Description, + Code: parsedTask.Code, + Runner: parsedTask.Runner, + Workdir: parsedTask.Workdir, + } + + // convert pre-tasks, post-tasks and environment variables + // into []string + if task.Pre, err = parseStringSlice(parsedTask.Pre); err != nil { + return err + } + if task.Post, err = parseStringSlice(parsedTask.Post); err != nil { + return err + } + if task.Env, err = parseStringSlice(parsedTask.Env); err != nil { + return err + } + + // backwards compatibility support for 'run' and 'exec', now called + // 'code' and 'runner' respectively. + if parsedTask.Code == "" && parsedTask.Run != "" { + deprecationWarningRun = true + task.Code = parsedTask.Run + } + if parsedTask.Runner == "" && parsedTask.Exec != "" { + deprecationWarningExec = true + task.Runner = parsedTask.Exec + } + + // set default runner if not specified + if task.Runner == "" { + task.Runner = DefaultRunner + } + + if d.Tasks == nil { + d.Tasks = make(map[string]*Task) + } + d.Tasks[task.Name] = task + } + } + + return nil +} + +// DeprecationWarnings writes deprecation warnings if they have been found on +// parse time. +// +// Call it with os.Stderr as a parameter to print warnings to STDERR. +func DeprecationWarnings(w io.Writer) { + if deprecationWarningRun { + fmt.Fprintln(w, + "dog: 'run' directive will be deprecated in v0.6.0, use 'code' instead.") + } + if deprecationWarningExec { + fmt.Fprintln(w, + "dog: 'exec' directive will be deprecated in v0.6.0, use 'runner' instead.") + } +} + +// parseStringSlice takes an interface from a pre, post or env field +// and returns a slice of strings representing the found values. +func parseStringSlice(str interface{}) ([]string, error) { + switch h := str.(type) { + case string: + return []string{h}, nil + case []interface{}: + s := make([]string, len(h)) + for i, hook := range h { + sHook, ok := hook.(string) + if !ok { + return nil, ErrMalformedStringArray + } + s[i] = sHook + } + return s, nil + case nil: + return []string{}, nil + default: + return nil, ErrMalformedStringArray + } +} + +// ParseFromDisk finds a Dogfile in disk and parses it. +func (d *Dogfile) ParseFromDisk(dir string) error { + if dir == "" { + dir = "." + } + + dir, err := filepath.Abs(dir) + if err != nil { + return err + } + d.Path = dir + + files, err := FindDogfiles(dir) + if err != nil { + return err + } + if len(files) == 0 { + return ErrNoDogfile + } + d.Files = files + + for _, file := range d.Files { + var fileData []byte + fileData, err = ioutil.ReadFile(file) + if err != nil { + return err + } + + if err = d.Parse(fileData); err != nil { + return err + } + } + + return nil +} + +// Validate checks that all tasks in a Dogfile are valid. +func (d *Dogfile) Validate() error { + for _, t := range d.Tasks { + if err := t.Validate(); err != nil { + return err + } + } + return nil +} + +// FindDogfiles finds Dogfiles in disk for a given path. +// +// It traverses directories until it finds one containing Dogfiles. +// If such a directory is found, the function returns the full path +// for each valid Dogfile in that directory. +func FindDogfiles(p string) ([]string, error) { + var dogfilePaths []string + + currentPath, err := filepath.Abs(p) + if err != nil { + return nil, err + } + + for { + var files []os.FileInfo + files, err = ioutil.ReadDir(currentPath) + if err != nil { + return nil, err + } + + for _, file := range files { + if validDogfileName(file.Name()) { + dogfilePath := path.Join(currentPath, file.Name()) + dogfilePaths = append(dogfilePaths, dogfilePath) + } + } + + if len(dogfilePaths) > 0 { + return dogfilePaths, nil + } + + nextPath := path.Dir(currentPath) + if nextPath == currentPath { + return dogfilePaths, nil + } + currentPath = nextPath + } +} + +// validDogfileName checks if a Dogfile name is valid as defined +// by the Dogfile Spec. +func validDogfileName(name string) bool { + var match bool + match, err := regexp.MatchString("^(Dogfile|🐕)", name) + if err != nil { + return false + } + return match +} + +// validTaskName checks if a task name is valid as defined +// by the Dogfile Spec. +func validTaskName(name string) bool { + var match bool + match, err := regexp.MatchString("^[a-z0-9-]+$", name) + if err != nil { + return false + } + return match +} diff --git a/parser/parser.go b/parser/parser.go deleted file mode 100644 index 0dc406f..0000000 --- a/parser/parser.go +++ /dev/null @@ -1,163 +0,0 @@ -package parser - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - "path" - "path/filepath" - "regexp" - - "github.com/dogtools/dog/types" - "github.com/ghodss/yaml" -) - -var ErrMalformedStringArray = errors.New("Malformed strings array") -var ErrNoDogfile = errors.New("No dogfile found") - -type task struct { - Name string `json:"task"` - Description string `json:"description,omitempty"` - Time bool `json:"time,omitempty"` - Run string `json:"run"` - Executor string `json:"exec,omitempty"` - Pre interface{} `json:"pre,omitempty"` - Post interface{} `json:"post,omitempty"` - Env interface{} `json:"env,omitempty"` - Workdir string `json:"workdir,omitempty"` -} - -func parseStringSlice(str interface{}) ([]string, error) { - switch h := str.(type) { - case string: - return []string{h}, nil - case []interface{}: - s := make([]string, len(h)) - for i, hook := range h { - sHook, ok := hook.(string) - if !ok { - return nil, ErrMalformedStringArray - } - s[i] = sHook - } - return s, nil - case nil: - return []string{}, nil - default: - return nil, ErrMalformedStringArray - } -} - -// ParseDogfile takes a byte slice and process it to return a TaskMap. -func ParseDogfile(d []byte, tm types.TaskMap) (err error) { - const validTaskName = "^[a-z0-9-]+$" - var tasksToParse []*task - - err = yaml.Unmarshal(d, &tasksToParse) - if err != nil { - return - } - - for _, t := range tasksToParse { - if _, ok := tm[t.Name]; ok { - return fmt.Errorf("Duplicated task name %s", t.Name) - } else if matches, _ := regexp.MatchString(validTaskName, t.Name); !matches { - return fmt.Errorf("Invalid name for task %s", t.Name) - } else { - task := &types.Task{ - Name: t.Name, - Description: t.Description, - Time: t.Time, - Run: t.Run, - Executor: t.Executor, - Workdir: t.Workdir, - } - if task.Pre, err = parseStringSlice(t.Pre); err != nil { - return - } - if task.Post, err = parseStringSlice(t.Post); err != nil { - return - } - if task.Env, err = parseStringSlice(t.Env); err != nil { - return - } - tm[t.Name] = task - } - } - - return -} - -// FindDogFiles finds Dogfiles in disk, traversing directories up from the -// given path until it finds a directory containing Dogfiles, and returns -// their paths. -func FindDogFiles(startPath string) (dogfilePaths []string, err error) { - const validDogfileName = "^(Dogfile|🐕)" - currentPath, err := filepath.Abs(startPath) - if err != nil { - return - } - - for { - var files []os.FileInfo - files, err = ioutil.ReadDir(currentPath) - if err != nil { - return - } - - for _, file := range files { - var match bool - match, err = regexp.MatchString(validDogfileName, file.Name()) - if err != nil { - return - } - - if match { - dogfilePath := path.Join(currentPath, file.Name()) - dogfilePaths = append(dogfilePaths, dogfilePath) - } - } - - if len(dogfilePaths) > 0 { - return - } - - nextPath := path.Dir(currentPath) - if nextPath == currentPath { - return - } - currentPath = nextPath - } -} - -// LoadDogFile finds a Dogfile in disk, parses YAML and returns a map. -func LoadDogFile(directory string) (tm types.TaskMap, err error) { - if directory == "" { - directory = "." - } - - tm = make(types.TaskMap) - files, err := FindDogFiles(directory) - if err != nil { - return - } - if len(files) == 0 { - err = ErrNoDogfile - return - } - - for _, file := range files { - var fileData []byte - fileData, err = ioutil.ReadFile(file) - if err != nil { - return - } - - if err = ParseDogfile(fileData, tm); err != nil { - return - } - } - - return -} diff --git a/run/cmd.go b/run/cmd.go new file mode 100644 index 0000000..9d3df67 --- /dev/null +++ b/run/cmd.go @@ -0,0 +1,73 @@ +package run + +import ( + "errors" + "io/ioutil" + "os" + "os/exec" +) + +// runCmd embeds and extends exec.Cmd. +type runCmd struct { + exec.Cmd + tmpFile *os.File +} + +// Wait waits until the command finishes running and provides exit information. +// +// This method overrites the Wait method that comes from the embedded exec.Cmd +// type, adding the removal of the temporary file. +func (c *runCmd) Wait() error { + defer func() { + _ = os.Remove(c.tmpFile.Name()) + }() + + err := c.Cmd.Wait() + if err != nil { + return err + } + return nil +} + +// writeTempFile copies the code in a temporary file that will get passed as an +// argument to the runner (as in `sh `). +func (c *runCmd) writeTempFile(data string) error { + f, err := ioutil.TempFile("", "dog") + if err != nil { + return err + } + _, err = f.WriteString(data) + if err != nil { + return err + } + c.tmpFile = f + return nil +} + +// newCmdRunner creates a cmd type runner of the choosen executor. +func newCmdRunner(runner string, code string, workdir string, env []string) (Runner, error) { + if code == "" { + return nil, errors.New("No code specified to run") + } + + cmd := runCmd{} + + path, err := exec.LookPath(runner) + if err != nil { + return nil, err + } + cmd.Path = path + + err = cmd.writeTempFile(code) + if err != nil { + return nil, err + } + + cmd.Stdin = os.Stdin + cmd.Dir = workdir + cmd.Args = append(cmd.Args, runner, cmd.tmpFile.Name()) + cmd.Env = append(cmd.Env, os.Environ()...) + cmd.Env = append(cmd.Env, env...) + + return &cmd, nil +} diff --git a/run/run.go b/run/run.go new file mode 100644 index 0000000..69251e8 --- /dev/null +++ b/run/run.go @@ -0,0 +1,67 @@ +package run + +import ( + "bufio" + "io" +) + +// Runner just runs anything. +type Runner interface { + // StdoutPipe returns a pipe that will be connected to the runner's + // standard output when the command starts. + StdoutPipe() (io.ReadCloser, error) + + // StderrPipe returns a pipe that will be connected to the runner's + // standard error when the command starts. + StderrPipe() (io.ReadCloser, error) + + // Start starts the runner but does not wait for it to complete. + Start() error + + // Wait waits for the runner to exit. It must have been started by Start. + // + // The returned error is nil if the runner has no problems copying + // stdin, stdout, and stderr, and exits with a zero exit status. + Wait() error +} + +// NewShRunner creates a system standard shell script runner. +func NewShRunner(code string, workdir string, env []string) (Runner, error) { + return newCmdRunner("sh", code, workdir, env) +} + +// NewBashRunner creates a Bash runner. +func NewBashRunner(code string, workdir string, env []string) (Runner, error) { + return newCmdRunner("bash", code, workdir, env) +} + +// NewPythonRunner creates a Python runner. +func NewPythonRunner(code string, workdir string, env []string) (Runner, error) { + return newCmdRunner("python", code, workdir, env) +} + +// NewRubyRunner creates a Ruby runner. +func NewRubyRunner(code string, workdir string, env []string) (Runner, error) { + return newCmdRunner("ruby", code, workdir, env) +} + +// NewPerlRunner creates a Perl runner. +func NewPerlRunner(code string, workdir string, env []string) (Runner, error) { + return newCmdRunner("perl", code, workdir, env) +} + +// GetOutputs is a helper method that returns both stdout and stderr outputs +// from the runner. +func GetOutputs(r Runner) (io.Reader, io.Reader, error) { + stdout, err := r.StdoutPipe() + if err != nil { + return nil, nil, err + } + + stderr, err := r.StderrPipe() + if err != nil { + return nil, nil, err + } + + return bufio.NewReader(stdout), bufio.NewReader(stderr), nil +} diff --git a/scripts/test-dogfiles.sh b/scripts/test-dogfiles.sh index 3c23098..e9ce261 100755 --- a/scripts/test-dogfiles.sh +++ b/scripts/test-dogfiles.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -e cd testdata for task in $(dog | awk '{print $1}'); do diff --git a/task.go b/task.go new file mode 100644 index 0000000..d826208 --- /dev/null +++ b/task.go @@ -0,0 +1,53 @@ +package dog + +import "fmt" + +// Task represents a task described in the Dogfile format. +type Task struct { + // Name of the task. + Name string + + // Description of the task. + Description string + + // The code that will be executed. + Code string + + // Defaults to operating system main shell. + Runner string + + // Pre-hooks execute other tasks before starting the current one. + Pre []string + + // Post-hooks are analog to pre-hooks but they are executed after + // current task finishes its execution. + Post []string + + // Default values for environment variables can be provided in the Dogfile. + // They can be modified at execution time. + Env []string + + // Sets the working directory for the task. Relative paths are + // considered relative to the location of the Dogfile. + Workdir string +} + +// Validate runs a series of validations against a task. +// +// It checks if it has a non standard name and also if +// the resulting task chain have undesired cycles. +func (t *Task) Validate() error { + + if !validTaskName(t.Name) { + return fmt.Errorf("Invalid name for task %s", t.Name) + } + + var d Dogfile + d.Tasks[t.Name] = t + + var tc TaskChain + if err := tc.Generate(d, t.Name); err != nil { + return err + } + return nil +} diff --git a/testdata/.env b/testdata/.env deleted file mode 100644 index b75a4c9..0000000 --- a/testdata/.env +++ /dev/null @@ -1 +0,0 @@ -S3_BUCKET=sdfghjkjhgfdfghjksdfghjklkjhgfghsjghjsdgfhjbhsjkdfhjksgdfhjk \ No newline at end of file diff --git a/testdata/Dogfile-bash.json b/testdata/Dogfile-bash.json index 974b965..d93ed26 100644 --- a/testdata/Dogfile-bash.json +++ b/testdata/Dogfile-bash.json @@ -1,8 +1,8 @@ [ { "task": "bash-conditional-echo-json", - "description": "Conditional and echo from BASH", - "exec": "bash", - "run": "if [[ 1 == 1 ]]; then\n echo \"Yo Dawg ! Now parsing JSON too !!!\"\nfi\n" + "description": "Conditional and echo from BASH in JSON Dogfile", + "runner": "bash", + "code": "if [[ 1 == 1 ]]; then\n echo \"Yo Dawg ! Now parsing JSON too !!!\"\nfi\n" } -] \ No newline at end of file +] diff --git a/testdata/Dogfile-bash.yml b/testdata/Dogfile-bash.yml index eeb5759..4c27254 100644 --- a/testdata/Dogfile-bash.yml +++ b/testdata/Dogfile-bash.yml @@ -1,7 +1,7 @@ - task: bash-conditional-echo description: Conditional and echo from BASH - exec: bash - run: | + runner: bash + code: | if [[ 1 == 1 ]]; then echo "Hello Dog from BASH!" fi diff --git a/testdata/Dogfile-env.yml b/testdata/Dogfile-env.yml deleted file mode 100644 index b441c67..0000000 --- a/testdata/Dogfile-env.yml +++ /dev/null @@ -1,6 +0,0 @@ -- task: bash-env-file-echo - description: Conditional and echo from BASH - exec: bash - run: | - echo "Hello env values !" - echo $S3_BUCKET diff --git a/testdata/Dogfile-hooks.yml b/testdata/Dogfile-hooks.yml index 6eda68c..705c2e1 100644 --- a/testdata/Dogfile-hooks.yml +++ b/testdata/Dogfile-hooks.yml @@ -1,18 +1,15 @@ - task: pre-task - description: Pre Task - run: echo "I am a prehook" + code: echo "I am a prehook" - task: post-task - description: Post Task post: post-task-2 - run: echo "I am a post hook" + code: echo "I am a post hook" - task: post-task-2 - description: Post Task 2 - run: echo "I am another post hook" + code: echo "I am another post hook" - task: task-with-hooks description: Run a task with pre and post hooks pre: pre-task post: post-task - run: echo "I am the main task" + code: echo "I am the main task" diff --git a/testdata/Dogfile-longrun.yml b/testdata/Dogfile-longrun.yml index f443185..81a3b9f 100644 --- a/testdata/Dogfile-longrun.yml +++ b/testdata/Dogfile-longrun.yml @@ -1,6 +1,6 @@ - task: long-run description: A task that takes 5 seconds - run: | + code: | for i in $(seq 1 5); do sleep 1 echo "> $i/5" diff --git a/testdata/Dogfile-mustfail.yml b/testdata/Dogfile-mustfail.yml index 22a6b9d..569024e 100644 --- a/testdata/Dogfile-mustfail.yml +++ b/testdata/Dogfile-mustfail.yml @@ -1,4 +1,4 @@ - task: must-fail description: A task that fails - exec: ruby - run: putss "I fail" + runner: ruby + code: putss "I fail" diff --git a/testdata/Dogfile-perl.yml b/testdata/Dogfile-perl.yml new file mode 100644 index 0000000..d7d9fc1 --- /dev/null +++ b/testdata/Dogfile-perl.yml @@ -0,0 +1,4 @@ +- task: perl-print + description: Perl print says hello + runner: perl + code: print "Hello from Perl!\n"; diff --git a/testdata/Dogfile-python.yml b/testdata/Dogfile-python.yml index 10e97a6..386c493 100644 --- a/testdata/Dogfile-python.yml +++ b/testdata/Dogfile-python.yml @@ -1,4 +1,4 @@ - task: python-print description: Python print says hello - exec: python - run: print("Hello Dog from Python!") + runner: python + code: print("Hello Dog from Python!") diff --git a/testdata/Dogfile-ruby.yml b/testdata/Dogfile-ruby.yml index cb4f550..847c867 100644 --- a/testdata/Dogfile-ruby.yml +++ b/testdata/Dogfile-ruby.yml @@ -1,4 +1,4 @@ - task: ruby-puts description: Ruby puts says hello - exec: ruby - run: puts "Hello Dog from Ruby!" + runner: ruby + code: puts "Hello Dog from Ruby!" diff --git a/testdata/Dogfile-sh.yml b/testdata/Dogfile-sh.yml index ce262e9..c5303e3 100644 --- a/testdata/Dogfile-sh.yml +++ b/testdata/Dogfile-sh.yml @@ -1,6 +1,6 @@ - task: sh-blank-lines description: Output with blank lines - run: | + code: | for i in $(seq 1 5); do echo echo $i @@ -8,11 +8,11 @@ - task: sh-echo description: Shell says hello - run: echo "Hello Dog from sh!" + code: echo "Hello Dog from sh!" - task: sh-count-animals description: Count animals - run: | + code: | mammals="dog cat dolphin dog cat human dog dog cat" reptiles="crocodile lizard snake" birds="owl eagle owl" diff --git a/types/event.go b/types/event.go deleted file mode 100644 index 7618b13..0000000 --- a/types/event.go +++ /dev/null @@ -1,45 +0,0 @@ -package types - -import "time" - -type EventType int - -const ( - StartEvent = EventType(iota + 1) - OutputEvent - EndEvent -) - -type Event struct { - Type EventType - Task string - Time time.Time - Body []byte - ExitCode int -} - -func NewStartEvent(taskName string) *Event { - return &Event{ - Type: StartEvent, - Task: taskName, - Time: time.Now(), - } -} - -func NewOutputEvent(taskName string, body []byte) *Event { - return &Event{ - Type: OutputEvent, - Task: taskName, - Time: time.Now(), - Body: body, - } -} - -func NewEndEvent(taskName string, exitCode int) *Event { - return &Event{ - Type: EndEvent, - Task: taskName, - Time: time.Now(), - ExitCode: exitCode, - } -} diff --git a/types/task.go b/types/task.go deleted file mode 100644 index 90f6c9d..0000000 --- a/types/task.go +++ /dev/null @@ -1,17 +0,0 @@ -package types - -// Task is a representation of a dogfile task -type Task struct { - Name string - Description string - Time bool - Run string - Executor string - Pre []string - Post []string - Env []string - Workdir string -} - -// TaskMap is a map in which the key is a task name and the value is a Task object -type TaskMap map[string]*Task