feat: allow to spawn and run a local reusable workflow (#1423)

* 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

* feat: add workflow_call inputs

* test: improve inputs test

* feat: add input defaults

* feat: allow expressions in inputs

* feat: use context specific expression evaluator

* refactor: prepare for better re-usability

* feat: add secrets for reusable workflows

* test: use secrets during test run

* feat: handle reusable workflow outputs

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Markus Wolf 2022-12-15 17:45:22 +01:00 committed by GitHub
parent d281230cce
commit a8e05cded6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 470 additions and 137 deletions

View file

@ -100,6 +100,44 @@ func (w *Workflow) WorkflowDispatchConfig() *WorkflowDispatch {
return &config 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 // Job is the structure of one job in a workflow
type Job struct { type Job struct {
Name string `yaml:"name"` Name string `yaml:"name"`
@ -115,6 +153,8 @@ type Job struct {
Defaults Defaults `yaml:"defaults"` Defaults Defaults `yaml:"defaults"`
Outputs map[string]string `yaml:"outputs"` Outputs map[string]string `yaml:"outputs"`
Uses string `yaml:"uses"` Uses string `yaml:"uses"`
With map[string]interface{} `yaml:"with"`
RawSecrets yaml.Node `yaml:"secrets"`
Result string Result string
} }
@ -169,6 +209,34 @@ func (s Strategy) GetFailFast() bool {
return failFast 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 // Container details for the job
func (j *Job) Container() *ContainerSpec { func (j *Job) Container() *ContainerSpec {
var val *ContainerSpec var val *ContainerSpec

View file

@ -55,7 +55,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
// todo: should be unavailable // todo: should be unavailable
// but required to interpolate/evaluate the step outputs on the job // but required to interpolate/evaluate the step outputs on the job
Steps: rc.getStepsContext(), Steps: rc.getStepsContext(),
Secrets: rc.Config.Secrets, Secrets: getWorkflowSecrets(ctx, rc),
Strategy: strategy, Strategy: strategy,
Matrix: rc.Matrix, Matrix: rc.Matrix,
Needs: using, Needs: using,
@ -101,7 +101,7 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
Env: *step.getEnv(), Env: *step.getEnv(),
Job: rc.getJobContext(), Job: rc.getJobContext(),
Steps: rc.getStepsContext(), Steps: rc.getStepsContext(),
Secrets: rc.Config.Secrets, Secrets: getWorkflowSecrets(ctx, rc),
Strategy: strategy, Strategy: strategy,
Matrix: rc.Matrix, Matrix: rc.Matrix,
Needs: using, 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{} { func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]interface{} {
inputs := map[string]interface{}{} inputs := map[string]interface{}{}
setupWorkflowInputs(ctx, &inputs, rc)
var env map[string]string var env map[string]string
if step != nil { if step != nil {
env = *step.getEnv() env = *step.getEnv()
@ -347,3 +349,54 @@ func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *mod
return inputs 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
}

View file

@ -104,6 +104,8 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
err = info.stopContainer()(ctx) err = info.stopContainer()(ctx)
} }
setJobResult(ctx, info, rc, jobError == nil) setJobResult(ctx, info, rc, jobError == nil)
setJobOutputs(ctx, rc)
return err return err
}) })
@ -135,9 +137,27 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
jobResultMessage = "failed" jobResultMessage = "failed"
} }
info.result(jobResult) 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) 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 { func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
ctx = withStepLogger(ctx, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String()) ctx = withStepLogger(ctx, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String())

View file

@ -15,15 +15,15 @@ import (
func TestJobExecutor(t *testing.T) { func TestJobExecutor(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
{workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", 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}, {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}, {workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, secrets},
{workdir, "uses-github-root", "push", "", platforms}, {workdir, "uses-github-root", "push", "", platforms, secrets},
{workdir, "uses-github-path", "push", "", platforms}, {workdir, "uses-github-path", "push", "", platforms, secrets},
{workdir, "uses-docker-url", "push", "", platforms}, {workdir, "uses-docker-url", "push", "", platforms, secrets},
{workdir, "uses-github-full-sha", "push", "", platforms}, {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}, {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}, {workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms, secrets},
} }
// These tests are sufficient to only check syntax. // These tests are sufficient to only check syntax.
ctx := common.WithDryrun(context.Background(), true) ctx := common.WithDryrun(context.Background(), true)

View file

@ -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()
}

View file

@ -46,6 +46,7 @@ type RunContext struct {
Parent *RunContext Parent *RunContext
Masks []string Masks []string
cleanUpJobContainer common.Executor cleanUpJobContainer common.Executor
caller *caller // job calling this RunContext (reusable workflows)
} }
func (rc *RunContext) AddMask(mask string) { func (rc *RunContext) AddMask(mask string) {
@ -58,7 +59,13 @@ type MappableOutput struct {
} }
func (rc *RunContext) String() string { 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 // 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 // Executor returns a pipeline executor for all the steps in the job
func (rc *RunContext) Executor() common.Executor { 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 { return func(ctx context.Context) error {
isEnabled, err := rc.isEnabled(ctx) res, err := rc.isEnabled(ctx)
if err != nil { if err != nil {
return err return err
} }
if res {
if isEnabled { return executor(ctx)
return newJobExecutor(rc, &stepFactoryImpl{}, rc)(ctx)
} }
return nil return nil
} }
} }
@ -458,6 +474,10 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
return false, nil return false, nil
} }
if job.Type() != model.JobTypeDefault {
return true, nil
}
img := rc.platformImage(ctx) img := rc.platformImage(ctx)
if img == "" { if img == "" {
if job.RunsOn() == nil { if job.RunsOn() == nil {

View file

@ -53,9 +53,14 @@ type Config struct {
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub. ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
} }
type caller struct {
runContext *RunContext
}
type runnerImpl struct { type runnerImpl struct {
config *Config config *Config
eventJSON string eventJSON string
caller *caller // the job calling this runner (caller of a reusable workflow)
} }
// New Creates a new Runner // New Creates a new Runner
@ -64,8 +69,12 @@ func New(runnerConfig *Config) (Runner, error) {
config: runnerConfig, config: runnerConfig,
} }
return runner.configure()
}
func (runner *runnerImpl) configure() (Runner, error) {
runner.eventJSON = "{}" runner.eventJSON = "{}"
if runnerConfig.EventPath != "" { if runner.config.EventPath != "" {
log.Debugf("Reading event.json from %s", runner.config.EventPath) log.Debugf("Reading event.json from %s", runner.config.EventPath)
eventJSONBytes, err := os.ReadFile(runner.config.EventPath) eventJSONBytes, err := os.ReadFile(runner.config.EventPath)
if err != nil { if err != nil {
@ -89,10 +98,6 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
stageExecutor := make([]common.Executor, 0) stageExecutor := make([]common.Executor, 0)
job := run.Job() 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 { if job.Strategy != nil {
strategyRc := runner.newRunContext(ctx, run, nil) strategyRc := runner.newRunContext(ctx, run, nil)
if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != 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, EventJSON: runner.eventJSON,
StepResults: make(map[string]*model.StepResult), StepResults: make(map[string]*model.StepResult),
Matrix: matrix, Matrix: matrix,
caller: runner.caller,
} }
rc.ExprEval = rc.NewExpressionEvaluator(ctx) rc.ExprEval = rc.NewExpressionEvaluator(ctx)
rc.Name = rc.ExprEval.Interpolate(ctx, run.String()) rc.Name = rc.ExprEval.Interpolate(ctx, run.String())
return rc return rc
} }

View file

@ -24,6 +24,7 @@ var (
platforms map[string]string platforms map[string]string
logLevel = log.DebugLevel logLevel = log.DebugLevel
workdir = "testdata" workdir = "testdata"
secrets map[string]string
) )
func init() { func init() {
@ -44,6 +45,8 @@ func init() {
if wd, err := filepath.Abs(workdir); err == nil { if wd, err := filepath.Abs(workdir); err == nil {
workdir = wd workdir = wd
} }
secrets = map[string]string{}
} }
func TestGraphEvent(t *testing.T) { func TestGraphEvent(t *testing.T) {
@ -70,6 +73,7 @@ type TestJobFileInfo struct {
eventName string eventName string
errorMessage string errorMessage string
platforms map[string]string platforms map[string]string
secrets map[string]string
} }
func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) { func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) {
@ -121,84 +125,87 @@ func TestRunEvent(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
// Shells // Shells
{workdir, "shells/defaults", "push", "", platforms}, {workdir, "shells/defaults", "push", "", platforms, secrets},
// TODO: figure out why it fails // 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/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/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh
{workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/bash", "push", "", platforms, secrets},
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
{workdir, "shells/sh", "push", "", platforms}, {workdir, "shells/sh", "push", "", platforms, secrets},
// Local action // Local action
{workdir, "local-action-docker-url", "push", "", platforms}, {workdir, "local-action-docker-url", "push", "", platforms, secrets},
{workdir, "local-action-dockerfile", "push", "", platforms}, {workdir, "local-action-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms}, {workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-js", "push", "", platforms}, {workdir, "local-action-js", "push", "", platforms, secrets},
// Uses // Uses
{workdir, "uses-composite", "push", "", platforms}, {workdir, "uses-composite", "push", "", platforms, secrets},
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms}, {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
{workdir, "uses-nested-composite", "push", "", platforms}, {workdir, "uses-nested-composite", "push", "", platforms, secrets},
{workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms}, {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}, {workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms, secrets},
{workdir, "uses-docker-url", "push", "", platforms}, {workdir, "uses-workflow", "pull_request", "", platforms, map[string]string{"secret": "keep_it_private"}},
{workdir, "act-composite-env-test", "push", "", platforms}, {workdir, "uses-docker-url", "push", "", platforms, secrets},
{workdir, "act-composite-env-test", "push", "", platforms, secrets},
// Eval // Eval
{workdir, "evalmatrix", "push", "", platforms}, {workdir, "evalmatrix", "push", "", platforms, secrets},
{workdir, "evalmatrixneeds", "push", "", platforms}, {workdir, "evalmatrixneeds", "push", "", platforms, secrets},
{workdir, "evalmatrixneeds2", "push", "", platforms}, {workdir, "evalmatrixneeds2", "push", "", platforms, secrets},
{workdir, "evalmatrix-merge-map", "push", "", platforms}, {workdir, "evalmatrix-merge-map", "push", "", platforms, secrets},
{workdir, "evalmatrix-merge-array", "push", "", platforms}, {workdir, "evalmatrix-merge-array", "push", "", platforms, secrets},
{workdir, "issue-1195", "push", "", platforms}, {workdir, "issue-1195", "push", "", platforms, secrets},
{workdir, "basic", "push", "", platforms}, {workdir, "basic", "push", "", platforms, secrets},
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms}, {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets},
{workdir, "runs-on", "push", "", platforms}, {workdir, "runs-on", "push", "", platforms, secrets},
{workdir, "checkout", "push", "", platforms}, {workdir, "checkout", "push", "", platforms, secrets},
{workdir, "job-container", "push", "", platforms}, {workdir, "job-container", "push", "", platforms, secrets},
{workdir, "job-container-non-root", "push", "", platforms}, {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}, {workdir, "job-container-invalid-credentials", "push", "failed to handle credentials: failed to interpolate container.credentials.password", platforms, secrets},
{workdir, "container-hostname", "push", "", platforms}, {workdir, "container-hostname", "push", "", platforms, secrets},
{workdir, "remote-action-docker", "push", "", platforms}, {workdir, "remote-action-docker", "push", "", platforms, secrets},
{workdir, "remote-action-js", "push", "", platforms}, {workdir, "remote-action-js", "push", "", platforms, secrets},
{workdir, "remote-action-js", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:runner-latest"}}, // Test if this works with non root container {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}, {workdir, "matrix", "push", "", platforms, secrets},
{workdir, "matrix-include-exclude", "push", "", platforms}, {workdir, "matrix-include-exclude", "push", "", platforms, secrets},
{workdir, "commands", "push", "", platforms}, {workdir, "commands", "push", "", platforms, secrets},
{workdir, "workdir", "push", "", platforms}, {workdir, "workdir", "push", "", platforms, secrets},
{workdir, "defaults-run", "push", "", platforms}, {workdir, "defaults-run", "push", "", platforms, secrets},
{workdir, "composite-fail-with-output", "push", "", platforms}, {workdir, "composite-fail-with-output", "push", "", platforms, secrets},
{workdir, "issue-597", "push", "", platforms}, {workdir, "issue-597", "push", "", platforms, secrets},
{workdir, "issue-598", "push", "", platforms}, {workdir, "issue-598", "push", "", platforms, secrets},
{workdir, "if-env-act", "push", "", platforms}, {workdir, "if-env-act", "push", "", platforms, secrets},
{workdir, "env-and-path", "push", "", platforms}, {workdir, "env-and-path", "push", "", platforms, secrets},
{workdir, "environment-files", "push", "", platforms}, {workdir, "environment-files", "push", "", platforms, secrets},
{workdir, "GITHUB_STATE", "push", "", platforms}, {workdir, "GITHUB_STATE", "push", "", platforms, secrets},
{workdir, "environment-files-parser-bug", "push", "", platforms}, {workdir, "environment-files-parser-bug", "push", "", platforms, secrets},
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms}, {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets},
{workdir, "outputs", "push", "", platforms}, {workdir, "outputs", "push", "", platforms, secrets},
{workdir, "networking", "push", "", platforms}, {workdir, "networking", "push", "", platforms, secrets},
{workdir, "steps-context/conclusion", "push", "", platforms}, {workdir, "steps-context/conclusion", "push", "", platforms, secrets},
{workdir, "steps-context/outcome", "push", "", platforms}, {workdir, "steps-context/outcome", "push", "", platforms, secrets},
{workdir, "job-status-check", "push", "job 'fail' failed", platforms}, {workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets},
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets},
{workdir, "actions-environment-and-context-tests", "push", "", platforms}, {workdir, "actions-environment-and-context-tests", "push", "", platforms, secrets},
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms}, {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets},
{workdir, "evalenv", "push", "", platforms}, {workdir, "evalenv", "push", "", platforms, secrets},
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms}, {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets},
{workdir, "workflow_dispatch", "workflow_dispatch", "", platforms}, {workdir, "workflow_dispatch", "workflow_dispatch", "", platforms, secrets},
{workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms}, {workdir, "workflow_dispatch_no_inputs_mapping", "workflow_dispatch", "", platforms, secrets},
{workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms}, {workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms, secrets},
{workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms}, {workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms, secrets},
{"../model/testdata", "strategy", "push", "", platforms}, // TODO: move all testdata into pkg so we can validate it with planner and runner {"../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 // {"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 { for _, table := range tables {
t.Run(table.workflowPath, func(t *testing.T) { t.Run(table.workflowPath, func(t *testing.T) {
config := &Config{} config := &Config{
Secrets: table.secrets,
}
eventFile := filepath.Join(workdir, table.workflowPath, "event.json") eventFile := filepath.Join(workdir, table.workflowPath, "event.json")
if _, err := os.Stat(eventFile); err == nil { if _, err := os.Stat(eventFile); err == nil {
@ -226,51 +233,51 @@ func TestRunEventHostEnvironment(t *testing.T) {
tables = append(tables, []TestJobFileInfo{ tables = append(tables, []TestJobFileInfo{
// Shells // Shells
{workdir, "shells/defaults", "push", "", platforms}, {workdir, "shells/defaults", "push", "", platforms, secrets},
{workdir, "shells/pwsh", "push", "", platforms}, {workdir, "shells/pwsh", "push", "", platforms, secrets},
{workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/bash", "push", "", platforms, secrets},
{workdir, "shells/python", "push", "", platforms}, {workdir, "shells/python", "push", "", platforms, secrets},
{workdir, "shells/sh", "push", "", platforms}, {workdir, "shells/sh", "push", "", platforms, secrets},
// Local action // Local action
{workdir, "local-action-js", "push", "", platforms}, {workdir, "local-action-js", "push", "", platforms, secrets},
// Uses // Uses
{workdir, "uses-composite", "push", "", platforms}, {workdir, "uses-composite", "push", "", platforms, secrets},
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms}, {workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
{workdir, "uses-nested-composite", "push", "", platforms}, {workdir, "uses-nested-composite", "push", "", platforms, secrets},
{workdir, "act-composite-env-test", "push", "", platforms}, {workdir, "act-composite-env-test", "push", "", platforms, secrets},
// Eval // Eval
{workdir, "evalmatrix", "push", "", platforms}, {workdir, "evalmatrix", "push", "", platforms, secrets},
{workdir, "evalmatrixneeds", "push", "", platforms}, {workdir, "evalmatrixneeds", "push", "", platforms, secrets},
{workdir, "evalmatrixneeds2", "push", "", platforms}, {workdir, "evalmatrixneeds2", "push", "", platforms, secrets},
{workdir, "evalmatrix-merge-map", "push", "", platforms}, {workdir, "evalmatrix-merge-map", "push", "", platforms, secrets},
{workdir, "evalmatrix-merge-array", "push", "", platforms}, {workdir, "evalmatrix-merge-array", "push", "", platforms, secrets},
{workdir, "issue-1195", "push", "", platforms}, {workdir, "issue-1195", "push", "", platforms, secrets},
{workdir, "fail", "push", "exit with `FAILURE`: 1", platforms}, {workdir, "fail", "push", "exit with `FAILURE`: 1", platforms, secrets},
{workdir, "runs-on", "push", "", platforms}, {workdir, "runs-on", "push", "", platforms, secrets},
{workdir, "checkout", "push", "", platforms}, {workdir, "checkout", "push", "", platforms, secrets},
{workdir, "remote-action-js", "push", "", platforms}, {workdir, "remote-action-js", "push", "", platforms, secrets},
{workdir, "matrix", "push", "", platforms}, {workdir, "matrix", "push", "", platforms, secrets},
{workdir, "matrix-include-exclude", "push", "", platforms}, {workdir, "matrix-include-exclude", "push", "", platforms, secrets},
{workdir, "commands", "push", "", platforms}, {workdir, "commands", "push", "", platforms, secrets},
{workdir, "defaults-run", "push", "", platforms}, {workdir, "defaults-run", "push", "", platforms, secrets},
{workdir, "composite-fail-with-output", "push", "", platforms}, {workdir, "composite-fail-with-output", "push", "", platforms, secrets},
{workdir, "issue-597", "push", "", platforms}, {workdir, "issue-597", "push", "", platforms, secrets},
{workdir, "issue-598", "push", "", platforms}, {workdir, "issue-598", "push", "", platforms, secrets},
{workdir, "if-env-act", "push", "", platforms}, {workdir, "if-env-act", "push", "", platforms, secrets},
{workdir, "env-and-path", "push", "", platforms}, {workdir, "env-and-path", "push", "", platforms, secrets},
{workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms}, {workdir, "non-existent-action", "push", "Job 'nopanic' failed", platforms, secrets},
{workdir, "outputs", "push", "", platforms}, {workdir, "outputs", "push", "", platforms, secrets},
{workdir, "steps-context/conclusion", "push", "", platforms}, {workdir, "steps-context/conclusion", "push", "", platforms, secrets},
{workdir, "steps-context/outcome", "push", "", platforms}, {workdir, "steps-context/outcome", "push", "", platforms, secrets},
{workdir, "job-status-check", "push", "job 'fail' failed", platforms}, {workdir, "job-status-check", "push", "job 'fail' failed", platforms, secrets},
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms}, {workdir, "if-expressions", "push", "Job 'mytest' failed", platforms, secrets},
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms}, {workdir, "uses-action-with-pre-and-post-step", "push", "", platforms, secrets},
{workdir, "evalenv", "push", "", platforms}, {workdir, "evalenv", "push", "", platforms, secrets},
{workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms}, {workdir, "ensure-post-steps", "push", "Job 'second-post-step-should-fail' failed", platforms, secrets},
}...) }...)
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -279,8 +286,8 @@ func TestRunEventHostEnvironment(t *testing.T) {
} }
tables = append(tables, []TestJobFileInfo{ tables = append(tables, []TestJobFileInfo{
{workdir, "windows-prepend-path", "push", "", platforms}, {workdir, "windows-prepend-path", "push", "", platforms, secrets},
{workdir, "windows-add-env", "push", "", platforms}, {workdir, "windows-add-env", "push", "", platforms, secrets},
}...) }...)
} else { } else {
platforms := map[string]string{ platforms := map[string]string{
@ -288,8 +295,8 @@ func TestRunEventHostEnvironment(t *testing.T) {
} }
tables = append(tables, []TestJobFileInfo{ tables = append(tables, []TestJobFileInfo{
{workdir, "nix-prepend-path", "push", "", platforms}, {workdir, "nix-prepend-path", "push", "", platforms, secrets},
{workdir, "inputs-via-env-context", "push", "", platforms}, {workdir, "inputs-via-env-context", "push", "", platforms, secrets},
}...) }...)
} }
@ -309,17 +316,17 @@ func TestDryrunEvent(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
// Shells // Shells
{workdir, "shells/defaults", "push", "", platforms}, {workdir, "shells/defaults", "push", "", platforms, secrets},
{workdir, "shells/pwsh", "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"}, secrets}, // custom image with pwsh
{workdir, "shells/bash", "push", "", platforms}, {workdir, "shells/bash", "push", "", platforms, secrets},
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}}, // slim doesn't have python {workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
{workdir, "shells/sh", "push", "", platforms}, {workdir, "shells/sh", "push", "", platforms, secrets},
// Local action // Local action
{workdir, "local-action-docker-url", "push", "", platforms}, {workdir, "local-action-docker-url", "push", "", platforms, secrets},
{workdir, "local-action-dockerfile", "push", "", platforms}, {workdir, "local-action-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-via-composite-dockerfile", "push", "", platforms}, {workdir, "local-action-via-composite-dockerfile", "push", "", platforms, secrets},
{workdir, "local-action-js", "push", "", platforms}, {workdir, "local-action-js", "push", "", platforms, secrets},
} }
for _, table := range tables { for _, table := range tables {

View file

@ -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 }}

View file

@ -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