diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 78914e82e2f..3da7a13394b 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -100,6 +100,44 @@ 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 WorkflowCallOutput struct { + Description string `yaml:"description"` + Value string `yaml:"value"` +} + +type WorkflowCall struct { + Inputs map[string]WorkflowCallInput `yaml:"inputs"` + Outputs map[string]WorkflowCallOutput `yaml:"outputs"` +} + +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"` @@ -115,6 +153,8 @@ type Job struct { Defaults Defaults `yaml:"defaults"` Outputs map[string]string `yaml:"outputs"` Uses string `yaml:"uses"` + With map[string]interface{} `yaml:"with"` + RawSecrets yaml.Node `yaml:"secrets"` Result string } @@ -169,6 +209,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 6a621f7b3d2..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, @@ -315,6 +315,8 @@ 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{}{} + setupWorkflowInputs(ctx, &inputs, rc) + var env map[string]string if step != nil { env = *step.getEnv() @@ -347,3 +349,54 @@ 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.runContext.Run.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 { + // evaluate using the called RunContext (inside) + value = rc.ExprEval.Interpolate(ctx, str) + } + } + } + + (*inputs)[name] = value + } + } +} + +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.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/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 new file mode 100644 index 00000000000..87b7bde944b --- /dev/null +++ b/pkg/runner/reusable_workflow.go @@ -0,0 +1,45 @@ +package runner + +import ( + "fmt" + "path" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/model" +) + +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 { + 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) + if err != nil { + return common.NewErrorExecutor(err) + } + + return runner.NewPlanExecutor(plan) +} + +func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) { + runner := &runnerImpl{ + config: rc.Config, + eventJSON: rc.EventJSON, + caller: &caller{ + runContext: rc, + }, + } + + return runner.configure() +} diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index c653cc7e89a..854e628db19 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -46,6 +46,7 @@ type RunContext struct { Parent *RunContext Masks []string cleanUpJobContainer common.Executor + caller *caller // job calling this RunContext (reusable workflows) } func (rc *RunContext) AddMask(mask string) { @@ -58,7 +59,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 @@ -399,16 +406,25 @@ 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 = newRemoteReusableWorkflowExecutor(rc) + } + 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 } } @@ -458,6 +474,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..2ed967db160 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -53,9 +53,14 @@ type Config struct { ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub. } +type caller struct { + runContext *RunContext +} + type runnerImpl struct { config *Config eventJSON string + caller *caller // the job calling this runner (caller of a reusable workflow) } // New Creates a new Runner @@ -64,8 +69,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 { @@ -89,10 +98,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 { @@ -161,8 +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()) + return rc } diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index faa13ced30a..1f6141177da 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -24,6 +24,7 @@ var ( platforms map[string]string logLevel = log.DebugLevel workdir = "testdata" + secrets map[string]string ) func init() { @@ -44,6 +45,8 @@ func init() { if wd, err := filepath.Abs(workdir); err == nil { workdir = wd } + + secrets = map[string]string{} } func TestGraphEvent(t *testing.T) { @@ -70,6 +73,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) { @@ -121,84 +125,87 @@ 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-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 { 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 { @@ -226,51 +233,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" { @@ -279,8 +286,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{ @@ -288,8 +295,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}, }...) } @@ -309,17 +316,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 new file mode 100644 index 00000000000..b04fe72da03 --- /dev/null +++ b/pkg/runner/testdata/.github/workflows/local-reusable-workflow.yml @@ -0,0 +1,77 @@ +name: reusable + +on: + workflow_call: + inputs: + 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 }} + outputs: + output: + description: "A workflow output" + value: ${{ jobs.reusable_workflow_job.outputs.output }} + +jobs: + reusable_workflow_job: + runs-on: ubuntu-latest + steps: + - name: test required string + run: | + 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 + + - name: test secret + 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 new file mode 100644 index 00000000000..070e4d0c1d7 --- /dev/null +++ b/pkg/runner/testdata/uses-workflow/local-workflow.yml @@ -0,0 +1,36 @@ +name: local-reusable-workflows +on: pull_request + +jobs: + reusable-workflow: + uses: ./.github/workflows/local-reusable-workflow.yml + with: + 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 + + 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