From 46c6e59f47d1aaa8a61ba14631b7813b4f421cf6 Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Thu, 3 Nov 2022 16:09:05 +0100 Subject: [PATCH 01/10] feat: allow to spawn and run a local reusable workflow This change contains the ability to parse/plan/run a local reusable workflow. There are still numerous things missing: - inputs - secrets - outputs --- pkg/runner/reusable_workflow.go | 29 +++++++++++++++++++ pkg/runner/run_context.go | 25 ++++++++++++---- pkg/runner/runner.go | 4 --- pkg/runner/runner_test.go | 1 + .../workflows/local-reusable-workflow.yml | 7 +++++ .../testdata/uses-workflow/local-workflow.yml | 5 ++++ 6 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 pkg/runner/reusable_workflow.go create mode 100644 pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml create mode 100644 pkg/runner/testdata/uses-workflow/local-workflow.yml diff --git a/pkg/runner/reusable_workflow.go b/pkg/runner/reusable_workflow.go new file mode 100644 index 00000000000..4c904dced99 --- /dev/null +++ b/pkg/runner/reusable_workflow.go @@ -0,0 +1,29 @@ +package runner + +import ( + "context" + "path" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/model" +) + +func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor { + return func(ctx context.Context) error { + planner, err := model.NewWorkflowPlanner(path.Join(rc.Config.Workdir, rc.Run.Job().Uses), true) + if err != nil { + return err + } + + plan := planner.PlanEvent("workflow_call") + + r, err := New(rc.Config) + if err != nil { + return err + } + + executor := r.NewPlanExecutor(plan) + + return executor(ctx) + } +} diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 30548f55420..4e657504ef5 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -369,16 +369,27 @@ func (rc *RunContext) steps() []*model.Step { // Executor returns a pipeline executor for all the steps in the job func (rc *RunContext) Executor() common.Executor { + var executor common.Executor + + switch rc.Run.Job().Type() { + case model.JobTypeDefault: + executor = newJobExecutor(rc, &stepFactoryImpl{}, rc) + case model.JobTypeReusableWorkflowLocal: + executor = newLocalReusableWorkflowExecutor(rc) + case model.JobTypeReusableWorkflowRemote: + executor = common.NewErrorExecutor(fmt.Errorf("remote reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)")) + } + + // todo: simplify + // return executor.If(rc.isEnabled) return func(ctx context.Context) error { - isEnabled, err := rc.isEnabled(ctx) + res, err := rc.isEnabled(ctx) if err != nil { return err } - - if isEnabled { - return newJobExecutor(rc, &stepFactoryImpl{}, rc)(ctx) + if res { + return executor(ctx) } - return nil } } @@ -428,6 +439,10 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) { return false, nil } + if job.Type() != model.JobTypeDefault { + return true, nil + } + img := rc.platformImage(ctx) if img == "" { if job.RunsOn() == nil { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 60819133ad8..706944a8d68 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -89,10 +89,6 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { stageExecutor := make([]common.Executor, 0) job := run.Job() - if job.Uses != "" { - return fmt.Errorf("reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)") - } - if job.Strategy != nil { strategyRc := runner.newRunContext(ctx, run, nil) if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil { diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 21b584f0487..08365822946 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -139,6 +139,7 @@ func TestRunEvent(t *testing.T) { {workdir, "uses-nested-composite", "push", "", platforms}, {workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms}, {workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms}, + {workdir, "uses-workflow", "pull_request", "", platforms}, {workdir, "uses-docker-url", "push", "", platforms}, {workdir, "act-composite-env-test", "push", "", platforms}, diff --git a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml new file mode 100644 index 00000000000..5dc401d4e46 --- /dev/null +++ b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml @@ -0,0 +1,7 @@ +on: workflow_call + +jobs: + reusable_workflow_job: + runs-on: ubuntu-latest + steps: + - run: echo hello diff --git a/pkg/runner/testdata/uses-workflow/local-workflow.yml b/pkg/runner/testdata/uses-workflow/local-workflow.yml new file mode 100644 index 00000000000..b7a62383a7e --- /dev/null +++ b/pkg/runner/testdata/uses-workflow/local-workflow.yml @@ -0,0 +1,5 @@ +on: pull_request + +jobs: + reusable-workflow: + uses: ./.github/workflows/local-reusable-workflow.yml From 2427e3ca1e153e08c4329d722f7b7bbbecb311b5 Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Wed, 23 Nov 2022 16:00:57 +0100 Subject: [PATCH 02/10] feat: add workflow_call inputs --- pkg/model/workflow.go | 1 + pkg/runner/expression.go | 6 ++++ pkg/runner/reusable_workflow.go | 32 +++++++++++-------- pkg/runner/run_context.go | 1 + pkg/runner/runner.go | 8 ++++- .../workflows/local-reusable-workflow.yml | 17 ++++++++-- .../testdata/uses-workflow/local-workflow.yml | 4 +++ 7 files changed, 53 insertions(+), 16 deletions(-) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index f567a1f7806..2d35021e196 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -115,6 +115,7 @@ type Job struct { Defaults Defaults `yaml:"defaults"` Outputs map[string]string `yaml:"outputs"` Uses string `yaml:"uses"` + With map[string]interface{} `yaml:"with"` Result string } diff --git a/pkg/runner/expression.go b/pkg/runner/expression.go index 6a621f7b3d2..449778d2d67 100644 --- a/pkg/runner/expression.go +++ b/pkg/runner/expression.go @@ -315,6 +315,12 @@ func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (str func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]interface{} { inputs := map[string]interface{}{} + if rc.caller != nil { + for k, v := range rc.caller.With { + inputs[k] = v + } + } + var env map[string]string if step != nil { env = *step.getEnv() diff --git a/pkg/runner/reusable_workflow.go b/pkg/runner/reusable_workflow.go index 4c904dced99..1d21e1b41c4 100644 --- a/pkg/runner/reusable_workflow.go +++ b/pkg/runner/reusable_workflow.go @@ -1,7 +1,6 @@ package runner import ( - "context" "path" "github.com/nektos/act/pkg/common" @@ -9,21 +8,28 @@ import ( ) func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor { - return func(ctx context.Context) error { - planner, err := model.NewWorkflowPlanner(path.Join(rc.Config.Workdir, rc.Run.Job().Uses), true) - if err != nil { - return err - } + job := rc.Run.Job() - plan := planner.PlanEvent("workflow_call") + planner, err := model.NewWorkflowPlanner(path.Join(rc.Config.Workdir, job.Uses), true) + if err != nil { + return common.NewErrorExecutor(err) + } - r, err := New(rc.Config) - if err != nil { - return err - } + plan := planner.PlanEvent("workflow_call") - executor := r.NewPlanExecutor(plan) + runner, err := NewReusableWorkflowRunner(rc.Config, job) + if err != nil { + return common.NewErrorExecutor(err) + } - return executor(ctx) + return runner.NewPlanExecutor(plan) +} + +func NewReusableWorkflowRunner(runnerConfig *Config, caller *model.Job) (Runner, error) { + runner := &runnerImpl{ + config: runnerConfig, + caller: caller, } + + return runner.configure() } diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 4e657504ef5..6813d76eaa5 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -43,6 +43,7 @@ type RunContext struct { Parent *RunContext Masks []string cleanUpJobContainer common.Executor + caller *model.Job // job calling this RunContext (reusable workflows) } func (rc *RunContext) AddMask(mask string) { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 706944a8d68..f58e998c02f 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -56,6 +56,7 @@ type Config struct { type runnerImpl struct { config *Config eventJSON string + caller *model.Job // the job calling this runner (caller of a reusable workflow) } // New Creates a new Runner @@ -64,8 +65,12 @@ func New(runnerConfig *Config) (Runner, error) { config: runnerConfig, } + return runner.configure() +} + +func (runner *runnerImpl) configure() (Runner, error) { runner.eventJSON = "{}" - if runnerConfig.EventPath != "" { + if runner.config.EventPath != "" { log.Debugf("Reading event.json from %s", runner.config.EventPath) eventJSONBytes, err := os.ReadFile(runner.config.EventPath) if err != nil { @@ -157,6 +162,7 @@ func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, mat EventJSON: runner.eventJSON, StepResults: make(map[string]*model.StepResult), Matrix: matrix, + caller: runner.caller, } rc.ExprEval = rc.NewExpressionEvaluator(ctx) rc.Name = rc.ExprEval.Interpolate(ctx, run.String()) diff --git a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml index 5dc401d4e46..09b9ccd86dd 100644 --- a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml +++ b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml @@ -1,7 +1,20 @@ -on: workflow_call +on: + workflow_call: + inputs: + string_required: + required: true + type: string + bool_required: + required: true + type: boolean + number_required: + required: true + type: number jobs: reusable_workflow_job: runs-on: ubuntu-latest steps: - - run: echo hello + - run: echo ${{ inputs.string_required }} + - run: echo ${{ inputs.bool_required }} + - run: echo ${{ inputs.number_required }} diff --git a/pkg/runner/testdata/uses-workflow/local-workflow.yml b/pkg/runner/testdata/uses-workflow/local-workflow.yml index b7a62383a7e..0d251dea555 100644 --- a/pkg/runner/testdata/uses-workflow/local-workflow.yml +++ b/pkg/runner/testdata/uses-workflow/local-workflow.yml @@ -3,3 +3,7 @@ on: pull_request jobs: reusable-workflow: uses: ./.github/workflows/local-reusable-workflow.yml + with: + string_required: value + bool_required: true + number_required: 1 From be411f3a6f3763eef2eb524f2da33531f7cc2706 Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Wed, 23 Nov 2022 16:12:53 +0100 Subject: [PATCH 03/10] test: improve inputs test --- .../workflows/local-reusable-workflow.yml | 17 ++++++++++++++--- .../testdata/uses-workflow/local-workflow.yml | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml index 09b9ccd86dd..2e776d001ae 100644 --- a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml +++ b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml @@ -15,6 +15,17 @@ jobs: reusable_workflow_job: runs-on: ubuntu-latest steps: - - run: echo ${{ inputs.string_required }} - - run: echo ${{ inputs.bool_required }} - - run: echo ${{ inputs.number_required }} + - name: test required string + run: | + echo inputs.string_required=${{ inputs.string_required }} + [[ "${{ inputs.string_required == 'string' }}" = "true" ]] || exit 1 + + - name: test required bool + run: | + echo inputs.bool_required=${{ inputs.bool_required }} + [[ "${{ inputs.bool_required }}" = "true" ]] || exit 1 + + - name: test required number + run: | + echo inputs.number_required=${{ inputs.number_required }} + [[ "${{ inputs.number_required == 1 }}" = "true" ]] || exit 1 diff --git a/pkg/runner/testdata/uses-workflow/local-workflow.yml b/pkg/runner/testdata/uses-workflow/local-workflow.yml index 0d251dea555..6b7cbdf178a 100644 --- a/pkg/runner/testdata/uses-workflow/local-workflow.yml +++ b/pkg/runner/testdata/uses-workflow/local-workflow.yml @@ -4,6 +4,6 @@ jobs: reusable-workflow: uses: ./.github/workflows/local-reusable-workflow.yml with: - string_required: value + string_required: string bool_required: true number_required: 1 From 8457be573fb3131dc4e79167b245b6a430edff8a Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Wed, 23 Nov 2022 17:03:29 +0100 Subject: [PATCH 04/10] feat: add input defaults --- pkg/model/workflow.go | 32 +++++++++++++++++++ pkg/runner/expression.go | 20 +++++++++--- .../workflows/local-reusable-workflow.yml | 27 ++++++++++++++++ 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 2d35021e196..45d473d9b71 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -100,6 +100,38 @@ func (w *Workflow) WorkflowDispatchConfig() *WorkflowDispatch { return &config } +type WorkflowCallInput struct { + Description string `yaml:"description"` + Required bool `yaml:"required"` + Default string `yaml:"default"` + Type string `yaml:"type"` +} + +type WorkflowCall struct { + Inputs map[string]WorkflowCallInput `yaml:"inputs"` +} + +func (w *Workflow) WorkflowCallConfig() *WorkflowCall { + if w.RawOn.Kind != yaml.MappingNode { + return nil + } + + var val map[string]yaml.Node + err := w.RawOn.Decode(&val) + if err != nil { + log.Fatal(err) + } + + var config WorkflowCall + node := val["workflow_call"] + err = node.Decode(&config) + if err != nil { + log.Fatal(err) + } + + return &config +} + // Job is the structure of one job in a workflow type Job struct { Name string `yaml:"name"` diff --git a/pkg/runner/expression.go b/pkg/runner/expression.go index 449778d2d67..0197f8b68f0 100644 --- a/pkg/runner/expression.go +++ b/pkg/runner/expression.go @@ -315,11 +315,7 @@ func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (str func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]interface{} { inputs := map[string]interface{}{} - if rc.caller != nil { - for k, v := range rc.caller.With { - inputs[k] = v - } - } + setupWorkflowInputs(ctx, &inputs, rc) var env map[string]string if step != nil { @@ -353,3 +349,17 @@ func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *mod return inputs } + +func setupWorkflowInputs(ctx context.Context, inputs *map[string]interface{}, rc *RunContext) { + if rc.caller != nil { + config := rc.Run.Workflow.WorkflowCallConfig() + + for name, input := range config.Inputs { + value := rc.caller.With[name] + if value == nil && config != nil && config.Inputs != nil { + value = input.Default + } + (*inputs)[name] = value + } + } +} diff --git a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml index 2e776d001ae..2d4ebdcabea 100644 --- a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml +++ b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml @@ -4,12 +4,24 @@ on: string_required: required: true type: string + string_optional: + required: false + type: string + default: string bool_required: required: true type: boolean + bool_optional: + required: false + type: boolean + default: true number_required: required: true type: number + number_optional: + required: false + type: number + default: 1 jobs: reusable_workflow_job: @@ -20,12 +32,27 @@ jobs: echo inputs.string_required=${{ inputs.string_required }} [[ "${{ inputs.string_required == 'string' }}" = "true" ]] || exit 1 + - name: test optional string + run: | + echo inputs.string_optional=${{ inputs.string_optional }} + [[ "${{ inputs.string_optional == 'string' }}" = "true" ]] || exit 1 + - name: test required bool run: | echo inputs.bool_required=${{ inputs.bool_required }} [[ "${{ inputs.bool_required }}" = "true" ]] || exit 1 + - name: test optional bool + run: | + echo inputs.bool_optional=${{ inputs.bool_optional }} + [[ "${{ inputs.bool_optional }}" = "true" ]] || exit 1 + - name: test required number run: | echo inputs.number_required=${{ inputs.number_required }} [[ "${{ inputs.number_required == 1 }}" = "true" ]] || exit 1 + + - name: test optional number + run: | + echo inputs.number_optional=${{ inputs.number_optional }} + [[ "${{ inputs.number_optional == 1 }}" = "true" ]] || exit 1 From 3c1e3402945dfd7fc399eab9c1bb1b4608895e31 Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Wed, 23 Nov 2022 17:09:21 +0100 Subject: [PATCH 05/10] feat: allow expressions in inputs --- pkg/runner/expression.go | 8 ++++++++ pkg/runner/testdata/uses-workflow/local-workflow.yml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/runner/expression.go b/pkg/runner/expression.go index 0197f8b68f0..2348ae39d3c 100644 --- a/pkg/runner/expression.go +++ b/pkg/runner/expression.go @@ -356,9 +356,17 @@ func setupWorkflowInputs(ctx context.Context, inputs *map[string]interface{}, rc for name, input := range config.Inputs { value := rc.caller.With[name] + if value == nil && config != nil && config.Inputs != nil { value = input.Default } + + if rc.ExprEval != nil { + if str, ok := value.(string); ok { + value = rc.ExprEval.Interpolate(ctx, str) + } + } + (*inputs)[name] = value } } diff --git a/pkg/runner/testdata/uses-workflow/local-workflow.yml b/pkg/runner/testdata/uses-workflow/local-workflow.yml index 6b7cbdf178a..d6921a635f0 100644 --- a/pkg/runner/testdata/uses-workflow/local-workflow.yml +++ b/pkg/runner/testdata/uses-workflow/local-workflow.yml @@ -5,5 +5,5 @@ jobs: uses: ./.github/workflows/local-reusable-workflow.yml with: string_required: string - bool_required: true + bool_required: ${{ true }} number_required: 1 From 0ebe02b4b1e23f6d12d96561723a5138e7c2c4aa Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Wed, 23 Nov 2022 17:24:07 +0100 Subject: [PATCH 06/10] feat: use context specific expression evaluator --- pkg/runner/expression.go | 18 ++++++++++++------ pkg/runner/reusable_workflow.go | 6 ++++-- pkg/runner/run_context.go | 2 +- pkg/runner/runner.go | 14 ++++++++++++-- .../workflows/local-reusable-workflow.yml | 2 +- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pkg/runner/expression.go b/pkg/runner/expression.go index 2348ae39d3c..843f593879d 100644 --- a/pkg/runner/expression.go +++ b/pkg/runner/expression.go @@ -355,15 +355,21 @@ func setupWorkflowInputs(ctx context.Context, inputs *map[string]interface{}, rc config := rc.Run.Workflow.WorkflowCallConfig() for name, input := range config.Inputs { - value := rc.caller.With[name] + value := rc.caller.job.With[name] + if value != nil { + if str, ok := value.(string); ok { + // evaluate using the calling RunContext (outside) + value = rc.caller.runContext.ExprEval.Interpolate(ctx, str) + } + } if value == nil && config != nil && config.Inputs != nil { value = input.Default - } - - if rc.ExprEval != nil { - if str, ok := value.(string); ok { - value = rc.ExprEval.Interpolate(ctx, str) + if rc.ExprEval != nil { + if str, ok := value.(string); ok { + // evaluate using the called RunContext (inside) + value = rc.ExprEval.Interpolate(ctx, str) + } } } diff --git a/pkg/runner/reusable_workflow.go b/pkg/runner/reusable_workflow.go index 1d21e1b41c4..fc989260bd6 100644 --- a/pkg/runner/reusable_workflow.go +++ b/pkg/runner/reusable_workflow.go @@ -25,10 +25,12 @@ func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor { return runner.NewPlanExecutor(plan) } -func NewReusableWorkflowRunner(runnerConfig *Config, caller *model.Job) (Runner, error) { +func NewReusableWorkflowRunner(runnerConfig *Config, job *model.Job) (Runner, error) { runner := &runnerImpl{ config: runnerConfig, - caller: caller, + caller: &caller{ + job: job, + }, } return runner.configure() diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 6813d76eaa5..2eda06505ea 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -43,7 +43,7 @@ type RunContext struct { Parent *RunContext Masks []string cleanUpJobContainer common.Executor - caller *model.Job // job calling this RunContext (reusable workflows) + caller *caller // job calling this RunContext (reusable workflows) } func (rc *RunContext) AddMask(mask string) { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index f58e998c02f..22b9378c636 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -53,10 +53,15 @@ type Config struct { ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub. } +type caller struct { + job *model.Job + runContext *RunContext +} + type runnerImpl struct { config *Config eventJSON string - caller *model.Job // the job calling this runner (caller of a reusable workflow) + caller *caller // the job calling this runner (caller of a reusable workflow) } // New Creates a new Runner @@ -162,9 +167,14 @@ func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, mat EventJSON: runner.eventJSON, StepResults: make(map[string]*model.StepResult), Matrix: matrix, - caller: runner.caller, } rc.ExprEval = rc.NewExpressionEvaluator(ctx) rc.Name = rc.ExprEval.Interpolate(ctx, run.String()) + + if runner.caller != nil { + rc.caller = runner.caller + rc.caller.runContext = rc + } + return rc } diff --git a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml index 2d4ebdcabea..ff2219ea0f9 100644 --- a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml +++ b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml @@ -21,7 +21,7 @@ on: number_optional: required: false type: number - default: 1 + default: ${{ 1 }} jobs: reusable_workflow_job: From 5589cf1ce7d93808d2db7a3b9aa9f13268db8498 Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Wed, 23 Nov 2022 17:31:00 +0100 Subject: [PATCH 07/10] refactor: prepare for better re-usability --- pkg/runner/reusable_workflow.go | 11 ++++++++++- pkg/runner/run_context.go | 4 +--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/runner/reusable_workflow.go b/pkg/runner/reusable_workflow.go index fc989260bd6..bf124d39b4d 100644 --- a/pkg/runner/reusable_workflow.go +++ b/pkg/runner/reusable_workflow.go @@ -1,6 +1,7 @@ package runner import ( + "fmt" "path" "github.com/nektos/act/pkg/common" @@ -8,9 +9,17 @@ import ( ) func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor { + return newReusableWorkflowExecutor(rc, rc.Config.Workdir) +} + +func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor { + return common.NewErrorExecutor(fmt.Errorf("remote reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)")) +} + +func newReusableWorkflowExecutor(rc *RunContext, directory string) common.Executor { job := rc.Run.Job() - planner, err := model.NewWorkflowPlanner(path.Join(rc.Config.Workdir, job.Uses), true) + planner, err := model.NewWorkflowPlanner(path.Join(directory, job.Uses), true) if err != nil { return common.NewErrorExecutor(err) } diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 2eda06505ea..f37c16e174b 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -378,11 +378,9 @@ func (rc *RunContext) Executor() common.Executor { case model.JobTypeReusableWorkflowLocal: executor = newLocalReusableWorkflowExecutor(rc) case model.JobTypeReusableWorkflowRemote: - executor = common.NewErrorExecutor(fmt.Errorf("remote reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)")) + executor = newRemoteReusableWorkflowExecutor(rc) } - // todo: simplify - // return executor.If(rc.isEnabled) return func(ctx context.Context) error { res, err := rc.isEnabled(ctx) if err != nil { From 92955f5b5fe24721ba4876c302b048c3fba229cc Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Tue, 6 Dec 2022 12:01:53 +0100 Subject: [PATCH 08/10] feat: add secrets for reusable workflows --- pkg/model/workflow.go | 29 +++ pkg/runner/expression.go | 29 ++- pkg/runner/job_executor_test.go | 18 +- pkg/runner/reusable_workflow.go | 13 +- pkg/runner/run_context.go | 8 +- pkg/runner/runner.go | 7 +- pkg/runner/runner_test.go | 238 +++++++++--------- .../workflows/local-reusable-workflow.yml | 7 + .../testdata/uses-workflow/local-workflow.yml | 11 + 9 files changed, 217 insertions(+), 143 deletions(-) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 45d473d9b71..729a5631a95 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -148,6 +148,7 @@ type Job struct { Outputs map[string]string `yaml:"outputs"` Uses string `yaml:"uses"` With map[string]interface{} `yaml:"with"` + RawSecrets yaml.Node `yaml:"secrets"` Result string } @@ -202,6 +203,34 @@ func (s Strategy) GetFailFast() bool { return failFast } +func (j *Job) InheritSecrets() bool { + if j.RawSecrets.Kind != yaml.ScalarNode { + return false + } + + var val string + err := j.RawSecrets.Decode(&val) + if err != nil { + log.Fatal(err) + } + + return val == "inherit" +} + +func (j *Job) Secrets() map[string]string { + if j.RawSecrets.Kind != yaml.MappingNode { + return nil + } + + var val map[string]string + err := j.RawSecrets.Decode(&val) + if err != nil { + log.Fatal(err) + } + + return val +} + // Container details for the job func (j *Job) Container() *ContainerSpec { var val *ContainerSpec diff --git a/pkg/runner/expression.go b/pkg/runner/expression.go index 843f593879d..dc6b0e5b75c 100644 --- a/pkg/runner/expression.go +++ b/pkg/runner/expression.go @@ -55,7 +55,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map // todo: should be unavailable // but required to interpolate/evaluate the step outputs on the job Steps: rc.getStepsContext(), - Secrets: rc.Config.Secrets, + Secrets: getWorkflowSecrets(ctx, rc), Strategy: strategy, Matrix: rc.Matrix, Needs: using, @@ -101,7 +101,7 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step) Env: *step.getEnv(), Job: rc.getJobContext(), Steps: rc.getStepsContext(), - Secrets: rc.Config.Secrets, + Secrets: getWorkflowSecrets(ctx, rc), Strategy: strategy, Matrix: rc.Matrix, Needs: using, @@ -355,7 +355,7 @@ func setupWorkflowInputs(ctx context.Context, inputs *map[string]interface{}, rc config := rc.Run.Workflow.WorkflowCallConfig() for name, input := range config.Inputs { - value := rc.caller.job.With[name] + value := rc.caller.runContext.Run.Job().With[name] if value != nil { if str, ok := value.(string); ok { // evaluate using the calling RunContext (outside) @@ -377,3 +377,26 @@ func setupWorkflowInputs(ctx context.Context, inputs *map[string]interface{}, rc } } } + +func getWorkflowSecrets(ctx context.Context, rc *RunContext) map[string]string { + if rc.caller != nil { + job := rc.caller.runContext.Run.Job() + secrets := job.Secrets() + + if secrets == nil && job.InheritSecrets() { + secrets = rc.caller.runContext.Config.Secrets + } + + if secrets == nil { + secrets = map[string]string{} + } + + for k, v := range secrets { + secrets[k] = rc.caller.runContext.ExprEval.Interpolate(ctx, v) + } + + return secrets + } + + return rc.Config.Secrets +} diff --git a/pkg/runner/job_executor_test.go b/pkg/runner/job_executor_test.go index e00a4fd656f..87c58886705 100644 --- a/pkg/runner/job_executor_test.go +++ b/pkg/runner/job_executor_test.go @@ -15,15 +15,15 @@ import ( func TestJobExecutor(t *testing.T) { tables := []TestJobFileInfo{ - {workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms}, - {workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms}, - {workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms}, - {workdir, "uses-github-root", "push", "", platforms}, - {workdir, "uses-github-path", "push", "", platforms}, - {workdir, "uses-docker-url", "push", "", platforms}, - {workdir, "uses-github-full-sha", "push", "", platforms}, - {workdir, "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms}, - {workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms}, + {workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms, secrets}, + {workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets}, + {workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets}, + {workdir, "uses-github-root", "push", "", platforms, secrets}, + {workdir, "uses-github-path", "push", "", platforms, secrets}, + {workdir, "uses-docker-url", "push", "", platforms, secrets}, + {workdir, "uses-github-full-sha", "push", "", platforms, secrets}, + {workdir, "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms, secrets}, + {workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms, secrets}, } // These tests are sufficient to only check syntax. ctx := common.WithDryrun(context.Background(), true) diff --git a/pkg/runner/reusable_workflow.go b/pkg/runner/reusable_workflow.go index bf124d39b4d..87b7bde944b 100644 --- a/pkg/runner/reusable_workflow.go +++ b/pkg/runner/reusable_workflow.go @@ -17,16 +17,14 @@ func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor { } func newReusableWorkflowExecutor(rc *RunContext, directory string) common.Executor { - job := rc.Run.Job() - - planner, err := model.NewWorkflowPlanner(path.Join(directory, job.Uses), true) + planner, err := model.NewWorkflowPlanner(path.Join(directory, rc.Run.Job().Uses), true) if err != nil { return common.NewErrorExecutor(err) } plan := planner.PlanEvent("workflow_call") - runner, err := NewReusableWorkflowRunner(rc.Config, job) + runner, err := NewReusableWorkflowRunner(rc) if err != nil { return common.NewErrorExecutor(err) } @@ -34,11 +32,12 @@ func newReusableWorkflowExecutor(rc *RunContext, directory string) common.Execut return runner.NewPlanExecutor(plan) } -func NewReusableWorkflowRunner(runnerConfig *Config, job *model.Job) (Runner, error) { +func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) { runner := &runnerImpl{ - config: runnerConfig, + config: rc.Config, + eventJSON: rc.EventJSON, caller: &caller{ - job: job, + runContext: rc, }, } diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index f37c16e174b..2d60be157ae 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -56,7 +56,13 @@ type MappableOutput struct { } func (rc *RunContext) String() string { - return fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name) + name := fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name) + if rc.caller != nil { + // prefix the reusable workflow with the caller job + // this is required to create unique container names + name = fmt.Sprintf("%s/%s", rc.caller.runContext.Run.JobID, name) + } + return name } // GetEnv returns the env for the context diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 22b9378c636..2ed967db160 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -54,7 +54,6 @@ type Config struct { } type caller struct { - job *model.Job runContext *RunContext } @@ -167,14 +166,10 @@ func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, mat EventJSON: runner.eventJSON, StepResults: make(map[string]*model.StepResult), Matrix: matrix, + caller: runner.caller, } rc.ExprEval = rc.NewExpressionEvaluator(ctx) rc.Name = rc.ExprEval.Interpolate(ctx, run.String()) - if runner.caller != nil { - rc.caller = runner.caller - rc.caller.runContext = rc - } - return rc } diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 08365822946..beb8860bd6c 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -22,6 +22,7 @@ var ( platforms map[string]string logLevel = log.DebugLevel workdir = "testdata" + secrets map[string]string ) func init() { @@ -42,6 +43,8 @@ func init() { if wd, err := filepath.Abs(workdir); err == nil { workdir = wd } + + secrets = map[string]string{} } func TestGraphEvent(t *testing.T) { @@ -68,6 +71,7 @@ type TestJobFileInfo struct { eventName string errorMessage string platforms map[string]string + secrets map[string]string } func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) { @@ -119,80 +123,80 @@ func TestRunEvent(t *testing.T) { tables := []TestJobFileInfo{ // Shells - {workdir, "shells/defaults", "push", "", platforms}, + {workdir, "shells/defaults", "push", "", platforms, secrets}, // TODO: figure out why it fails // {workdir, "shells/custom", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, }, // custom image with pwsh - {workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh - {workdir, "shells/bash", "push", "", platforms}, - {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python - {workdir, "shells/sh", "push", "", platforms}, + {workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh + {workdir, "shells/bash", "push", "", platforms, secrets}, + {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python + {workdir, "shells/sh", "push", "", platforms, secrets}, // Local action - {workdir, "local-action-docker-url", "push", "", platforms}, - {workdir, "local-action-dockerfile", "push", "", platforms}, - {workdir, "local-action-via-composite-dockerfile", "push", "", platforms}, - {workdir, "local-action-js", "push", "", platforms}, + {workdir, "local-action-docker-url", "push", "", platforms, secrets}, + {workdir, "local-action-dockerfile", "push", "", platforms, secrets}, + {workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets}, + {workdir, "local-action-js", "push", "", platforms, secrets}, // Uses - {workdir, "uses-composite", "push", "", platforms}, - {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms}, - {workdir, "uses-nested-composite", "push", "", platforms}, - {workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms}, - {workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms}, - {workdir, "uses-workflow", "pull_request", "", platforms}, - {workdir, "uses-docker-url", "push", "", platforms}, - {workdir, "act-composite-env-test", "push", "", platforms}, + {workdir, "uses-composite", "push", "", platforms, secrets}, + {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets}, + {workdir, "uses-nested-composite", "push", "", platforms, secrets}, + {workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms, secrets}, + {workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms, secrets}, + {workdir, "uses-workflow", "pull_request", "", platforms, map[string]string{"secret": "keep_it_private"}}, + {workdir, "uses-docker-url", "push", "", platforms, secrets}, + {workdir, "act-composite-env-test", "push", "", platforms, secrets}, // Eval - {workdir, "evalmatrix", "push", "", platforms}, - {workdir, "evalmatrixneeds", "push", "", platforms}, - {workdir, "evalmatrixneeds2", "push", "", platforms}, - {workdir, "evalmatrix-merge-map", "push", "", platforms}, - {workdir, "evalmatrix-merge-array", "push", "", platforms}, - {workdir, "issue-1195", "push", "", platforms}, - - {workdir, "basic", "push", "", platforms}, - {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms}, - {workdir, "runs-on", "push", "", platforms}, - {workdir, "checkout", "push", "", platforms}, - {workdir, "job-container", "push", "", platforms}, - {workdir, "job-container-non-root", "push", "", platforms}, - {workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms}, - {workdir, "container-hostname", "push", "", platforms}, - {workdir, "remote-action-docker", "push", "", platforms}, - {workdir, "remote-action-js", "push", "", platforms}, - {workdir, "remote-action-js", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:runner-latest"}}, // Test if this works with non root container - {workdir, "matrix", "push", "", platforms}, - {workdir, "matrix-include-exclude", "push", "", platforms}, - {workdir, "commands", "push", "", platforms}, - {workdir, "workdir", "push", "", platforms}, - {workdir, "defaults-run", "push", "", platforms}, - {workdir, "composite-fail-with-output", "push", "", platforms}, - {workdir, "issue-597", "push", "", platforms}, - {workdir, "issue-598", "push", "", platforms}, - {workdir, "if-env-act", "push", "", platforms}, - {workdir, "env-and-path", "push", "", platforms}, - {workdir, "environment-files", "push", "", platforms}, - {workdir, "GITHUB_STATE", "push", "", platforms}, - {workdir, "environment-files-parser-bug", "push", "", platforms}, - {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms}, - {workdir, "outputs", "push", "", platforms}, - {workdir, "networking", "push", "", platforms}, - {workdir, "steps-context/conclusion", "push", "", platforms}, - {workdir, "steps-context/outcome", "push", "", platforms}, - {workdir, "job-status-check", "push", "job 'fail' failed", platforms}, - {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, - {workdir, "actions-environment-and-context-tests", "push", "", platforms}, - {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms}, - {workdir, "evalenv", "push", "", platforms}, - {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms}, - {workdir, "workflow_dispatch", "workflow_dispatch", "", platforms}, - {workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms}, - {workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms}, - {workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms}, - {"../model/testdata", "strategy", "push", "", platforms}, // TODO: move all testdata into pkg so we can validate it with planner and runner + {workdir, "evalmatrix", "push", "", platforms, secrets}, + {workdir, "evalmatrixneeds", "push", "", platforms, secrets}, + {workdir, "evalmatrixneeds2", "push", "", platforms, secrets}, + {workdir, "evalmatrix-merge-map", "push", "", platforms, secrets}, + {workdir, "evalmatrix-merge-array", "push", "", platforms, secrets}, + {workdir, "issue-1195", "push", "", platforms, secrets}, + + {workdir, "basic", "push", "", platforms, secrets}, + {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets}, + {workdir, "runs-on", "push", "", platforms, secrets}, + {workdir, "checkout", "push", "", platforms, secrets}, + {workdir, "job-container", "push", "", platforms, secrets}, + {workdir, "job-container-non-root", "push", "", platforms, secrets}, + {workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms, secrets}, + {workdir, "container-hostname", "push", "", platforms, secrets}, + {workdir, "remote-action-docker", "push", "", platforms, secrets}, + {workdir, "remote-action-js", "push", "", platforms, secrets}, + {workdir, "remote-action-js", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:runner-latest"}, secrets}, // Test if this works with non root container + {workdir, "matrix", "push", "", platforms, secrets}, + {workdir, "matrix-include-exclude", "push", "", platforms, secrets}, + {workdir, "commands", "push", "", platforms, secrets}, + {workdir, "workdir", "push", "", platforms, secrets}, + {workdir, "defaults-run", "push", "", platforms, secrets}, + {workdir, "composite-fail-with-output", "push", "", platforms, secrets}, + {workdir, "issue-597", "push", "", platforms, secrets}, + {workdir, "issue-598", "push", "", platforms, secrets}, + {workdir, "if-env-act", "push", "", platforms, secrets}, + {workdir, "env-and-path", "push", "", platforms, secrets}, + {workdir, "environment-files", "push", "", platforms, secrets}, + {workdir, "GITHUB_STATE", "push", "", platforms, secrets}, + {workdir, "environment-files-parser-bug", "push", "", platforms, secrets}, + {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets}, + {workdir, "outputs", "push", "", platforms, secrets}, + {workdir, "networking", "push", "", platforms, secrets}, + {workdir, "steps-context/conclusion", "push", "", platforms, secrets}, + {workdir, "steps-context/outcome", "push", "", platforms, secrets}, + {workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets}, + {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets}, + {workdir, "actions-environment-and-context-tests", "push", "", platforms, secrets}, + {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets}, + {workdir, "evalenv", "push", "", platforms, secrets}, + {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets}, + {workdir, "workflow_dispatch", "workflow_dispatch", "", platforms, secrets}, + {workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms, secrets}, + {workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms, secrets}, + {workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms, secrets}, + {"../model/testdata", "strategy", "push", "", platforms, secrets}, // TODO: move all testdata into pkg so we can validate it with planner and runner // {"testdata", "issue-228", "push", "", platforms, }, // TODO [igni]: Remove this once everything passes - {"../model/testdata", "container-volumes", "push", "", platforms}, + {"../model/testdata", "container-volumes", "push", "", platforms, secrets}, } for _, table := range tables { @@ -225,51 +229,51 @@ func TestRunEventHostEnvironment(t *testing.T) { tables = append(tables, []TestJobFileInfo{ // Shells - {workdir, "shells/defaults", "push", "", platforms}, - {workdir, "shells/pwsh", "push", "", platforms}, - {workdir, "shells/bash", "push", "", platforms}, - {workdir, "shells/python", "push", "", platforms}, - {workdir, "shells/sh", "push", "", platforms}, + {workdir, "shells/defaults", "push", "", platforms, secrets}, + {workdir, "shells/pwsh", "push", "", platforms, secrets}, + {workdir, "shells/bash", "push", "", platforms, secrets}, + {workdir, "shells/python", "push", "", platforms, secrets}, + {workdir, "shells/sh", "push", "", platforms, secrets}, // Local action - {workdir, "local-action-js", "push", "", platforms}, + {workdir, "local-action-js", "push", "", platforms, secrets}, // Uses - {workdir, "uses-composite", "push", "", platforms}, - {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms}, - {workdir, "uses-nested-composite", "push", "", platforms}, - {workdir, "act-composite-env-test", "push", "", platforms}, + {workdir, "uses-composite", "push", "", platforms, secrets}, + {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets}, + {workdir, "uses-nested-composite", "push", "", platforms, secrets}, + {workdir, "act-composite-env-test", "push", "", platforms, secrets}, // Eval - {workdir, "evalmatrix", "push", "", platforms}, - {workdir, "evalmatrixneeds", "push", "", platforms}, - {workdir, "evalmatrixneeds2", "push", "", platforms}, - {workdir, "evalmatrix-merge-map", "push", "", platforms}, - {workdir, "evalmatrix-merge-array", "push", "", platforms}, - {workdir, "issue-1195", "push", "", platforms}, - - {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms}, - {workdir, "runs-on", "push", "", platforms}, - {workdir, "checkout", "push", "", platforms}, - {workdir, "remote-action-js", "push", "", platforms}, - {workdir, "matrix", "push", "", platforms}, - {workdir, "matrix-include-exclude", "push", "", platforms}, - {workdir, "commands", "push", "", platforms}, - {workdir, "defaults-run", "push", "", platforms}, - {workdir, "composite-fail-with-output", "push", "", platforms}, - {workdir, "issue-597", "push", "", platforms}, - {workdir, "issue-598", "push", "", platforms}, - {workdir, "if-env-act", "push", "", platforms}, - {workdir, "env-and-path", "push", "", platforms}, - {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms}, - {workdir, "outputs", "push", "", platforms}, - {workdir, "steps-context/conclusion", "push", "", platforms}, - {workdir, "steps-context/outcome", "push", "", platforms}, - {workdir, "job-status-check", "push", "job 'fail' failed", platforms}, - {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, - {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms}, - {workdir, "evalenv", "push", "", platforms}, - {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms}, + {workdir, "evalmatrix", "push", "", platforms, secrets}, + {workdir, "evalmatrixneeds", "push", "", platforms, secrets}, + {workdir, "evalmatrixneeds2", "push", "", platforms, secrets}, + {workdir, "evalmatrix-merge-map", "push", "", platforms, secrets}, + {workdir, "evalmatrix-merge-array", "push", "", platforms, secrets}, + {workdir, "issue-1195", "push", "", platforms, secrets}, + + {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets}, + {workdir, "runs-on", "push", "", platforms, secrets}, + {workdir, "checkout", "push", "", platforms, secrets}, + {workdir, "remote-action-js", "push", "", platforms, secrets}, + {workdir, "matrix", "push", "", platforms, secrets}, + {workdir, "matrix-include-exclude", "push", "", platforms, secrets}, + {workdir, "commands", "push", "", platforms, secrets}, + {workdir, "defaults-run", "push", "", platforms, secrets}, + {workdir, "composite-fail-with-output", "push", "", platforms, secrets}, + {workdir, "issue-597", "push", "", platforms, secrets}, + {workdir, "issue-598", "push", "", platforms, secrets}, + {workdir, "if-env-act", "push", "", platforms, secrets}, + {workdir, "env-and-path", "push", "", platforms, secrets}, + {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets}, + {workdir, "outputs", "push", "", platforms, secrets}, + {workdir, "steps-context/conclusion", "push", "", platforms, secrets}, + {workdir, "steps-context/outcome", "push", "", platforms, secrets}, + {workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets}, + {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets}, + {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets}, + {workdir, "evalenv", "push", "", platforms, secrets}, + {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets}, }...) } if runtime.GOOS == "windows" { @@ -278,8 +282,8 @@ func TestRunEventHostEnvironment(t *testing.T) { } tables = append(tables, []TestJobFileInfo{ - {workdir, "windows-prepend-path", "push", "", platforms}, - {workdir, "windows-add-env", "push", "", platforms}, + {workdir, "windows-prepend-path", "push", "", platforms, secrets}, + {workdir, "windows-add-env", "push", "", platforms, secrets}, }...) } else { platforms := map[string]string{ @@ -287,8 +291,8 @@ func TestRunEventHostEnvironment(t *testing.T) { } tables = append(tables, []TestJobFileInfo{ - {workdir, "nix-prepend-path", "push", "", platforms}, - {workdir, "inputs-via-env-context", "push", "", platforms}, + {workdir, "nix-prepend-path", "push", "", platforms, secrets}, + {workdir, "inputs-via-env-context", "push", "", platforms, secrets}, }...) } @@ -308,17 +312,17 @@ func TestDryrunEvent(t *testing.T) { tables := []TestJobFileInfo{ // Shells - {workdir, "shells/defaults", "push", "", platforms}, - {workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}}, // custom image with pwsh - {workdir, "shells/bash", "push", "", platforms}, - {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python - {workdir, "shells/sh", "push", "", platforms}, + {workdir, "shells/defaults", "push", "", platforms, secrets}, + {workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh + {workdir, "shells/bash", "push", "", platforms, secrets}, + {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python + {workdir, "shells/sh", "push", "", platforms, secrets}, // Local action - {workdir, "local-action-docker-url", "push", "", platforms}, - {workdir, "local-action-dockerfile", "push", "", platforms}, - {workdir, "local-action-via-composite-dockerfile", "push", "", platforms}, - {workdir, "local-action-js", "push", "", platforms}, + {workdir, "local-action-docker-url", "push", "", platforms, secrets}, + {workdir, "local-action-dockerfile", "push", "", platforms, secrets}, + {workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets}, + {workdir, "local-action-js", "push", "", platforms, secrets}, } for _, table := range tables { diff --git a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml index ff2219ea0f9..31302d79095 100644 --- a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml +++ b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml @@ -1,3 +1,5 @@ +name: reusable + on: workflow_call: inputs: @@ -56,3 +58,8 @@ jobs: run: | echo inputs.number_optional=${{ inputs.number_optional }} [[ "${{ inputs.number_optional == 1 }}" = "true" ]] || exit 1 + + - name: test secret + run: | + echo secrets.secret=${{ secrets.secret }} + [[ "${{ secrets.secret == 'keep_it_private' }}" = "true" ]] || exit 1 diff --git a/pkg/runner/testdata/uses-workflow/local-workflow.yml b/pkg/runner/testdata/uses-workflow/local-workflow.yml index d6921a635f0..48f38bd8e01 100644 --- a/pkg/runner/testdata/uses-workflow/local-workflow.yml +++ b/pkg/runner/testdata/uses-workflow/local-workflow.yml @@ -1,3 +1,4 @@ +name: local-reusable-workflows on: pull_request jobs: @@ -7,3 +8,13 @@ jobs: string_required: string bool_required: ${{ true }} number_required: 1 + secrets: + secret: keep_it_private + + reusable-workflow-with-inherited-secrets: + uses: ./.github/workflows/local-reusable-workflow.yml + with: + string_required: string + bool_required: ${{ true }} + number_required: 1 + secrets: inherit From b14105e4bf475d8cc6639405cfae4dd45573f3ba Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Tue, 6 Dec 2022 13:29:27 +0100 Subject: [PATCH 09/10] test: use secrets during test run --- pkg/runner/runner_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index beb8860bd6c..cee034516f8 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -201,7 +201,9 @@ func TestRunEvent(t *testing.T) { for _, table := range tables { t.Run(table.workflowPath, func(t *testing.T) { - config := &Config{} + config := &Config{ + Secrets: table.secrets, + } eventFile := filepath.Join(workdir, table.workflowPath, "event.json") if _, err := os.Stat(eventFile); err == nil { From 74da5b085c0c4d08c5e5bf53501e555cb585b26c Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Wed, 7 Dec 2022 11:26:40 +0100 Subject: [PATCH 10/10] feat: handle reusable workflow outputs --- pkg/model/workflow.go | 8 +++++++- pkg/runner/job_executor.go | 20 +++++++++++++++++++ .../workflows/local-reusable-workflow.yml | 12 +++++++++++ .../testdata/uses-workflow/local-workflow.yml | 16 +++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 729a5631a95..2b27bbc1a34 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -107,8 +107,14 @@ type WorkflowCallInput struct { Type string `yaml:"type"` } +type WorkflowCallOutput struct { + Description string `yaml:"description"` + Value string `yaml:"value"` +} + type WorkflowCall struct { - Inputs map[string]WorkflowCallInput `yaml:"inputs"` + Inputs map[string]WorkflowCallInput `yaml:"inputs"` + Outputs map[string]WorkflowCallOutput `yaml:"outputs"` } func (w *Workflow) WorkflowCallConfig() *WorkflowCall { diff --git a/pkg/runner/job_executor.go b/pkg/runner/job_executor.go index b445708552f..4ae778791f6 100644 --- a/pkg/runner/job_executor.go +++ b/pkg/runner/job_executor.go @@ -104,6 +104,8 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo err = info.stopContainer()(ctx) } setJobResult(ctx, info, rc, jobError == nil) + setJobOutputs(ctx, rc) + return err }) @@ -135,9 +137,27 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo jobResultMessage = "failed" } info.result(jobResult) + if rc.caller != nil { + // set reusable workflow job result + rc.caller.runContext.result(jobResult) + } logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage) } +func setJobOutputs(ctx context.Context, rc *RunContext) { + if rc.caller != nil { + // map outputs for reusable workflows + callerOutputs := make(map[string]string) + + ee := rc.NewExpressionEvaluator(ctx) + for k, v := range rc.Run.Job().Outputs { + callerOutputs[k] = ee.Interpolate(ctx, v) + } + + rc.caller.runContext.Run.Job().Outputs = callerOutputs + } +} + func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor { return func(ctx context.Context) error { ctx = withStepLogger(ctx, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String()) diff --git a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml index 31302d79095..b04fe72da03 100644 --- a/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml +++ b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml @@ -24,6 +24,10 @@ on: required: false type: number default: ${{ 1 }} + outputs: + output: + description: "A workflow output" + value: ${{ jobs.reusable_workflow_job.outputs.output }} jobs: reusable_workflow_job: @@ -63,3 +67,11 @@ jobs: run: | echo secrets.secret=${{ secrets.secret }} [[ "${{ secrets.secret == 'keep_it_private' }}" = "true" ]] || exit 1 + + - name: test output + id: output_test + run: | + echo "value=${{ inputs.string_required }}" >> $GITHUB_OUTPUT + + outputs: + output: ${{ steps.output_test.outputs.value }} diff --git a/pkg/runner/testdata/uses-workflow/local-workflow.yml b/pkg/runner/testdata/uses-workflow/local-workflow.yml index 48f38bd8e01..070e4d0c1d7 100644 --- a/pkg/runner/testdata/uses-workflow/local-workflow.yml +++ b/pkg/runner/testdata/uses-workflow/local-workflow.yml @@ -18,3 +18,19 @@ jobs: bool_required: ${{ true }} number_required: 1 secrets: inherit + + output-test: + runs-on: ubuntu-latest + needs: + - reusable-workflow + - reusable-workflow-with-inherited-secrets + steps: + - name: output with secrets map + run: | + echo reusable-workflow.output=${{ needs.reusable-workflow.outputs.output }} + [[ "${{ needs.reusable-workflow.outputs.output == 'string' }}" = "true" ]] || exit 1 + + - name: output with inherited secrets + run: | + echo reusable-workflow-with-inherited-secrets.output=${{ needs.reusable-workflow-with-inherited-secrets.outputs.output }} + [[ "${{ needs.reusable-workflow-with-inherited-secrets.outputs.output == 'string' }}" = "true" ]] || exit 1