fix(actions): keep github.event.inputs as strings for workflow_dispatch (#38899)

`github.event.inputs` must mirror the raw `workflow_dispatch` payload,
where
GitHub keeps every input as a string. Only the separate `inputs` context
preserves declared types, e.g. booleans. A previous fix coerced boolean
inputs in the single map that fed both contexts, so
`github.event.inputs.someBool` became a real boolean and comparisons
like
`== 'true'` stopped matching.

`github.event.inputs` now stays string-only again. The `inputs` context
used
for server-side `if:` evaluation of needs-gated/matrix-deferred jobs
re-coerces booleans independently, from the job's own workflow
declaration,
so that path keeps working correctly.

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

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
bircni
2026-08-13 09:36:41 +02:00
committed by GitHub
parent 5287860efb
commit 01e9febbea
13 changed files with 138 additions and 31 deletions
+14 -16
View File
@@ -6,6 +6,7 @@ package actions
import (
"fmt"
"gitea.dev/actionslib/pkg/exprparser"
"gitea.dev/actionslib/pkg/model"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/perm"
@@ -129,10 +130,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
return 0, fmt.Errorf("failed to unmarshal workflow content: %w", err)
}
// get inputs from post
workflow := &model.Workflow{
RawOn: singleWorkflow.RawOn,
}
workflowDispatch := workflow.WorkflowDispatchConfig()
workflowDispatch := singleWorkflow.WorkflowDispatchConfig()
if workflowDispatch == nil {
return 0, util.ErrorWrapTranslatable(
util.NewInvalidArgumentErrorf("workflow %q has no workflow_dispatch event trigger", workflowID),
@@ -144,10 +142,6 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
if err = processInputs(workflowDispatch, inputsWithDefaults); err != nil {
return 0, err
}
// The dispatch callbacks fill boolean inputs as the strings "true"/"false". Normalize them to
// native JSON booleans so `type: boolean` inputs match GitHub, whose `inputs` context preserves
// booleans as booleans. Without this, a server-side needs-gated job `if: inputs.flag == true`
// evaluates against the string "true" and never matches, leaving the job blocked forever.
coerceDispatchInputTypes(workflowDispatch, inputsWithDefaults)
// ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
@@ -157,7 +151,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
Workflow: workflowID,
Ref: ref,
Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
Inputs: inputsWithDefaults,
Inputs: dispatchEventInputs(inputsWithDefaults),
Sender: convert.ToUserWithAccessMode(ctx, doer, perm.AccessModeNone),
}
@@ -174,23 +168,27 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
return run.ID, nil
}
// coerceDispatchInputTypes normalizes workflow_dispatch input values to the JSON types declared by
// the workflow. Only booleans are coerced, matching GitHub, whose `inputs` context "preserves
// Boolean values as Booleans instead of converting them to strings" while every other type stays a
// string. workflow_dispatch has no `number` type (its input types are string, choice, boolean and
// environment), so booleans are the complete set to coerce here.
// A value that is already a bool is left untouched, so the coercion is idempotent.
// coerceDispatchInputTypes types `inputs`, where boolean is the only non-string dispatch input type.
func coerceDispatchInputTypes(dispatch *model.WorkflowDispatch, inputs map[string]any) {
for name, cfg := range dispatch.Inputs {
if cfg.Type != "boolean" {
continue
}
if s, ok := inputs[name].(string); ok {
inputs[name] = s == "true"
inputs[name] = util.ParseYamlBool(s)
}
}
}
// dispatchEventInputs stringifies the typed inputs for `github.event.inputs`.
func dispatchEventInputs(inputs map[string]any) map[string]any {
eventInputs := make(map[string]any, len(inputs))
for name, value := range inputs {
eventInputs[name] = exprparser.CoerceToString(value)
}
return eventInputs
}
// resolveDispatchWorkflowContent returns the YAML for a dispatched workflow and records its source on the run.
// - Repo-level: from the consumer's runTargetCommit.
// - Scoped: from the source repo's default branch.