diff --git a/act/runner/expression.go b/act/runner/expression.go index aa8566d5..8b0adee6 100644 --- a/act/runner/expression.go +++ b/act/runner/expression.go @@ -248,6 +248,14 @@ func inputsFromEnv(env map[string]string) map[string]any { func getEvaluatorInputs(rc *RunContext, stepInputs map[string]any, ghc *model.GithubContext) map[string]any { inputs := map[string]any{} + // only a called workflow's job itself takes the inputs Gitea resolved, + // composite actions copy its Config but have their own `inputs` + if rc.Config.WorkflowCallInputs != nil && rc.Parent == nil { + maps.Copy(inputs, rc.Config.WorkflowCallInputs) + maps.Copy(inputs, stepInputs) + return inputs + } + maps.Copy(inputs, rc.workflowCallInputs) maps.Copy(inputs, stepInputs) diff --git a/act/runner/expression_test.go b/act/runner/expression_test.go index 6ec57dd7..81c5615b 100644 --- a/act/runner/expression_test.go +++ b/act/runner/expression_test.go @@ -392,6 +392,28 @@ jobs: }) } +func TestGetEvaluatorInputsResolvedByGitea(t *testing.T) { + wf, err := model.ReadWorkflow(strings.NewReader(` +on: + workflow_call: + inputs: + name: {type: string, default: callee} + workflow_dispatch: + inputs: + name: {type: string} +`)) + require.NoError(t, err) + config := &Config{Workdir: ".", WorkflowCallInputs: map[string]any{"name": "caller", "target": "dispatch"}} + ghc := &model.GithubContext{EventName: "workflow_dispatch", Event: map[string]any{"inputs": map[string]any{"name": "dispatch"}}} + + // the event's own inputs must not override those Gitea resolved + job := &RunContext{Config: config, Run: &model.Run{JobID: "job1", Workflow: wf}} + assert.Equal(t, map[string]any{"name": "caller", "target": "dispatch"}, getEvaluatorInputs(job, nil, ghc)) + + composite := &RunContext{Config: config, Run: &model.Run{JobID: "job1", Workflow: &model.Workflow{}}, Parent: job} + assert.Equal(t, map[string]any{"arg": "x"}, getEvaluatorInputs(composite, map[string]any{"arg": "x"}, ghc)) +} + func TestResolveWorkflowCallToleratesMismatchesAndPassesInheritedSecretsVerbatim(t *testing.T) { callerWorkflow, err := model.ReadWorkflow(strings.NewReader(` jobs: diff --git a/act/runner/runner.go b/act/runner/runner.go index a2b8870d..dc5662ee 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -62,6 +62,7 @@ type Config struct { NoActionPatch bool // run actions exactly as published, applying no compatibility patches, see patch_actions.go PresetGitHubContext *model.GithubContext // overrides actor, ref, repository, token and related context fields + WorkflowCallInputs map[string]any // the `inputs` Gitea resolved for a called workflow's job, nil for other jobs EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath ContainerNamePrefix string // the prefix of container name ContainerMaxLifetime time.Duration // the max lifetime of job containers diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index 1814fd26..90aaeacf 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -350,6 +350,25 @@ func (r *Runner) cloneEnvs() map[string]string { return envs } +// restoreCalledWorkflowContext undoes Gitea's rewrite of `event_name` and `event.inputs` for older runners, +// and returns the job's `inputs` from `gitea_workflow_call`, nil if it is not a called workflow's job. +// See https://github.com/go-gitea/gitea/pull/39452 +func restoreCalledWorkflowContext(task *runnerv1.Task) map[string]any { + workflowCall := task.GetContext().GetFields()["gitea_workflow_call"].GetStructValue().GetFields() + if workflowCall == nil { + return nil + } + task.Context.Fields["event_name"] = workflowCall["original_event_name"] + if event := task.Context.Fields["event"].GetStructValue(); event != nil { + if inputs, ok := workflowCall["original_event_inputs"]; ok { + event.Fields["inputs"] = inputs + } else { + delete(event.Fields, "inputs") + } + } + return workflowCall["inputs"].GetStructValue().AsMap() +} + // getDefaultActionsURL // when DEFAULT_ACTIONS_URL == "https://github.com" and GithubMirror is not blank, // it should be set to GithubMirror first. @@ -398,6 +417,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. } }() + workflowCallInputs := restoreCalledWorkflowContext(task) r.reportSetup(reporter, task) workflow, jobID, err := generateWorkflow(task) @@ -536,6 +556,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. NoSkipCheckout: true, DisableActEnv: r.cfg.Runner.SetActEnv != nil && !*r.cfg.Runner.SetActEnv, PresetGitHubContext: preset, + WorkflowCallInputs: workflowCallInputs, EventJSON: string(eventJSON), ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id), ContainerMaxLifetime: maxLifetime, diff --git a/internal/app/run/runner_test.go b/internal/app/run/runner_test.go index 87f57757..a61c00cc 100644 --- a/internal/app/run/runner_test.go +++ b/internal/app/run/runner_test.go @@ -358,3 +358,48 @@ func TestNewRunnerCacheServiceV2(t *testing.T) { } } } + +func TestRestoreCalledWorkflowContext(t *testing.T) { + newTask := func(workflowCall map[string]any) *runnerv1.Task { + values := map[string]any{ + "event_name": "workflow_call", + "event": map[string]any{"ref": "refs/heads/main", "inputs": map[string]any{"env": "leaf"}}, + } + if workflowCall != nil { + values["gitea_workflow_call"] = workflowCall + } + taskCtx, err := structpb.NewStruct(values) + require.NoError(t, err) + return &runnerv1.Task{Context: taskCtx} + } + + t.Run("dispatched run", func(t *testing.T) { + task := newTask(map[string]any{ + "original_event_name": "workflow_dispatch", + "original_event_inputs": map[string]any{"target": "prod"}, + "inputs": map[string]any{"target": "prod", "env": "leaf"}, + }) + assert.Equal(t, map[string]any{"target": "prod", "env": "leaf"}, restoreCalledWorkflowContext(task)) + taskCtx := task.Context.AsMap() + assert.Equal(t, "workflow_dispatch", taskCtx["event_name"]) + assert.Equal(t, map[string]any{"ref": "refs/heads/main", "inputs": map[string]any{"target": "prod"}}, taskCtx["event"]) + assert.Equal(t, []string{"::group::Inputs", "env: leaf", "target: prod", "::endgroup::"}, inputLines(task.Context.Fields)) + }) + + t.Run("event without inputs", func(t *testing.T) { + task := newTask(map[string]any{ + "original_event_name": "push", + "inputs": map[string]any{"env": "leaf"}, + }) + assert.Equal(t, map[string]any{"env": "leaf"}, restoreCalledWorkflowContext(task)) + taskCtx := task.Context.AsMap() + assert.Equal(t, "push", taskCtx["event_name"]) + assert.Equal(t, map[string]any{"ref": "refs/heads/main"}, taskCtx["event"]) + }) + + t.Run("not a called workflow's job", func(t *testing.T) { + task := newTask(nil) + assert.Nil(t, restoreCalledWorkflowContext(task)) + assert.Equal(t, "workflow_call", task.Context.AsMap()["event_name"]) + }) +} diff --git a/internal/app/run/setup.go b/internal/app/run/setup.go index 654e8284..ae117d34 100644 --- a/internal/app/run/setup.go +++ b/internal/app/run/setup.go @@ -56,10 +56,15 @@ func (r *Runner) setupLines(task *runnerv1.Task) []string { return append(lines, "::endgroup::") } -// inputLines lists the inputs the run was triggered with, as workflow_dispatch and workflow_call -// carry them in the event payload. Empty when the event has none. +// inputLines lists the inputs of the job: those Gitea resolved for a called workflow's job, +// or the ones a workflow_dispatch event carries in its payload. Empty when there are none. func inputLines(fields map[string]*structpb.Value) []string { inputs := fields["event"].GetStructValue().GetFields()["inputs"].GetStructValue().GetFields() + // For a called workflow's job, restore its inputs from `gitea_workflow_call`. + // See https://github.com/go-gitea/gitea/pull/39452. + if workflowCall := fields["gitea_workflow_call"].GetStructValue(); workflowCall != nil { + inputs = workflowCall.GetFields()["inputs"].GetStructValue().GetFields() + } if len(inputs) == 0 { return nil }