Files
gitea/services/actions/helper_test.go
T
Zettat123 0d09986790 fix(actions): keep the caller's event and inputs in reusable workflow jobs (#39452)
Fixes https://github.com/go-gitea/gitea/issues/38705
Fixes https://gitea.com/gitea/runner/issues/1232

Jobs of a called workflow saw `gitea.event_name` as `workflow_call` and
`gitea.event.inputs` replaced by the caller's `with:`, so a condition
like `gitea.event_name == 'push'` never held in them, and a dispatched
run's own inputs were lost there.

They now keep the caller's trigger event and their `inputs` are the
run's `workflow_dispatch` inputs overlaid with the caller's `with:`, as
on GitHub. For example, with `workflow_dispatch` inputs `{target: prod,
debug: true}` and caller's `with: {target: dev}`, the called workflow's
`inputs` are `{target: dev, debug: true}`.

A runner cannot resolve these inputs itself, so they are sent in a new
`gitea_workflow_call` context entry, with the original event name and
inputs for the runner to restore. For more details, see the runner PR:
https://gitea.com/gitea/runner/pulls/1250

Co-authored-by: silverwind <me@silverwind.io>
2026-09-27 19:08:46 +02:00

146 lines
5.5 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
actions_module "gitea.dev/modules/actions"
"gitea.dev/modules/json"
api "gitea.dev/modules/structs"
webhook_module "gitea.dev/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDispatchInputsForRunJobs(t *testing.T) {
// a child carries the callee's `on: workflow_call`, so only a top-level job answers for the run
run := &actions_model.ActionRun{Event: "workflow_dispatch", EventPayload: `{"inputs":{"deploy":"true"}}`}
job := &actions_model.ActionRunJob{
ID: 1, JobID: "deploy",
WorkflowPayload: []byte("on: {workflow_dispatch: {inputs: {deploy: {type: boolean}}}}\njobs:\n deploy:\n steps: [{run: echo}]\n"),
}
child := &actions_model.ActionRunJob{
ID: 2, JobID: "called", ParentJobID: job.ID,
WorkflowPayload: []byte("on: workflow_call\njobs:\n called:\n steps: [{run: echo}]\n"),
}
inputs, err := dispatchInputsForRunJobs(run, []*actions_model.ActionRunJob{child, job})
require.NoError(t, err)
assert.Equal(t, true, inputs["deploy"])
}
func TestReusableChildInputs(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
const runID = 9801
insertJob := func(jobID string, parentID int64, payload, callPayload string) *actions_model.ActionRunJob {
job := &actions_model.ActionRunJob{RunID: runID, JobID: jobID, ParentJobID: parentID, WorkflowPayload: []byte(payload), CallPayload: callPayload}
require.NoError(t, db.Insert(ctx, job))
return job
}
caller := insertJob("caller", 0,
"on: {workflow_dispatch: {inputs: {flag: {type: boolean}, Shared: {type: string}}}}\njobs:\n caller:\n uses: ./.gitea/workflows/mid.yml\n",
`{"inputs":{"shared":"from-call","mid_only":"mid"}}`)
mid := insertJob("mid", caller.ID, "", `{"inputs":{"env":"leaf"}}`)
leaf := insertJob("leaf", mid.ID, "", "")
dispatchRun := &actions_model.ActionRun{ID: runID, Event: "workflow_dispatch", EventPayload: `{"inputs":{"flag":"true","Shared":"from-dispatch"}}`}
pushRun := &actions_model.ActionRun{ID: runID, Event: "push", EventPayload: `{}`}
t.Run("dispatch inputs overlaid case-insensitively with the caller's with", func(t *testing.T) {
inputs, err := getInputsForJob(ctx, dispatchRun, mid)
require.NoError(t, err)
assert.Equal(t, map[string]any{"flag": true, "shared": "from-call", "mid_only": "mid"}, inputs)
})
t.Run("task context of a nested child keeps the older runners' form and carries the original event", func(t *testing.T) {
leaf.Run = dispatchRun
gitCtx := GiteaContext{
"event_name": "workflow_dispatch",
"event": map[string]any{"inputs": map[string]any{"flag": "true", "Shared": "from-dispatch"}},
}
require.NoError(t, setCalledWorkflowContext(ctx, leaf, gitCtx))
inputs := map[string]any{"flag": true, "Shared": "from-dispatch", "env": "leaf"}
assert.Equal(t, "workflow_call", gitCtx["event_name"])
assert.Equal(t, map[string]any{"inputs": inputs}, gitCtx["event"])
assert.Equal(t, map[string]any{
"original_event_name": "workflow_dispatch",
"original_event_inputs": map[string]any{"flag": "true", "Shared": "from-dispatch"},
"inputs": inputs,
}, gitCtx["gitea_workflow_call"])
})
t.Run("task context of a non-dispatch run", func(t *testing.T) {
mid.Run = pushRun
gitCtx := GiteaContext{"event_name": "push", "event": map[string]any{}}
require.NoError(t, setCalledWorkflowContext(ctx, mid, gitCtx))
inputs := map[string]any{"shared": "from-call", "mid_only": "mid"}
assert.Equal(t, "workflow_call", gitCtx["event_name"])
assert.Equal(t, map[string]any{"inputs": inputs}, gitCtx["event"])
assert.Equal(t, map[string]any{"original_event_name": "push", "inputs": inputs}, gitCtx["gitea_workflow_call"])
})
}
func TestPullRequestTargetBaseSHA(t *testing.T) {
prPayload := func(baseSHA string) string {
payload, err := json.Marshal(api.PullRequestPayload{
PullRequest: &api.PullRequest{
Base: &api.PRBranchInfo{Sha: baseSHA},
},
})
require.NoError(t, err)
return string(payload)
}
t.Run("pull_request_target with base SHA", func(t *testing.T) {
run := &actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: actions_module.GithubEventPullRequestTarget,
EventPayload: prPayload("base-sha"),
}
got, ok := pullRequestTargetBaseSHA(run)
assert.True(t, ok)
assert.Equal(t, "base-sha", got)
})
t.Run("non pull_request_target trigger", func(t *testing.T) {
run := &actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: actions_module.GithubEventPullRequest,
EventPayload: prPayload("base-sha"),
}
got, ok := pullRequestTargetBaseSHA(run)
assert.False(t, ok)
assert.Empty(t, got)
})
t.Run("missing base SHA", func(t *testing.T) {
run := &actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: actions_module.GithubEventPullRequestTarget,
EventPayload: prPayload(""),
}
got, ok := pullRequestTargetBaseSHA(run)
assert.False(t, ok)
assert.Empty(t, got)
})
t.Run("invalid payload", func(t *testing.T) {
run := &actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: actions_module.GithubEventPullRequestTarget,
EventPayload: "{",
}
got, ok := pullRequestTargetBaseSHA(run)
assert.False(t, ok)
assert.Empty(t, got)
})
}