fix: coerce every expression value kind to a string and drop the format() panic (#1135)

`coerceToString` covered only a few kinds and returned the unconverted `reflect.Value` for the rest, so callers rendered contexts as `<*model.GithubContext Value>` and mangled sized integers and floats the same way. It now returns a string, which makes that placeholder unrepresentable, and is exported as `CoerceToString` so Gitea can drop its own diverging copy. It also takes an already reflected value, so internal callers need no conversion and no guard against the zero `Value`.

`format()` panicked whenever a lone `}` was followed by anything other than another `}`. Only a trailing `}` reached the existing unmatched-brace check, so an expression such as `format('a}b')`, which any workflow can write, took the process down instead. It now returns that same error.

Reviewed-on: https://gitea.com/gitea/runner/pulls/1135
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-08-02 22:18:20 +00:00
committed by silverwind
parent b7aeda6e7f
commit 47366f8f34
4 changed files with 111 additions and 45 deletions

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"math"
"reflect"
"strconv"
"strings"
"gitea.com/gitea/runner/act/model"
@@ -429,41 +430,54 @@ func (impl *interperterImpl) coerceToNumber(value reflect.Value) reflect.Value {
return reflect.ValueOf(math.NaN())
}
func (impl *interperterImpl) coerceToString(value reflect.Value) reflect.Value {
switch value.Kind() {
case reflect.Invalid:
return reflect.ValueOf("")
case reflect.Bool:
switch value.Bool() {
case true:
return reflect.ValueOf("true")
case false:
return reflect.ValueOf("false")
}
case reflect.String:
return value
case reflect.Int:
return reflect.ValueOf(fmt.Sprint(value))
case reflect.Float64:
if math.IsInf(value.Float(), 1) {
return reflect.ValueOf("Infinity")
} else if math.IsInf(value.Float(), -1) {
return reflect.ValueOf("-Infinity")
}
return reflect.ValueOf(fmt.Sprintf("%.15G", value.Float()))
case reflect.Slice:
return reflect.ValueOf("Array")
case reflect.Map:
return reflect.ValueOf("Object")
// CoerceToString converts an evaluated expression value to a string the way GitHub does,
// see https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#operators
// An already reflected value is accepted as-is, since Interface() would panic on an invalid one.
func CoerceToString(v any) string {
value, ok := v.(reflect.Value)
if !ok {
value = reflect.ValueOf(v)
}
return value
switch value.Kind() {
case reflect.Invalid:
return ""
case reflect.Bool:
return strconv.FormatBool(value.Bool())
case reflect.String:
return value.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(value.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(value.Uint(), 10)
case reflect.Float32, reflect.Float64:
if math.IsInf(value.Float(), 1) {
return "Infinity"
} else if math.IsInf(value.Float(), -1) {
return "-Infinity"
}
return fmt.Sprintf("%.15G", value.Float())
case reflect.Slice, reflect.Array:
return "Array"
// contexts such as `github` are pointers to structs, so they stringify as objects too
case reflect.Map, reflect.Struct:
return "Object"
case reflect.Interface, reflect.Pointer:
if value.IsNil() {
return ""
}
return CoerceToString(value.Elem())
}
return fmt.Sprintf("%v", value)
}
func (impl *interperterImpl) compareString(left, right string, kind actionlint.CompareOpNodeKind) (bool, error) {