package exprparser import ( "encoding" "fmt" "math" "reflect" "strings" eval "gitea.com/gitea/act_runner/internal/eval/v2" exprparser "gitea.com/gitea/act_runner/internal/expr" "gitea.com/gitea/act_runner/pkg/model" ) type EvaluationEnvironment struct { Github *model.GithubContext Env map[string]string Job *model.JobContext Jobs *map[string]*model.WorkflowCallResult Steps map[string]*model.StepResult Runner map[string]any Secrets map[string]string Vars map[string]string Strategy map[string]any Matrix map[string]any Needs map[string]Needs Inputs map[string]any HashFiles func([]reflect.Value) (any, error) EnvCS bool CtxData map[string]any } type CaseSensitiveDict map[string]string type Needs struct { Outputs map[string]string `json:"outputs"` Result string `json:"result"` } type Config struct { Run *model.Run WorkingDir string Context string MainContextNames []string // e.g. "github", "gitea", "forgejo" } type DefaultStatusCheck int const ( DefaultStatusCheckNone DefaultStatusCheck = iota DefaultStatusCheckSuccess DefaultStatusCheckAlways DefaultStatusCheckCanceled DefaultStatusCheckFailure ) func (dsc DefaultStatusCheck) String() string { switch dsc { case DefaultStatusCheckSuccess: return "success" case DefaultStatusCheckAlways: return "always" case DefaultStatusCheckCanceled: return "cancelled" case DefaultStatusCheckFailure: return "failure" } return "" } type Interpreter interface { Evaluate(input string, defaultStatusCheck DefaultStatusCheck) (any, error) } type interperterImpl struct { env *EvaluationEnvironment config Config } func NewInterpeter(env *EvaluationEnvironment, config Config) Interpreter { return &interperterImpl{ env: env, config: config, } } func toRawObj(left reflect.Value) map[string]any { res, _ := toRaw(left).(map[string]any) return res } func toRaw(left reflect.Value) any { if left.IsZero() { return nil } switch left.Kind() { case reflect.Pointer: if left.IsNil() { return nil } return toRaw(left.Elem()) case reflect.Map: iter := left.MapRange() m := map[string]any{} for iter.Next() { key := iter.Key() if key.Kind() == reflect.String { nv := toRaw(iter.Value()) if nv != nil { m[key.String()] = nv } } } if len(m) == 0 { return nil } return m case reflect.Struct: m := map[string]any{} leftType := left.Type() for i := 0; i < leftType.NumField(); i++ { var name string if jsonName := leftType.Field(i).Tag.Get("json"); jsonName != "" { name, _, _ = strings.Cut(jsonName, ",") } if name == "" { name = leftType.Field(i).Name } v := left.Field(i).Interface() if t, ok := v.(encoding.TextMarshaler); ok { text, _ := t.MarshalText() if len(text) > 0 { m[name] = string(text) } } else { nv := toRaw(left.Field(i)) if nv != nil { m[name] = nv } } } return m } return left.Interface() } // All values are evaluated as string, funcs that takes objects are implemented elsewhere type externalFunc struct { f func([]reflect.Value) (any, error) } func (e externalFunc) Evaluate(ev *eval.Evaluator, args []exprparser.Node) (*eval.EvaluationResult, error) { rargs := []reflect.Value{} for _, arg := range args { res, err := ev.Evaluate(arg) if err != nil { return nil, err } rargs = append(rargs, reflect.ValueOf(res.ConvertToString())) } res, err := e.f(rargs) if err != nil { return nil, err } return eval.CreateIntermediateResult(ev.Context(), res), nil } func (impl *interperterImpl) Evaluate(input string, defaultStatusCheck DefaultStatusCheck) (any, error) { input = strings.TrimPrefix(input, "${{") input = strings.TrimSuffix(input, "}}") if defaultStatusCheck != DefaultStatusCheckNone && input == "" { input = "success()" } exprNode, err := exprparser.Parse(input) if err != nil { return nil, fmt.Errorf("failed to parse: %s", err.Error()) } if defaultStatusCheck != DefaultStatusCheckNone { hasStatusCheckFunction := false exprparser.VisitNode(exprNode, func(node exprparser.Node) { if funcCallNode, ok := node.(*exprparser.FunctionNode); ok { switch strings.ToLower(funcCallNode.Name) { case "success", "always", "cancelled", "failure": hasStatusCheckFunction = true } } }) if !hasStatusCheckFunction { exprNode = &exprparser.BinaryNode{ Op: "&&", Left: &exprparser.FunctionNode{ Name: defaultStatusCheck.String(), Args: []exprparser.Node{}, }, Right: exprNode, } } } functions := impl.GetFunctions() vars := impl.GetVariables() ctx := eval.EvaluationContext{ Functions: functions, Variables: vars, } evaluator := eval.NewEvaluator(&ctx) res, err := evaluator.Evaluate(exprNode) if err != nil { return nil, err } return evaluator.ToRaw(res) } func (impl *interperterImpl) GetFunctions() eval.CaseInsensitiveObject[eval.Function] { functions := eval.GetFunctions() if impl.env.HashFiles != nil { functions["hashfiles"] = &externalFunc{impl.env.HashFiles} } functions["always"] = &externalFunc{func(_ []reflect.Value) (any, error) { return impl.always() }} functions["success"] = &externalFunc{func(_ []reflect.Value) (any, error) { if impl.config.Context == "job" { return impl.jobSuccess() } if impl.config.Context == "step" { return impl.stepSuccess() } return nil, fmt.Errorf("context '%s' must be one of 'job' or 'step'", impl.config.Context) }} functions["failure"] = &externalFunc{func(_ []reflect.Value) (any, error) { if impl.config.Context == "job" { return impl.jobFailure() } if impl.config.Context == "step" { return impl.stepFailure() } return nil, fmt.Errorf("context '%s' must be one of 'job' or 'step'", impl.config.Context) }} functions["cancelled"] = &externalFunc{func(_ []reflect.Value) (any, error) { return impl.cancelled() }} return functions } func (impl *interperterImpl) GetVariables() eval.ReadOnlyObject[any] { mainCtx := eval.CaseInsensitiveObject[any](toRawObj(reflect.ValueOf(impl.env.Github))) var env any if impl.env.EnvCS { env = eval.CaseSensitiveObject[any](toRawObj(reflect.ValueOf(impl.env.Env))) } else { env = eval.CaseInsensitiveObject[any](toRawObj(reflect.ValueOf(impl.env.Env))) } vars := eval.CaseInsensitiveObject[any]{ "env": env, "vars": toRawObj(reflect.ValueOf(impl.env.Vars)), "steps": toRawObj(reflect.ValueOf(impl.env.Steps)), "strategy": toRawObj(reflect.ValueOf(impl.env.Strategy)), "matrix": toRawObj(reflect.ValueOf(impl.env.Matrix)), "secrets": toRawObj(reflect.ValueOf(impl.env.Secrets)), "job": toRawObj(reflect.ValueOf(impl.env.Job)), "runner": toRawObj(reflect.ValueOf(impl.env.Runner)), "needs": toRawObj(reflect.ValueOf(impl.env.Needs)), "jobs": toRawObj(reflect.ValueOf(impl.env.Jobs)), "inputs": toRawObj(reflect.ValueOf(impl.env.Inputs)), } ctxNames := impl.config.MainContextNames if len(ctxNames) == 0 { ctxNames = []string{"github"} } for _, cn := range ctxNames { vars[cn] = mainCtx } for name, cd := range impl.env.CtxData { if rawOtherCtx := vars.Get(name); rawOtherCtx != nil { res := eval.CreateIntermediateResult(eval.NewEvaluationContext(), rawOtherCtx) if rOtherCd, ok := res.TryGetCollectionInterface(); ok { if otherCd, ok := rOtherCd.(eval.ReadOnlyObject[any]); ok { if rawPayload, ok := cd.(map[string]any); ok { for k, v := range rawPayload { // skip empty values, because github.workspace was set by Gitea Actions to an empty string if mk, _ := otherCd.GetKv(k); v != "" && v != nil { otherCd.GetEnumerator()[mk] = v } } continue } } } } vars[name] = cd } return vars } func IsTruthy(input any) bool { value := reflect.ValueOf(input) switch value.Kind() { case reflect.Bool: return value.Bool() case reflect.String: return value.String() != "" case reflect.Int: return value.Int() != 0 case reflect.Float64: if math.IsNaN(value.Float()) { return false } return value.Float() != 0 case reflect.Map, reflect.Slice: return true default: return false } }