matrix is done
Signed-off-by: Casey Lee <cplee@nektos.com>
This commit is contained in:
parent
5b7019cd0b
commit
f8fb88816a
8 changed files with 219 additions and 46 deletions
|
@ -62,7 +62,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||||
var eventName string
|
var eventName string
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
eventName = args[0]
|
eventName = args[0]
|
||||||
} else if events := planner.GetEvents(); len(events) > 1 {
|
} else if events := planner.GetEvents(); len(events) > 0 {
|
||||||
// set default event type to first event
|
// set default event type to first event
|
||||||
// this way user dont have to specify the event.
|
// this way user dont have to specify the event.
|
||||||
log.Debugf("Using detected workflow event: %s", events[0])
|
log.Debugf("Using detected workflow event: %s", events[0])
|
||||||
|
|
54
pkg/common/cartesian.go
Normal file
54
pkg/common/cartesian.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
// CartesianProduct takes map of lists and returns list of unique tuples
|
||||||
|
func CartesianProduct(mapOfLists map[string][]interface{}) []map[string]interface{} {
|
||||||
|
listNames := make([]string, 0)
|
||||||
|
lists := make([][]interface{}, 0)
|
||||||
|
for k, v := range mapOfLists {
|
||||||
|
listNames = append(listNames, k)
|
||||||
|
lists = append(lists, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
listCart := cartN(lists...)
|
||||||
|
|
||||||
|
rtn := make([]map[string]interface{}, 0)
|
||||||
|
for _, list := range listCart {
|
||||||
|
vMap := make(map[string]interface{})
|
||||||
|
for i, v := range list {
|
||||||
|
vMap[listNames[i]] = v
|
||||||
|
}
|
||||||
|
rtn = append(rtn, vMap)
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func cartN(a ...[]interface{}) [][]interface{} {
|
||||||
|
c := 1
|
||||||
|
for _, a := range a {
|
||||||
|
c *= len(a)
|
||||||
|
}
|
||||||
|
if c == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p := make([][]interface{}, c)
|
||||||
|
b := make([]interface{}, c*len(a))
|
||||||
|
n := make([]int, len(a))
|
||||||
|
s := 0
|
||||||
|
for i := range p {
|
||||||
|
e := s + len(a)
|
||||||
|
pi := b[s:e]
|
||||||
|
p[i] = pi
|
||||||
|
s = e
|
||||||
|
for j, n := range n {
|
||||||
|
pi[j] = a[j][n]
|
||||||
|
}
|
||||||
|
for j := len(n) - 1; j >= 0; j-- {
|
||||||
|
n[j]++
|
||||||
|
if n[j] < len(a[j]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n[j] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
28
pkg/common/cartesian_test.go
Normal file
28
pkg/common/cartesian_test.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCartisianProduct(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
input := map[string][]interface{}{
|
||||||
|
"foo": []interface{}{1, 2, 3, 4},
|
||||||
|
"bar": []interface{}{"a", "b", "c"},
|
||||||
|
"baz": []interface{}{false, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
output := CartesianProduct(input)
|
||||||
|
assert.Len(output, 24)
|
||||||
|
|
||||||
|
for _, v := range output {
|
||||||
|
assert.Len(v, 3)
|
||||||
|
|
||||||
|
assert.Contains(v, "foo")
|
||||||
|
assert.Contains(v, "bar")
|
||||||
|
assert.Contains(v, "baz")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -187,6 +187,9 @@ func ReadWorkflow(in io.Reader) (*Workflow, error) {
|
||||||
func (w *Workflow) GetJob(jobID string) *Job {
|
func (w *Workflow) GetJob(jobID string) *Job {
|
||||||
for id, j := range w.Jobs {
|
for id, j := range w.Jobs {
|
||||||
if jobID == id {
|
if jobID == id {
|
||||||
|
if j.Name == "" {
|
||||||
|
j.Name = id
|
||||||
|
}
|
||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,16 @@ import (
|
||||||
|
|
||||||
// RunContext contains info about current job
|
// RunContext contains info about current job
|
||||||
type RunContext struct {
|
type RunContext struct {
|
||||||
Config *Config
|
Config *Config
|
||||||
Matrix map[string]interface{}
|
Matrix map[string]interface{}
|
||||||
Run *model.Run
|
Run *model.Run
|
||||||
EventJSON string
|
EventJSON string
|
||||||
Env map[string]string
|
Env map[string]string
|
||||||
Tempdir string
|
Tempdir string
|
||||||
ExtraPath []string
|
ExtraPath []string
|
||||||
CurrentStep string
|
CurrentStep string
|
||||||
StepResults map[string]*stepResult
|
StepResults map[string]*stepResult
|
||||||
PlatformName string
|
ExprEval ExpressionEvaluator
|
||||||
}
|
}
|
||||||
|
|
||||||
type stepResult struct {
|
type stepResult struct {
|
||||||
|
@ -56,9 +56,6 @@ func (rc *RunContext) Close(ctx context.Context) error {
|
||||||
|
|
||||||
// Executor returns a pipeline executor for all the steps in the job
|
// Executor returns a pipeline executor for all the steps in the job
|
||||||
func (rc *RunContext) Executor() common.Executor {
|
func (rc *RunContext) Executor() common.Executor {
|
||||||
if img := platformImage(rc.PlatformName); img == "" {
|
|
||||||
return common.NewInfoExecutor(" \U0001F6A7 Skipping unsupported platform '%s'", rc.PlatformName)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := rc.setupTempDir()
|
err := rc.setupTempDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,6 +74,13 @@ func (rc *RunContext) Executor() common.Executor {
|
||||||
Success: true,
|
Success: true,
|
||||||
Outputs: make(map[string]string),
|
Outputs: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
rc.ExprEval = rc.NewStepExpressionEvaluator(s)
|
||||||
|
|
||||||
|
if !rc.EvalBool(s.If) {
|
||||||
|
log.Debugf("Skipping step '%s' due to '%s'", s.String(), s.If)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
common.Logger(ctx).Infof("\u2B50 Run %s", s)
|
common.Logger(ctx).Infof("\u2B50 Run %s", s)
|
||||||
err := rc.newStepExecutor(s)(ctx)
|
err := rc.newStepExecutor(s)(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -88,7 +92,36 @@ func (rc *RunContext) Executor() common.Executor {
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return common.NewPipelineExecutor(steps...).Finally(rc.Close)
|
return func(ctx context.Context) error {
|
||||||
|
defer rc.Close(ctx)
|
||||||
|
job := rc.Run.Job()
|
||||||
|
log := common.Logger(ctx)
|
||||||
|
if !rc.EvalBool(job.If) {
|
||||||
|
log.Debugf("Skipping job '%s' due to '%s'", job.Name, job.If)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
platformName := rc.ExprEval.Interpolate(rc.Run.Job().RunsOn)
|
||||||
|
if img := platformImage(platformName); img == "" {
|
||||||
|
log.Infof(" \U0001F6A7 Skipping unsupported platform '%s'", platformName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return common.NewPipelineExecutor(steps...)(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EvalBool evaluates an expression against current run context
|
||||||
|
func (rc *RunContext) EvalBool(expr string) bool {
|
||||||
|
if expr != "" {
|
||||||
|
v, err := rc.ExprEval.Evaluate(expr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error evaluating expression '%s' - %v", expr, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return v == "true"
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeMaps(maps ...map[string]string) map[string]string {
|
func mergeMaps(maps ...map[string]string) map[string]string {
|
||||||
|
@ -141,10 +174,10 @@ func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Ex
|
||||||
}
|
}
|
||||||
var cmd, entrypoint []string
|
var cmd, entrypoint []string
|
||||||
if containerSpec.Args != "" {
|
if containerSpec.Args != "" {
|
||||||
cmd = strings.Fields(containerSpec.Args)
|
cmd = strings.Fields(rc.ExprEval.Interpolate(containerSpec.Args))
|
||||||
}
|
}
|
||||||
if containerSpec.Entrypoint != "" {
|
if containerSpec.Entrypoint != "" {
|
||||||
entrypoint = strings.Fields(containerSpec.Entrypoint)
|
entrypoint = strings.Fields(rc.ExprEval.Interpolate(containerSpec.Entrypoint))
|
||||||
}
|
}
|
||||||
|
|
||||||
rawLogger := common.Logger(ctx).WithField("raw_output", true)
|
rawLogger := common.Logger(ctx).WithField("raw_output", true)
|
||||||
|
|
|
@ -53,18 +53,49 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||||
for _, stage := range plan.Stages {
|
for _, stage := range plan.Stages {
|
||||||
stageExecutor := make([]common.Executor, 0)
|
stageExecutor := make([]common.Executor, 0)
|
||||||
for _, run := range stage.Runs {
|
for _, run := range stage.Runs {
|
||||||
// TODO - don't just grab first index of each dimension
|
job := run.Job()
|
||||||
matrix := make(map[string]interface{})
|
matrixes := make([]map[string]interface{}, 0)
|
||||||
if run.Job().Strategy != nil {
|
if job.Strategy != nil {
|
||||||
for mkey, mvals := range run.Job().Strategy.Matrix {
|
includes := make([]map[string]interface{}, 0)
|
||||||
if mkey == "include" || mkey == "exclude" {
|
for _, v := range job.Strategy.Matrix["include"] {
|
||||||
continue
|
includes = append(includes, v.(map[string]interface{}))
|
||||||
}
|
|
||||||
matrix[mkey] = mvals[0]
|
|
||||||
}
|
}
|
||||||
|
delete(job.Strategy.Matrix, "include")
|
||||||
|
|
||||||
|
excludes := make([]map[string]interface{}, 0)
|
||||||
|
for _, v := range job.Strategy.Matrix["exclude"] {
|
||||||
|
excludes = append(excludes, v.(map[string]interface{}))
|
||||||
|
}
|
||||||
|
delete(job.Strategy.Matrix, "exclude")
|
||||||
|
|
||||||
|
matrixProduct := common.CartesianProduct(job.Strategy.Matrix)
|
||||||
|
|
||||||
|
MATRIX:
|
||||||
|
for _, matrix := range matrixProduct {
|
||||||
|
for _, exclude := range excludes {
|
||||||
|
if commonKeysMatch(matrix, exclude) {
|
||||||
|
log.Debugf("Skipping matrix '%v' due to exclude '%v'", matrix, exclude)
|
||||||
|
continue MATRIX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, include := range includes {
|
||||||
|
if commonKeysMatch(matrix, include) {
|
||||||
|
log.Debugf("Setting add'l values on matrix '%v' due to include '%v'", matrix, include)
|
||||||
|
for k, v := range include {
|
||||||
|
matrix[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matrixes = append(matrixes, matrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
matrixes = append(matrixes, make(map[string]interface{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
stageExecutor = append(stageExecutor, runner.NewRunExecutor(run, matrix))
|
for _, matrix := range matrixes {
|
||||||
|
stageExecutor = append(stageExecutor, runner.NewRunExecutor(run, matrix))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...))
|
pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...))
|
||||||
}
|
}
|
||||||
|
@ -72,6 +103,15 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||||
return common.NewPipelineExecutor(pipeline...)
|
return common.NewPipelineExecutor(pipeline...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool {
|
||||||
|
for aKey, aVal := range a {
|
||||||
|
if bVal, ok := b[aKey]; ok && aVal != bVal {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (runner *runnerImpl) NewRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor {
|
func (runner *runnerImpl) NewRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor {
|
||||||
rc := new(RunContext)
|
rc := new(RunContext)
|
||||||
rc.Config = runner.config
|
rc.Config = runner.config
|
||||||
|
@ -79,11 +119,12 @@ func (runner *runnerImpl) NewRunExecutor(run *model.Run, matrix map[string]inter
|
||||||
rc.EventJSON = runner.eventJSON
|
rc.EventJSON = runner.eventJSON
|
||||||
rc.StepResults = make(map[string]*stepResult)
|
rc.StepResults = make(map[string]*stepResult)
|
||||||
rc.Matrix = matrix
|
rc.Matrix = matrix
|
||||||
|
rc.ExprEval = rc.NewExpressionEvaluator()
|
||||||
ee := rc.NewExpressionEvaluator()
|
|
||||||
rc.PlatformName = ee.Interpolate(run.Job().RunsOn)
|
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
ctx = WithJobLogger(ctx, rc.Run.String())
|
ctx = WithJobLogger(ctx, rc.Run.String())
|
||||||
|
if len(rc.Matrix) > 0 {
|
||||||
|
common.Logger(ctx).Infof("\U0001F9EA Matrix: %v", rc.Matrix)
|
||||||
|
}
|
||||||
return rc.Executor()(ctx)
|
return rc.Executor()(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,17 +15,33 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (rc *RunContext) StepEnv(step *model.Step) map[string]string {
|
||||||
|
var env map[string]string
|
||||||
|
job := rc.Run.Job()
|
||||||
|
if job.Container != nil {
|
||||||
|
env = mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv())
|
||||||
|
} else {
|
||||||
|
env = mergeMaps(rc.GetEnv(), step.GetEnv())
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range env {
|
||||||
|
env[k] = rc.ExprEval.Interpolate(v)
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RunContext) setupEnv(containerSpec *model.ContainerSpec, step *model.Step) common.Executor {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
containerSpec.Env = rc.withGithubEnv(rc.StepEnv(step))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
||||||
ee := rc.NewStepExpressionEvaluator(step)
|
|
||||||
job := rc.Run.Job()
|
job := rc.Run.Job()
|
||||||
containerSpec := new(model.ContainerSpec)
|
containerSpec := new(model.ContainerSpec)
|
||||||
containerSpec.Env = rc.withGithubEnv(rc.StepEnv(step))
|
|
||||||
containerSpec.Name = rc.createContainerName(step.ID)
|
containerSpec.Name = rc.createContainerName(step.ID)
|
||||||
|
|
||||||
for k, v := range containerSpec.Env {
|
|
||||||
containerSpec.Env[k] = ee.Interpolate(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch step.Type() {
|
switch step.Type() {
|
||||||
case model.StepTypeRun:
|
case model.StepTypeRun:
|
||||||
if job.Container != nil {
|
if job.Container != nil {
|
||||||
|
@ -34,9 +50,11 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
||||||
containerSpec.Volumes = job.Container.Volumes
|
containerSpec.Volumes = job.Container.Volumes
|
||||||
containerSpec.Options = job.Container.Options
|
containerSpec.Options = job.Container.Options
|
||||||
} else {
|
} else {
|
||||||
containerSpec.Image = platformImage(rc.PlatformName)
|
platformName := rc.ExprEval.Interpolate(rc.Run.Job().RunsOn)
|
||||||
|
containerSpec.Image = platformImage(platformName)
|
||||||
}
|
}
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
|
rc.setupEnv(containerSpec, step),
|
||||||
rc.setupShellCommand(containerSpec, step.Shell, step.Run),
|
rc.setupShellCommand(containerSpec, step.Shell, step.Run),
|
||||||
rc.pullImage(containerSpec),
|
rc.pullImage(containerSpec),
|
||||||
rc.runContainer(containerSpec),
|
rc.runContainer(containerSpec),
|
||||||
|
@ -47,6 +65,7 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
||||||
containerSpec.Entrypoint = step.With["entrypoint"]
|
containerSpec.Entrypoint = step.With["entrypoint"]
|
||||||
containerSpec.Args = step.With["args"]
|
containerSpec.Args = step.With["args"]
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
|
rc.setupEnv(containerSpec, step),
|
||||||
rc.pullImage(containerSpec),
|
rc.pullImage(containerSpec),
|
||||||
rc.runContainer(containerSpec),
|
rc.runContainer(containerSpec),
|
||||||
)
|
)
|
||||||
|
@ -54,6 +73,7 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
||||||
case model.StepTypeUsesActionLocal:
|
case model.StepTypeUsesActionLocal:
|
||||||
containerSpec.Image = fmt.Sprintf("%s:%s", containerSpec.Name, "latest")
|
containerSpec.Image = fmt.Sprintf("%s:%s", containerSpec.Name, "latest")
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
|
rc.setupEnv(containerSpec, step),
|
||||||
rc.setupAction(containerSpec, filepath.Join(rc.Config.Workdir, step.Uses)),
|
rc.setupAction(containerSpec, filepath.Join(rc.Config.Workdir, step.Uses)),
|
||||||
applyWith(containerSpec, step),
|
applyWith(containerSpec, step),
|
||||||
rc.pullImage(containerSpec),
|
rc.pullImage(containerSpec),
|
||||||
|
@ -78,6 +98,7 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
||||||
Ref: remoteAction.Ref,
|
Ref: remoteAction.Ref,
|
||||||
Dir: cloneDir,
|
Dir: cloneDir,
|
||||||
}),
|
}),
|
||||||
|
rc.setupEnv(containerSpec, step),
|
||||||
rc.setupAction(containerSpec, filepath.Join(cloneDir, remoteAction.Path)),
|
rc.setupAction(containerSpec, filepath.Join(cloneDir, remoteAction.Path)),
|
||||||
applyWith(containerSpec, step),
|
applyWith(containerSpec, step),
|
||||||
rc.pullImage(containerSpec),
|
rc.pullImage(containerSpec),
|
||||||
|
@ -100,15 +121,6 @@ func applyWith(containerSpec *model.ContainerSpec, step *model.Step) common.Exec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StepEnv returns the env for a step
|
|
||||||
func (rc *RunContext) StepEnv(step *model.Step) map[string]string {
|
|
||||||
job := rc.Run.Job()
|
|
||||||
if job.Container != nil {
|
|
||||||
return mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv())
|
|
||||||
}
|
|
||||||
return mergeMaps(rc.GetEnv(), step.GetEnv())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) setupShellCommand(containerSpec *model.ContainerSpec, shell string, run string) common.Executor {
|
func (rc *RunContext) setupShellCommand(containerSpec *model.ContainerSpec, shell string, run string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
shellCommand := ""
|
shellCommand := ""
|
||||||
|
@ -140,6 +152,8 @@ func (rc *RunContext) setupShellCommand(containerSpec *model.ContainerSpec, shel
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
run = rc.ExprEval.Interpolate(run)
|
||||||
|
|
||||||
if _, err := tempScript.WriteString(run); err != nil {
|
if _, err := tempScript.WriteString(run); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
2
pkg/runner/testdata/matrix/push.yml
vendored
2
pkg/runner/testdata/matrix/push.yml
vendored
|
@ -5,7 +5,7 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- run: echo ${NODE_VERSION} | grep 4
|
- run: echo ${NODE_VERSION} | grep ${{ matrix.node }}
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: ${{ matrix.node }}
|
NODE_VERSION: ${{ matrix.node }}
|
||||||
strategy:
|
strategy:
|
||||||
|
|
Loading…
Reference in a new issue