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

Backport #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
This commit is contained in:
bircni
2026-08-13 14:15:03 +00:00
committed by GitHub
parent ba4db8a2d9
commit 3604189b08
13 changed files with 149 additions and 31 deletions
+20 -16
View File
@@ -5,6 +5,7 @@ package actions
import (
"fmt"
"strconv"
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,33 @@ 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`.
// workflow_dispatch input types are string, choice, boolean and environment, so after
// coerceDispatchInputTypes a value is either already a string or a bool.
func dispatchEventInputs(inputs map[string]any) map[string]any {
eventInputs := make(map[string]any, len(inputs))
for name, value := range inputs {
if b, ok := value.(bool); ok {
eventInputs[name] = strconv.FormatBool(b)
} else {
eventInputs[name] = 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.