Repository: nektos/act (language: go)

Issue:
Difference between github and act when running contains(needs.*.result, 'success')
### Bug report info

```plain text
act version:            0.2.70
GOOS:                   linux
GOARCH:                 amd64
NumCPU:                 24
Docker host:            unix:///run/user/1002//podman/podman.sock
Sockets found:
        /var/run/docker.sock(broken)
        $XDG_RUNTIME_DIR/podman/podman.sock
Config files:
        /home/xx/.config/act/actrc:
                -P ubuntu-latest=catthehacker/ubuntu:act-latest
                -P ubuntu-22.04=catthehacker/ubuntu:act-22.04
                -P ubuntu-20.04=catthehacker/ubuntu:act-20.04
                -P ubuntu-18.04=catthehacker/ubuntu:act-18.04
Build info:
        Go version:            go1.21.13
        Module path:           github.com/nektos/act
        Main version:          (devel)
        Main path:             github.com/nektos/act
        Main checksum:
        Build settings:
                -buildmode:           exe
                -compiler:            gc
                -ldflags:             -s -w -X main.version=0.2.70 -X main.commit=9c7f103bb39f11430f58c1a68d5629a42e6d56a1 -X main.date=2024-12-01T02:37:38Z -X main.builtBy=goreleaser
                CGO_ENABLED:          0
                GOARCH:               amd64
                GOOS:                 linux
                GOAMD64:              v1
                vcs:                  git
                vcs.revision:         9c7f103bb39f11430f58c1a68d5629a42e6d56a1
                vcs.time:             2024-12-01T02:37:19Z
                vcs.modified:         false
Docker Engine:
        Engine version:        4.9.3
        Engine runtime:        crun
        Cgroup version:        1
        Cgroup driver:         cgroupfs
        Storage driver:        overlay
        Registry URI:
        OS:                    ubuntu
        OS type:               linux
        OS version:            24.04
        OS arch:               amd64
        OS kernel:             5.15.167.4-microsoft-standard-WSL2
        OS CPU:                24
        OS memory:             32037 MB
        Security options:
                name=seccomp,profile=default
                name=rootless
```


### Command used with act

```sh
act
```


### Describe issue

Hello and thank you for this awesome tool!

Wanted to report an issue I am having where there seems to be a difference between how github actions handles it and this tool.

I am trying to specify a job which is dependant on two other jobs where the other jobs are reporting success but the job which is dependant on the other two jobs is finishing with an error when running it with act.

The relevant part is there I am checking for `${{ contains(needs.*.result, 'success') }}` which on github actions reports true:
![image](https://github.com/user-attachments/assets/0999201e-af7b-48f0-91ef-161bf335f5d1)


 but this tool (act) reports false:
![image](https://github.com/user-attachments/assets/9b4d57dd-b2e3-433f-8189-ee0230a44482)

I created a sample repository to demonstrate this: https://github.com/ldomesjo/testing-github-actions

BR
Lars


### Link to GitHub repository

https://github.com/ldomesjo/testing-github-actions

### Workflow content

```yml
name: learn-github-actions
run-name: ${{ github.actor }} is learning GitHub Actions
on: [push]
jobs:
  first-job:
    runs-on: ubuntu-latest
    steps:
      - run: echo meh
  second-job:
    runs-on: ubuntu-latest
    steps:
      - run: echo meh
  third-job:
    runs-on: ubuntu-latest
    needs:
      - first-job
      - second-job
    steps:
      - run: echo '${{ toJSON(needs) }}'
      - run: echo ${{ contains(needs.*.result, 'success') }}
```


### Relevant log output

```sh
[learn-github-actions/third-job ]   ✅  Success - Main echo '{
  "first-job": {
    "outputs": null,
    "result": "success"
  },
  "second-job": {
    "outputs": null,
    "result": "success"
  }
}'
[learn-github-actions/third-job ] ⭐ Run Main echo false
[learn-github-actions/third-job ]   🐳  docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir=
| false
[learn-github-actions/third-job ]   ✅  Success - Main echo false
[learn-github-actions/third-job ] Cleaning up container for job third-job
[learn-github-actions/third-job ] 🏁  Job succeeded
```


### Additional information

_No response_


Discussion / hints:

Fix patch:
diff --git a/pkg/exprparser/interpreter.go b/pkg/exprparser/interpreter.go
index 7630854f28e..8d50913d9d3 100644
--- a/pkg/exprparser/interpreter.go
+++ b/pkg/exprparser/interpreter.go
@@ -229,6 +229,10 @@ func (impl *interperterImpl) evaluateObjectDeref(objectDerefNode *actionlint.Obj
 		return nil, err
 	}
 
+	_, receiverIsDeref := objectDerefNode.Receiver.(*actionlint.ArrayDerefNode)
+	if receiverIsDeref {
+		return impl.getPropertyValueDereferenced(reflect.ValueOf(left), objectDerefNode.Property)
+	}
 	return impl.getPropertyValue(reflect.ValueOf(left), objectDerefNode.Property)
 }
 
@@ -312,6 +316,34 @@ func (impl *interperterImpl) getPropertyValue(left reflect.Value, property strin
 	return nil, nil
 }
 
+func (impl *interperterImpl) getPropertyValueDereferenced(left reflect.Value, property string) (value interface{}, err error) {
+	switch left.Kind() {
+	case reflect.Ptr:
+		return impl.getPropertyValue(left, property)
+
+	case reflect.Struct:
+		return impl.getPropertyValue(left, property)
+	case reflect.Map:
+		iter := left.MapRange()
+
+		var values []interface{}
+		for iter.Next() {
+			value, err := impl.getPropertyValue(iter.Value(), property)
+			if err != nil {
+				return nil, err
+			}
+
+			values = append(values, value)
+		}
+
+		return values, nil
+	case reflect.Slice:
+		return impl.getPropertyValue(left, property)
+	}
+
+	return nil, nil
+}
+
 func (impl *interperterImpl) getMapValue(value reflect.Value) (interface{}, error) {
 	if value.Kind() == reflect.Ptr {
 		return impl.getMapValue(value.Elem())


Test patch:
diff --git a/pkg/exprparser/functions_test.go b/pkg/exprparser/functions_test.go
index ea51a2bc8ba..7241ddf82e6 100644
--- a/pkg/exprparser/functions_test.go
+++ b/pkg/exprparser/functions_test.go
@@ -31,6 +31,13 @@ func TestFunctionContains(t *testing.T) {
 		{`contains(fromJSON('[3.14,"second"]'), 3.14) }}`, true, "contains-item-number-number"},
 		{`contains(fromJSON('["","second"]'), fromJSON('[]')) }}`, false, "contains-item-str-arr"},
 		{`contains(fromJSON('["","second"]'), fromJSON('{}')) }}`, false, "contains-item-str-obj"},
+		{`contains(fromJSON('[{ "first": { "result": "success" }},{ "second": { "result": "success" }}]').first.result, 'success') }}`, true, "multiple-contains-item"},
+		{`contains(fromJSON('[{ "result": "success" },{ "result": "failure" }]').*.result, 'failure') }}`, true, "multiple-contains-dereferenced-failure-item"},
+		{`contains(fromJSON('[{ "result": "failure" },{ "result": "success" }]').*.result, 'success') }}`, true, "multiple-contains-dereferenced-success-item"},
+		{`contains(fromJSON('[{ "result": "failure" },{ "result": "success" }]').*.result, 'notthere') }}`, false, "multiple-contains-dereferenced-missing-item"},
+		{`contains(fromJSON('[{ "result": "failure", "outputs": { "key": "val1" } },{ "result": "success", "outputs": { "key": "val2" } }]').*.outputs.key, 'val1') }}`, true, "multiple-contains-dereferenced-output-item"},
+		{`contains(fromJSON('[{ "result": "failure", "outputs": { "key": "val1" } },{ "result": "success", "outputs": { "key": "val2" } }]').*.outputs.key, 'val2') }}`, true, "multiple-contains-dereferenced-output-item-2"},
+		{`contains(fromJSON('[{ "result": "failure", "outputs": { "key": "val1" } },{ "result": "success", "outputs": { "key": "val2" } }]').*.outputs.key, 'missing') }}`, false, "multiple-contains-dereferenced-output-misssing-item"},
 	}
 
 	env := &EvaluationEnvironment{}
@@ -249,3 +256,23 @@ func TestFunctionFormat(t *testing.T) {
 		})
 	}
 }
+
+func TestMapContains(t *testing.T) {
+	env := &EvaluationEnvironment{
+		Needs: map[string]Needs{
+			"first-job": {
+				Outputs: map[string]string{},
+				Result:  "success",
+			},
+			"second-job": {
+				Outputs: map[string]string{},
+				Result:  "failure",
+			},
+		},
+	}
+
+	output, err := NewInterpeter(env, Config{}).Evaluate("contains(needs.*.result, 'failure')", DefaultStatusCheckNone)
+	assert.Nil(t, err)
+
+	assert.Equal(t, true, output)
+}
diff --git a/pkg/exprparser/interpreter_test.go b/pkg/exprparser/interpreter_test.go
index f45851d1f44..8019ace1ef0 100644
--- a/pkg/exprparser/interpreter_test.go
+++ b/pkg/exprparser/interpreter_test.go
@@ -562,6 +562,8 @@ func TestContexts(t *testing.T) {
 		{"matrix.os", "Linux", "matrix-context"},
 		{"needs.job-id.outputs.output-name", "value", "needs-context"},
 		{"needs.job-id.result", "success", "needs-context"},
+		{"contains(needs.*.result, 'success')", true, "needs-wildcard-context-contains-success"},
+		{"contains(needs.*.result, 'failure')", false, "needs-wildcard-context-contains-failure"},
 		{"inputs.name", "value", "inputs-context"},
 	}
 
@@ -610,6 +612,12 @@ func TestContexts(t *testing.T) {
 				},
 				Result: "success",
 			},
+			"another-job-id": {
+				Outputs: map[string]string{
+					"output-name": "value",
+				},
+				Result: "success",
+			},
 		},
 		Inputs: map[string]interface{}{
 			"name": "value",