Skip to content

Commit

Permalink
Introduce before and after steps for a task. (#56)
Browse files Browse the repository at this point in the history
* Introduce before and after steps for a task.

A task is now able to have several steps which are executed before and after its own execution. The config syntax for this is the same as for the task itself: 'command', 'script' and 'exec' are supported. A task and all of the preceeding steps will still be executed even when the before steps are failing.

Running a task now returns a list of errors which are logged according to their occurrence for easier troubleshooting.

Co-authored-by: Jens Petersohn <[email protected]>
  • Loading branch information
jenpet and Jens Petersohn authored Jun 20, 2022
1 parent fe26677 commit a1ab412
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 50 deletions.
24 changes: 24 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,30 @@ $ robo aws-stage ...

Note that you cannot use shell featurs in the environment key.

### Setup / Cleanup
Some tasks or even your entire robo configuration may require steps upfront for setup or afterwards for a cleanup. The keywords `before` and `after` can be embedded into a task or into the overall robo configuration. It has the same executable syntax as a task: `script`, `exec` and `command`.
Defining it on a task level causes the steps to be executed before (respectively after) the task. Global before or after steps are invoked for _every_ task in the configuration.
All steps get interpolated the same way tasks and variables are interpolated.

```yaml
before:
- command: echo "global before {{ .foo }}"
after:
- script: /global/after-script.sh
foo:
before:
- command: echo "local before {{ .foo }}"
- exec: git pull -r
after:
- command: echo "local after"
- exec: git reset --hard HEAD
exec: git status
variables:
foo: bar
```

### Templates

Task `list` and `help` output may be re-configured, for example if you
Expand Down
27 changes: 22 additions & 5 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/fatih/color"
"github.com/tj/robo/config"
"github.com/tj/robo/task"
)

// Template helpers.
Expand Down Expand Up @@ -104,16 +105,32 @@ func Help(c *config.Config, name string) {

// Run the task.
func Run(c *config.Config, name string, args []string) {
task, ok := c.Tasks[name]
t, ok := c.Tasks[name]
if !ok {
Fatalf("undefined task %q", name)
}
lookupPath := filepath.Dir(c.File)
t.LookupPath = lookupPath

var errs []error
if err := task.RunOptionals("before", "GLOBAL", c.Before, args, lookupPath, nil); err != nil {
errs = append(errs, err)
}

task.LookupPath = filepath.Dir(c.File)
if runErrs := t.Run(args); len(runErrs) > 0 {
errs = append(errs, runErrs...)
}

err := task.Run(args)
if err != nil {
Fatalf("error: %s", err)
if err := task.RunOptionals("after", "GLOBAL", c.After, args, lookupPath, nil); err != nil {
errs = append(errs, err)
}

if len(errs) > 0 {
var msg string
for _, err := range errs {
msg += fmt.Sprintf(" - %+v\n", err)
}
Fatalf("error(s): \n%s", msg)
}
}

Expand Down
27 changes: 20 additions & 7 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ package config

import (
"fmt"
"github.com/tj/robo/interpolation"
"gopkg.in/yaml.v2"
"io/ioutil"
"os/user"
"path"

"github.com/tj/robo/interpolation"
"gopkg.in/yaml.v2"

"github.com/tj/robo/task"
)

// Config represents the main YAML configuration
// loaded for Robo tasks.
type Config struct {
File string
Tasks map[string]*task.Task `yaml:",inline"`
Variables map[string]interface{}
Templates struct {
Before []*task.Runnable
After []*task.Runnable
File string
Tasks map[string]*task.Task `yaml:",inline"`
Variables map[string]interface{}
Templates struct {
List string
Help string
Variables string
Expand All @@ -37,6 +40,16 @@ func (c *Config) Eval() error {
if err != nil {
return fmt.Errorf("failed interpolating tasks. Error: %v", err)
}

err = interpolation.Optionals("before", c.Before, c.Variables)
if err != nil {
return fmt.Errorf("failed interpolating before optionals. Error: %v", err)
}

err = interpolation.Optionals("after", c.After, c.Variables)
if err != nil {
return fmt.Errorf("failed interpolating after optionals. Error: %v", err)
}
return nil
}

Expand Down Expand Up @@ -102,4 +115,4 @@ func NewString(s string) (*Config, error) {
}

return c, nil
}
}
16 changes: 15 additions & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@ import (
)

var s = `
before:
- command: echo "global before"
after:
- command: echo "global after"
foo:
summary: Command foo.
before:
- command: echo "before"
command: echo "foo"
after:
- command: echo "after"
bar:
summary: Command bar.
Expand Down Expand Up @@ -50,9 +60,13 @@ func TestNewString(t *testing.T) {
assert.Equal(t, `ssh bastion-stage -t robo`, c.Tasks["stage"].Command)
assert.Equal(t, `ssh bastion-prod -t robo`, c.Tasks["prod"].Command)

assert.Equal(t, `echo "global before"`, c.Before[0].Command)
assert.Equal(t, `echo "global after"`, c.After[0].Command)
assert.Equal(t, `foo`, c.Tasks["foo"].Name)
assert.Equal(t, `Command foo.`, c.Tasks["foo"].Summary)
assert.Equal(t, `echo "foo"`, c.Tasks["foo"].Command)
assert.Equal(t, `echo "before"`, c.Tasks["foo"].Before[0].Command)
assert.Equal(t, `echo "after"`, c.Tasks["foo"].After[0].Command)

assert.Equal(t, `Command bar.`, c.Tasks["bar"].Summary)
assert.Equal(t, `echo "bar"`, c.Tasks["bar"].Command)
Expand Down Expand Up @@ -82,4 +96,4 @@ func TestNew(t *testing.T) {
c, err := config.New(file)
assert.Equal(t, nil, err)
assert.Equal(t, file, c.File)
}
}
42 changes: 35 additions & 7 deletions interpolation/interpolation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ package interpolation
import (
"bytes"
"fmt"
"github.com/tj/robo/task"
"gopkg.in/yaml.v2"
"os"
"os/exec"
"regexp"
"strings"
"text/template"

"github.com/tj/robo/task"
"gopkg.in/yaml.v2"
)

var commandPattern = regexp.MustCompile("\\$\\((.+)\\)")

// Vars interpolates a given map of interfaces (strings or submaps) with itself
// returning it mit populated template values.
// returning it with populated template values.
func Vars(vars *map[string]interface{}) error {
b, err := yaml.Marshal(*vars)
if err != nil {
Expand Down Expand Up @@ -71,11 +72,12 @@ func captureCommandOutput(args string) (string, error) {
cmd.Stdout = &b
cmd.Stderr = os.Stderr
err := cmd.Run()
return strings.TrimSuffix(string(b.Bytes()), "\n"), err
return strings.TrimSuffix(b.String(), "\n"), err
}

// Tasks interpolates a given task with a set of data replacing placeholders
// in the command, summary, script, exec and envs property.
// in the command, summary, script, exec and envs properties. If applicable the optionals 'before' and 'after' are also
// interpolated.
func Tasks(tasks map[string]*task.Task, data map[string]interface{}) error {
for _, task := range tasks {
// interpolate the tasks main fields
Expand All @@ -91,13 +93,39 @@ func Tasks(tasks map[string]*task.Task, data map[string]interface{}) error {
return err
}

// interpolate a tasks environment data
// interpolate a task's environment data
for i, item := range task.Env {
if err := interpolate("env-var", data, &item); err != nil {
return err
}
task.Env[i] = item
}

// interpolate a task's before and after steps
if err := Optionals("before", task.Before, data); err != nil {
return err
}
if err := Optionals("after", task.After, data); err != nil {
return err
}
}
return nil
}

// Optionals interpolates a list of runnables (i.e. optional steps for a task or the overall robo configuration).
func Optionals(id string, rs []*task.Runnable, data map[string]interface{}) error {
for i, step := range rs {
err := interpolate(
id,
data,
&step.Command,
&step.Exec,
&step.Script,
)
if err != nil {
return err
}
rs[i] = step
}
return nil
}
Expand All @@ -119,4 +147,4 @@ func interpolate(name string, data interface{}, temps ...*string) error {
*temp = string(b.Bytes())
}
return nil
}
}
38 changes: 27 additions & 11 deletions interpolation/interpolation_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package interpolation

import (
"testing"

"github.com/bmizerany/assert"
"github.com/tj/robo/task"
"testing"
)

func TestVars_whenValueReferencesOtherKey_shouldReplaceAccordingly(t *testing.T) {
Expand All @@ -21,10 +22,9 @@ func TestVars_whenValueReferencesOtherKey_shouldReplaceAccordingly(t *testing.T)
func TestVars_whenValueIsCommand_shouldReplaceWithCommandResult(t *testing.T) {
vars := map[string]interface{}{
"foo": "$(echo Hello)",
"bar":
map[string]interface{}{
"sub": "$(echo World!)",
},
"bar": map[string]interface{}{
"sub": "$(echo World!)",
},
}

err := Vars(&vars)
Expand All @@ -36,20 +36,36 @@ func TestVars_whenValueIsCommand_shouldReplaceWithCommandResult(t *testing.T) {

func TestTasks(t *testing.T) {
tk := task.Task{
Summary: "This task handles {{ .foo }} World!",
Command: "echo {{ .foo }} World!",
Script: "/path/to/{{ .foo }}.sh",
Exec: "{{ .foo }} World!",
Env: []string{"bar={{ .foo }} World!"},
Summary: "This task handles {{ .foo }} World!",
Before: []*task.Runnable{
{Command: "{{ .foo }}"},
{Script: "{{ .foo }}"},
{Exec: "{{ .foo }}"},
},
After: []*task.Runnable{
{Command: "{{ .bar }}"},
{Script: "{{ .bar }}"},
{Exec: "{{ .bar }}"},
},
Command: "echo {{ .foo }} World!",
Script: "/path/to/{{ .foo }}.sh",
Exec: "{{ .foo }} World!",
Env: []string{"bar={{ .foo }} World!"},
}

vars := map[string]interface{}{"foo": "Hello"}
vars := map[string]interface{}{"foo": "Hello", "bar": "Bye"}

err := Tasks(map[string]*task.Task{"tk": &tk}, vars)
assert.Equal(t, nil, err)
assert.Equal(t, "This task handles Hello World!", tk.Summary)
assert.Equal(t, "echo Hello World!", tk.Command)
assert.Equal(t, "/path/to/Hello.sh", tk.Script)
assert.Equal(t, "Hello World!", tk.Exec)
assert.Equal(t, "Hello", tk.Before[0].Command)
assert.Equal(t, "Hello", tk.Before[1].Script)
assert.Equal(t, "Hello", tk.Before[2].Exec)
assert.Equal(t, "Bye", tk.After[0].Command)
assert.Equal(t, "Bye", tk.After[1].Script)
assert.Equal(t, "Bye", tk.After[2].Exec)
assert.Equal(t, "bar=Hello World!", tk.Env[0])
}
Loading

0 comments on commit a1ab412

Please sign in to comment.