fix: restore the caller's event and inputs in reusable workflow jobs (#1250)

Fixes https://github.com/go-gitea/gitea/issues/38705

Gitea side: https://github.com/go-gitea/gitea/pull/39452

Jobs of a called workflow currently see `gitea.event_name == "workflow_call"`, and their `gitea.event.inputs` is replaced by the caller's `with:`. On GitHub they keep the caller's event, and their `inputs` are the run's `workflow_dispatch` inputs overlaid with the caller's `with:`.

For compatibility with older runners, Gitea keeps sending that form and adds a `gitea_workflow_call` key with the original trigger event name and inputs of the run, and the `inputs` Gitea resolved for the job. When the key is present, the runner restores `gitea.event_name` and `gitea.event.inputs` from it, and uses the resolved `inputs` for the job's expressions and the setup log.

Without that key (older Gitea, or jobs outside called workflows), nothing changes.

Reviewed-on: https://gitea.com/gitea/runner/pulls/1250
Reviewed-by: bircni <bircni@icloud.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
Zettat123
2026-09-27 16:13:14 +00:00
committed by silverwind
parent 81add27459
commit 990ebaa2ac
6 changed files with 104 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"])
})
}

View File

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