initial support for expressons

Signed-off-by: Casey Lee <cplee@nektos.com>
This commit is contained in:
Casey Lee 2020-02-14 00:41:20 -08:00
parent 09773f7c5c
commit 73559207c7
No known key found for this signature in database
GPG key ID: 1899120ECD0A1784
10 changed files with 286 additions and 108 deletions

View file

@ -34,6 +34,7 @@ func TestNewDockerRunExecutor(t *testing.T) {
runner := NewDockerRunExecutor(NewDockerRunExecutorInput{
Image: "hello-world",
Stdout: buf,
})
puller := NewDockerPullExecutor(NewDockerPullExecutorInput{
@ -44,7 +45,8 @@ func TestNewDockerRunExecutor(t *testing.T) {
err := pipeline(ctx)
assert.NoError(t, err)
expected := `docker pull hello-worlddocker run image=hello-world entrypoint=[] cmd=[]Hello from Docker!`
actual := buf.String()
assert.Equal(t, expected, actual[:len(expected)])
assert.Contains(t, actual, `docker pull hello-world`)
assert.Contains(t, actual, `docker run image=hello-world entrypoint=[] cmd=[]`)
assert.Contains(t, actual, `Hello from Docker!`)
}

View file

@ -62,6 +62,14 @@ type Job struct {
TimeoutMinutes int64 `yaml:"timeout-minutes"`
Container *ContainerSpec `yaml:"container"`
Services map[string]*ContainerSpec `yaml:"services"`
Strategy *Strategy `yaml:"strategy"`
}
// Strategy for the job
type Strategy struct {
FailFast bool `yaml:"fail-fast"`
MaxParallel int `yaml:"max-parallel"`
Matrix map[string][]interface{} `yaml:"matrix"`
}
// Needs list for Job

View file

@ -64,10 +64,7 @@ func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg
}
func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) {
common.Logger(ctx).Infof(" \U00002699 ::set-output:: %s=%s", kvPairs["name"], arg)
if rc.Outputs == nil {
rc.Outputs = make(map[string]string)
}
rc.Outputs[kvPairs["name"]] = arg
rc.StepResults[rc.CurrentStep].Outputs[kvPairs["name"]] = arg
}
func (rc *RunContext) addPath(ctx context.Context, arg string) {
common.Logger(ctx).Infof(" \U00002699 ::add-path:: %s", arg)

View file

@ -21,10 +21,15 @@ func TestSetOutput(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
rc := new(RunContext)
rc.StepResults = make(map[string]*stepResult)
handler := rc.commandHandler(ctx)
rc.CurrentStep = "my-step"
rc.StepResults[rc.CurrentStep] = &stepResult{
Outputs: make(map[string]string),
}
handler("::set-output name=x::valz\n")
assert.Equal("valz", rc.Outputs["x"])
assert.Equal("valz", rc.StepResults["my-step"].Outputs["x"])
}
func TestAddpath(t *testing.T) {

View file

@ -11,6 +11,7 @@ import (
"regexp"
"strings"
"github.com/nektos/act/pkg/model"
"github.com/robertkrimen/otto"
"github.com/sirupsen/logrus"
"gopkg.in/godo.v2/glob"
@ -33,10 +34,25 @@ func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
}
}
// NewStepExpressionEvaluator creates a new evaluator
func (rc *RunContext) NewStepExpressionEvaluator(step *model.Step) ExpressionEvaluator {
vm := rc.newVM()
configers := []func(*otto.Otto){
rc.vmEnv(step),
}
for _, configer := range configers {
configer(vm)
}
return &expressionEvaluator{
vm,
}
}
// ExpressionEvaluator is the interface for evaluating expressions
type ExpressionEvaluator interface {
Evaluate(string) (string, error)
Interpolate(string) (string, error)
Interpolate(string) string
}
type expressionEvaluator struct {
@ -51,7 +67,7 @@ func (ee *expressionEvaluator) Evaluate(in string) (string, error) {
return val.ToString()
}
func (ee *expressionEvaluator) Interpolate(in string) (string, error) {
func (ee *expressionEvaluator) Interpolate(in string) string {
errList := make([]error, 0)
out := pattern.ReplaceAllStringFunc(in, func(match string) string {
expression := strings.TrimPrefix(strings.TrimSuffix(match, suffix), prefix)
@ -62,9 +78,10 @@ func (ee *expressionEvaluator) Interpolate(in string) (string, error) {
return evaluated
})
if len(errList) > 0 {
return "", fmt.Errorf("Unable to interpolate string '%s' - %v", in, errList)
logrus.Errorf("Unable to interpolate string '%s' - %v", in, errList)
return in
}
return out, nil
return out
}
func (rc *RunContext) newVM() *otto.Otto {
@ -76,16 +93,19 @@ func (rc *RunContext) newVM() *otto.Otto {
vmJoin,
vmToJSON,
vmAlways,
vmCancelled,
rc.vmHashFiles(),
rc.vmCancelled(),
rc.vmSuccess(),
rc.vmFailure(),
rc.vmHashFiles(),
rc.vmGithub(),
rc.vmEnv(),
rc.vmJob(),
rc.vmSteps(),
rc.vmRunner(),
rc.vmSecrets(),
rc.vmStrategy(),
rc.vmMatrix(),
}
vm := otto.New()
for _, configer := range configers {
@ -183,14 +203,14 @@ func (rc *RunContext) vmHashFiles() func(*otto.Otto) {
func (rc *RunContext) vmSuccess() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("success", func() bool {
return !rc.PriorStepFailed
return rc.getJobContext().Status == "success"
})
}
}
func (rc *RunContext) vmFailure() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("failure", func() bool {
return rc.PriorStepFailed
return rc.getJobContext().Status == "failure"
})
}
}
@ -200,55 +220,31 @@ func vmAlways(vm *otto.Otto) {
return true
})
}
func vmCancelled(vm *otto.Otto) {
func (rc *RunContext) vmCancelled() func(vm *otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("cancelled", func() bool {
return false
return rc.getJobContext().Status == "cancelled"
})
}
}
func (rc *RunContext) vmGithub() func(*otto.Otto) {
github := map[string]interface{}{
"event": make(map[string]interface{}),
"event_path": "/github/workflow/event.json",
"workflow": rc.Run.Workflow.Name,
"run_id": "1",
"run_number": "1",
"actor": "nektos/act",
// TODO
"repository": "",
"event_name": "",
"sha": "",
"ref": "",
"head_ref": "",
"base_ref": "",
"token": "",
"workspace": rc.Config.Workdir,
"action": "",
}
err := json.Unmarshal([]byte(rc.EventJSON), github["event"])
if err != nil {
logrus.Error(err)
}
github := rc.getGithubContext()
return func(vm *otto.Otto) {
_ = vm.Set("github", github)
}
}
func (rc *RunContext) vmEnv() func(*otto.Otto) {
env := map[string]interface{}{}
// TODO
func (rc *RunContext) vmEnv(step *model.Step) func(*otto.Otto) {
return func(vm *otto.Otto) {
env := rc.StepEnv(step)
_ = vm.Set("env", env)
}
}
func (rc *RunContext) vmJob() func(*otto.Otto) {
job := map[string]interface{}{}
// TODO
job := rc.getJobContext()
return func(vm *otto.Otto) {
_ = vm.Set("job", job)
@ -256,8 +252,7 @@ func (rc *RunContext) vmJob() func(*otto.Otto) {
}
func (rc *RunContext) vmSteps() func(*otto.Otto) {
steps := map[string]interface{}{}
// TODO
steps := rc.getStepsContext()
return func(vm *otto.Otto) {
_ = vm.Set("steps", steps)
@ -275,3 +270,28 @@ func (rc *RunContext) vmRunner() func(*otto.Otto) {
_ = vm.Set("runner", runner)
}
}
func (rc *RunContext) vmSecrets() func(*otto.Otto) {
secrets := make(map[string]string)
return func(vm *otto.Otto) {
_ = vm.Set("secrets", secrets)
}
}
func (rc *RunContext) vmStrategy() func(*otto.Otto) {
job := rc.Run.Job()
strategy := make(map[string]interface{})
if job.Strategy != nil {
strategy["fail-fast"] = job.Strategy.FailFast
strategy["max-parallel"] = job.Strategy.MaxParallel
}
return func(vm *otto.Otto) {
_ = vm.Set("strategy", strategy)
}
}
func (rc *RunContext) vmMatrix() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("matrix", rc.Matrix)
}
}

View file

@ -17,6 +17,28 @@ func TestEvaluate(t *testing.T) {
JobID: "job1",
Workflow: &model.Workflow{
Name: "test-workflow",
Jobs: map[string]*model.Job{
"job1": &model.Job{
Strategy: &model.Strategy{
Matrix: map[string][]interface{}{
"os": []interface{}{"Linux", "Windows"},
"foo": []interface{}{"bar", "baz"},
},
},
},
},
},
},
Matrix: map[string]interface{}{
"os": "Linux",
"foo": "bar",
},
StepResults: map[string]*stepResult{
"id1": &stepResult{
Outputs: map[string]string{
"foo": "bar",
},
Success: true,
},
},
}
@ -51,7 +73,11 @@ func TestEvaluate(t *testing.T) {
{"github.actor", "nektos/act", ""},
{"github.run_id", "1", ""},
{"github.run_number", "1", ""},
{"job.status", "success", ""},
{"steps.id1.outputs.foo", "bar", ""},
{"runner.os", "Linux", ""},
{"matrix.os", "Linux", ""},
{"matrix.foo", "bar", ""},
}
for _, table := range tables {
@ -60,10 +86,10 @@ func TestEvaluate(t *testing.T) {
out, err := ee.Evaluate(table.in)
if table.errMesg == "" {
assert.NoError(err, table.in)
assert.Equal(table.out, out)
assert.Equal(table.out, out, table.in)
} else {
assert.Error(err)
assert.Equal(table.errMesg, err.Error())
assert.Error(err, table.in)
assert.Equal(table.errMesg, err.Error(), table.in)
}
})
}
@ -79,13 +105,15 @@ func TestInterpolate(t *testing.T) {
JobID: "job1",
Workflow: &model.Workflow{
Name: "test-workflow",
Jobs: map[string]*model.Job{
"job1": &model.Job{},
},
},
},
}
ee := rc.NewExpressionEvaluator()
out, err := ee.Interpolate(" ${{1}} to ${{2}} ")
out := ee.Interpolate(" ${{1}} to ${{2}} ")
assert.NoError(err)
assert.Equal(" 1 to 2 ", out)
}

View file

@ -4,6 +4,7 @@ import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@ -17,19 +18,26 @@ import (
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
)
// RunContext contains info about current job
type RunContext struct {
Config *Config
Matrix map[string]interface{}
Run *model.Run
EventJSON string
Env map[string]string
Outputs map[string]string
Tempdir string
PriorStepFailed bool
ExtraPath []string
CurrentStep string
StepResults map[string]*stepResult
}
type stepResult struct {
Success bool `json:"success"`
Outputs map[string]string `json:"outputs"`
}
// GetEnv returns the env for the context
@ -59,14 +67,18 @@ func (rc *RunContext) Executor() common.Executor {
}
s := step
steps = append(steps, func(ctx context.Context) error {
rc.CurrentStep = s.ID
rc.StepResults[rc.CurrentStep] = &stepResult{
Success: true,
Outputs: make(map[string]string),
}
common.Logger(ctx).Infof("\u2B50 Run %s", s)
err := rc.newStepExecutor(s)(ctx)
if err == nil {
common.Logger(ctx).Infof(" \u2705 Success - %s", s)
rc.PriorStepFailed = false
} else {
common.Logger(ctx).Errorf(" \u274C Failure - %s", s)
rc.PriorStepFailed = true
rc.StepResults[rc.CurrentStep].Success = false
}
return err
})
@ -207,3 +219,110 @@ func trimToLen(s string, l int) string {
}
return s
}
type jobContext struct {
Status string `json:"status"`
Container struct {
ID string `json:"id"`
Network string `json:"network"`
} `json:"container"`
Services map[string]struct {
ID string `json:"id"`
} `json:"services"`
}
func (rc *RunContext) getJobContext() *jobContext {
jobStatus := "success"
for _, stepStatus := range rc.StepResults {
if !stepStatus.Success {
jobStatus = "failure"
break
}
}
return &jobContext{
Status: jobStatus,
}
}
func (rc *RunContext) getStepsContext() map[string]*stepResult {
return rc.StepResults
}
type githubContext struct {
Event map[string]interface{} `json:"event"`
EventPath string `json:"event_path"`
Workflow string `json:"workflow"`
RunID string `json:"run_id"`
RunNumber string `json:"run_number"`
Actor string `json:"actor"`
Repository string `json:"repository"`
EventName string `json:"event_name"`
Sha string `json:"sha"`
Ref string `json:"ref"`
HeadRef string `json:"head_ref"`
BaseRef string `json:"base_ref"`
Token string `json:"token"`
Workspace string `json:"workspace"`
Action string `json:"action"`
}
func (rc *RunContext) getGithubContext() *githubContext {
ghc := &githubContext{
Event: make(map[string]interface{}),
EventPath: "/github/workflow/event.json",
Workflow: rc.Run.Workflow.Name,
RunID: "1",
RunNumber: "1",
Actor: "nektos/act",
EventName: rc.Config.EventName,
Token: os.Getenv("GITHUB_TOKEN"),
Workspace: "/github/workspace",
Action: rc.CurrentStep,
}
repoPath := rc.Config.Workdir
repo, err := common.FindGithubRepo(repoPath)
if err != nil {
log.Warningf("unable to get git repo: %v", err)
} else {
ghc.Repository = repo
}
_, sha, err := common.FindGitRevision(repoPath)
if err != nil {
log.Warningf("unable to get git revision: %v", err)
} else {
ghc.Sha = sha
}
ref, err := common.FindGitRef(repoPath)
if err != nil {
log.Warningf("unable to get git ref: %v", err)
} else {
log.Debugf("using github ref: %s", ref)
ghc.Ref = ref
}
err = json.Unmarshal([]byte(rc.EventJSON), &ghc.Event)
if err != nil {
logrus.Error(err)
}
return ghc
}
func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string {
github := rc.getGithubContext()
env["HOME"] = "/github/home"
env["GITHUB_WORKFLOW"] = github.Workflow
env["GITHUB_RUN_ID"] = github.RunID
env["GITHUB_RUN_NUMBER"] = github.RunNumber
env["GITHUB_ACTION"] = github.Action
env["GITHUB_ACTOR"] = github.Actor
env["GITHUB_REPOSITORY"] = github.Repository
env["GITHUB_EVENT_NAME"] = github.EventName
env["GITHUB_EVENT_PATH"] = github.EventPath
env["GITHUB_WORKSPACE"] = github.Workspace
env["GITHUB_SHA"] = github.Sha
env["GITHUB_REF"] = github.Ref
return env
}

View file

@ -12,7 +12,7 @@ import (
// Runner provides capabilities to run GitHub actions
type Runner interface {
NewPlanExecutor(plan *model.Plan) common.Executor
NewRunExecutor(run *model.Run) common.Executor
NewRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor
}
// Config contains the config for a new runner
@ -53,7 +53,18 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
for _, stage := range plan.Stages {
stageExecutor := make([]common.Executor, 0)
for _, run := range stage.Runs {
stageExecutor = append(stageExecutor, runner.NewRunExecutor(run))
// TODO - don't just grab first index of each dimension
matrix := make(map[string]interface{})
if run.Job().Strategy != nil {
for mkey, mvals := range run.Job().Strategy.Matrix {
if mkey == "include" || mkey == "exclude" {
continue
}
matrix[mkey] = mvals[0]
}
}
stageExecutor = append(stageExecutor, runner.NewRunExecutor(run, matrix))
}
pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...))
}
@ -61,11 +72,13 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
return common.NewPipelineExecutor(pipeline...)
}
func (runner *runnerImpl) NewRunExecutor(run *model.Run) common.Executor {
func (runner *runnerImpl) NewRunExecutor(run *model.Run, matrix map[string]interface{}) common.Executor {
rc := new(RunContext)
rc.Config = runner.config
rc.Run = run
rc.EventJSON = runner.eventJSON
rc.StepResults = make(map[string]*stepResult)
rc.Matrix = matrix
return func(ctx context.Context) error {
ctx = WithJobLogger(ctx, rc.Run.String())
return rc.Executor()(ctx)

View file

@ -16,11 +16,16 @@ import (
)
func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
ee := rc.NewStepExpressionEvaluator(step)
job := rc.Run.Job()
containerSpec := new(model.ContainerSpec)
containerSpec.Env = rc.StepEnv(step)
containerSpec.Env = rc.withGithubEnv(rc.StepEnv(step))
containerSpec.Name = rc.createContainerName(step.ID)
for k, v := range containerSpec.Env {
containerSpec.Env[k] = ee.Interpolate(v)
}
switch step.Type() {
case model.StepTypeRun:
if job.Container != nil {
@ -29,7 +34,7 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
containerSpec.Volumes = job.Container.Volumes
containerSpec.Options = job.Container.Options
} else {
containerSpec.Image = platformImage(job.RunsOn)
containerSpec.Image = platformImage(ee.Interpolate(job.RunsOn))
}
return common.NewPipelineExecutor(
rc.setupShellCommand(containerSpec, step.Shell, step.Run),
@ -97,44 +102,11 @@ 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 {
env := make(map[string]string)
env["HOME"] = "/github/home"
env["GITHUB_WORKFLOW"] = rc.Run.Workflow.Name
env["GITHUB_RUN_ID"] = "1"
env["GITHUB_RUN_NUMBER"] = "1"
env["GITHUB_ACTION"] = step.ID
env["GITHUB_ACTOR"] = "nektos/act"
repoPath := rc.Config.Workdir
repo, err := common.FindGithubRepo(repoPath)
if err != nil {
log.Warningf("unable to get git repo: %v", err)
} else {
env["GITHUB_REPOSITORY"] = repo
}
env["GITHUB_EVENT_NAME"] = rc.Config.EventName
env["GITHUB_EVENT_PATH"] = "/github/workflow/event.json"
env["GITHUB_WORKSPACE"] = "/github/workspace"
_, rev, err := common.FindGitRevision(repoPath)
if err != nil {
log.Warningf("unable to get git revision: %v", err)
} else {
env["GITHUB_SHA"] = rev
}
ref, err := common.FindGitRef(repoPath)
if err != nil {
log.Warningf("unable to get git ref: %v", err)
} else {
log.Debugf("using github ref: %s", ref)
env["GITHUB_REF"] = ref
}
job := rc.Run.Job()
if job.Container != nil {
return mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv(), env)
return mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv())
}
return mergeMaps(rc.GetEnv(), step.GetEnv(), env)
return mergeMaps(rc.GetEnv(), step.GetEnv())
}
func (rc *RunContext) setupShellCommand(containerSpec *model.ContainerSpec, shell string, run string) common.Executor {

14
pkg/runner/testdata/matrix/push.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: matrix
on: push
jobs:
build:
runs-on: ${{ matrix.os }}
steps:
- run: echo ${NODE_VERSION} | grep 4
env:
NODE_VERSION: ${{ matrix.node }}
strategy:
matrix:
os: [ubuntu-18.04, macos-latest]
node: [4, 6, 8, 10]