Repository: nektos/act (language: go)

Issue:
act 0.2.66 Error: workflow is not valid. 'spelling.yml': Line: 112 Column 5: Failed to match job-factory: Line: 119 Column 9: Unknown Function Call success
### Bug report info

```plain text
act version:            0.2.66
GOOS:                   darwin
GOARCH:                 arm64
NumCPU:                 10
Docker host:            DOCKER_HOST environment variable is not set
Sockets found:
	/var/run/docker.sock
Config files:           
	/Users/jsoref/.actrc:
		#-P ubuntu-latest=node:12.20.1-buster-slim
		#-P ubuntu-20.04=node:12.20.1-buster-slim
		#-P ubuntu-18.04=node:12.20.1-buster-slim
		-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
		-P ubuntu-16.04=catthehacker/ubuntu:act-16.04
		-P self-hosted=catthehacker/ubuntu:act-latest
		-P ubuntu-latest-4cpu=ubuntu:act-latest
		-P ubuntu-latest-8cpu=ubuntu:act-latest
		--use-new-action-cache
Build info:
	Go version:            go1.23.0
	Module path:           command-line-arguments
	Main version:          
	Main path:             
	Main checksum:         
	Build settings:
		-buildmode:           exe
		-compiler:            gc
		-ldflags:             -X main.version=0.2.66
		DefaultGODEBUG:       asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1
		CGO_ENABLED:          1
		CGO_CFLAGS:           
		CGO_CPPFLAGS:         
		CGO_CXXFLAGS:         
		CGO_LDFLAGS:          
		GOARCH:               arm64
		GOOS:                 darwin
		GOARM64:              v8.0
Docker Engine:
	Engine version:        26.1.3
	Engine runtime:        runc
	Cgroup version:        2
	Cgroup driver:         cgroupfs
	Storage driver:        overlay2
	Registry URI:          https://index.docker.io/v1/
	OS:                    Alpine Linux v3.20
	OS type:               linux
	OS version:            3.20.2
	OS arch:               aarch64
	OS kernel:             6.6.41-0-virt
	OS CPU:                2
	OS memory:             1906 MB
	Security options:
		name=seccomp,profile=builtin
		name=cgroupns
```


### Command used with act

```sh
/opt/homebrew/bin/act -l -v
```


### Describe issue

Historically, I'd get this output:
```
% act -l
INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock'
WARN  ⚠ You are using Apple M-series chip and you have not specified container architecture, you might encounter issues while running act. If so, try running it with '--container-architecture linux/amd64'. ⚠
Stage  Job ID        Job name        Workflow name   Workflow file  Events
0      update        Update PR       Check Spelling  spelling.yml   push,pull_request_target,issue_comment
0      spelling      Check Spelling  Check Spelling  spelling.yml   push,pull_request_target,issue_comment
1      comment-push  Report (Push)   Check Spelling  spelling.yml   push,pull_request_target,issue_comment
1      comment-pr    Report (PR)     Check Spelling  spelling.yml   pull_request_target,issue_comment,push
```


### Link to GitHub repository

https://github.com/check-spelling/spell-check-this/tree/0a3288fa36998de2eb46db1e43a134b3acf33e64

### Workflow content

```yml
name: Check Spelling

# Comment management is handled through a secondary job, for details see:
# https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions
#
# `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment
#   (in odd cases, it might actually run just to collapse a comment, but that's fairly rare)
#   it needs `contents: write` in order to add a comment.
#
# `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment
#   or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment)
#   it needs `pull-requests: write` in order to manipulate those comments.

# Updating pull request branches is managed via comment handling.
# For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list
#
# These elements work together to make it happen:
#
# `on.issue_comment`
#   This event listens to comments by users asking to update the metadata.
#
# `jobs.update`
#   This job runs in response to an issue_comment and will push a new commit
#   to update the spelling metadata.
#
# `with.experimental_apply_changes_via_bot`
#   Tells the action to support and generate messages that enable it
#   to make a commit to update the spelling metadata.
#
# `with.ssh_key`
#   In order to trigger workflows when the commit is made, you can provide a
#   secret (typically, a write-enabled github deploy key).
#
#   For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key

# SARIF reporting
#
# Access to SARIF reports is generally restricted (by GitHub) to members of the repository.
#
# Requires enabling `security-events: write`
# and configuring the action with `use_sarif: 1`
#
#   For information on the feature, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-SARIF-output

# Minimal workflow structure:
#
# on:
#   push:
#     ...
#   pull_request_target:
#     ...
# jobs:
#   # you only want the spelling job, all others should be omitted
#   spelling:
#     # remove `security-events: write` and `use_sarif: 1`
#     # remove `experimental_apply_changes_via_bot: 1`
#     ... otherwise adjust the `with:` as you wish

on:
  push:
    branches:
      - "**"
    tags-ignore:
      - "**"
  pull_request_target:
    branches:
      - "**"
    types:
      - "opened"
      - "reopened"
      - "synchronize"
  issue_comment:
    types:
      - "created"

jobs:
  spelling:
    name: Check Spelling
    permissions:
      contents: read
      pull-requests: read
      actions: read
      security-events: write
    outputs:
      followup: ${{ steps.spelling.outputs.followup }}
    runs-on: ubuntu-latest
    if: ${{ contains(github.event_name, 'pull_request') || github.event_name == 'push' }}
    concurrency:
      group: spelling-${{ github.event.pull_request.number || github.ref }}
      # note: If you use only_check_changed_files, you do not want cancel-in-progress
      cancel-in-progress: true
    steps:
      - name: check-spelling
        id: spelling
        uses: check-spelling/check-spelling@prerelease
        with:
          suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
          checkout: true
          check_file_names: 1
          spell_check_this: check-spelling/spell-check-this@prerelease
          post_comment: 0
          use_magic_file: 1
          report-timing: 1
          warnings: bad-regex,binary-file,deprecated-feature,ignored-expect-variant,large-file,limited-references,no-newline-at-eof,noisy-file,non-alpha-in-dictionary,token-is-substring,unexpected-line-ending,whitespace-in-dictionary,minified-file,unsupported-configuration,no-files-to-check,unclosed-block-ignore-begin,unclosed-block-ignore-end
          experimental_apply_changes_via_bot: 1
          use_sarif: ${{ (!github.event.pull_request || (github.event.pull_request.head.repo.full_name == github.repository)) && 1 }}
          extra_dictionary_limit: 20
          extra_dictionaries: |
            cspell:software-terms/dict/softwareTerms.txt

  comment-push:
    name: Report (Push)
    # If your workflow isn't running on push, you can remove this job
    runs-on: ubuntu-latest
    needs: spelling
    permissions:
      actions: read
      contents: write
    if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
    steps:
      - name: comment
        uses: check-spelling/check-spelling@prerelease
        with:
          checkout: true
          spell_check_this: check-spelling/spell-check-this@prerelease
          task: ${{ needs.spelling.outputs.followup }}

  comment-pr:
    name: Report (PR)
    # If you workflow isn't running on pull_request*, you can remove this job
    runs-on: ubuntu-latest
    needs: spelling
    permissions:
      actions: read
      contents: read
      pull-requests: write
    if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
    steps:
      - name: comment
        uses: check-spelling/check-spelling@prerelease
        with:
          checkout: true
          spell_check_this: check-spelling/spell-check-this@prerelease
          task: ${{ needs.spelling.outputs.followup }}
          experimental_apply_changes_via_bot: 1

  update:
    name: Update PR
    permissions:
      contents: write
      pull-requests: write
      actions: read
    runs-on: ubuntu-latest
    if: ${{
      github.event_name == 'issue_comment' &&
      github.event.issue.pull_request &&
      contains(github.event.comment.body, '@check-spelling-bot apply') &&
      contains(github.event.comment.body, 'https://')
      }}
    concurrency:
      group: spelling-update-${{ github.event.issue.number }}
      cancel-in-progress: false
    steps:
      - name: apply spelling updates
        uses: check-spelling/check-spelling@prerelease
        with:
          experimental_apply_changes_via_bot: 1
          checkout: true
          ssh_key: "${{ secrets.CHECK_SPELLING }}"
```


### Relevant log output

```sh
DEBU[0000] Handling container host and socket
DEBU[0000] Defaulting container socket to DOCKER_HOST
INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock'
WARN  ⚠ You are using Apple M-series chip and you have not specified container architecture, you might encounter issues while running act. If so, try running it with '--container-architecture linux/amd64'. ⚠
DEBU[0000] Loading environment from /Users/jsoref/code/spelling-org/spell-check-this/.env
DEBU[0000] Loading action inputs from /Users/jsoref/code/spelling-org/spell-check-this/.input
DEBU[0000] Conditional GET for notices etag=01d15b77-16bd-46ee-bef4-5ddba1fba3f7
DEBU[0000] Loading secrets from /Users/jsoref/code/spelling-org/spell-check-this/.secrets
DEBU[0000] Loading vars from /Users/jsoref/code/spelling-org/spell-check-this/.vars
DEBU[0000] Evaluated matrix inclusions: map[]
DEBU[0000] Loading workflows from '/Users/jsoref/code/spelling-org/spell-check-this/.github/workflows'
DEBU[0000] Loading workflows recursively
DEBU[0000] Found workflow 'spelling.yml' in '/Users/jsoref/code/spelling-org/spell-check-this/.github/workflows/spelling.yml'
DEBU[0000] Reading workflow '/Users/jsoref/code/spelling-org/spell-check-this/.github/workflows/spelling.yml'
Error: workflow is not valid. 'spelling.yml': Line: 112 Column 5: Failed to match job-factory: Line: 119 Column 9: Unknown Function Call success
Unknown Function Call failure
Line: 112 Column 5: Failed to match workflow-job: Line: 114 Column 5: Unknown Property runs-on
Line: 119 Column 9: Unknown Function Call success
Unknown Function Call failure
Line: 120 Column 5: Unknown Property steps
Line: 129 Column 5: Failed to match job-factory: Line: 137 Column 9: Unknown Function Call success
Unknown Function Call failure
Line: 129 Column 5: Failed to match workflow-job: Line: 131 Column 5: Unknown Property runs-on
Line: 137 Column 9: Unknown Function Call success
Unknown Function Call failure
Line: 138 Column 5: Unknown Property steps
```


### Additional information

_No response_


Discussion / hints:
Tbh. I expected such issues to land before the 9th September.

I wonder if this is accepted, when you use `${{ success() }}` via explicit expressions.

Seems like a lack of CI test material for this case

I will look into this tomorrow.
I have my own self-build of `act` in my path and thus the `brew` auto update didn't give me the failure ...
I'd encourage you to just add spell-check-this to the CI test material 😉.
Note, the error reporting is lousy.

> Error: workflow is not valid. 'spelling.yml': Line: 112 Column 5: Failed to match job-factory: Line: 119 Column 9: Unknown Function Call success

Here's line 112:
https://github.com/check-spelling/spell-check-this/blob/0a3288fa36998de2eb46db1e43a134b3acf33e64/.github/workflows/spelling.yml#L112
>     name: Report (Push)


> Line: 119 Column 9: Unknown Function Call success
https://github.com/check-spelling/spell-check-this/blob/0a3288fa36998de2eb46db1e43a134b3acf33e64/.github/workflows/spelling.yml#L119
>     if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
That's apparently the problem.

https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsif
> When you use expressions in an `if` conditional, you can, optionally, omit the `${{ }}` expression syntax because GitHub Actions automatically evaluates the `if` conditional as an expression. However, this exception does not apply everywhere.


Wrapping `${{ ... }}` doesn't help. The only thing I can do is to remove the `success()` and `failure()` bits
> Note, the error reporting is lousy

Yes I know, the goal was error reporting not pretty error reporting.

I thoughtback then that I fixed the parsing problem for special functions.

However it could be that I only fixed hashfiles

https://github.com/nektos/act/blob/d8b6f618d9bf00231c491c76c980371eb57f6de3/pkg/schema/workflow_schema.json#L1514

My validation code might be just unable to parse the success(0,MAX) and excludes those functions.

hashfiles is defined as (0,255)
Hmm job step if is ok, because it also uses 255 not MAX, seems like I didn't inspect the whole schema file from GitHub Employees for additional aliases.

I used scan with integer placeholder
What's the (end-user) solution here?  Roll back to v0.2.65?

Fix patch:
diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go
index 0d5584a8089..a523e721f6d 100644
--- a/pkg/schema/schema.go
+++ b/pkg/schema/schema.go
@@ -5,7 +5,9 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"math"
 	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/rhysd/actionlint"
@@ -18,6 +20,8 @@ var workflowSchema string
 //go:embed action_schema.json
 var actionSchema string
 
+var functions = regexp.MustCompile(`^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$`)
+
 type Schema struct {
 	Definitions map[string]Definition
 }
@@ -138,10 +142,10 @@ func (s *Node) checkSingleExpression(exprNode actionlint.ExprNode) error {
 			for _, v := range *funcs {
 				if strings.EqualFold(funcCallNode.Callee, v.name) {
 					if v.min > len(funcCallNode.Args) {
-						err = errors.Join(err, fmt.Errorf("Missing parameters for %s expected > %v got %v", funcCallNode.Callee, v.min, len(funcCallNode.Args)))
+						err = errors.Join(err, fmt.Errorf("Missing parameters for %s expected >= %v got %v", funcCallNode.Callee, v.min, len(funcCallNode.Args)))
 					}
 					if v.max < len(funcCallNode.Args) {
-						err = errors.Join(err, fmt.Errorf("To many parameters for %s expected < %v got %v", funcCallNode.Callee, v.max, len(funcCallNode.Args)))
+						err = errors.Join(err, fmt.Errorf("Too many parameters for %s expected <= %v got %v", funcCallNode.Callee, v.max, len(funcCallNode.Args)))
 					}
 					return
 				}
@@ -174,11 +178,22 @@ func (s *Node) GetFunctions() *[]FunctionInfo {
 		if i == -1 {
 			continue
 		}
-		fun := FunctionInfo{
-			name: v[:i],
-		}
-		if n, err := fmt.Sscanf(v[i:], "(%d,%d)", &fun.min, &fun.max); n == 2 && err == nil {
-			*funcs = append(*funcs, fun)
+		smatch := functions.FindStringSubmatch(v)
+		if len(smatch) > 0 {
+			functionName := smatch[1]
+			minParameters, _ := strconv.ParseInt(smatch[2], 10, 32)
+			maxParametersRaw := smatch[3]
+			var maxParameters int64
+			if strings.EqualFold(maxParametersRaw, "MAX") {
+				maxParameters = math.MaxInt32
+			} else {
+				maxParameters, _ = strconv.ParseInt(maxParametersRaw, 10, 32)
+			}
+			*funcs = append(*funcs, FunctionInfo{
+				name: functionName,
+				min:  int(minParameters),
+				max:  int(maxParameters),
+			})
 		}
 	}
 	return funcs
@@ -220,6 +235,9 @@ func AddFunction(funcs *[]FunctionInfo, s string, i1, i2 int) {
 }
 
 func (s *Node) UnmarshalYAML(node *yaml.Node) error {
+	if node != nil && node.Kind == yaml.DocumentNode {
+		return s.UnmarshalYAML(node.Content[0])
+	}
 	def := s.Schema.GetDefinition(s.Definition)
 	if s.Context == nil {
 		s.Context = def.Context


Test patch:
diff --git a/pkg/schema/schema_test.go b/pkg/schema/schema_test.go
new file mode 100644
index 00000000000..ce571c96533
--- /dev/null
+++ b/pkg/schema/schema_test.go
@@ -0,0 +1,92 @@
+package schema
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/yaml.v3"
+)
+
+func TestAdditionalFunctions(t *testing.T) {
+	var node yaml.Node
+	err := yaml.Unmarshal([]byte(`
+on: push
+jobs:
+  job-with-condition:
+    runs-on: self-hosted
+    if: success() || success('joba', 'jobb') || failure() || failure('joba', 'jobb') || always() || cancelled()
+    steps:
+    - run: exit 0
+`), &node)
+	if !assert.NoError(t, err) {
+		return
+	}
+	err = (&Node{
+		Definition: "workflow-root-strict",
+		Schema:     GetWorkflowSchema(),
+	}).UnmarshalYAML(&node)
+	assert.NoError(t, err)
+}
+
+func TestAdditionalFunctionsFailure(t *testing.T) {
+	var node yaml.Node
+	err := yaml.Unmarshal([]byte(`
+on: push
+jobs:
+  job-with-condition:
+    runs-on: self-hosted
+    if: success() || success('joba', 'jobb') || failure() || failure('joba', 'jobb') || always('error')
+    steps:
+    - run: exit 0
+`), &node)
+	if !assert.NoError(t, err) {
+		return
+	}
+	err = (&Node{
+		Definition: "workflow-root-strict",
+		Schema:     GetWorkflowSchema(),
+	}).UnmarshalYAML(&node)
+	assert.Error(t, err)
+}
+
+func TestAdditionalFunctionsSteps(t *testing.T) {
+	var node yaml.Node
+	err := yaml.Unmarshal([]byte(`
+on: push
+jobs:
+  job-with-condition:
+    runs-on: self-hosted
+    steps:
+    - run: exit 0
+      if: success() || failure() || always()
+`), &node)
+	if !assert.NoError(t, err) {
+		return
+	}
+	err = (&Node{
+		Definition: "workflow-root-strict",
+		Schema:     GetWorkflowSchema(),
+	}).UnmarshalYAML(&node)
+	assert.NoError(t, err)
+}
+
+func TestAdditionalFunctionsStepsExprSyntax(t *testing.T) {
+	var node yaml.Node
+	err := yaml.Unmarshal([]byte(`
+on: push
+jobs:
+  job-with-condition:
+    runs-on: self-hosted
+    steps:
+    - run: exit 0
+      if: ${{ success() || failure() || always() }}
+`), &node)
+	if !assert.NoError(t, err) {
+		return
+	}
+	err = (&Node{
+		Definition: "workflow-root-strict",
+		Schema:     GetWorkflowSchema(),
+	}).UnmarshalYAML(&node)
+	assert.NoError(t, err)
+}