From a9d47fb5ed9451d797a2606737eb753d8f68e845 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 3 Aug 2026 17:53:08 -0700 Subject: [PATCH] refactor: move act/model and act/exprparser to actions-proto-go Gitea needs the workflow model and the expression evaluator to parse workflows and to build the task payload this runner consumes, so it had to depend on gitea.com/gitea/runner for it. Both packages now live in gitea.dev/actions-proto-go, the module both sides already share, and this repository consumes them from there. The GithubContext helpers that need a git checkout on disk (SetRef, SetSha and SetRepositoryAndOwner) are not moved: they are runner only and would drag a git client and the act context logger into the shared module. They become functions of the new act/ghcontext package, together with their tests. act/common.CartesianProduct moved to the shared model package as well, act/model was its only user. act/model/testdata/container-volumes is now act/runner/testdata/container-volumes, its only user is runner_test.go. The x-runner-uuid and x-runner-token header names come from actions-proto-go too, so the runner and Gitea cannot drift apart. Signed-off-by: Lunny Xiao --- act/common/cartesian.go | 60 - act/common/cartesian_test.go | 43 - act/exprparser/functions.go | 309 ----- act/exprparser/functions_test.go | 280 ---- act/exprparser/interpreter.go | 644 --------- act/exprparser/interpreter_test.go | 635 --------- act/exprparser/testdata/for-hashing-1.txt | 1 - act/exprparser/testdata/for-hashing-2.txt | 1 - .../testdata/for-hashing-3/data.txt | 1 - .../for-hashing-3/nested/nested-data.txt | 1 - act/ghcontext/github_context.go | 138 ++ .../github_context_test.go | 15 +- act/model/action.go | 138 -- act/model/action_test.go | 82 -- act/model/github_context.go | 222 ---- act/model/job_context.go | 16 - act/model/planner.go | 410 ------ act/model/planner_test.go | 199 --- act/model/step_result.go | 49 - act/model/testdata/empty-workflow/push.yml | 0 .../testdata/invalid-job-name/invalid-1.yml | 12 - .../testdata/invalid-job-name/invalid-2.yml | 8 - .../testdata/invalid-job-name/valid-1.yml | 8 - .../testdata/invalid-job-name/valid-2.yml | 8 - act/model/testdata/nested/success.yml | 9 - act/model/testdata/nested/workflows/fail.yml | 0 act/model/testdata/strategy/push.yml | 50 - act/model/workflow.go | 910 ------------- act/model/workflow_test.go | 1163 ----------------- act/runner/action.go | 2 +- act/runner/action_cache_test.go | 2 +- act/runner/action_composite.go | 3 +- act/runner/action_test.go | 2 +- act/runner/cancellation_test.go | 4 +- act/runner/command_test.go | 2 +- act/runner/expression.go | 4 +- act/runner/expression_test.go | 5 +- act/runner/job_executor.go | 5 +- act/runner/job_executor_test.go | 2 +- act/runner/job_hooks_test.go | 2 +- act/runner/max_parallel_test.go | 3 +- act/runner/reusable_workflow.go | 3 +- act/runner/reusable_workflow_test.go | 2 +- act/runner/run_context.go | 11 +- act/runner/run_context_test.go | 4 +- act/runner/runner.go | 2 +- act/runner/runner_test.go | 4 +- act/runner/step.go | 5 +- act/runner/step_action_local.go | 3 +- act/runner/step_action_local_test.go | 2 +- act/runner/step_action_remote.go | 2 +- act/runner/step_action_remote_test.go | 2 +- act/runner/step_docker.go | 2 +- act/runner/step_docker_test.go | 2 +- act/runner/step_factory.go | 2 +- act/runner/step_factory_test.go | 3 +- act/runner/step_run.go | 2 +- act/runner/step_run_print_test.go | 2 +- act/runner/step_run_test.go | 2 +- act/runner/step_test.go | 2 +- .../testdata/container-volumes/push.yml | 0 act/runner/toolkit_patch.go | 3 +- act/runner/toolkit_patch_test.go | 3 +- go.mod | 4 +- go.sum | 4 +- internal/app/cmd/exec.go | 2 +- internal/app/cmd/exec_test.go | 3 +- internal/app/run/runner.go | 2 +- internal/app/run/workflow.go | 3 +- internal/app/run/workflow_test.go | 3 +- internal/pkg/client/header.go | 8 +- 71 files changed, 212 insertions(+), 5328 deletions(-) delete mode 100644 act/common/cartesian.go delete mode 100644 act/common/cartesian_test.go delete mode 100644 act/exprparser/functions.go delete mode 100644 act/exprparser/functions_test.go delete mode 100644 act/exprparser/interpreter.go delete mode 100644 act/exprparser/interpreter_test.go delete mode 100644 act/exprparser/testdata/for-hashing-1.txt delete mode 100644 act/exprparser/testdata/for-hashing-2.txt delete mode 100644 act/exprparser/testdata/for-hashing-3/data.txt delete mode 100644 act/exprparser/testdata/for-hashing-3/nested/nested-data.txt create mode 100644 act/ghcontext/github_context.go rename act/{model => ghcontext}/github_context_test.go (93%) delete mode 100644 act/model/action.go delete mode 100644 act/model/action_test.go delete mode 100644 act/model/github_context.go delete mode 100644 act/model/job_context.go delete mode 100644 act/model/planner.go delete mode 100644 act/model/planner_test.go delete mode 100644 act/model/step_result.go delete mode 100644 act/model/testdata/empty-workflow/push.yml delete mode 100644 act/model/testdata/invalid-job-name/invalid-1.yml delete mode 100644 act/model/testdata/invalid-job-name/invalid-2.yml delete mode 100644 act/model/testdata/invalid-job-name/valid-1.yml delete mode 100644 act/model/testdata/invalid-job-name/valid-2.yml delete mode 100644 act/model/testdata/nested/success.yml delete mode 100644 act/model/testdata/nested/workflows/fail.yml delete mode 100644 act/model/testdata/strategy/push.yml delete mode 100644 act/model/workflow.go delete mode 100644 act/model/workflow_test.go rename act/{model => runner}/testdata/container-volumes/push.yml (100%) diff --git a/act/common/cartesian.go b/act/common/cartesian.go deleted file mode 100644 index c4f7540c..00000000 --- a/act/common/cartesian.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2026 The Gitea Authors. All rights reserved. -// Copyright 2020 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package common - -import "slices" - -// CartesianProduct takes map of lists and returns list of unique tuples -func CartesianProduct(mapOfLists map[string][]any) []map[string]any { - listNames := make([]string, 0) - lists := make([][]any, 0) - for k, v := range mapOfLists { - listNames = append(listNames, k) - lists = append(lists, v) - } - - listCart := cartN(lists...) - - rtn := make([]map[string]any, 0) - for _, list := range listCart { - vMap := make(map[string]any) - for i, v := range list { - vMap[listNames[i]] = v - } - rtn = append(rtn, vMap) - } - return rtn -} - -func cartN(a ...[]any) [][]any { - c := 1 - for _, a := range a { - c *= len(a) - } - if c == 0 || len(a) == 0 { - return nil - } - p := make([][]any, c) - b := make([]any, c*len(a)) - n := make([]int, len(a)) - s := 0 - for i := range p { - e := s + len(a) - pi := b[s:e] - p[i] = pi - s = e - for j, n := range n { - pi[j] = a[j][n] - } - for j := range slices.Backward(n) { - n[j]++ - if n[j] < len(a[j]) { - break - } - n[j] = 0 - } - } - return p -} diff --git a/act/common/cartesian_test.go b/act/common/cartesian_test.go deleted file mode 100644 index 91f748de..00000000 --- a/act/common/cartesian_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2026 The Gitea Authors. All rights reserved. -// Copyright 2020 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package common - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCartesianProduct(t *testing.T) { - assert := assert.New(t) - input := map[string][]any{ - "foo": {1, 2, 3, 4}, - "bar": {"a", "b", "c"}, - "baz": {false, true}, - } - - output := CartesianProduct(input) - assert.Len(output, 24) - - for _, v := range output { - assert.Len(v, 3) - - assert.Contains(v, "foo") - assert.Contains(v, "bar") - assert.Contains(v, "baz") - } - - input = map[string][]any{ - "foo": {1, 2, 3, 4}, - "bar": {}, - "baz": {false, true}, - } - output = CartesianProduct(input) - assert.Empty(output) - - input = map[string][]any{} - output = CartesianProduct(input) - assert.Empty(output) -} diff --git a/act/exprparser/functions.go b/act/exprparser/functions.go deleted file mode 100644 index a4853bfb..00000000 --- a/act/exprparser/functions.go +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2022 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package exprparser - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "reflect" - "strconv" - "strings" - - "gitea.com/gitea/runner/act/model" - - "github.com/go-git/go-git/v5/plumbing/format/gitignore" - "github.com/rhysd/actionlint" -) - -func (impl *interperterImpl) contains(search, item reflect.Value) (bool, error) { - switch search.Kind() { - case reflect.String, reflect.Int, reflect.Float64, reflect.Bool, reflect.Invalid: - return strings.Contains( - strings.ToLower(impl.coerceToString(search).String()), - strings.ToLower(impl.coerceToString(item).String()), - ), nil - - case reflect.Slice: - for i := 0; i < search.Len(); i++ { - arrayItem := search.Index(i).Elem() - result, err := impl.compareValues(arrayItem, item, actionlint.CompareOpNodeKindEq) - if err != nil { - return false, err - } - - if isEqual, ok := result.(bool); ok && isEqual { - return true, nil - } - } - } - - return false, nil -} - -func (impl *interperterImpl) startsWith(searchString, searchValue reflect.Value) (bool, error) { //nolint:unparam // pre-existing issue from nektos/act - return strings.HasPrefix( - strings.ToLower(impl.coerceToString(searchString).String()), - strings.ToLower(impl.coerceToString(searchValue).String()), - ), nil -} - -func (impl *interperterImpl) endsWith(searchString, searchValue reflect.Value) (bool, error) { //nolint:unparam // pre-existing issue from nektos/act - return strings.HasSuffix( - strings.ToLower(impl.coerceToString(searchString).String()), - strings.ToLower(impl.coerceToString(searchValue).String()), - ), nil -} - -const ( - passThrough = iota - bracketOpen - bracketClose -) - -func (impl *interperterImpl) format(str reflect.Value, replaceValue ...reflect.Value) (string, error) { - input := impl.coerceToString(str).String() - var output strings.Builder - replacementIndex := "" - - state := passThrough - for _, character := range input { - switch state { - case passThrough: // normal buffer output - switch character { - case '{': - state = bracketOpen - - case '}': - state = bracketClose - - default: - output.WriteRune(character) - } - - case bracketOpen: // found { - switch character { - case '{': - output.WriteString("{") - replacementIndex = "" - state = passThrough - - case '}': - index, err := strconv.ParseInt(replacementIndex, 10, 32) - if err != nil { - return "", fmt.Errorf("The following format string is invalid: '%s'", input) - } - - replacementIndex = "" - - if len(replaceValue) <= int(index) { - return "", fmt.Errorf("The following format string references more arguments than were supplied: '%s'", input) - } - - output.WriteString(impl.coerceToString(replaceValue[index]).String()) - - state = passThrough - - default: - replacementIndex += string(character) - } - - case bracketClose: // found } - switch character { - case '}': - output.WriteString("}") - replacementIndex = "" - state = passThrough - - default: - panic("Invalid format parser state") - } - } - } - - if state != passThrough { - switch state { - case bracketOpen: - return "", fmt.Errorf("Unclosed brackets. The following format string is invalid: '%s'", input) - - case bracketClose: - return "", fmt.Errorf("Closing bracket without opening one. The following format string is invalid: '%s'", input) - } - } - - return output.String(), nil -} - -func (impl *interperterImpl) join(array, sep reflect.Value) (string, error) { //nolint:unparam // pre-existing issue from nektos/act - separator := impl.coerceToString(sep).String() - switch array.Kind() { - case reflect.Slice: - var items []string - for i := 0; i < array.Len(); i++ { - items = append(items, impl.coerceToString(array.Index(i).Elem()).String()) - } - - return strings.Join(items, separator), nil - default: - return strings.Join([]string{impl.coerceToString(array).String()}, separator), nil - } -} - -func (impl *interperterImpl) toJSON(value reflect.Value) (string, error) { - if value.Kind() == reflect.Invalid { - return "null", nil - } - - json, err := json.MarshalIndent(value.Interface(), "", " ") - if err != nil { - return "", fmt.Errorf("Cannot convert value to JSON. Cause: %v", err) - } - - return string(json), nil -} - -func (impl *interperterImpl) fromJSON(value reflect.Value) (any, error) { - if value.Kind() != reflect.String { - return nil, fmt.Errorf("Cannot parse non-string type %v as JSON", value.Kind()) - } - - var data any - - err := json.Unmarshal([]byte(value.String()), &data) - if err != nil { - return nil, fmt.Errorf("Invalid JSON: %v", err) - } - - return data, nil -} - -func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) { - var ps []gitignore.Pattern - - const cwdPrefix = "." + string(filepath.Separator) - const excludeCwdPrefix = "!" + cwdPrefix - for _, path := range paths { - if path.Kind() == reflect.String { - cleanPath := path.String() - if strings.HasPrefix(cleanPath, cwdPrefix) { - cleanPath = cleanPath[len(cwdPrefix):] - } else if strings.HasPrefix(cleanPath, excludeCwdPrefix) { - cleanPath = "!" + cleanPath[len(excludeCwdPrefix):] - } - ps = append(ps, gitignore.ParsePattern(cleanPath, nil)) - } else { - return "", errors.New("Non-string path passed to hashFiles") - } - } - - matcher := gitignore.NewMatcher(ps) - - var files []string - if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error { - if err != nil { - return err - } - sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator)) - parts := strings.Split(sansPrefix, string(filepath.Separator)) - if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) { - return nil - } - files = append(files, path) - return nil - }); err != nil { - return "", fmt.Errorf("Unable to filepath.Walk: %v", err) - } - - if len(files) == 0 { - return "", nil - } - - hasher := sha256.New() - - for _, file := range files { - f, err := os.Open(file) - if err != nil { - return "", fmt.Errorf("Unable to os.Open: %v", err) - } - - if _, err := io.Copy(hasher, f); err != nil { - return "", fmt.Errorf("Unable to io.Copy: %v", err) - } - - if err := f.Close(); err != nil { - return "", fmt.Errorf("Unable to Close file: %v", err) - } - } - - return hex.EncodeToString(hasher.Sum(nil)), nil -} - -func (impl *interperterImpl) getNeedsTransitive(job *model.Job) []string { - needs := job.Needs() - - for _, need := range needs { - parentNeeds := impl.getNeedsTransitive(impl.config.Run.Workflow.GetJob(need)) - needs = append(needs, parentNeeds...) - } - - return needs -} - -func (impl *interperterImpl) always() (bool, error) { - return true, nil -} - -func (impl *interperterImpl) jobSuccess() (bool, error) { //nolint:unparam // pre-existing issue from nektos/act - jobs := impl.config.Run.Workflow.Jobs - jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job()) - - for _, needs := range jobNeeds { - if jobs[needs].NeedsResult() != "success" { - return false, nil - } - } - - return true, nil -} - -// jobStatus returns the current job status, treating a nil Job context as an -// empty status so status-check functions never panic on a nil dereference. -func (impl *interperterImpl) jobStatus() string { - if impl.env.Job == nil { - return "" - } - return impl.env.Job.Status -} - -func (impl *interperterImpl) stepSuccess() (bool, error) { //nolint:unparam // pre-existing issue from nektos/act - return impl.jobStatus() == "success", nil -} - -func (impl *interperterImpl) jobFailure() (bool, error) { //nolint:unparam // pre-existing issue from nektos/act - jobs := impl.config.Run.Workflow.Jobs - jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job()) - - for _, needs := range jobNeeds { - if jobs[needs].NeedsResult() == "failure" { - return true, nil - } - } - - return false, nil -} - -func (impl *interperterImpl) stepFailure() (bool, error) { //nolint:unparam // pre-existing issue from nektos/act - return impl.jobStatus() == "failure", nil -} - -func (impl *interperterImpl) cancelled() (bool, error) { //nolint:unparam // pre-existing issue from nektos/act - return impl.jobStatus() == "cancelled", nil -} diff --git a/act/exprparser/functions_test.go b/act/exprparser/functions_test.go deleted file mode 100644 index 707bf6ab..00000000 --- a/act/exprparser/functions_test.go +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2022 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package exprparser - -import ( - "path/filepath" - "testing" - - "gitea.com/gitea/runner/act/model" - - "github.com/stretchr/testify/assert" -) - -func TestFunctionContains(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"contains('search', 'item') }}", false, "contains-str-str"}, - {`cOnTaInS('Hello', 'll') }}`, true, "contains-str-casing"}, - {`contains('HELLO', 'll') }}`, true, "contains-str-casing"}, - {`contains('3.141592', 3.14) }}`, true, "contains-str-number"}, - {`contains(3.141592, '3.14') }}`, true, "contains-number-str"}, - {`contains(3.141592, 3.14) }}`, true, "contains-number-number"}, - {`contains(true, 'u') }}`, true, "contains-bool-str"}, - {`contains(null, '') }}`, true, "contains-null-str"}, - {`contains(fromJSON('["first","second"]'), 'first') }}`, true, "contains-item"}, - {`contains(fromJSON('[null,"second"]'), '') }}`, true, "contains-item-null-empty-str"}, - {`contains(fromJSON('["","second"]'), null) }}`, true, "contains-item-empty-str-null"}, - {`contains(fromJSON('[true,"second"]'), 'true') }}`, false, "contains-item-bool-arr"}, - {`contains(fromJSON('["true","second"]'), true) }}`, false, "contains-item-str-bool"}, - {`contains(fromJSON('[3.14,"second"]'), '3.14') }}`, true, "contains-item-number-str"}, - {`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"}, - } - - env := &EvaluationEnvironment{} - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestFunctionStartsWith(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"startsWith('search', 'se') }}", true, "startswith-string"}, - {"startsWith('search', 'sa') }}", false, "startswith-string"}, - {"startsWith('123search', '123s') }}", true, "startswith-string"}, - {"startsWith(123, 's') }}", false, "startswith-string"}, - {"startsWith(123, '12') }}", true, "startswith-string"}, - {"startsWith('123', 12) }}", true, "startswith-string"}, - {"startsWith(null, '42') }}", false, "startswith-string"}, - {"startsWith('null', null) }}", true, "startswith-string"}, - {"startsWith('null', '') }}", true, "startswith-string"}, - } - - env := &EvaluationEnvironment{} - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestFunctionEndsWith(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"endsWith('search', 'ch') }}", true, "endsWith-string"}, - {"endsWith('search', 'sa') }}", false, "endsWith-string"}, - {"endsWith('search123s', '123s') }}", true, "endsWith-string"}, - {"endsWith(123, 's') }}", false, "endsWith-string"}, - {"endsWith(123, '23') }}", true, "endsWith-string"}, - {"endsWith('123', 23) }}", true, "endsWith-string"}, - {"endsWith(null, '42') }}", false, "endsWith-string"}, - {"endsWith('null', null) }}", true, "endsWith-string"}, - {"endsWith('null', '') }}", true, "endsWith-string"}, - } - - env := &EvaluationEnvironment{} - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestFunctionJoin(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"join(fromJSON('[\"a\", \"b\"]'), ',')", "a,b", "join-arr"}, - {"join('string', ',')", "string", "join-str"}, - {"join(1, ',')", "1", "join-number"}, - {"join(null, ',')", "", "join-number"}, - {"join(fromJSON('[\"a\", \"b\", null]'), null)", "ab", "join-number"}, - {"join(fromJSON('[\"a\", \"b\"]'))", "a,b", "join-number"}, - {"join(fromJSON('[\"a\", \"b\", null]'), 1)", "a1b1", "join-number"}, - } - - env := &EvaluationEnvironment{} - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestFunctionToJSON(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"toJSON(env) }}", "{\n \"key\": \"value\"\n}", "toJSON"}, - {"toJSON(null)", "null", "toJSON-null"}, - } - - env := &EvaluationEnvironment{ - Env: map[string]string{ - "key": "value", - }, - } - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestFunctionFromJSON(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"fromJSON('{\"foo\":\"bar\"}') }}", map[string]any{ - "foo": "bar", - }, "fromJSON"}, - } - - env := &EvaluationEnvironment{} - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestFunctionHashFiles(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"hashFiles('**/non-extant-files') }}", "", "hash-non-existing-file"}, - {"hashFiles('**/non-extant-files', '**/more-non-extant-files') }}", "", "hash-multiple-non-existing-files"}, - {"hashFiles('./for-hashing-1.txt') }}", "66a045b452102c59d840ec097d59d9467e13a3f34f6494e539ffd32c1bb35f18", "hash-single-file"}, - {"hashFiles('./for-hashing-*.txt') }}", "8e5935e7e13368cd9688fe8f48a0955293676a021562582c7e848dafe13fb046", "hash-multiple-files"}, - {"hashFiles('./for-hashing-*.txt', '!./for-hashing-2.txt') }}", "66a045b452102c59d840ec097d59d9467e13a3f34f6494e539ffd32c1bb35f18", "hash-negative-pattern"}, - {"hashFiles('./for-hashing-**') }}", "c418ba693753c84115ced0da77f876cddc662b9054f4b129b90f822597ee2f94", "hash-multiple-files-and-directories"}, - {"hashFiles('./for-hashing-3/**') }}", "6f5696b546a7a9d6d42a449dc9a56bef244aaa826601ef27466168846139d2c2", "hash-nested-directories"}, - {"hashFiles('./for-hashing-3/**/nested-data.txt') }}", "8ecadfb49f7f978d0a9f3a957e9c8da6cc9ab871f5203b5d9f9d1dc87d8af18c", "hash-nested-directories-2"}, - } - - env := &EvaluationEnvironment{} - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - workdir, err := filepath.Abs("testdata") - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - output, err := NewInterpeter(env, Config{WorkingDir: workdir}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestFunctionFormat(t *testing.T) { - table := []struct { - input string - expected any - error any - name string - }{ - {"format('text')", "text", nil, "format-plain-string"}, - {"format('Hello {0} {1} {2}!', 'Mona', 'the', 'Octocat')", "Hello Mona the Octocat!", nil, "format-with-placeholders"}, - {"format('{{Hello {0} {1} {2}!}}', 'Mona', 'the', 'Octocat')", "{Hello Mona the Octocat!}", nil, "format-with-escaped-braces"}, - {"format('{{0}}', 'test')", "{0}", nil, "format-with-escaped-braces"}, - {"format('{{{0}}}', 'test')", "{test}", nil, "format-with-escaped-braces-and-value"}, - {"format('}}')", "}", nil, "format-output-closing-brace"}, - {`format('Hello "{0}" {1} {2} {3} {4}', null, true, -3.14, NaN, Infinity)`, `Hello "" true -3.14 NaN Infinity`, nil, "format-with-primitives"}, - {`format('Hello "{0}" {1} {2}', fromJSON('[0, true, "abc"]'), fromJSON('[{"a":1}]'), fromJSON('{"a":{"b":1}}'))`, `Hello "Array" Array Object`, nil, "format-with-complex-types"}, - {"format(true)", "true", nil, "format-with-primitive-args"}, - {"format('echo Hello {0} ${{Test}}', github.undefined_property)", "echo Hello ${Test}", nil, "format-with-undefined-value"}, - {"format('{0}}', '{1}', 'World')", nil, "Closing bracket without opening one. The following format string is invalid: '{0}}'", "format-invalid-format-string"}, - {"format('{0', '{1}', 'World')", nil, "Unclosed brackets. The following format string is invalid: '{0'", "format-invalid-format-string"}, - {"format('{2}', '{1}', 'World')", "", "The following format string references more arguments than were supplied: '{2}'", "format-invalid-replacement-reference"}, - {"format('{2147483648}')", "", "The following format string is invalid: '{2147483648}'", "format-invalid-replacement-reference"}, - {"format('{0} {1} {2} {3}', 1.0, 1.1, 1234567890.0, 12345678901234567890.0)", "1 1.1 1234567890 1.23456789012346E+19", nil, "format-floats"}, - } - - env := &EvaluationEnvironment{ - Github: &model.GithubContext{}, - } - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - if tt.error != nil { - assert.Equal(t, tt.error, err.Error()) - } else { - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, tt.expected, output) - } - }) - } -} - -func TestStatusFunctionsNilJob(t *testing.T) { - // A nil Job context must not panic: the status-check functions should treat - // it as an empty status and return false rather than dereferencing nil. - env := &EvaluationEnvironment{} - - table := []struct { - input string - context string - name string - }{ - {"cancelled()", "job", "cancelled-nil-job"}, - {"success()", "step", "step-success-nil-job"}, - {"failure()", "step", "step-failure-nil-job"}, - } - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{Context: tt.context}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, false, output) - }) - } -} diff --git a/act/exprparser/interpreter.go b/act/exprparser/interpreter.go deleted file mode 100644 index 6de554eb..00000000 --- a/act/exprparser/interpreter.go +++ /dev/null @@ -1,644 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2022 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package exprparser - -import ( - "encoding" - "errors" - "fmt" - "math" - "reflect" - "strings" - - "gitea.com/gitea/runner/act/model" - - "github.com/rhysd/actionlint" -) - -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) -} - -type Needs struct { - Outputs map[string]string `json:"outputs"` - Result string `json:"result"` -} - -type Config struct { - Run *model.Run - WorkingDir string - Context string -} - -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 (impl *interperterImpl) Evaluate(input string, defaultStatusCheck DefaultStatusCheck) (any, error) { - input = strings.TrimPrefix(input, "${{") - if defaultStatusCheck != DefaultStatusCheckNone && input == "" { - input = "success()" - } - parser := actionlint.NewExprParser() - exprNode, err := parser.Parse(actionlint.NewExprLexer(input + "}}")) - if err != nil { - return nil, fmt.Errorf("Failed to parse: %s", err.Message) - } - - if defaultStatusCheck != DefaultStatusCheckNone { - hasStatusCheckFunction := false - actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) { - if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok { - switch strings.ToLower(funcCallNode.Callee) { - case "success", "always", "cancelled", "failure": - hasStatusCheckFunction = true - } - } - }) - - if !hasStatusCheckFunction { - exprNode = &actionlint.LogicalOpNode{ - Kind: actionlint.LogicalOpNodeKindAnd, - Left: &actionlint.FuncCallNode{ - Callee: defaultStatusCheck.String(), - Args: []actionlint.ExprNode{}, - }, - Right: exprNode, - } - } - } - - result, err2 := impl.evaluateNode(exprNode) - - return result, err2 -} - -func (impl *interperterImpl) evaluateNode(exprNode actionlint.ExprNode) (any, error) { - switch node := exprNode.(type) { - case *actionlint.VariableNode: - return impl.evaluateVariable(node) - case *actionlint.BoolNode: - return node.Value, nil - case *actionlint.NullNode: - return nil, nil //nolint:nilnil // pre-existing issue from nektos/act - case *actionlint.IntNode: - return node.Value, nil - case *actionlint.FloatNode: - return node.Value, nil - case *actionlint.StringNode: - return node.Value, nil - case *actionlint.IndexAccessNode: - return impl.evaluateIndexAccess(node) - case *actionlint.ObjectDerefNode: - return impl.evaluateObjectDeref(node) - case *actionlint.ArrayDerefNode: - return impl.evaluateArrayDeref(node) - case *actionlint.NotOpNode: - return impl.evaluateNot(node) - case *actionlint.CompareOpNode: - return impl.evaluateCompare(node) - case *actionlint.LogicalOpNode: - return impl.evaluateLogicalCompare(node) - case *actionlint.FuncCallNode: - return impl.evaluateFuncCall(node) - default: - return nil, fmt.Errorf("Fatal error! Unknown node type: %s node: %+v", reflect.TypeOf(exprNode), exprNode) - } -} - -func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (any, error) { - switch strings.ToLower(variableNode.Name) { - case "github": - return impl.env.Github, nil - case "gitea": // compatible with Gitea - return impl.env.Github, nil - case "env": - return impl.env.Env, nil - case "job": - return impl.env.Job, nil - case "jobs": - if impl.env.Jobs == nil { - return nil, errors.New("Unavailable context: jobs") - } - return impl.env.Jobs, nil - case "steps": - return impl.env.Steps, nil - case "runner": - return impl.env.Runner, nil - case "secrets": - return impl.env.Secrets, nil - case "vars": - return impl.env.Vars, nil - case "strategy": - return impl.env.Strategy, nil - case "matrix": - return impl.env.Matrix, nil - case "needs": - return impl.env.Needs, nil - case "inputs": - return impl.env.Inputs, nil - case "infinity": - return math.Inf(1), nil - case "nan": - return math.NaN(), nil - default: - return nil, fmt.Errorf("Unavailable context: %s", variableNode.Name) - } -} - -func (impl *interperterImpl) evaluateIndexAccess(indexAccessNode *actionlint.IndexAccessNode) (any, error) { - left, err := impl.evaluateNode(indexAccessNode.Operand) - if err != nil { - return nil, err - } - - leftValue := reflect.ValueOf(left) - - right, err := impl.evaluateNode(indexAccessNode.Index) - if err != nil { - return nil, err - } - - rightValue := reflect.ValueOf(right) - - switch rightValue.Kind() { - case reflect.String: - return impl.getPropertyValue(leftValue, rightValue.String()) - - case reflect.Int: - switch leftValue.Kind() { - case reflect.Slice: - if rightValue.Int() < 0 || rightValue.Int() >= int64(leftValue.Len()) { - return nil, nil //nolint:nilnil // pre-existing issue from nektos/act - } - return leftValue.Index(int(rightValue.Int())).Interface(), nil - default: - return nil, nil //nolint:nilnil // pre-existing issue from nektos/act - } - - default: - return nil, nil //nolint:nilnil // pre-existing issue from nektos/act - } -} - -func (impl *interperterImpl) evaluateObjectDeref(objectDerefNode *actionlint.ObjectDerefNode) (any, error) { - left, err := impl.evaluateNode(objectDerefNode.Receiver) - if err != nil { - return nil, err - } - - return impl.getPropertyValue(reflect.ValueOf(left), objectDerefNode.Property) -} - -func (impl *interperterImpl) evaluateArrayDeref(arrayDerefNode *actionlint.ArrayDerefNode) (any, error) { - left, err := impl.evaluateNode(arrayDerefNode.Receiver) - if err != nil { - return nil, err - } - - return impl.getSafeValue(reflect.ValueOf(left)), nil -} - -func (impl *interperterImpl) getPropertyValue(left reflect.Value, property string) (value any, err error) { - switch left.Kind() { - case reflect.Pointer: - return impl.getPropertyValue(left.Elem(), property) - - case reflect.Struct: - leftType := left.Type() - for field := range leftType.Fields() { - jsonName := field.Tag.Get("json") - if jsonName == property { - property = field.Name - break - } - } - - fieldValue := left.FieldByNameFunc(func(name string) bool { - return strings.EqualFold(name, property) - }) - - if fieldValue.Kind() == reflect.Invalid { - return "", nil - } - - i := fieldValue.Interface() - // The type stepStatus int is an integer, but should be treated as string - if m, ok := i.(encoding.TextMarshaler); ok { - text, err := m.MarshalText() - if err != nil { - return nil, err - } - return string(text), nil - } - return i, nil - - case reflect.Map: - iter := left.MapRange() - - for iter.Next() { - key := iter.Key() - - switch key.Kind() { - case reflect.String: - if strings.EqualFold(key.String(), property) { - return impl.getMapValue(iter.Value()) - } - - default: - return nil, fmt.Errorf("'%s' in map key not implemented", key.Kind()) - } - } - - return nil, nil //nolint:nilnil // pre-existing issue from nektos/act - - case reflect.Slice: - var values []any - - for i := 0; i < left.Len(); i++ { - value, err := impl.getPropertyValue(left.Index(i).Elem(), property) - if err != nil { - return nil, err - } - - values = append(values, value) - } - - return values, nil - } - - return nil, nil //nolint:nilnil // pre-existing issue from nektos/act -} - -func (impl *interperterImpl) getMapValue(value reflect.Value) (any, error) { - if value.Kind() == reflect.Pointer { - return impl.getMapValue(value.Elem()) - } - - return value.Interface(), nil -} - -func (impl *interperterImpl) evaluateNot(notNode *actionlint.NotOpNode) (any, error) { - operand, err := impl.evaluateNode(notNode.Operand) - if err != nil { - return nil, err - } - - return !IsTruthy(operand), nil -} - -func (impl *interperterImpl) evaluateCompare(compareNode *actionlint.CompareOpNode) (any, error) { - left, err := impl.evaluateNode(compareNode.Left) - if err != nil { - return nil, err - } - - right, err := impl.evaluateNode(compareNode.Right) - if err != nil { - return nil, err - } - - leftValue := reflect.ValueOf(left) - rightValue := reflect.ValueOf(right) - - return impl.compareValues(leftValue, rightValue, compareNode.Kind) -} - -func (impl *interperterImpl) compareValues(leftValue, rightValue reflect.Value, kind actionlint.CompareOpNodeKind) (any, error) { - if leftValue.Kind() != rightValue.Kind() { - if !impl.isNumber(leftValue) { - leftValue = impl.coerceToNumber(leftValue) - } - if !impl.isNumber(rightValue) { - rightValue = impl.coerceToNumber(rightValue) - } - } - - switch leftValue.Kind() { - case reflect.Bool: - return impl.compareNumber(float64(impl.coerceToNumber(leftValue).Int()), float64(impl.coerceToNumber(rightValue).Int()), kind) - case reflect.String: - return impl.compareString(strings.ToLower(leftValue.String()), strings.ToLower(rightValue.String()), kind) - - case reflect.Int: - if rightValue.Kind() == reflect.Float64 { - return impl.compareNumber(float64(leftValue.Int()), rightValue.Float(), kind) - } - - return impl.compareNumber(float64(leftValue.Int()), float64(rightValue.Int()), kind) - - case reflect.Float64: - if rightValue.Kind() == reflect.Int { - return impl.compareNumber(leftValue.Float(), float64(rightValue.Int()), kind) - } - - return impl.compareNumber(leftValue.Float(), rightValue.Float(), kind) - - case reflect.Invalid: - if rightValue.Kind() == reflect.Invalid { - return true, nil - } - - // not possible situation - params are converted to the same type in code above - return nil, fmt.Errorf("Compare params of Invalid type: left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind()) - - default: - return nil, fmt.Errorf("Compare not implemented for types: left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind()) - } -} - -func (impl *interperterImpl) coerceToNumber(value reflect.Value) reflect.Value { - switch value.Kind() { - case reflect.Invalid: - return reflect.ValueOf(0) - - case reflect.Bool: - switch value.Bool() { - case true: - return reflect.ValueOf(1) - case false: - return reflect.ValueOf(0) - } - - case reflect.String: - if value.String() == "" { - return reflect.ValueOf(0) - } - - // try to parse the string as a number - evaluated, err := impl.Evaluate(value.String(), DefaultStatusCheckNone) - if err != nil { - return reflect.ValueOf(math.NaN()) - } - - if value := reflect.ValueOf(evaluated); impl.isNumber(value) { - return 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") - } - - return value -} - -func (impl *interperterImpl) compareString(left, right string, kind actionlint.CompareOpNodeKind) (bool, error) { - switch kind { - case actionlint.CompareOpNodeKindLess: - return left < right, nil - case actionlint.CompareOpNodeKindLessEq: - return left <= right, nil - case actionlint.CompareOpNodeKindGreater: - return left > right, nil - case actionlint.CompareOpNodeKindGreaterEq: - return left >= right, nil - case actionlint.CompareOpNodeKindEq: - return left == right, nil - case actionlint.CompareOpNodeKindNotEq: - return left != right, nil - default: - return false, fmt.Errorf("TODO: not implemented to compare '%+v'", kind) - } -} - -func (impl *interperterImpl) compareNumber(left, right float64, kind actionlint.CompareOpNodeKind) (bool, error) { - switch kind { - case actionlint.CompareOpNodeKindLess: - return left < right, nil - case actionlint.CompareOpNodeKindLessEq: - return left <= right, nil - case actionlint.CompareOpNodeKindGreater: - return left > right, nil - case actionlint.CompareOpNodeKindGreaterEq: - return left >= right, nil - case actionlint.CompareOpNodeKindEq: - return left == right, nil - case actionlint.CompareOpNodeKindNotEq: - return left != right, nil - default: - return false, fmt.Errorf("TODO: not implemented to compare '%+v'", kind) - } -} - -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 - } -} - -func (impl *interperterImpl) isNumber(value reflect.Value) bool { - switch value.Kind() { - case reflect.Int, reflect.Float64: - return true - default: - return false - } -} - -func (impl *interperterImpl) getSafeValue(value reflect.Value) any { - switch value.Kind() { - case reflect.Invalid: - return nil - - case reflect.Float64: - if value.Float() == 0 { - return 0 - } - } - - return value.Interface() -} - -func (impl *interperterImpl) evaluateLogicalCompare(compareNode *actionlint.LogicalOpNode) (any, error) { - left, err := impl.evaluateNode(compareNode.Left) - if err != nil { - return nil, err - } - - leftValue := reflect.ValueOf(left) - - if IsTruthy(left) == (compareNode.Kind == actionlint.LogicalOpNodeKindOr) { - return impl.getSafeValue(leftValue), nil - } - - right, err := impl.evaluateNode(compareNode.Right) - if err != nil { - return nil, err - } - - rightValue := reflect.ValueOf(right) - - switch compareNode.Kind { - case actionlint.LogicalOpNodeKindAnd: - return impl.getSafeValue(rightValue), nil - case actionlint.LogicalOpNodeKindOr: - return impl.getSafeValue(rightValue), nil - } - - return nil, fmt.Errorf("Unable to compare incompatibles types '%s' and '%s'", leftValue.Kind(), rightValue.Kind()) -} - -func (impl *interperterImpl) evaluateFuncCall(funcCallNode *actionlint.FuncCallNode) (any, error) { - args := make([]reflect.Value, 0) - - for _, arg := range funcCallNode.Args { - value, err := impl.evaluateNode(arg) - if err != nil { - return nil, err - } - - args = append(args, reflect.ValueOf(value)) - } - - switch strings.ToLower(funcCallNode.Callee) { - case "contains": - return impl.contains(args[0], args[1]) - case "startswith": - return impl.startsWith(args[0], args[1]) - case "endswith": - return impl.endsWith(args[0], args[1]) - case "format": - return impl.format(args[0], args[1:]...) - case "join": - if len(args) == 1 { - return impl.join(args[0], reflect.ValueOf(",")) - } - return impl.join(args[0], args[1]) - case "tojson": - return impl.toJSON(args[0]) - case "fromjson": - return impl.fromJSON(args[0]) - case "hashfiles": - if impl.env.HashFiles != nil { - return impl.env.HashFiles(args) - } - return impl.hashFiles(args...) - case "always": - return impl.always() - case "success": - 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) - case "failure": - 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) - case "cancelled": - return impl.cancelled() - default: - return nil, fmt.Errorf("TODO: '%s' not implemented", funcCallNode.Callee) - } -} diff --git a/act/exprparser/interpreter_test.go b/act/exprparser/interpreter_test.go deleted file mode 100644 index b1484e11..00000000 --- a/act/exprparser/interpreter_test.go +++ /dev/null @@ -1,635 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2022 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package exprparser - -import ( - "math" - "testing" - - "gitea.com/gitea/runner/act/model" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLiterals(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"true", true, "true"}, - {"false", false, "false"}, - {"null", nil, "null"}, - {"123", 123, "integer"}, - {"-9.7", -9.7, "float"}, - {"0xff", 255, "hex"}, - {"-2.99e-2", -2.99e-2, "exponential"}, - {"'foo'", "foo", "string"}, - {"'it''s foo'", "it's foo", "string"}, - } - - env := &EvaluationEnvironment{} - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestOperators(t *testing.T) { - table := []struct { - input string - expected any - name string - error string - }{ - {"(false || (false || true))", true, "logical-grouping", ""}, - {"github.action", "push", "property-dereference", ""}, - {"github['action']", "push", "property-index", ""}, - {"github.action[0]", nil, "string-index", ""}, - {"github.action['0']", nil, "string-index", ""}, - {"fromJSON('[0,1]')[1]", 1.0, "array-index", ""}, - {"fromJSON('[0,1]')[1.1]", nil, "array-index", ""}, - // Disabled weird things are happening - // {"fromJSON('[0,1]')['1.1']", nil, "array-index", ""}, - {"(github.event.commits.*.author.username)[0]", "someone", "array-index-0", ""}, - {"fromJSON('[0,1]')[2]", nil, "array-index-out-of-bounds-0", ""}, - {"fromJSON('[0,1]')[34553]", nil, "array-index-out-of-bounds-1", ""}, - {"fromJSON('[0,1]')[-1]", nil, "array-index-out-of-bounds-2", ""}, - {"fromJSON('[0,1]')[-34553]", nil, "array-index-out-of-bounds-3", ""}, - {"!true", false, "not", ""}, - {"1 < 2", true, "less-than", ""}, - {`'b' <= 'a'`, false, "less-than-or-equal", ""}, - {"1 > 2", false, "greater-than", ""}, - {`'b' >= 'a'`, true, "greater-than-or-equal", ""}, - {`'a' == 'a'`, true, "equal", ""}, - {`'a' != 'a'`, false, "not-equal", ""}, - {`true && false`, false, "and", ""}, - {`true || false`, true, "or", ""}, - {`fromJSON('{}') && true`, true, "and-boolean-object", ""}, - {`fromJSON('{}') || false`, make(map[string]any), "or-boolean-object", ""}, - {"github.event.commits[0].author.username != github.event.commits[1].author.username", true, "property-comparison1", ""}, - {"github.event.commits[0].author.username1 != github.event.commits[1].author.username", true, "property-comparison2", ""}, - {"github.event.commits[0].author.username != github.event.commits[1].author.username1", true, "property-comparison3", ""}, - {"github.event.commits[0].author.username1 != github.event.commits[1].author.username2", true, "property-comparison4", ""}, - {"secrets != env", nil, "property-comparison5", "Compare not implemented for types: left: map, right: map"}, - } - - env := &EvaluationEnvironment{ - Github: &model.GithubContext{ - Action: "push", - Event: map[string]any{ - "commits": []any{ - map[string]any{ - "author": map[string]any{ - "username": "someone", - }, - }, - map[string]any{ - "author": map[string]any{ - "username": "someone-else", - }, - }, - }, - }, - }, - } - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - if tt.error != "" { - assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, tt.error, err.Error()) - } else { - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - } - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestOperatorsCompare(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"!null", true, "not-null"}, - {"!-10", false, "not-neg-num"}, - {"!0", true, "not-zero"}, - {"!3.14", false, "not-pos-float"}, - {"!''", true, "not-empty-str"}, - {"!'abc'", false, "not-str"}, - {"!fromJSON('{}')", false, "not-obj"}, - {"!fromJSON('[]')", false, "not-arr"}, - {`null == 0 }}`, true, "null-coercion"}, - {`true == 1 }}`, true, "boolean-coercion"}, - {`'' == 0 }}`, true, "string-0-coercion"}, - {`'3' == 3 }}`, true, "string-3-coercion"}, - {`0 == null }}`, true, "null-coercion-alt"}, - {`1 == true }}`, true, "boolean-coercion-alt"}, - {`0 == '' }}`, true, "string-0-coercion-alt"}, - {`3 == '3' }}`, true, "string-3-coercion-alt"}, - {`'TEST' == 'test' }}`, true, "string-casing"}, - {"true > false }}", true, "bool-greater-than"}, - {"true >= false }}", true, "bool-greater-than-eq"}, - {"true >= true }}", true, "bool-greater-than-1"}, - {"true != false }}", true, "bool-not-equal"}, - {`fromJSON('{}') < 2 }}`, false, "object-with-less"}, - {`fromJSON('{}') < fromJSON('[]') }}`, false, "object/arr-with-lt"}, - {`fromJSON('{}') > fromJSON('[]') }}`, false, "object/arr-with-gt"}, - } - - env := &EvaluationEnvironment{ - Github: &model.GithubContext{ - Action: "push", - }, - } - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestOperatorsBooleanEvaluation(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - // true && - {"true && true", true, "true-and"}, - {"true && false", false, "true-and"}, - {"true && null", nil, "true-and"}, - {"true && -10", -10, "true-and"}, - {"true && 0", 0, "true-and"}, - {"true && 10", 10, "true-and"}, - {"true && 3.14", 3.14, "true-and"}, - {"true && 0.0", 0, "true-and"}, - {"true && Infinity", math.Inf(1), "true-and"}, - // {"true && -Infinity", math.Inf(-1), "true-and"}, - {"true && NaN", math.NaN(), "true-and"}, - {"true && ''", "", "true-and"}, - {"true && 'abc'", "abc", "true-and"}, - // false && - {"false && true", false, "false-and"}, - {"false && false", false, "false-and"}, - {"false && null", false, "false-and"}, - {"false && -10", false, "false-and"}, - {"false && 0", false, "false-and"}, - {"false && 10", false, "false-and"}, - {"false && 3.14", false, "false-and"}, - {"false && 0.0", false, "false-and"}, - {"false && Infinity", false, "false-and"}, - // {"false && -Infinity", false, "false-and"}, - {"false && NaN", false, "false-and"}, - {"false && ''", false, "false-and"}, - {"false && 'abc'", false, "false-and"}, - // true || - {"true || true", true, "true-or"}, - {"true || false", true, "true-or"}, - {"true || null", true, "true-or"}, - {"true || -10", true, "true-or"}, - {"true || 0", true, "true-or"}, - {"true || 10", true, "true-or"}, - {"true || 3.14", true, "true-or"}, - {"true || 0.0", true, "true-or"}, - {"true || Infinity", true, "true-or"}, - // {"true || -Infinity", true, "true-or"}, - {"true || NaN", true, "true-or"}, - {"true || ''", true, "true-or"}, - {"true || 'abc'", true, "true-or"}, - // false || - {"false || true", true, "false-or"}, - {"false || false", false, "false-or"}, - {"false || null", nil, "false-or"}, - {"false || -10", -10, "false-or"}, - {"false || 0", 0, "false-or"}, - {"false || 10", 10, "false-or"}, - {"false || 3.14", 3.14, "false-or"}, - {"false || 0.0", 0, "false-or"}, - {"false || Infinity", math.Inf(1), "false-or"}, - // {"false || -Infinity", math.Inf(-1), "false-or"}, - {"false || NaN", math.NaN(), "false-or"}, - {"false || ''", "", "false-or"}, - {"false || 'abc'", "abc", "false-or"}, - // null && - {"null && true", nil, "null-and"}, - {"null && false", nil, "null-and"}, - {"null && null", nil, "null-and"}, - {"null && -10", nil, "null-and"}, - {"null && 0", nil, "null-and"}, - {"null && 10", nil, "null-and"}, - {"null && 3.14", nil, "null-and"}, - {"null && 0.0", nil, "null-and"}, - {"null && Infinity", nil, "null-and"}, - // {"null && -Infinity", nil, "null-and"}, - {"null && NaN", nil, "null-and"}, - {"null && ''", nil, "null-and"}, - {"null && 'abc'", nil, "null-and"}, - // null || - {"null || true", true, "null-or"}, - {"null || false", false, "null-or"}, - {"null || null", nil, "null-or"}, - {"null || -10", -10, "null-or"}, - {"null || 0", 0, "null-or"}, - {"null || 10", 10, "null-or"}, - {"null || 3.14", 3.14, "null-or"}, - {"null || 0.0", 0, "null-or"}, - {"null || Infinity", math.Inf(1), "null-or"}, - // {"null || -Infinity", math.Inf(-1), "null-or"}, - {"null || NaN", math.NaN(), "null-or"}, - {"null || ''", "", "null-or"}, - {"null || 'abc'", "abc", "null-or"}, - // -10 && - {"-10 && true", true, "neg-num-and"}, - {"-10 && false", false, "neg-num-and"}, - {"-10 && null", nil, "neg-num-and"}, - {"-10 && -10", -10, "neg-num-and"}, - {"-10 && 0", 0, "neg-num-and"}, - {"-10 && 10", 10, "neg-num-and"}, - {"-10 && 3.14", 3.14, "neg-num-and"}, - {"-10 && 0.0", 0, "neg-num-and"}, - {"-10 && Infinity", math.Inf(1), "neg-num-and"}, - // {"-10 && -Infinity", math.Inf(-1), "neg-num-and"}, - {"-10 && NaN", math.NaN(), "neg-num-and"}, - {"-10 && ''", "", "neg-num-and"}, - {"-10 && 'abc'", "abc", "neg-num-and"}, - // -10 || - {"-10 || true", -10, "neg-num-or"}, - {"-10 || false", -10, "neg-num-or"}, - {"-10 || null", -10, "neg-num-or"}, - {"-10 || -10", -10, "neg-num-or"}, - {"-10 || 0", -10, "neg-num-or"}, - {"-10 || 10", -10, "neg-num-or"}, - {"-10 || 3.14", -10, "neg-num-or"}, - {"-10 || 0.0", -10, "neg-num-or"}, - {"-10 || Infinity", -10, "neg-num-or"}, - // {"-10 || -Infinity", -10, "neg-num-or"}, - {"-10 || NaN", -10, "neg-num-or"}, - {"-10 || ''", -10, "neg-num-or"}, - {"-10 || 'abc'", -10, "neg-num-or"}, - // 0 && - {"0 && true", 0, "zero-and"}, - {"0 && false", 0, "zero-and"}, - {"0 && null", 0, "zero-and"}, - {"0 && -10", 0, "zero-and"}, - {"0 && 0", 0, "zero-and"}, - {"0 && 10", 0, "zero-and"}, - {"0 && 3.14", 0, "zero-and"}, - {"0 && 0.0", 0, "zero-and"}, - {"0 && Infinity", 0, "zero-and"}, - // {"0 && -Infinity", 0, "zero-and"}, - {"0 && NaN", 0, "zero-and"}, - {"0 && ''", 0, "zero-and"}, - {"0 && 'abc'", 0, "zero-and"}, - // 0 || - {"0 || true", true, "zero-or"}, - {"0 || false", false, "zero-or"}, - {"0 || null", nil, "zero-or"}, - {"0 || -10", -10, "zero-or"}, - {"0 || 0", 0, "zero-or"}, - {"0 || 10", 10, "zero-or"}, - {"0 || 3.14", 3.14, "zero-or"}, - {"0 || 0.0", 0, "zero-or"}, - {"0 || Infinity", math.Inf(1), "zero-or"}, - // {"0 || -Infinity", math.Inf(-1), "zero-or"}, - {"0 || NaN", math.NaN(), "zero-or"}, - {"0 || ''", "", "zero-or"}, - {"0 || 'abc'", "abc", "zero-or"}, - // 10 && - {"10 && true", true, "pos-num-and"}, - {"10 && false", false, "pos-num-and"}, - {"10 && null", nil, "pos-num-and"}, - {"10 && -10", -10, "pos-num-and"}, - {"10 && 0", 0, "pos-num-and"}, - {"10 && 10", 10, "pos-num-and"}, - {"10 && 3.14", 3.14, "pos-num-and"}, - {"10 && 0.0", 0, "pos-num-and"}, - {"10 && Infinity", math.Inf(1), "pos-num-and"}, - // {"10 && -Infinity", math.Inf(-1), "pos-num-and"}, - {"10 && NaN", math.NaN(), "pos-num-and"}, - {"10 && ''", "", "pos-num-and"}, - {"10 && 'abc'", "abc", "pos-num-and"}, - // 10 || - {"10 || true", 10, "pos-num-or"}, - {"10 || false", 10, "pos-num-or"}, - {"10 || null", 10, "pos-num-or"}, - {"10 || -10", 10, "pos-num-or"}, - {"10 || 0", 10, "pos-num-or"}, - {"10 || 10", 10, "pos-num-or"}, - {"10 || 3.14", 10, "pos-num-or"}, - {"10 || 0.0", 10, "pos-num-or"}, - {"10 || Infinity", 10, "pos-num-or"}, - // {"10 || -Infinity", 10, "pos-num-or"}, - {"10 || NaN", 10, "pos-num-or"}, - {"10 || ''", 10, "pos-num-or"}, - {"10 || 'abc'", 10, "pos-num-or"}, - // 3.14 && - {"3.14 && true", true, "pos-float-and"}, - {"3.14 && false", false, "pos-float-and"}, - {"3.14 && null", nil, "pos-float-and"}, - {"3.14 && -10", -10, "pos-float-and"}, - {"3.14 && 0", 0, "pos-float-and"}, - {"3.14 && 10", 10, "pos-float-and"}, - {"3.14 && 3.14", 3.14, "pos-float-and"}, - {"3.14 && 0.0", 0, "pos-float-and"}, - {"3.14 && Infinity", math.Inf(1), "pos-float-and"}, - // {"3.14 && -Infinity", math.Inf(-1), "pos-float-and"}, - {"3.14 && NaN", math.NaN(), "pos-float-and"}, - {"3.14 && ''", "", "pos-float-and"}, - {"3.14 && 'abc'", "abc", "pos-float-and"}, - // 3.14 || - {"3.14 || true", 3.14, "pos-float-or"}, - {"3.14 || false", 3.14, "pos-float-or"}, - {"3.14 || null", 3.14, "pos-float-or"}, - {"3.14 || -10", 3.14, "pos-float-or"}, - {"3.14 || 0", 3.14, "pos-float-or"}, - {"3.14 || 10", 3.14, "pos-float-or"}, - {"3.14 || 3.14", 3.14, "pos-float-or"}, - {"3.14 || 0.0", 3.14, "pos-float-or"}, - {"3.14 || Infinity", 3.14, "pos-float-or"}, - // {"3.14 || -Infinity", 3.14, "pos-float-or"}, - {"3.14 || NaN", 3.14, "pos-float-or"}, - {"3.14 || ''", 3.14, "pos-float-or"}, - {"3.14 || 'abc'", 3.14, "pos-float-or"}, - // Infinity && - {"Infinity && true", true, "pos-inf-and"}, - {"Infinity && false", false, "pos-inf-and"}, - {"Infinity && null", nil, "pos-inf-and"}, - {"Infinity && -10", -10, "pos-inf-and"}, - {"Infinity && 0", 0, "pos-inf-and"}, - {"Infinity && 10", 10, "pos-inf-and"}, - {"Infinity && 3.14", 3.14, "pos-inf-and"}, - {"Infinity && 0.0", 0, "pos-inf-and"}, - {"Infinity && Infinity", math.Inf(1), "pos-inf-and"}, - // {"Infinity && -Infinity", math.Inf(-1), "pos-inf-and"}, - {"Infinity && NaN", math.NaN(), "pos-inf-and"}, - {"Infinity && ''", "", "pos-inf-and"}, - {"Infinity && 'abc'", "abc", "pos-inf-and"}, - // Infinity || - {"Infinity || true", math.Inf(1), "pos-inf-or"}, - {"Infinity || false", math.Inf(1), "pos-inf-or"}, - {"Infinity || null", math.Inf(1), "pos-inf-or"}, - {"Infinity || -10", math.Inf(1), "pos-inf-or"}, - {"Infinity || 0", math.Inf(1), "pos-inf-or"}, - {"Infinity || 10", math.Inf(1), "pos-inf-or"}, - {"Infinity || 3.14", math.Inf(1), "pos-inf-or"}, - {"Infinity || 0.0", math.Inf(1), "pos-inf-or"}, - {"Infinity || Infinity", math.Inf(1), "pos-inf-or"}, - // {"Infinity || -Infinity", math.Inf(1), "pos-inf-or"}, - {"Infinity || NaN", math.Inf(1), "pos-inf-or"}, - {"Infinity || ''", math.Inf(1), "pos-inf-or"}, - {"Infinity || 'abc'", math.Inf(1), "pos-inf-or"}, - // -Infinity && - // {"-Infinity && true", true, "neg-inf-and"}, - // {"-Infinity && false", false, "neg-inf-and"}, - // {"-Infinity && null", nil, "neg-inf-and"}, - // {"-Infinity && -10", -10, "neg-inf-and"}, - // {"-Infinity && 0", 0, "neg-inf-and"}, - // {"-Infinity && 10", 10, "neg-inf-and"}, - // {"-Infinity && 3.14", 3.14, "neg-inf-and"}, - // {"-Infinity && 0.0", 0, "neg-inf-and"}, - // {"-Infinity && Infinity", math.Inf(1), "neg-inf-and"}, - // {"-Infinity && -Infinity", math.Inf(-1), "neg-inf-and"}, - // {"-Infinity && NaN", math.NaN(), "neg-inf-and"}, - // {"-Infinity && ''", "", "neg-inf-and"}, - // {"-Infinity && 'abc'", "abc", "neg-inf-and"}, - // -Infinity || - // {"-Infinity || true", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || false", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || null", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || -10", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || 0", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || 10", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || 3.14", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || 0.0", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || Infinity", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || -Infinity", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || NaN", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || ''", math.Inf(-1), "neg-inf-or"}, - // {"-Infinity || 'abc'", math.Inf(-1), "neg-inf-or"}, - // NaN && - {"NaN && true", math.NaN(), "nan-and"}, - {"NaN && false", math.NaN(), "nan-and"}, - {"NaN && null", math.NaN(), "nan-and"}, - {"NaN && -10", math.NaN(), "nan-and"}, - {"NaN && 0", math.NaN(), "nan-and"}, - {"NaN && 10", math.NaN(), "nan-and"}, - {"NaN && 3.14", math.NaN(), "nan-and"}, - {"NaN && 0.0", math.NaN(), "nan-and"}, - {"NaN && Infinity", math.NaN(), "nan-and"}, - // {"NaN && -Infinity", math.NaN(), "nan-and"}, - {"NaN && NaN", math.NaN(), "nan-and"}, - {"NaN && ''", math.NaN(), "nan-and"}, - {"NaN && 'abc'", math.NaN(), "nan-and"}, - // NaN || - {"NaN || true", true, "nan-or"}, - {"NaN || false", false, "nan-or"}, - {"NaN || null", nil, "nan-or"}, - {"NaN || -10", -10, "nan-or"}, - {"NaN || 0", 0, "nan-or"}, - {"NaN || 10", 10, "nan-or"}, - {"NaN || 3.14", 3.14, "nan-or"}, - {"NaN || 0.0", 0, "nan-or"}, - {"NaN || Infinity", math.Inf(1), "nan-or"}, - // {"NaN || -Infinity", math.Inf(-1), "nan-or"}, - {"NaN || NaN", math.NaN(), "nan-or"}, - {"NaN || ''", "", "nan-or"}, - {"NaN || 'abc'", "abc", "nan-or"}, - // "" && - {"'' && true", "", "empty-str-and"}, - {"'' && false", "", "empty-str-and"}, - {"'' && null", "", "empty-str-and"}, - {"'' && -10", "", "empty-str-and"}, - {"'' && 0", "", "empty-str-and"}, - {"'' && 10", "", "empty-str-and"}, - {"'' && 3.14", "", "empty-str-and"}, - {"'' && 0.0", "", "empty-str-and"}, - {"'' && Infinity", "", "empty-str-and"}, - // {"'' && -Infinity", "", "empty-str-and"}, - {"'' && NaN", "", "empty-str-and"}, - {"'' && ''", "", "empty-str-and"}, - {"'' && 'abc'", "", "empty-str-and"}, - // "" || - {"'' || true", true, "empty-str-or"}, - {"'' || false", false, "empty-str-or"}, - {"'' || null", nil, "empty-str-or"}, - {"'' || -10", -10, "empty-str-or"}, - {"'' || 0", 0, "empty-str-or"}, - {"'' || 10", 10, "empty-str-or"}, - {"'' || 3.14", 3.14, "empty-str-or"}, - {"'' || 0.0", 0, "empty-str-or"}, - {"'' || Infinity", math.Inf(1), "empty-str-or"}, - // {"'' || -Infinity", math.Inf(-1), "empty-str-or"}, - {"'' || NaN", math.NaN(), "empty-str-or"}, - {"'' || ''", "", "empty-str-or"}, - {"'' || 'abc'", "abc", "empty-str-or"}, - // "abc" && - {"'abc' && true", true, "str-and"}, - {"'abc' && false", false, "str-and"}, - {"'abc' && null", nil, "str-and"}, - {"'abc' && -10", -10, "str-and"}, - {"'abc' && 0", 0, "str-and"}, - {"'abc' && 10", 10, "str-and"}, - {"'abc' && 3.14", 3.14, "str-and"}, - {"'abc' && 0.0", 0, "str-and"}, - {"'abc' && Infinity", math.Inf(1), "str-and"}, - // {"'abc' && -Infinity", math.Inf(-1), "str-and"}, - {"'abc' && NaN", math.NaN(), "str-and"}, - {"'abc' && ''", "", "str-and"}, - {"'abc' && 'abc'", "abc", "str-and"}, - // "abc" || - {"'abc' || true", "abc", "str-or"}, - {"'abc' || false", "abc", "str-or"}, - {"'abc' || null", "abc", "str-or"}, - {"'abc' || -10", "abc", "str-or"}, - {"'abc' || 0", "abc", "str-or"}, - {"'abc' || 10", "abc", "str-or"}, - {"'abc' || 3.14", "abc", "str-or"}, - {"'abc' || 0.0", "abc", "str-or"}, - {"'abc' || Infinity", "abc", "str-or"}, - // {"'abc' || -Infinity", "abc", "str-or"}, - {"'abc' || NaN", "abc", "str-or"}, - {"'abc' || ''", "abc", "str-or"}, - {"'abc' || 'abc'", "abc", "str-or"}, - // extra tests - {"0.0 && true", 0, "float-evaluation-0-alt"}, - {"-1.5 && true", true, "float-evaluation-neg-alt"}, - } - - env := &EvaluationEnvironment{ - Github: &model.GithubContext{ - Action: "push", - }, - } - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) { - number, ok := output.(float64) - require.True(t, ok, "want a number, got %T", output) - assert.True(t, math.IsNaN(number)) - } else { - assert.Equal(t, tt.expected, output) - } - }) - } -} - -func TestContexts(t *testing.T) { - table := []struct { - input string - expected any - name string - }{ - {"github.action", "push", "github-context"}, - {"github.event.commits[0].message", nil, "github-context-noexist-prop"}, - {"fromjson('{\"commits\":[]}').commits[0].message", nil, "github-context-noexist-prop"}, - {"github.event.pull_request.labels.*.name", nil, "github-context-noexist-prop"}, - {"env.TEST", "value", "env-context"}, - {"job.status", "success", "job-context"}, - {"steps.step-id.outputs.name", "value", "steps-context"}, - {"steps.step-id.conclusion", "success", "steps-context-conclusion"}, - {"steps.step-id.conclusion && true", true, "steps-context-conclusion"}, - {"steps.step-id2.conclusion", "skipped", "steps-context-conclusion"}, - {"steps.step-id2.conclusion && true", true, "steps-context-conclusion"}, - {"steps.step-id.outcome", "success", "steps-context-outcome"}, - {"steps.step-id['outcome']", "success", "steps-context-outcome"}, - {"steps.step-id.outcome == 'success'", true, "steps-context-outcome"}, - {"steps.step-id['outcome'] == 'success'", true, "steps-context-outcome"}, - {"steps.step-id.outcome && true", true, "steps-context-outcome"}, - {"steps['step-id']['outcome'] && true", true, "steps-context-outcome"}, - {"steps.step-id2.outcome", "failure", "steps-context-outcome"}, - {"steps.step-id2.outcome && true", true, "steps-context-outcome"}, - // Disabled, since the interpreter is still too broken - // {"contains(steps.*.outcome, 'success')", true, "steps-context-array-outcome"}, - // {"contains(steps.*.outcome, 'failure')", true, "steps-context-array-outcome"}, - // {"contains(steps.*.outputs.name, 'value')", true, "steps-context-array-outputs"}, - {"runner.os", "Linux", "runner-context"}, - {"secrets.name", "value", "secrets-context"}, - {"vars.name", "value", "vars-context"}, - {"strategy.fail-fast", true, "strategy-context"}, - {"matrix.os", "Linux", "matrix-context"}, - {"needs.job-id.outputs.output-name", "value", "needs-context"}, - {"needs.job-id.result", "success", "needs-context"}, - {"inputs.name", "value", "inputs-context"}, - } - - env := &EvaluationEnvironment{ - Github: &model.GithubContext{ - Action: "push", - }, - Env: map[string]string{ - "TEST": "value", - }, - Job: &model.JobContext{ - Status: "success", - }, - Steps: map[string]*model.StepResult{ - "step-id": { - Outputs: map[string]string{ - "name": "value", - }, - }, - "step-id2": { - Outcome: model.StepStatusFailure, - Conclusion: model.StepStatusSkipped, - }, - }, - Runner: map[string]any{ - "os": "Linux", - "temp": "/tmp", - "tool_cache": "/opt/hostedtoolcache", - }, - Secrets: map[string]string{ - "name": "value", - }, - Vars: map[string]string{ - "name": "value", - }, - Strategy: map[string]any{ - "fail-fast": true, - }, - Matrix: map[string]any{ - "os": "Linux", - }, - Needs: map[string]Needs{ - "job-id": { - Outputs: map[string]string{ - "output-name": "value", - }, - Result: "success", - }, - }, - Inputs: map[string]any{ - "name": "value", - }, - } - - for _, tt := range table { - t.Run(tt.name, func(t *testing.T) { - output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, tt.expected, output) - }) - } -} diff --git a/act/exprparser/testdata/for-hashing-1.txt b/act/exprparser/testdata/for-hashing-1.txt deleted file mode 100644 index e965047a..00000000 --- a/act/exprparser/testdata/for-hashing-1.txt +++ /dev/null @@ -1 +0,0 @@ -Hello diff --git a/act/exprparser/testdata/for-hashing-2.txt b/act/exprparser/testdata/for-hashing-2.txt deleted file mode 100644 index 496c8755..00000000 --- a/act/exprparser/testdata/for-hashing-2.txt +++ /dev/null @@ -1 +0,0 @@ -World! diff --git a/act/exprparser/testdata/for-hashing-3/data.txt b/act/exprparser/testdata/for-hashing-3/data.txt deleted file mode 100644 index 5ac7bf9b..00000000 --- a/act/exprparser/testdata/for-hashing-3/data.txt +++ /dev/null @@ -1 +0,0 @@ -Knock knock! diff --git a/act/exprparser/testdata/for-hashing-3/nested/nested-data.txt b/act/exprparser/testdata/for-hashing-3/nested/nested-data.txt deleted file mode 100644 index ebe288b2..00000000 --- a/act/exprparser/testdata/for-hashing-3/nested/nested-data.txt +++ /dev/null @@ -1 +0,0 @@ -Anybody home? diff --git a/act/ghcontext/github_context.go b/act/ghcontext/github_context.go new file mode 100644 index 00000000..32a64e37 --- /dev/null +++ b/act/ghcontext/github_context.go @@ -0,0 +1,138 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2021 The nektos/act Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Package ghcontext fills a model.GithubContext from the local git checkout. +// The pure data parts of the context live in the shared +// gitea.dev/actions-proto-go/pkg/model package, only the helpers that need a +// git repository on disk are kept here. +package ghcontext + +import ( + "context" + "fmt" + "strings" + + "gitea.com/gitea/runner/act/common" + "gitea.com/gitea/runner/act/common/git" + + "gitea.dev/actions-proto-go/pkg/model" +) + +var ( + findGitRef = git.FindGitRef + findGitRevision = git.FindGitRevision + findGithubRepo = git.FindGithubRepo +) + +func withDefaultBranch(ctx context.Context, b string, event map[string]any) map[string]any { + repoI, ok := event["repository"] + if !ok { + repoI = make(map[string]any) + } + + repo, ok := repoI.(map[string]any) + if !ok { + common.Logger(ctx).Warnf("unable to set default branch to %v", b) + return event + } + + // if the branch is already there return with no changes + if _, ok = repo["default_branch"]; ok { + return event + } + + repo["default_branch"] = b + event["repository"] = repo + + return event +} + +// SetRef resolves the ref of the context from its event payload, falling back +// to the ref checked out in repoPath. +func SetRef(ctx context.Context, ghc *model.GithubContext, defaultBranch, repoPath string) { + logger := common.Logger(ctx) + + // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows + // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads + switch ghc.EventName { + case "pull_request_target": + ghc.Ref = "refs/heads/" + ghc.BaseRef + case "pull_request", "pull_request_review", "pull_request_review_comment": + ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"]) + case "deployment", "deployment_status": + ghc.Ref = model.AsString(model.NestedMapLookup(ghc.Event, "deployment", "ref")) + case "release": + ghc.Ref = "refs/tags/" + model.AsString(model.NestedMapLookup(ghc.Event, "release", "tag_name")) + case "push", "create", "workflow_dispatch": + ghc.Ref = model.AsString(ghc.Event["ref"]) + default: + defaultBranch := model.AsString(model.NestedMapLookup(ghc.Event, "repository", "default_branch")) + if defaultBranch != "" { + ghc.Ref = "refs/heads/" + defaultBranch + } + } + + if ghc.Ref == "" { + ref, err := findGitRef(ctx, repoPath) + if err != nil { + logger.Warningf("unable to get git ref: %v", err) + } else { + logger.Debugf("using github ref: %s", ref) + ghc.Ref = ref + } + + // set the branch in the event data + if defaultBranch != "" { + ghc.Event = withDefaultBranch(ctx, defaultBranch, ghc.Event) + } else { + ghc.Event = withDefaultBranch(ctx, "master", ghc.Event) + } + + if ghc.Ref == "" { + ghc.Ref = "refs/heads/" + model.AsString(model.NestedMapLookup(ghc.Event, "repository", "default_branch")) + } + } +} + +// SetSha resolves the commit of the context from its event payload, falling +// back to the revision checked out in repoPath. +func SetSha(ctx context.Context, ghc *model.GithubContext, repoPath string) { + logger := common.Logger(ctx) + + // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows + // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads + switch ghc.EventName { + case "pull_request_target": + ghc.Sha = model.AsString(model.NestedMapLookup(ghc.Event, "pull_request", "base", "sha")) + case "deployment", "deployment_status": + ghc.Sha = model.AsString(model.NestedMapLookup(ghc.Event, "deployment", "sha")) + case "push", "create", "workflow_dispatch": + if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted { + ghc.Sha = model.AsString(ghc.Event["after"]) + } + } + + if ghc.Sha == "" { + _, sha, err := findGitRevision(ctx, repoPath) + if err != nil { + logger.Warningf("unable to get git revision: %v", err) + } else { + ghc.Sha = sha + } + } +} + +// SetRepositoryAndOwner resolves the repository of the context from the git +// remote in repoPath when it is not set yet, and derives its owner. +func SetRepositoryAndOwner(ctx context.Context, ghc *model.GithubContext, githubInstance, remoteName, repoPath string) { + if ghc.Repository == "" { + repo, err := findGithubRepo(ctx, repoPath, githubInstance, remoteName) + if err != nil { + common.Logger(ctx).Warningf("unable to get git repo (githubInstance: %v; remoteName: %v, repoPath: %v): %v", githubInstance, remoteName, repoPath, err) + return + } + ghc.Repository = repo + } + ghc.RepositoryOwner = strings.Split(ghc.Repository, "/")[0] +} diff --git a/act/model/github_context_test.go b/act/ghcontext/github_context_test.go similarity index 93% rename from act/model/github_context_test.go rename to act/ghcontext/github_context_test.go index f0c7cba5..445a7a26 100644 --- a/act/model/github_context_test.go +++ b/act/ghcontext/github_context_test.go @@ -2,13 +2,14 @@ // Copyright 2022 The nektos/act Authors. All rights reserved. // SPDX-License-Identifier: MIT -package model +package ghcontext import ( "context" "errors" "testing" + "gitea.dev/actions-proto-go/pkg/model" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) @@ -97,13 +98,13 @@ func TestSetRef(t *testing.T) { for _, table := range tables { t.Run(table.eventName, func(t *testing.T) { - ghc := &GithubContext{ + ghc := &model.GithubContext{ EventName: table.eventName, BaseRef: "master", Event: table.event, } - ghc.SetRef(context.Background(), "main", "/some/dir") + SetRef(context.Background(), ghc, "main", "/some/dir") ghc.SetRefTypeAndName() assert.Equal(t, table.ref, ghc.Ref) @@ -116,12 +117,12 @@ func TestSetRef(t *testing.T) { return "", errors.New("no default branch") } - ghc := &GithubContext{ + ghc := &model.GithubContext{ EventName: "no-default-branch", Event: map[string]any{}, } - ghc.SetRef(context.Background(), "", "/some/dir") + SetRef(context.Background(), ghc, "", "/some/dir") assert.Equal(t, "refs/heads/master", ghc.Ref) }) @@ -202,13 +203,13 @@ func TestSetSha(t *testing.T) { for _, table := range tables { t.Run(table.eventName, func(t *testing.T) { - ghc := &GithubContext{ + ghc := &model.GithubContext{ EventName: table.eventName, BaseRef: "master", Event: table.event, } - ghc.SetSha(context.Background(), "/some/dir") + SetSha(context.Background(), ghc, "/some/dir") assert.Equal(t, table.sha, ghc.Sha) }) diff --git a/act/model/action.go b/act/model/action.go deleted file mode 100644 index 7e00e0fd..00000000 --- a/act/model/action.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// Copyright 2020 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -import ( - "fmt" - "io" - "strings" - - "go.yaml.in/yaml/v4" -) - -// ActionRunsUsing is the type of runner for the action -type ActionRunsUsing string - -func (a *ActionRunsUsing) UnmarshalYAML(unmarshal func(any) error) error { - var using string - if err := unmarshal(&using); err != nil { - return err - } - - // Force input to lowercase for case insensitive comparison - format := ActionRunsUsing(strings.ToLower(using)) - switch format { - case ActionRunsUsingNode24, ActionRunsUsingNode20, ActionRunsUsingNode16, ActionRunsUsingNode12, ActionRunsUsingDocker, ActionRunsUsingComposite, ActionRunsUsingGo: - *a = format - default: - return fmt.Errorf("The runs.using key in action.yml must be one of: %v, got %s", []string{ - ActionRunsUsingComposite, - ActionRunsUsingDocker, - ActionRunsUsingNode12, - ActionRunsUsingNode16, - ActionRunsUsingNode20, - ActionRunsUsingNode24, - ActionRunsUsingGo, - }, format) - } - return nil -} - -const ( - // ActionRunsUsingNode12 for running with node12 - ActionRunsUsingNode12 = "node12" - // ActionRunsUsingNode16 for running with node16 - ActionRunsUsingNode16 = "node16" - // ActionRunsUsingNode20 for running with node20 - ActionRunsUsingNode20 = "node20" - // ActionRunsUsingNode24 for running with node24 - ActionRunsUsingNode24 = "node24" - // ActionRunsUsingDocker for running with docker - ActionRunsUsingDocker = "docker" - // ActionRunsUsingComposite for running composite - ActionRunsUsingComposite = "composite" - // ActionRunsUsingGo for running with go - ActionRunsUsingGo = "go" -) - -func (a ActionRunsUsing) IsNode() bool { - switch a { - case ActionRunsUsingNode12, ActionRunsUsingNode16, ActionRunsUsingNode20, ActionRunsUsingNode24: - return true - default: - return false - } -} - -func (a ActionRunsUsing) IsDocker() bool { - return a == ActionRunsUsingDocker -} - -func (a ActionRunsUsing) IsComposite() bool { - return a == ActionRunsUsingComposite -} - -// ActionRuns are a field in Action -type ActionRuns struct { - Using ActionRunsUsing `yaml:"using"` - Env map[string]string `yaml:"env"` - Main string `yaml:"main"` - Pre string `yaml:"pre"` - PreIf string `yaml:"pre-if"` - Post string `yaml:"post"` - PostIf string `yaml:"post-if"` - Image string `yaml:"image"` - PreEntrypoint string `yaml:"pre-entrypoint"` - Entrypoint string `yaml:"entrypoint"` - PostEntrypoint string `yaml:"post-entrypoint"` - Args []string `yaml:"args"` - Steps []Step `yaml:"steps"` -} - -// Action describes a metadata file for GitHub actions. The metadata filename must be either action.yml or action.yaml. The data in the metadata file defines the inputs, outputs and main entrypoint for your action. -type Action struct { - Name string `yaml:"name"` - Author string `yaml:"author"` - Description string `yaml:"description"` - Inputs map[string]Input `yaml:"inputs"` - Outputs map[string]Output `yaml:"outputs"` - Runs ActionRuns `yaml:"runs"` - Branding struct { - Color string `yaml:"color"` - Icon string `yaml:"icon"` - } `yaml:"branding"` -} - -// Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables. Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids. -type Input struct { - Description string `yaml:"description"` - Required bool `yaml:"required"` - Default string `yaml:"default"` -} - -// Output parameters allow you to declare data that an action sets. Actions that run later in a workflow can use the output data set in previously run actions. For example, if you had an action that performed the addition of two inputs (x + y = z), the action could output the sum (z) for other actions to use as an input. -type Output struct { - Description string `yaml:"description"` - Value string `yaml:"value"` -} - -// ReadAction reads an action from a reader -func ReadAction(in io.Reader) (*Action, error) { - a := new(Action) - err := yaml.NewDecoder(in).Decode(a) - if err != nil { - return nil, err - } - - // set defaults - if a.Runs.PreIf == "" { - a.Runs.PreIf = "always()" - } - if a.Runs.PostIf == "" { - a.Runs.PostIf = "always()" - } - - return a, nil -} diff --git a/act/model/action_test.go b/act/model/action_test.go deleted file mode 100644 index 96caa768..00000000 --- a/act/model/action_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2026 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -import ( - "strings" - "testing" -) - -func TestReadActionDefaultsAndCaseInsensitiveUsing(t *testing.T) { - action, err := ReadAction(strings.NewReader(` -name: example -runs: - using: NoDe24 - main: dist/index.js -`)) - if err != nil { - t.Fatal(err) - } - if action.Runs.Using != ActionRunsUsingNode24 { - t.Fatalf("using = %q, want %q", action.Runs.Using, ActionRunsUsingNode24) - } - if action.Runs.PreIf != "always()" { - t.Fatalf("pre-if = %q, want always()", action.Runs.PreIf) - } - if action.Runs.PostIf != "always()" { - t.Fatalf("post-if = %q, want always()", action.Runs.PostIf) - } -} - -func TestReadActionPreservesExplicitConditions(t *testing.T) { - action, err := ReadAction(strings.NewReader(` -runs: - using: composite - pre-if: success() - post-if: failure() - steps: - - run: echo hello -`)) - if err != nil { - t.Fatal(err) - } - if action.Runs.PreIf != "success()" || action.Runs.PostIf != "failure()" { - t.Fatalf("conditions = %q/%q, want explicit values", action.Runs.PreIf, action.Runs.PostIf) - } - if !action.Runs.Using.IsComposite() || action.Runs.Using.IsDocker() || action.Runs.Using.IsNode() { - t.Fatalf("unexpected using predicates for %q", action.Runs.Using) - } -} - -func TestReadActionRejectsUnknownUsing(t *testing.T) { - _, err := ReadAction(strings.NewReader(` -runs: - using: node99 -`)) - if err == nil { - t.Fatal("expected unknown runs.using to fail") - } - if !strings.Contains(err.Error(), "node99") { - t.Fatalf("error = %q, want invalid value", err) - } -} - -func TestReadActionDockerEntrypoints(t *testing.T) { - action, err := ReadAction(strings.NewReader(` -runs: - using: docker - image: Dockerfile - pre-entrypoint: pre.sh - post-entrypoint: post.sh -`)) - if err != nil { - t.Fatal(err) - } - if action.Runs.PreEntrypoint != "pre.sh" { - t.Fatalf("pre-entrypoint = %q, want pre.sh", action.Runs.PreEntrypoint) - } - if action.Runs.PostEntrypoint != "post.sh" { - t.Fatalf("post-entrypoint = %q, want post.sh", action.Runs.PostEntrypoint) - } -} diff --git a/act/model/github_context.go b/act/model/github_context.go deleted file mode 100644 index c70fb240..00000000 --- a/act/model/github_context.go +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2021 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -import ( - "context" - "fmt" - "strings" - - "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/common/git" -) - -type GithubContext struct { - Event map[string]any `json:"event"` - EventPath string `json:"event_path"` - Workflow string `json:"workflow"` - RunID string `json:"run_id"` - RunNumber string `json:"run_number"` - Actor string `json:"actor"` - Repository string `json:"repository"` - EventName string `json:"event_name"` - Sha string `json:"sha"` - Ref string `json:"ref"` - RefName string `json:"ref_name"` - RefType string `json:"ref_type"` - HeadRef string `json:"head_ref"` - BaseRef string `json:"base_ref"` - Token string `json:"token"` - Workspace string `json:"workspace"` - Action string `json:"action"` - ActionPath string `json:"action_path"` - ActionRef string `json:"action_ref"` - ActionRepository string `json:"action_repository"` - Job string `json:"job"` - JobName string `json:"job_name"` - RepositoryOwner string `json:"repository_owner"` - RetentionDays string `json:"retention_days"` - RunnerPerflog string `json:"runner_perflog"` - RunnerTrackingID string `json:"runner_tracking_id"` - ServerURL string `json:"server_url"` - APIURL string `json:"api_url"` - GraphQLURL string `json:"graphql_url"` - - // For Gitea - RunAttempt string `json:"run_attempt"` -} - -func asString(v any) string { - if v == nil { - return "" - } else if s, ok := v.(string); ok { - return s - } - return "" -} - -func nestedMapLookup(m map[string]any, ks ...string) (rval any) { - var ok bool - - if len(ks) == 0 { // degenerate input - return nil - } - if rval, ok = m[ks[0]]; !ok { - return nil - } else if len(ks) == 1 { // we've reached the final key - return rval - } else if m, ok = rval.(map[string]any); !ok { - return nil - } else { // 1+ more keys - return nestedMapLookup(m, ks[1:]...) - } -} - -func withDefaultBranch(ctx context.Context, b string, event map[string]any) map[string]any { - repoI, ok := event["repository"] - if !ok { - repoI = make(map[string]any) - } - - repo, ok := repoI.(map[string]any) - if !ok { - common.Logger(ctx).Warnf("unable to set default branch to %v", b) - return event - } - - // if the branch is already there return with no changes - if _, ok = repo["default_branch"]; ok { - return event - } - - repo["default_branch"] = b - event["repository"] = repo - - return event -} - -var ( - findGitRef = git.FindGitRef - findGitRevision = git.FindGitRevision -) - -func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch, repoPath string) { - logger := common.Logger(ctx) - - // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows - // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads - switch ghc.EventName { - case "pull_request_target": - ghc.Ref = "refs/heads/" + ghc.BaseRef - case "pull_request", "pull_request_review", "pull_request_review_comment": - ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"]) - case "deployment", "deployment_status": - ghc.Ref = asString(nestedMapLookup(ghc.Event, "deployment", "ref")) - case "release": - ghc.Ref = "refs/tags/" + asString(nestedMapLookup(ghc.Event, "release", "tag_name")) - case "push", "create", "workflow_dispatch": - ghc.Ref = asString(ghc.Event["ref"]) - default: - defaultBranch := asString(nestedMapLookup(ghc.Event, "repository", "default_branch")) - if defaultBranch != "" { - ghc.Ref = "refs/heads/" + defaultBranch - } - } - - if ghc.Ref == "" { - ref, err := findGitRef(ctx, repoPath) - if err != nil { - logger.Warningf("unable to get git ref: %v", err) - } else { - logger.Debugf("using github ref: %s", ref) - ghc.Ref = ref - } - - // set the branch in the event data - if defaultBranch != "" { - ghc.Event = withDefaultBranch(ctx, defaultBranch, ghc.Event) - } else { - ghc.Event = withDefaultBranch(ctx, "master", ghc.Event) - } - - if ghc.Ref == "" { - ghc.Ref = "refs/heads/" + asString(nestedMapLookup(ghc.Event, "repository", "default_branch")) - } - } -} - -func (ghc *GithubContext) SetSha(ctx context.Context, repoPath string) { - logger := common.Logger(ctx) - - // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows - // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads - switch ghc.EventName { - case "pull_request_target": - ghc.Sha = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "sha")) - case "deployment", "deployment_status": - ghc.Sha = asString(nestedMapLookup(ghc.Event, "deployment", "sha")) - case "push", "create", "workflow_dispatch": - if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted { - ghc.Sha = asString(ghc.Event["after"]) - } - } - - if ghc.Sha == "" { - _, sha, err := findGitRevision(ctx, repoPath) - if err != nil { - logger.Warningf("unable to get git revision: %v", err) - } else { - ghc.Sha = sha - } - } -} - -func (ghc *GithubContext) SetRepositoryAndOwner(ctx context.Context, githubInstance, remoteName, repoPath string) { - if ghc.Repository == "" { - repo, err := git.FindGithubRepo(ctx, repoPath, githubInstance, remoteName) - if err != nil { - common.Logger(ctx).Warningf("unable to get git repo (githubInstance: %v; remoteName: %v, repoPath: %v): %v", githubInstance, remoteName, repoPath, err) - return - } - ghc.Repository = repo - } - ghc.RepositoryOwner = strings.Split(ghc.Repository, "/")[0] -} - -func (ghc *GithubContext) SetRefTypeAndName() { - var refType, refName string - - // https://docs.github.com/en/actions/learn-github-actions/environment-variables - if strings.HasPrefix(ghc.Ref, "refs/tags/") { - refType = "tag" - refName = ghc.Ref[len("refs/tags/"):] - } else if strings.HasPrefix(ghc.Ref, "refs/heads/") { - refType = "branch" - refName = ghc.Ref[len("refs/heads/"):] - } else if strings.HasPrefix(ghc.Ref, "refs/pull/") { - refType = "" - refName = ghc.Ref[len("refs/pull/"):] - } - - if ghc.RefType == "" { - ghc.RefType = refType - } - - if ghc.RefName == "" { - ghc.RefName = refName - } -} - -func (ghc *GithubContext) SetBaseAndHeadRef() { - if ghc.EventName == "pull_request" || ghc.EventName == "pull_request_target" { - if ghc.BaseRef == "" { - ghc.BaseRef = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "ref")) - } - - if ghc.HeadRef == "" { - ghc.HeadRef = asString(nestedMapLookup(ghc.Event, "pull_request", "head", "ref")) - } - } -} diff --git a/act/model/job_context.go b/act/model/job_context.go deleted file mode 100644 index d5647803..00000000 --- a/act/model/job_context.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// Copyright 2021 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -type JobContext struct { - Status string `json:"status"` - Container struct { - ID string `json:"id"` - Network string `json:"network"` - } `json:"container"` - Services map[string]struct { - ID string `json:"id"` - } `json:"services"` -} diff --git a/act/model/planner.go b/act/model/planner.go deleted file mode 100644 index e09be245..00000000 --- a/act/model/planner.go +++ /dev/null @@ -1,410 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2020 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -import ( - "errors" - "fmt" - "io" - "io/fs" - "math" - "os" - "path/filepath" - "regexp" - "slices" - - log "github.com/sirupsen/logrus" -) - -// WorkflowPlanner contains methods for creating plans -type WorkflowPlanner interface { - PlanEvent(eventName string) (*Plan, error) - PlanJob(jobName string) (*Plan, error) - PlanAll() (*Plan, error) - GetEvents() []string -} - -// Plan contains a list of stages to run in series -type Plan struct { - Stages []*Stage -} - -// Stage contains a list of runs to execute in parallel -type Stage struct { - Runs []*Run -} - -// Run represents a job from a workflow that needs to be run -type Run struct { - Workflow *Workflow - JobID string -} - -func (r *Run) String() string { - jobName := r.Job().Name - if jobName == "" { - jobName = r.JobID - } - return jobName -} - -// Job returns the job for this Run -func (r *Run) Job() *Job { - return r.Workflow.GetJob(r.JobID) -} - -type WorkflowFiles struct { - workflowDirEntry os.DirEntry - dirPath string -} - -// NewWorkflowPlanner will load a specific workflow, all workflows from a directory or all workflows from a directory and its subdirectories -func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, error) { - path, err := filepath.Abs(path) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - return nil, err - } - - var workflows []WorkflowFiles - - if fi.IsDir() { - log.Debugf("Loading workflows from '%s'", path) - if noWorkflowRecurse { - files, err := os.ReadDir(path) - if err != nil { - return nil, err - } - - for _, v := range files { - workflows = append(workflows, WorkflowFiles{ - dirPath: path, - workflowDirEntry: v, - }) - } - } else { - log.Debug("Loading workflows recursively") - if err := filepath.Walk(path, - func(p string, f os.FileInfo, err error) error { - if err != nil { - return err - } - - if !f.IsDir() { - log.Debugf("Found workflow '%s' in '%s'", f.Name(), p) - workflows = append(workflows, WorkflowFiles{ - dirPath: filepath.Dir(p), - workflowDirEntry: fs.FileInfoToDirEntry(f), - }) - } - - return nil - }); err != nil { - return nil, err - } - } - } else { - log.Debugf("Loading workflow '%s'", path) - dirname := filepath.Dir(path) - - workflows = append(workflows, WorkflowFiles{ - dirPath: dirname, - workflowDirEntry: fs.FileInfoToDirEntry(fi), - }) - } - - wp := new(workflowPlanner) - for _, wf := range workflows { - ext := filepath.Ext(wf.workflowDirEntry.Name()) - if ext == ".yml" || ext == ".yaml" { - f, err := os.Open(filepath.Join(wf.dirPath, wf.workflowDirEntry.Name())) - if err != nil { - return nil, err - } - - log.Debugf("Reading workflow '%s'", f.Name()) - workflow, err := ReadWorkflow(f) - if err != nil { - _ = f.Close() - if err == io.EOF { - return nil, fmt.Errorf("unable to read workflow '%s': file is empty: %w", wf.workflowDirEntry.Name(), err) - } - return nil, fmt.Errorf("workflow is not valid. '%s': %w", wf.workflowDirEntry.Name(), err) - } - _, err = f.Seek(0, 0) - if err != nil { - _ = f.Close() - return nil, fmt.Errorf("error occurring when resetting io pointer in '%s': %w", wf.workflowDirEntry.Name(), err) - } - - workflow.File = wf.workflowDirEntry.Name() - if workflow.Name == "" { - workflow.Name = wf.workflowDirEntry.Name() - } - - err = validateJobName(workflow) - if err != nil { - _ = f.Close() - return nil, err - } - - wp.workflows = append(wp.workflows, workflow) - _ = f.Close() - } - } - - return wp, nil -} - -// CombineWorkflowPlanner combines workflows to a WorkflowPlanner -func CombineWorkflowPlanner(workflows ...*Workflow) WorkflowPlanner { - return &workflowPlanner{ - workflows: workflows, - } -} - -func NewSingleWorkflowPlanner(name string, f io.Reader) (WorkflowPlanner, error) { - wp := new(workflowPlanner) - - log.Debugf("Reading workflow %s", name) - workflow, err := ReadWorkflow(f) - if err != nil { - if err == io.EOF { - return nil, fmt.Errorf("unable to read workflow '%s': file is empty: %w", name, err) - } - return nil, fmt.Errorf("workflow is not valid. '%s': %w", name, err) - } - workflow.File = name - if workflow.Name == "" { - workflow.Name = name - } - - err = validateJobName(workflow) - if err != nil { - return nil, err - } - - wp.workflows = append(wp.workflows, workflow) - - return wp, nil -} - -func validateJobName(workflow *Workflow) error { - jobNameRegex := regexp.MustCompile(`^([[:alpha:]_][[:alnum:]_\-]*)$`) - for k := range workflow.Jobs { - if ok := jobNameRegex.MatchString(k); !ok { - return fmt.Errorf("workflow is not valid. '%s': Job name '%s' is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'", workflow.Name, k) - } - } - return nil -} - -type workflowPlanner struct { - workflows []*Workflow -} - -// PlanEvent builds a new list of runs to execute in parallel for an event name -func (wp *workflowPlanner) PlanEvent(eventName string) (*Plan, error) { - plan := new(Plan) - if len(wp.workflows) == 0 { - log.Debug("no workflows found by planner") - return plan, nil - } - var lastErr error - - for _, w := range wp.workflows { - events := w.On() - if len(events) == 0 { - log.Debugf("no events found for workflow: %s", w.File) - continue - } - - for _, e := range events { - if e == eventName { - stages, err := createStages(w, w.GetJobIDs()...) - if err != nil { - log.Warn(err) - lastErr = err - } else { - plan.mergeStages(stages) - } - } - } - } - return plan, lastErr -} - -// PlanJob builds a new run to execute in parallel for a job name -func (wp *workflowPlanner) PlanJob(jobName string) (*Plan, error) { - plan := new(Plan) - if len(wp.workflows) == 0 { - log.Debugf("no jobs found for workflow: %s", jobName) - } - var lastErr error - - for _, w := range wp.workflows { - stages, err := createStages(w, jobName) - if err != nil { - log.Warn(err) - lastErr = err - } else { - plan.mergeStages(stages) - } - } - return plan, lastErr -} - -// PlanAll builds a new run to execute in parallel all -func (wp *workflowPlanner) PlanAll() (*Plan, error) { - plan := new(Plan) - if len(wp.workflows) == 0 { - log.Debug("no workflows found by planner") - return plan, nil - } - var lastErr error - - for _, w := range wp.workflows { - stages, err := createStages(w, w.GetJobIDs()...) - if err != nil { - log.Warn(err) - lastErr = err - } else { - plan.mergeStages(stages) - } - } - - return plan, lastErr -} - -// GetEvents gets all the events in the workflows file -func (wp *workflowPlanner) GetEvents() []string { - events := make([]string, 0) - for _, w := range wp.workflows { - found := false - for _, e := range events { - if slices.Contains(w.On(), e) { - found = true - } - if found { - break - } - } - - if !found { - events = append(events, w.On()...) - } - } - - // sort the list based on depth of dependencies - slices.Sort(events) - - return events -} - -// MaxRunNameLen determines the max name length of all jobs -func (p *Plan) MaxRunNameLen() int { - maxRunNameLen := 0 - for _, stage := range p.Stages { - for _, run := range stage.Runs { - runNameLen := len(run.String()) - if runNameLen > maxRunNameLen { - maxRunNameLen = runNameLen - } - } - } - return maxRunNameLen -} - -// GetJobIDs will get all the job names in the stage -func (s *Stage) GetJobIDs() []string { - names := make([]string, 0) - for _, r := range s.Runs { - names = append(names, r.JobID) - } - return names -} - -// Merge stages with existing stages in plan -func (p *Plan) mergeStages(stages []*Stage) { - newStages := make([]*Stage, int(math.Max(float64(len(p.Stages)), float64(len(stages))))) - for i := range newStages { - newStages[i] = new(Stage) - if i >= len(p.Stages) { - newStages[i].Runs = append(newStages[i].Runs, stages[i].Runs...) - } else if i >= len(stages) { - newStages[i].Runs = append(newStages[i].Runs, p.Stages[i].Runs...) - } else { - newStages[i].Runs = append(newStages[i].Runs, p.Stages[i].Runs...) - newStages[i].Runs = append(newStages[i].Runs, stages[i].Runs...) - } - } - p.Stages = newStages -} - -func createStages(w *Workflow, jobIDs ...string) ([]*Stage, error) { - // first, build a list of all the necessary jobs to run, and their dependencies - jobDependencies := make(map[string][]string) - for len(jobIDs) > 0 { - newJobIDs := make([]string, 0) - for _, jID := range jobIDs { - // make sure we haven't visited this job yet - if _, ok := jobDependencies[jID]; !ok { - if job := w.GetJob(jID); job != nil { - jobDependencies[jID] = job.Needs() - newJobIDs = append(newJobIDs, job.Needs()...) - } - } - } - jobIDs = newJobIDs - } - - // next, build an execution graph - stages := make([]*Stage, 0) - for len(jobDependencies) > 0 { - stage := new(Stage) - for jID, jDeps := range jobDependencies { - // make sure all deps are in the graph already - if listInStages(jDeps, stages...) { - stage.Runs = append(stage.Runs, &Run{ - Workflow: w, - JobID: jID, - }) - delete(jobDependencies, jID) - } - } - if len(stage.Runs) == 0 { - return nil, fmt.Errorf("unable to build dependency graph for %s (%s)", w.Name, w.File) - } - stages = append(stages, stage) - } - - if len(stages) == 0 { - return nil, errors.New("Could not find any stages to run. View the valid jobs with `act --list`. Use `act --help` to find how to filter by Job ID/Workflow/Event Name") - } - - return stages, nil -} - -// return true iff all strings in srcList exist in at least one of the stages -func listInStages(srcList []string, stages ...*Stage) bool { - for _, src := range srcList { - found := false - for _, stage := range stages { - for _, search := range stage.GetJobIDs() { - if src == search { - found = true - } - } - } - if !found { - return false - } - } - return true -} diff --git a/act/model/planner_test.go b/act/model/planner_test.go deleted file mode 100644 index 4c4f9c67..00000000 --- a/act/model/planner_test.go +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// Copyright 2021 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -import ( - "path/filepath" - "strings" - "testing" - - log "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type WorkflowPlanTest struct { - workflowPath string - errorMessage string - noWorkflowRecurse bool -} - -func TestPlanner(t *testing.T) { - log.SetLevel(log.DebugLevel) - - tables := []WorkflowPlanTest{ - {"invalid-job-name/invalid-1.yml", "workflow is not valid. 'invalid-job-name-1': Job name 'invalid-JOB-Name-v1.2.3-docker_hub' is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'", false}, - {"invalid-job-name/invalid-2.yml", "workflow is not valid. 'invalid-job-name-2': Job name '1234invalid-JOB-Name-v123-docker_hub' is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'", false}, - {"invalid-job-name/valid-1.yml", "", false}, - {"invalid-job-name/valid-2.yml", "", false}, - {"empty-workflow", "unable to read workflow 'push.yml': file is empty: EOF", false}, - {"nested", "unable to read workflow 'fail.yml': file is empty: EOF", false}, - {"nested", "", true}, - } - - workdir, err := filepath.Abs("testdata") - assert.NoError(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act - for _, table := range tables { - fullWorkflowPath := filepath.Join(workdir, table.workflowPath) - _, err = NewWorkflowPlanner(fullWorkflowPath, table.noWorkflowRecurse) - if table.errorMessage == "" { - assert.NoError(t, err, "WorkflowPlanner should exit without any error") - } else { - assert.EqualError(t, err, table.errorMessage) - } - } -} - -func TestWorkflow(t *testing.T) { - log.SetLevel(log.DebugLevel) - - workflow := Workflow{ - Jobs: map[string]*Job{ - "valid_job": { - Name: "valid_job", - }, - }, - } - - // Check that an invalid job id returns error - result, err := createStages(&workflow, "invalid_job_id") - assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Nil(t, result) - - // Check that an valid job id returns non-error - result, err = createStages(&workflow, "valid_job") - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.NotNil(t, result) -} - -func TestNewSingleWorkflowPlannerAndPlanMethods(t *testing.T) { - planner, err := NewSingleWorkflowPlanner("ci.yml", strings.NewReader(` -name: CI -on: [push, pull_request] -jobs: - build: - name: Build project - runs-on: ubuntu-latest - steps: - - run: make build - test: - needs: build - runs-on: ubuntu-latest - steps: - - run: make test -`)) - require.NoError(t, err) - - assert.Equal(t, []string{"pull_request", "push"}, planner.GetEvents()) - - eventPlan, err := planner.PlanEvent("push") - require.NoError(t, err) - require.Len(t, eventPlan.Stages, 2) - assert.Equal(t, []string{"build"}, eventPlan.Stages[0].GetJobIDs()) - assert.Equal(t, []string{"test"}, eventPlan.Stages[1].GetJobIDs()) - assert.Equal(t, len("Build project"), eventPlan.MaxRunNameLen()) - assert.Equal(t, "Build project", eventPlan.Stages[0].Runs[0].String()) - assert.Equal(t, "build", eventPlan.Stages[0].Runs[0].JobID) - assert.NotNil(t, eventPlan.Stages[0].Runs[0].Job()) - - jobPlan, err := planner.PlanJob("test") - require.NoError(t, err) - require.Len(t, jobPlan.Stages, 2) - assert.Equal(t, []string{"build"}, jobPlan.Stages[0].GetJobIDs()) - assert.Equal(t, []string{"test"}, jobPlan.Stages[1].GetJobIDs()) - - allPlan, err := planner.PlanAll() - require.NoError(t, err) - require.Len(t, allPlan.Stages, 2) - assert.Equal(t, []string{"build"}, allPlan.Stages[0].GetJobIDs()) - assert.Equal(t, []string{"test"}, allPlan.Stages[1].GetJobIDs()) -} - -func TestCombineWorkflowPlannerMergesWorkflowStages(t *testing.T) { - first := mustReadWorkflow(t, ` -name: First -on: push -jobs: - build: - runs-on: ubuntu-latest - steps: - - run: make build -`) - second := mustReadWorkflow(t, ` -name: Second -on: push -jobs: - lint: - runs-on: ubuntu-latest - steps: - - run: make lint - test: - needs: lint - runs-on: ubuntu-latest - steps: - - run: make test -`) - - planner := CombineWorkflowPlanner(first, second) - plan, err := planner.PlanEvent("push") - require.NoError(t, err) - require.Len(t, plan.Stages, 2) - assert.ElementsMatch(t, []string{"build", "lint"}, plan.Stages[0].GetJobIDs()) - assert.Equal(t, []string{"test"}, plan.Stages[1].GetJobIDs()) - - empty, err := planner.PlanEvent("schedule") - require.NoError(t, err) - assert.Empty(t, empty.Stages) -} - -func TestPlannerErrorsForMissingAndCyclicJobs(t *testing.T) { - workflow := mustReadWorkflow(t, ` -name: Cyclic -on: push -jobs: - a: - needs: b - runs-on: ubuntu-latest - steps: - - run: echo a - b: - needs: a - runs-on: ubuntu-latest - steps: - - run: echo b -`) - planner := CombineWorkflowPlanner(workflow) - - plan, err := planner.PlanJob("missing") - require.Error(t, err) - assert.Empty(t, plan.Stages) - assert.Contains(t, err.Error(), "Could not find any stages") - - plan, err = planner.PlanEvent("push") - require.Error(t, err) - assert.Empty(t, plan.Stages) - assert.Contains(t, err.Error(), "unable to build dependency graph") -} - -func TestNewSingleWorkflowPlannerErrors(t *testing.T) { - _, err := NewSingleWorkflowPlanner("empty.yml", strings.NewReader("")) - require.Error(t, err) - assert.Contains(t, err.Error(), "file is empty") - - _, err = NewSingleWorkflowPlanner("invalid.yml", strings.NewReader("jobs: [")) - require.Error(t, err) - assert.Contains(t, err.Error(), "workflow is not valid") -} - -func mustReadWorkflow(t *testing.T, content string) *Workflow { - t.Helper() - - workflow, err := ReadWorkflow(strings.NewReader(content)) - require.NoError(t, err) - if workflow.Name == "" { - workflow.Name = "workflow" - } - return workflow -} diff --git a/act/model/step_result.go b/act/model/step_result.go deleted file mode 100644 index 7f624799..00000000 --- a/act/model/step_result.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2021 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -import "fmt" - -type stepStatus int - -const ( - StepStatusSuccess stepStatus = iota - StepStatusFailure - StepStatusSkipped -) - -var stepStatusStrings = [...]string{ - "success", - "failure", - "skipped", -} - -func (s stepStatus) MarshalText() ([]byte, error) { - return []byte(s.String()), nil -} - -func (s *stepStatus) UnmarshalText(b []byte) error { - str := string(b) - for i, name := range stepStatusStrings { - if name == str { - *s = stepStatus(i) - return nil - } - } - return fmt.Errorf("invalid step status %q", str) -} - -func (s stepStatus) String() string { - if int(s) >= len(stepStatusStrings) { - return "" - } - return stepStatusStrings[s] -} - -type StepResult struct { - Outputs map[string]string `json:"outputs"` - Conclusion stepStatus `json:"conclusion"` - Outcome stepStatus `json:"outcome"` -} diff --git a/act/model/testdata/empty-workflow/push.yml b/act/model/testdata/empty-workflow/push.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/act/model/testdata/invalid-job-name/invalid-1.yml b/act/model/testdata/invalid-job-name/invalid-1.yml deleted file mode 100644 index b7129d47..00000000 --- a/act/model/testdata/invalid-job-name/invalid-1.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: invalid-job-name-1 -on: push - -jobs: - invalid-JOB-Name-v1.2.3-docker_hub: - runs-on: ubuntu-latest - steps: - - run: echo hi - valid-JOB-Name-v123-docker_hub: - runs-on: ubuntu-latest - steps: - - run: echo hi diff --git a/act/model/testdata/invalid-job-name/invalid-2.yml b/act/model/testdata/invalid-job-name/invalid-2.yml deleted file mode 100644 index 33dd31d8..00000000 --- a/act/model/testdata/invalid-job-name/invalid-2.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: invalid-job-name-2 -on: push - -jobs: - 1234invalid-JOB-Name-v123-docker_hub: - runs-on: ubuntu-latest - steps: - - run: echo hi diff --git a/act/model/testdata/invalid-job-name/valid-1.yml b/act/model/testdata/invalid-job-name/valid-1.yml deleted file mode 100644 index 42e82936..00000000 --- a/act/model/testdata/invalid-job-name/valid-1.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: valid-job-name-1 -on: push - -jobs: - valid-JOB-Name-v123-docker_hub: - runs-on: ubuntu-latest - steps: - - run: echo hi diff --git a/act/model/testdata/invalid-job-name/valid-2.yml b/act/model/testdata/invalid-job-name/valid-2.yml deleted file mode 100644 index 3e5c7bc2..00000000 --- a/act/model/testdata/invalid-job-name/valid-2.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: valid-job-name-2 -on: push - -jobs: - ___valid-JOB-Name-v123-docker_hub: - runs-on: ubuntu-latest - steps: - - run: echo hi diff --git a/act/model/testdata/nested/success.yml b/act/model/testdata/nested/success.yml deleted file mode 100644 index f65ee910..00000000 --- a/act/model/testdata/nested/success.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: Hello World Workflow -on: push - -jobs: - hello-world: - name: Hello World Job - runs-on: ubuntu-latest - steps: - - run: echo "Hello World!" diff --git a/act/model/testdata/nested/workflows/fail.yml b/act/model/testdata/nested/workflows/fail.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/act/model/testdata/strategy/push.yml b/act/model/testdata/strategy/push.yml deleted file mode 100644 index 89bea6e4..00000000 --- a/act/model/testdata/strategy/push.yml +++ /dev/null @@ -1,50 +0,0 @@ ---- -jobs: - strategy-all: - name: ${{ matrix.node-version }} | ${{ matrix.site }} | ${{ matrix.datacenter }} - runs-on: ubuntu-latest - steps: - - run: echo 'Hello!' - strategy: - fail-fast: false - matrix: - datacenter: - - site-c - - site-d - exclude: - - datacenter: site-d - node-version: 14.x - site: staging - include: - - php-version: 5.4 - - datacenter: site-a - node-version: 10.x - site: prod - - datacenter: site-b - node-version: 12.x - site: dev - node-version: [14.x, 16.x] - site: - - staging - max-parallel: 2 - strategy-no-matrix: - runs-on: ubuntu-latest - steps: - - run: echo 'Hello!' - strategy: - fail-fast: false - max-parallel: 2 - strategy-only-fail-fast: - runs-on: ubuntu-latest - steps: - - run: echo 'Hello!' - strategy: - fail-fast: false - strategy-only-max-parallel: - runs-on: ubuntu-latest - steps: - - run: echo 'Hello!' - strategy: - max-parallel: 2 -'on': - push: null diff --git a/act/model/workflow.go b/act/model/workflow.go deleted file mode 100644 index 0b4370b4..00000000 --- a/act/model/workflow.go +++ /dev/null @@ -1,910 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2020 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -import ( - "crypto/sha256" - "fmt" - "io" - "maps" - "reflect" - "regexp" - "slices" - "strconv" - "strings" - - "gitea.com/gitea/runner/act/common" - - log "github.com/sirupsen/logrus" - "go.yaml.in/yaml/v4" -) - -// Workflow is the structure of the files in .github/workflows -type Workflow struct { - File string - Name string `yaml:"name"` - RawOn yaml.Node `yaml:"on"` - Env map[string]string `yaml:"env"` - Jobs map[string]*Job `yaml:"jobs"` - Defaults Defaults `yaml:"defaults"` - RawConcurrency *RawConcurrency `yaml:"concurrency"` - RawPermissions yaml.Node `yaml:"permissions"` -} - -// On events for the workflow -func (w *Workflow) On() []string { - switch w.RawOn.Kind { - case yaml.ScalarNode: - var val string - err := w.RawOn.Decode(&val) - if err != nil { - log.Fatal(err) - } - return []string{val} - case yaml.SequenceNode: - var val []string - err := w.RawOn.Decode(&val) - if err != nil { - log.Fatal(err) - } - return val - case yaml.MappingNode: - var val map[string]any - err := w.RawOn.Decode(&val) - if err != nil { - log.Fatal(err) - } - var keys []string - for k := range val { - keys = append(keys, k) - } - return keys - } - return nil -} - -func (w *Workflow) OnEvent(event string) any { - if w.RawOn.Kind == yaml.MappingNode { - var val map[string]any - if !decodeNode(w.RawOn, &val) { - return nil - } - return val[event] - } - return nil -} - -func (w *Workflow) OnSchedule() []string { - schedules := w.OnEvent("schedule") - if schedules == nil { - return []string{} - } - - switch val := schedules.(type) { - case []any: - allSchedules := []string{} - for _, v := range val { - entry, ok := v.(map[string]any) - if !ok { - continue - } - if cron, ok := entry["cron"].(string); ok { - allSchedules = append(allSchedules, cron) - } - } - return allSchedules - default: - } - - return []string{} -} - -type WorkflowDispatchInput struct { - Description string `yaml:"description"` - Required bool `yaml:"required"` - Default string `yaml:"default"` - Type string `yaml:"type"` - Options []string `yaml:"options"` -} - -type WorkflowDispatch struct { - Inputs map[string]WorkflowDispatchInput `yaml:"inputs"` -} - -func (w *Workflow) WorkflowDispatchConfig() *WorkflowDispatch { - switch w.RawOn.Kind { - case yaml.ScalarNode: - var val string - if !decodeNode(w.RawOn, &val) { - return nil - } - if val == "workflow_dispatch" { - return &WorkflowDispatch{} - } - case yaml.SequenceNode: - var val []string - if !decodeNode(w.RawOn, &val) { - return nil - } - if slices.Contains(val, "workflow_dispatch") { - return &WorkflowDispatch{} - } - case yaml.MappingNode: - var val map[string]yaml.Node - if !decodeNode(w.RawOn, &val) { - return nil - } - - n, found := val["workflow_dispatch"] - var workflowDispatch WorkflowDispatch - if found && decodeNode(n, &workflowDispatch) { - return &workflowDispatch - } - default: - return nil - } - return nil -} - -type WorkflowCallInput struct { - Description string `yaml:"description"` - Required bool `yaml:"required"` - Default string `yaml:"default"` - Type string `yaml:"type"` -} - -type WorkflowCallOutput struct { - Description string `yaml:"description"` - Value string `yaml:"value"` -} - -type WorkflowCall struct { - Inputs map[string]WorkflowCallInput `yaml:"inputs"` - Outputs map[string]WorkflowCallOutput `yaml:"outputs"` -} - -type WorkflowCallResult struct { - Outputs map[string]string -} - -func (w *Workflow) WorkflowCallConfig() *WorkflowCall { - if w.RawOn.Kind != yaml.MappingNode { - // The callers expect for "on: workflow_call" and "on: [ workflow_call ]" a non nil return value - return &WorkflowCall{} - } - - var val map[string]yaml.Node - if !decodeNode(w.RawOn, &val) { - return &WorkflowCall{} - } - - var config WorkflowCall - node := val["workflow_call"] - if !decodeNode(node, &config) { - return &WorkflowCall{} - } - - return &config -} - -// Job is the structure of one job in a workflow -type Job struct { - Name string `yaml:"name"` - RawNeeds yaml.Node `yaml:"needs"` - RawRunsOn yaml.Node `yaml:"runs-on"` - Env yaml.Node `yaml:"env"` - If yaml.Node `yaml:"if"` - Steps []*Step `yaml:"steps"` - TimeoutMinutes string `yaml:"timeout-minutes"` - RawContinueOnError string `yaml:"continue-on-error"` - Services map[string]*ContainerSpec `yaml:"services"` - Strategy *Strategy `yaml:"strategy"` - RawContainer yaml.Node `yaml:"container"` - Defaults Defaults `yaml:"defaults"` - Outputs map[string]string `yaml:"outputs"` - Uses string `yaml:"uses"` - With map[string]any `yaml:"with"` - RawSecrets yaml.Node `yaml:"secrets"` - RawPermissions yaml.Node `yaml:"permissions"` - Result string - // Runtime fields set during execution (not from YAML): - ContinueOnError bool // true when all failing matrix combinations had continue-on-error=true - hasFirmFailure bool // true once any combination failed without continue-on-error -} - -// SetContinueOnError records whether this combination's failure should not fail the workflow. -// Must be called under the job lock. Safe across parallel matrix combinations. -func (j *Job) SetContinueOnError(continueOnErr bool) { - if continueOnErr { - if !j.hasFirmFailure { - j.ContinueOnError = true - } - } else { - j.hasFirmFailure = true - j.ContinueOnError = false - } -} - -// NeedsResult returns the job result as seen by dependent jobs through the -// `needs` context. A job that failed but was tolerated via continue-on-error -// reports "success" to its dependents, matching GitHub: such a failure must not -// block jobs gated on the default `if: success()`, even though the overall -// workflow run is still marked as failed. -func (j *Job) NeedsResult() string { - if j.Result == "failure" && j.ContinueOnError { - return "success" - } - return j.Result -} - -// Strategy for the job -type Strategy struct { - FailFast bool - MaxParallel int - FailFastString string `yaml:"fail-fast"` - MaxParallelString string `yaml:"max-parallel"` - RawMatrix yaml.Node `yaml:"matrix"` -} - -// Default settings that will apply to all steps in the job or workflow -type Defaults struct { - Run RunDefaults `yaml:"run"` -} - -// Defaults for all run steps in the job or workflow -type RunDefaults struct { - Shell string `yaml:"shell"` - WorkingDirectory string `yaml:"working-directory"` -} - -// GetMaxParallel sets default and returns value for `max-parallel` -func (s Strategy) GetMaxParallel() int { - // MaxParallel default value is `GitHub will maximize the number of jobs run in parallel depending on the available runners on GitHub-hosted virtual machines` - // So I take the liberty to hardcode default limit to 4 and this is because: - // 1: tl;dr: self-hosted does only 1 parallel job - https://github.com/actions/runner/issues/639#issuecomment-825212735 - // 2: GH has 20 parallel job limit (for free tier) - https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/usage-limits-billing-and-administration.md?plain=1#L45 - // 3: I want to add support for MaxParallel to act and 20! parallel jobs is a bit overkill IMHO - maxParallel := 4 - if s.MaxParallelString != "" { - var err error - if maxParallel, err = strconv.Atoi(s.MaxParallelString); err != nil { - log.Errorf("Failed to parse 'max-parallel' option: %v", err) - } - } - return maxParallel -} - -// GetFailFast sets default and returns value for `fail-fast` -func (s Strategy) GetFailFast() bool { - // FailFast option is true by default: https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/workflow-syntax-for-github-actions.md?plain=1#L1107 - failFast := true - log.Debug(s.FailFastString) - if s.FailFastString != "" { - var err error - if failFast, err = strconv.ParseBool(s.FailFastString); err != nil { - log.Errorf("Failed to parse 'fail-fast' option: %v", err) - } - } - return failFast -} - -func (j *Job) InheritSecrets() bool { - if j.RawSecrets.Kind != yaml.ScalarNode { - return false - } - - var val string - if !decodeNode(j.RawSecrets, &val) { - return false - } - - return val == "inherit" -} - -func (j *Job) Secrets() map[string]string { - if j.RawSecrets.Kind != yaml.MappingNode { - return nil - } - - var val map[string]string - if !decodeNode(j.RawSecrets, &val) { - return nil - } - - return val -} - -// Container details for the job -func (j *Job) Container() *ContainerSpec { - var val *ContainerSpec - switch j.RawContainer.Kind { - case yaml.ScalarNode: - val = new(ContainerSpec) - if !decodeNode(j.RawContainer, &val.Image) { - return nil - } - case yaml.MappingNode: - val = new(ContainerSpec) - if !decodeNode(j.RawContainer, val) { - return nil - } - } - return val -} - -// Needs list for Job -func (j *Job) Needs() []string { - switch j.RawNeeds.Kind { - case yaml.ScalarNode: - var val string - if !decodeNode(j.RawNeeds, &val) { - return nil - } - return []string{val} - case yaml.SequenceNode: - var val []string - if !decodeNode(j.RawNeeds, &val) { - return nil - } - return val - } - return nil -} - -// RunsOn list for Job -func (j *Job) RunsOn() []string { - return RunsOnFromNode(j.RawRunsOn) -} - -// RunsOnFromNode parses the runs-on labels from a raw runs-on node, so callers can evaluate a -// copy of the node (avoiding mutation of the shared Job) before reading the labels. -func RunsOnFromNode(rawRunsOn yaml.Node) []string { - switch rawRunsOn.Kind { - case yaml.MappingNode: - var val struct { - Group string - Labels yaml.Node - } - - if !decodeNode(rawRunsOn, &val) { - return nil - } - - labels := nodeAsStringSlice(val.Labels) - - if val.Group != "" { - labels = append(labels, val.Group) - } - - return labels - default: - return nodeAsStringSlice(rawRunsOn) - } -} - -func nodeAsStringSlice(node yaml.Node) []string { - switch node.Kind { - case yaml.ScalarNode: - var val string - if !decodeNode(node, &val) { - return nil - } - return []string{val} - case yaml.SequenceNode: - var val []string - if !decodeNode(node, &val) { - return nil - } - return val - } - return nil -} - -func environment(yml yaml.Node) map[string]string { - env := make(map[string]string) - if yml.Kind == yaml.MappingNode { - if !decodeNode(yml, &env) { - return nil - } - } - return env -} - -// Environment returns string-based key=value map for a job -func (j *Job) Environment() map[string]string { - return environment(j.Env) -} - -// normalizeMatrixValue converts a matrix value to []interface{}. -// Arrays pass through unchanged; scalars are wrapped in a single-element array. -// Unevaluated template expressions are wrapped as a fallback — proper resolution -// happens via EvaluateYamlNode before Matrix() is called. Nested maps are rejected. -func normalizeMatrixValue(key string, val any) ([]any, error) { - switch t := val.(type) { - case []any: - // Already an array - use as-is - return t, nil - case string, int, float64, bool, nil: - // Valid scalar types that can appear in YAML - // These can be unevaluated template expressions (strings) or literal values - return []any{t}, nil - case map[string]any: - // Nested map indicates misconfiguration - likely user error - return nil, fmt.Errorf("matrix key %q has invalid nested object value - expected scalar or array, got map", key) - default: - // Unknown types might indicate parsing issues - log.Warnf("matrix key %q has unexpected type %T, wrapping as single value", key, t) - return []any{t}, nil - } -} - -// Matrix decodes the RawMatrix YAML node into a map[string][]interface{}. -// Scalar values are wrapped into single-element arrays automatically. -// Template expressions are resolved by EvaluateYamlNode before this method is -// called; if unresolved, the literal string is wrapped as a one-element fallback. -func (j *Job) Matrix() (map[string][]any, error) { - if j.Strategy == nil || j.Strategy.RawMatrix.Kind != yaml.MappingNode { - return map[string][]any{}, nil - } - - // Decode to flexible map first so that scalar values don't cause a type error. - var flexVal map[string]any - err := j.Strategy.RawMatrix.Decode(&flexVal) - if err != nil { - // Fall back to the strict array-only format for backward compatibility. - var val map[string][]any - if !decodeNode(j.Strategy.RawMatrix, &val) { - return map[string][]any{}, nil - } - return val, nil - } - - // Convert flexible format to expected format with validation - val := make(map[string][]any) - for k, v := range flexVal { - normalized, err := normalizeMatrixValue(k, v) - if err != nil { - return nil, err - } - val[k] = normalized - } - return val, nil -} - -// GetMatrixes returns the matrix cross product -// It skips includes and hard fails excludes for non-existing keys -func (j *Job) GetMatrixes() ([]map[string]any, error) { - matrixes := make([]map[string]any, 0) - if j.Strategy != nil { - // Always set these values, even if there's an error later - j.Strategy.FailFast = j.Strategy.GetFailFast() - j.Strategy.MaxParallel = j.Strategy.GetMaxParallel() - - m, err := j.Matrix() - if err != nil { - return nil, err - } - if len(m) > 0 { - includes := make([]map[string]any, 0) - extraIncludes := make([]map[string]any, 0) - addInclude := func(raw any) error { - include, ok := raw.(map[string]any) - if !ok { - return fmt.Errorf("the workflow is not valid. Matrix include %v is not a map of matrix keys to values", raw) - } - for k := range include { - if _, ok := m[k]; ok { - includes = append(includes, include) - return nil - } - } - extraIncludes = append(extraIncludes, include) - return nil - } - for _, v := range m["include"] { - switch t := v.(type) { - case []any: - for _, i := range t { - if err := addInclude(i); err != nil { - return nil, err - } - } - case any: - if err := addInclude(t); err != nil { - return nil, err - } - } - } - delete(m, "include") - - excludes := make([]map[string]any, 0) - for _, e := range m["exclude"] { - exclude, ok := e.(map[string]any) - if !ok { - return nil, fmt.Errorf("the workflow is not valid. Matrix exclude %v is not a map of matrix keys to values", e) - } - for k := range exclude { - if _, ok := m[k]; ok { - excludes = append(excludes, exclude) - } else { - // We fail completely here because that's what GitHub does for non-existing matrix keys, fail on exclude, silent skip on include - return nil, fmt.Errorf("the workflow is not valid. Matrix exclude key %q does not match any key within the matrix", k) - } - } - } - delete(m, "exclude") - - matrixProduct := common.CartesianProduct(m) - MATRIX: - for _, matrix := range matrixProduct { - for _, exclude := range excludes { - if commonKeysMatch(matrix, exclude) { - log.Debugf("Skipping matrix '%v' due to exclude '%v'", matrix, exclude) - continue MATRIX - } - } - matrixes = append(matrixes, matrix) - } - for _, include := range includes { - matched := false - for _, matrix := range matrixes { - if commonKeysMatch2(matrix, include, m) { - matched = true - log.Debugf("Adding include values '%v' to existing entry", include) - maps.Copy(matrix, include) - } - } - if !matched { - extraIncludes = append(extraIncludes, include) - } - } - for _, include := range extraIncludes { - log.Debugf("Adding include '%v'", include) - matrixes = append(matrixes, include) - } - if len(matrixes) == 0 { - matrixes = append(matrixes, make(map[string]any)) - } - } else { - matrixes = append(matrixes, make(map[string]any)) - } - } else { - matrixes = append(matrixes, make(map[string]any)) - log.Debugf("Empty Strategy, matrixes=%v", matrixes) - } - return matrixes, nil -} - -func commonKeysMatch(a, b map[string]any) bool { - for aKey, aVal := range a { - if bVal, ok := b[aKey]; ok && !reflect.DeepEqual(aVal, bVal) { - return false - } - } - return true -} - -func commonKeysMatch2(a, b map[string]any, m map[string][]any) bool { - for aKey, aVal := range a { - _, useKey := m[aKey] - if bVal, ok := b[aKey]; useKey && ok && !reflect.DeepEqual(aVal, bVal) { - return false - } - } - return true -} - -// JobType describes what type of job we are about to run -type JobType int - -const ( - // JobTypeDefault is all jobs that have a `run` attribute - JobTypeDefault JobType = iota - - // JobTypeReusableWorkflowLocal is all jobs that have a `uses` that is a local workflow in the .github/workflows directory - JobTypeReusableWorkflowLocal - - // JobTypeReusableWorkflowRemote is all jobs that have a `uses` that references a workflow file in a github repo - JobTypeReusableWorkflowRemote - - // JobTypeInvalid represents a job which is not configured correctly - JobTypeInvalid -) - -func (j JobType) String() string { - switch j { - case JobTypeDefault: - return "default" - case JobTypeReusableWorkflowLocal: - return "local-reusable-workflow" - case JobTypeReusableWorkflowRemote: - return "remote-reusable-workflow" - } - return "unknown" -} - -// Type returns the type of the job -func (j *Job) Type() (JobType, error) { - isReusable := j.Uses != "" - - if isReusable { - isYaml, _ := regexp.MatchString(`\.(ya?ml)(?:$|@)`, j.Uses) - - if isYaml { - isLocalPath := strings.HasPrefix(j.Uses, "./") - isRemotePath, _ := regexp.MatchString(`^[^.](.+?/){2,}.+\.ya?ml@`, j.Uses) - hasVersion, _ := regexp.MatchString(`\.ya?ml@`, j.Uses) - - if isLocalPath { - return JobTypeReusableWorkflowLocal, nil - } else if isRemotePath && hasVersion { - return JobTypeReusableWorkflowRemote, nil - } - } - - return JobTypeInvalid, fmt.Errorf("`uses` key references invalid workflow path '%s'. Must start with './' if it's a local workflow, or must start with '//' and include an '@' if it's a remote workflow", j.Uses) - } - - return JobTypeDefault, nil -} - -// ContainerSpec is the specification of the container to use for the job -type ContainerSpec struct { - Image string `yaml:"image"` - Env map[string]string `yaml:"env"` - Ports []string `yaml:"ports"` - Volumes []string `yaml:"volumes"` - Options string `yaml:"options"` - Credentials map[string]string `yaml:"credentials"` - Entrypoint string - Args string - Name string - Reuse bool - - // Gitea specific - Cmd []string `yaml:"cmd"` -} - -// Step is the structure of one step in a job -type Step struct { - Number int `yaml:"-"` - ID string `yaml:"id"` - If yaml.Node `yaml:"if"` - Name string `yaml:"name"` - Uses string `yaml:"uses"` - Run string `yaml:"run"` - WorkingDirectory string `yaml:"working-directory"` - Shell string `yaml:"shell"` - Env yaml.Node `yaml:"env"` - With map[string]string `yaml:"with"` - RawContinueOnError string `yaml:"continue-on-error"` - TimeoutMinutes string `yaml:"timeout-minutes"` -} - -// Clone returns a deep copy safe to mutate independently of s. Job steps are shared across -// parallel matrix runs, which mutate per-job fields (ID, Number, Shell) and evaluate the If/Env -// yaml.Nodes in place, so each job must own its copy. -func (s *Step) Clone() *Step { - clone := *s - clone.If = CloneYamlNode(s.If) - clone.Env = CloneYamlNode(s.Env) - clone.With = maps.Clone(s.With) - return &clone -} - -// CloneYamlNode returns a deep copy of a yaml.Node so callers can evaluate it in place without -// mutating a node shared across parallel jobs. -func CloneYamlNode(n yaml.Node) yaml.Node { - clone := n - if n.Content != nil { - clone.Content = make([]*yaml.Node, len(n.Content)) - for i, child := range n.Content { - if child != nil { - childClone := CloneYamlNode(*child) - clone.Content[i] = &childClone - } - } - } - return clone -} - -// String gets the name of step -func (s *Step) String() string { - if s.Name != "" { - return s.Name - } else if s.Uses != "" { - return s.Uses - } else if s.Run != "" { - return s.Run - } - return s.ID -} - -// Environment returns string-based key=value map for a step -func (s *Step) Environment() map[string]string { - return environment(s.Env) -} - -// GetEnv gets the env for a step -func (s *Step) GetEnv() map[string]string { - env := s.Environment() - - for k, v := range s.With { - envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(k), "_") - envKey = "INPUT_" + strings.ToUpper(envKey) - env[envKey] = v - } - return env -} - -// ShellCommand returns the command for the shell -func (s *Step) ShellCommand() string { - var shellCommand string - - // Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17 - switch s.Shell { - case "", "bash": - shellCommand = "bash --noprofile --norc -e -o pipefail {0}" - case "pwsh": - shellCommand = "pwsh -command . '{0}'" - case "python": - shellCommand = "python {0}" - case "sh": - shellCommand = "sh -e {0}" - case "cmd": - shellCommand = "cmd /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\"" - case "powershell": - shellCommand = "powershell -command . '{0}'" - default: - shellCommand = s.Shell - } - return shellCommand -} - -// StepType describes what type of step we are about to run -type StepType int - -const ( - // StepTypeRun is all steps that have a `run` attribute - StepTypeRun StepType = iota - - // StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...` - StepTypeUsesDockerURL - - // StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory - StepTypeUsesActionLocal - - // StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo - StepTypeUsesActionRemote - - // StepTypeReusableWorkflowLocal is all steps that have a `uses` that is a local workflow in the .github/workflows directory - StepTypeReusableWorkflowLocal - - // StepTypeReusableWorkflowRemote is all steps that have a `uses` that references a workflow file in a github repo - StepTypeReusableWorkflowRemote - - // StepTypeInvalid is for steps that have invalid step action - StepTypeInvalid -) - -func (s StepType) String() string { - switch s { - case StepTypeInvalid: - return "invalid" - case StepTypeRun: - return "run" - case StepTypeUsesActionLocal: - return "local-action" - case StepTypeUsesActionRemote: - return "remote-action" - case StepTypeUsesDockerURL: - return "docker" - case StepTypeReusableWorkflowLocal: - return "local-reusable-workflow" - case StepTypeReusableWorkflowRemote: - return "remote-reusable-workflow" - } - return "unknown" -} - -// Type returns the type of the step -func (s *Step) Type() StepType { - if s.Run == "" && s.Uses == "" { - return StepTypeInvalid - } - - if s.Run != "" { - if s.Uses != "" { - return StepTypeInvalid - } - return StepTypeRun - } else if strings.HasPrefix(s.Uses, "docker://") { - return StepTypeUsesDockerURL - } else if strings.HasPrefix(s.Uses, "./.github/workflows") && (strings.HasSuffix(s.Uses, ".yml") || strings.HasSuffix(s.Uses, ".yaml")) { - return StepTypeReusableWorkflowLocal - } else if !strings.HasPrefix(s.Uses, "./") && strings.Contains(s.Uses, ".github/workflows") && (strings.Contains(s.Uses, ".yml@") || strings.Contains(s.Uses, ".yaml@")) { - return StepTypeReusableWorkflowRemote - } else if strings.HasPrefix(s.Uses, "./") { - return StepTypeUsesActionLocal - } - return StepTypeUsesActionRemote -} - -// UsesHash returns a hash of the uses string. -// For Gitea. -func (s *Step) UsesHash() string { - return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Uses))) -} - -// ReadWorkflow returns a list of jobs for a given workflow file reader -func ReadWorkflow(in io.Reader) (*Workflow, error) { - w := new(Workflow) - err := yaml.NewDecoder(in).Decode(w) - return w, err -} - -// GetJob will get a job by name in the workflow -func (w *Workflow) GetJob(jobID string) *Job { - for id, j := range w.Jobs { - if jobID == id { - if j.Name == "" { - j.Name = id - } - if j.If.Value == "" { - j.If.Value = "success()" - } - return j - } - } - return nil -} - -// GetJobIDs will get all the job names in the workflow -func (w *Workflow) GetJobIDs() []string { - ids := make([]string, 0) - for id := range w.Jobs { - ids = append(ids, id) - } - return ids -} - -var OnDecodeNodeError = func(node yaml.Node, out any, err error) { - log.Fatalf("Failed to decode node %v into %T: %v", node, out, err) -} - -func decodeNode(node yaml.Node, out any) bool { - if err := node.Decode(out); err != nil { - if OnDecodeNodeError != nil { - OnDecodeNodeError(node, out, err) - } - return false - } - return true -} - -// For Gitea -// RawConcurrency represents a workflow concurrency or a job concurrency with uninterpolated options -type RawConcurrency struct { - Group string `yaml:"group,omitempty"` - CancelInProgress string `yaml:"cancel-in-progress,omitempty"` - RawExpression string `yaml:"-,omitempty"` -} - -type objectConcurrency RawConcurrency - -func (r *RawConcurrency) UnmarshalYAML(n *yaml.Node) error { - if err := n.Decode(&r.RawExpression); err == nil { - return nil - } - return n.Decode((*objectConcurrency)(r)) -} - -func (r *RawConcurrency) MarshalYAML() (any, error) { - if r.RawExpression != "" { - return r.RawExpression, nil - } - - return (*objectConcurrency)(r), nil -} diff --git a/act/model/workflow_test.go b/act/model/workflow_test.go deleted file mode 100644 index aa6be1d0..00000000 --- a/act/model/workflow_test.go +++ /dev/null @@ -1,1163 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2020 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package model - -import ( - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.yaml.in/yaml/v4" -) - -// TestStepCloneIsolatesMutableFields guards the parallel-matrix race fix: combinations share the -// job's *Step, and Clone() must hand each a copy whose If/Env nodes and With map can be mutated -// independently. A shallow copy would share Env.Content's backing array (and the With map) and -// leak writes across combinations. -func TestStepCloneIsolatesMutableFields(t *testing.T) { - var orig Step - require.NoError(t, yaml.Unmarshal([]byte("if: ${{ env.X == 'a' }}\nenv:\n KEY: original\nwith:\n arg: original\n"), &orig)) - require.Len(t, orig.Env.Content, 2) // [key, value] - - clone := orig.Clone() - clone.If.Value = "changed" - clone.Env.Content[1].Value = "changed" - clone.With["arg"] = "changed" - - assert.Equal(t, "${{ env.X == 'a' }}", orig.If.Value, "If must not be shared with the clone") - assert.Equal(t, "original", orig.Env.Content[1].Value, "Env nodes must not be shared with the clone") - assert.Equal(t, "original", orig.With["arg"], "With map must not be shared with the clone") -} - -// TestJobNeedsResult guards the continue-on-error semantics exposed to dependent -// jobs through the `needs` context: a failed-but-tolerated job reports "success" -// so it does not block dependents gated on the default `if: success()`, matching -// GitHub. A firm failure and any non-failure result are reported verbatim. -func TestJobNeedsResult(t *testing.T) { - cases := []struct { - name string - result string - continueOnError bool - want string - }{ - {"tolerated failure reports success", "failure", true, "success"}, - {"firm failure reports failure", "failure", false, "failure"}, - {"success is unchanged", "success", false, "success"}, - {"success with continue-on-error is unchanged", "success", true, "success"}, - {"empty result is unchanged", "", true, ""}, - {"skipped is unchanged", "skipped", true, "skipped"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - j := &Job{Result: tc.result, ContinueOnError: tc.continueOnError} - assert.Equal(t, tc.want, j.NeedsResult()) - }) - } -} - -func TestJobSetContinueOnErrorFirmFailureWins(t *testing.T) { - job := &Job{} - job.SetContinueOnError(true) - assert.True(t, job.ContinueOnError) - - job.SetContinueOnError(false) - assert.False(t, job.ContinueOnError) - - job.SetContinueOnError(true) - assert.False(t, job.ContinueOnError, "a later tolerated failure must not hide an earlier firm failure") -} - -func TestStepStatusText(t *testing.T) { - for _, tc := range []struct { - status stepStatus - text string - }{ - {StepStatusSuccess, "success"}, - {StepStatusFailure, "failure"}, - {StepStatusSkipped, "skipped"}, - } { - t.Run(tc.text, func(t *testing.T) { - got, err := tc.status.MarshalText() - require.NoError(t, err) - assert.Equal(t, tc.text, string(got)) - - var parsed stepStatus - require.NoError(t, parsed.UnmarshalText(got)) - assert.Equal(t, tc.status, parsed) - assert.Equal(t, tc.text, parsed.String()) - }) - } - - var parsed stepStatus - require.Error(t, parsed.UnmarshalText([]byte("cancelled"))) - assert.Empty(t, stepStatus(99).String()) -} - -func TestWorkflowCallConfig(t *testing.T) { - workflow, err := ReadWorkflow(strings.NewReader(` -on: - workflow_call: - inputs: - name: - required: true - type: string - outputs: - digest: - value: ${{ jobs.build.outputs.digest }} -jobs: {} -`)) - require.NoError(t, err) - - config := workflow.WorkflowCallConfig() - require.NotNil(t, config) - require.Contains(t, config.Inputs, "name") - assert.True(t, config.Inputs["name"].Required) - assert.Equal(t, "string", config.Inputs["name"].Type) - assert.Equal(t, "${{ jobs.build.outputs.digest }}", config.Outputs["digest"].Value) - - listWorkflow, err := ReadWorkflow(strings.NewReader("on: [workflow_call]\njobs: {}\n")) - require.NoError(t, err) - assert.NotNil(t, listWorkflow.WorkflowCallConfig()) - assert.Empty(t, listWorkflow.WorkflowCallConfig().Inputs) -} - -func TestJobSecretsAndEnvironment(t *testing.T) { - inheritJob := readJob(t, ` -secrets: inherit -env: - A: one - B: two -`) - assert.True(t, inheritJob.InheritSecrets()) - assert.Nil(t, inheritJob.Secrets()) - assert.Equal(t, map[string]string{"A": "one", "B": "two"}, inheritJob.Environment()) - - mappingJob := readJob(t, ` -secrets: - TOKEN: ${{ secrets.TOKEN }} -`) - assert.False(t, mappingJob.InheritSecrets()) - assert.Equal(t, map[string]string{"TOKEN": "${{ secrets.TOKEN }}"}, mappingJob.Secrets()) -} - -func TestJobTypeAndString(t *testing.T) { - tests := []struct { - job Job - want JobType - wantErr bool - }{ - {job: Job{}, want: JobTypeDefault}, - {job: Job{Uses: "./.github/workflows/reuse.yml"}, want: JobTypeReusableWorkflowLocal}, - {job: Job{Uses: "owner/repo/.github/workflows/reuse.yaml@v1"}, want: JobTypeReusableWorkflowRemote}, - {job: Job{Uses: "owner/repo/.github/workflows/reuse.yaml"}, want: JobTypeInvalid, wantErr: true}, - } - - for _, tc := range tests { - t.Run(fmt.Sprintf("%s/%s", tc.job.Uses, tc.want), func(t *testing.T) { - got, err := tc.job.Type() - if tc.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - assert.Equal(t, tc.want, got) - }) - } - - assert.Equal(t, "default", JobTypeDefault.String()) - assert.Equal(t, "local-reusable-workflow", JobTypeReusableWorkflowLocal.String()) - assert.Equal(t, "remote-reusable-workflow", JobTypeReusableWorkflowRemote.String()) - assert.Equal(t, "unknown", JobType(99).String()) -} - -func TestStepStringEnvironmentEnvAndType(t *testing.T) { - step := readStep(t, ` -id: example -env: - DIRECT: value -with: - mixed-key: input -`) - assert.Equal(t, "example", step.String()) - assert.Equal(t, map[string]string{"DIRECT": "value"}, step.Environment()) - assert.Equal(t, map[string]string{"DIRECT": "value", "INPUT_MIXED-KEY": "input"}, step.GetEnv()) - - for _, tc := range []struct { - step Step - want StepType - }{ - {step: Step{}, want: StepTypeInvalid}, - {step: Step{Run: "echo hi"}, want: StepTypeRun}, - {step: Step{Run: "echo hi", Uses: "actions/checkout@v4"}, want: StepTypeInvalid}, - {step: Step{Uses: "docker://alpine:latest"}, want: StepTypeUsesDockerURL}, - {step: Step{Uses: "./.github/workflows/reuse.yml"}, want: StepTypeReusableWorkflowLocal}, - {step: Step{Uses: "owner/repo/.github/workflows/reuse.yml@v1"}, want: StepTypeReusableWorkflowRemote}, - {step: Step{Uses: "./actions/local"}, want: StepTypeUsesActionLocal}, - {step: Step{Uses: "actions/checkout@v4"}, want: StepTypeUsesActionRemote}, - } { - t.Run(tc.want.String(), func(t *testing.T) { - assert.Equal(t, tc.want, tc.step.Type()) - }) - } - - assert.Equal(t, "invalid", StepTypeInvalid.String()) - assert.Equal(t, "run", StepTypeRun.String()) - assert.Equal(t, "local-action", StepTypeUsesActionLocal.String()) - assert.Equal(t, "remote-action", StepTypeUsesActionRemote.String()) - assert.Equal(t, "docker", StepTypeUsesDockerURL.String()) - assert.Equal(t, "local-reusable-workflow", StepTypeReusableWorkflowLocal.String()) - assert.Equal(t, "remote-reusable-workflow", StepTypeReusableWorkflowRemote.String()) - assert.Equal(t, "unknown", StepType(99).String()) - assert.NotEmpty(t, (&Step{Uses: "actions/checkout@v4"}).UsesHash()) -} - -func TestWorkflowGetJobAndIDs(t *testing.T) { - workflow := &Workflow{Jobs: map[string]*Job{"build": {}}} - assert.Equal(t, []string{"build"}, workflow.GetJobIDs()) - - job := workflow.GetJob("build") - require.NotNil(t, job) - assert.Equal(t, "build", job.Name) - assert.Equal(t, "success()", job.If.Value) - assert.Nil(t, workflow.GetJob("missing")) -} - -func TestRawConcurrencyYaml(t *testing.T) { - var expr RawConcurrency - require.NoError(t, yaml.Unmarshal([]byte("group-${{ github.ref }}"), &expr)) - assert.Equal(t, "group-${{ github.ref }}", expr.RawExpression) - marshaled, err := expr.MarshalYAML() - require.NoError(t, err) - assert.Equal(t, "group-${{ github.ref }}", marshaled) - - var object RawConcurrency - require.NoError(t, yaml.Unmarshal([]byte("group: ci\ncancel-in-progress: true\n"), &object)) - assert.Equal(t, "ci", object.Group) - assert.Equal(t, "true", object.CancelInProgress) - marshaled, err = object.MarshalYAML() - require.NoError(t, err) - assert.Equal(t, (*objectConcurrency)(&object), marshaled) -} - -func TestReadWorkflow_ScheduleEvent(t *testing.T) { - yaml := ` -name: local-action-docker-url -on: - schedule: - - cron: '30 5 * * 1,3' - - cron: '30 5 * * 2,4' - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - - schedules := workflow.OnEvent("schedule") - assert.Len(t, schedules, 2) - - newSchedules := workflow.OnSchedule() - assert.Len(t, newSchedules, 2) - - assert.Equal(t, "30 5 * * 1,3", newSchedules[0]) - assert.Equal(t, "30 5 * * 2,4", newSchedules[1]) - - yaml = ` -name: local-action-docker-url -on: - schedule: - test: '30 5 * * 1,3' - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - - newSchedules = workflow.OnSchedule() - assert.Empty(t, newSchedules) - - yaml = ` -name: local-action-docker-url -on: - schedule: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - - newSchedules = workflow.OnSchedule() - assert.Empty(t, newSchedules) - - yaml = ` -name: local-action-docker-url -on: [push, tag] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - - newSchedules = workflow.OnSchedule() - assert.Empty(t, newSchedules) -} - -func TestReadWorkflow_StringEvent(t *testing.T) { - yaml := ` -name: local-action-docker-url -on: push - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - - assert.Len(t, workflow.On(), 1) - assert.Contains(t, workflow.On(), "push") -} - -func TestReadWorkflow_ListEvent(t *testing.T) { - yaml := ` -name: local-action-docker-url -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - - assert.Len(t, workflow.On(), 2) - assert.Contains(t, workflow.On(), "push") - assert.Contains(t, workflow.On(), "pull_request") -} - -func TestReadWorkflow_MapEvent(t *testing.T) { - yaml := ` -name: local-action-docker-url -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Len(t, workflow.On(), 2) - assert.Contains(t, workflow.On(), "push") - assert.Contains(t, workflow.On(), "pull_request") -} - -func TestReadWorkflow_RunsOnLabels(t *testing.T) { - yaml := ` -name: local-action-docker-url - -jobs: - test: - container: nginx:latest - runs-on: - labels: ubuntu-latest - steps: - - uses: ./actions/docker-url` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, workflow.Jobs["test"].RunsOn(), []string{"ubuntu-latest"}) //nolint:testifylint // pre-existing issue from nektos/act -} - -func TestReadWorkflow_RunsOnLabelsWithGroup(t *testing.T) { - yaml := ` -name: local-action-docker-url - -jobs: - test: - container: nginx:latest - runs-on: - labels: [ubuntu-latest] - group: linux - steps: - - uses: ./actions/docker-url` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, workflow.Jobs["test"].RunsOn(), []string{"ubuntu-latest", "linux"}) //nolint:testifylint // pre-existing issue from nektos/act -} - -func TestReadWorkflow_StringContainer(t *testing.T) { - yaml := ` -name: local-action-docker-url - -jobs: - test: - container: nginx:latest - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url - test2: - container: - image: nginx:latest - env: - foo: bar - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Len(t, workflow.Jobs, 2) - assert.Contains(t, workflow.Jobs["test"].Container().Image, "nginx:latest") - assert.Contains(t, workflow.Jobs["test2"].Container().Image, "nginx:latest") - assert.Contains(t, workflow.Jobs["test2"].Container().Env["foo"], "bar") -} - -func TestReadWorkflow_ObjectContainer(t *testing.T) { - yaml := ` -name: local-action-docker-url - -jobs: - test: - container: - image: r.example.org/something:latest - credentials: - username: registry-username - password: registry-password - env: - HOME: /home/user - volumes: - - my_docker_volume:/volume_mount - - /data/my_data - - /source/directory:/destination/directory - runs-on: ubuntu-latest - steps: - - uses: ./actions/docker-url -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Len(t, workflow.Jobs, 1) - - container := workflow.GetJob("test").Container() - - assert.Contains(t, container.Image, "r.example.org/something:latest") - assert.Contains(t, container.Env["HOME"], "/home/user") - assert.Contains(t, container.Credentials["username"], "registry-username") - assert.Contains(t, container.Credentials["password"], "registry-password") - assert.ElementsMatch(t, container.Volumes, []string{ - "my_docker_volume:/volume_mount", - "/data/my_data", - "/source/directory:/destination/directory", - }) -} - -func TestReadWorkflow_JobTypes(t *testing.T) { - yaml := ` -name: invalid job definition - -jobs: - default-job: - runs-on: ubuntu-latest - steps: - - run: echo - remote-reusable-workflow-yml: - uses: remote/repo/some/path/to/workflow.yml@main - remote-reusable-workflow-yaml: - uses: remote/repo/some/path/to/workflow.yaml@main - remote-reusable-workflow-custom-path: - uses: remote/repo/path/to/workflow.yml@main - local-reusable-workflow-yml: - uses: ./some/path/to/workflow.yml - local-reusable-workflow-yaml: - uses: ./some/path/to/workflow.yaml -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Len(t, workflow.Jobs, 6) - - jobType, err := workflow.Jobs["default-job"].Type() - assert.Equal(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, JobTypeDefault, jobType) - - jobType, err = workflow.Jobs["remote-reusable-workflow-yml"].Type() - assert.Equal(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, JobTypeReusableWorkflowRemote, jobType) - - jobType, err = workflow.Jobs["remote-reusable-workflow-yaml"].Type() - assert.Equal(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, JobTypeReusableWorkflowRemote, jobType) - - jobType, err = workflow.Jobs["remote-reusable-workflow-custom-path"].Type() - assert.Equal(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, JobTypeReusableWorkflowRemote, jobType) - - jobType, err = workflow.Jobs["local-reusable-workflow-yml"].Type() - assert.Equal(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, JobTypeReusableWorkflowLocal, jobType) - - jobType, err = workflow.Jobs["local-reusable-workflow-yaml"].Type() - assert.Equal(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, JobTypeReusableWorkflowLocal, jobType) -} - -func TestReadWorkflow_JobTypes_InvalidPath(t *testing.T) { - yaml := ` -name: invalid job definition - -jobs: - remote-reusable-workflow-missing-version: - uses: remote/repo/some/path/to/workflow.yml - remote-reusable-workflow-bad-extension: - uses: remote/repo/some/path/to/workflow.json - local-reusable-workflow-bad-extension: - uses: ./some/path/to/workflow.json - local-reusable-workflow-bad-path: - uses: some/path/to/workflow.yaml -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Len(t, workflow.Jobs, 4) - - jobType, err := workflow.Jobs["remote-reusable-workflow-missing-version"].Type() - assert.Equal(t, JobTypeInvalid, jobType) - assert.NotEqual(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - - jobType, err = workflow.Jobs["remote-reusable-workflow-bad-extension"].Type() - assert.Equal(t, JobTypeInvalid, jobType) - assert.NotEqual(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - - jobType, err = workflow.Jobs["local-reusable-workflow-bad-extension"].Type() - assert.Equal(t, JobTypeInvalid, jobType) - assert.NotEqual(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act - - jobType, err = workflow.Jobs["local-reusable-workflow-bad-path"].Type() - assert.Equal(t, JobTypeInvalid, jobType) - assert.NotEqual(t, nil, err) //nolint:testifylint // pre-existing issue from nektos/act -} - -func TestReadWorkflow_StepsTypes(t *testing.T) { - yaml := ` -name: invalid step definition - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: test1 - uses: actions/checkout@v2 - run: echo - - name: test2 - run: echo - - name: test3 - uses: actions/checkout@v2 - - name: test4 - uses: docker://nginx:latest - - name: test5 - uses: ./local-action -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Len(t, workflow.Jobs, 1) - assert.Len(t, workflow.Jobs["test"].Steps, 5) - assert.Equal(t, workflow.Jobs["test"].Steps[0].Type(), StepTypeInvalid) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, workflow.Jobs["test"].Steps[1].Type(), StepTypeRun) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, workflow.Jobs["test"].Steps[2].Type(), StepTypeUsesActionRemote) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, workflow.Jobs["test"].Steps[3].Type(), StepTypeUsesDockerURL) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, workflow.Jobs["test"].Steps[4].Type(), StepTypeUsesActionLocal) //nolint:testifylint // pre-existing issue from nektos/act -} - -// See: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idoutputs -func TestReadWorkflow_JobOutputs(t *testing.T) { - yaml := ` -name: job outputs definition - -jobs: - test1: - runs-on: ubuntu-latest - steps: - - id: test1_1 - run: | - echo "::set-output name=a_key::some-a_value" - echo "::set-output name=b-key::some-b-value" - outputs: - some_a_key: ${{ steps.test1_1.outputs.a_key }} - some-b-key: ${{ steps.test1_1.outputs.b-key }} - - test2: - runs-on: ubuntu-latest - needs: - - test1 - steps: - - name: test2_1 - run: | - echo "${{ needs.test1.outputs.some_a_key }}" - echo "${{ needs.test1.outputs.some-b-key }}" -` - - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - assert.Len(t, workflow.Jobs, 2) - - assert.Len(t, workflow.Jobs["test1"].Steps, 1) - assert.Equal(t, StepTypeRun, workflow.Jobs["test1"].Steps[0].Type()) - assert.Equal(t, "test1_1", workflow.Jobs["test1"].Steps[0].ID) - assert.Len(t, workflow.Jobs["test1"].Outputs, 2) - assert.Contains(t, workflow.Jobs["test1"].Outputs, "some_a_key") - assert.Contains(t, workflow.Jobs["test1"].Outputs, "some-b-key") - assert.Equal(t, "${{ steps.test1_1.outputs.a_key }}", workflow.Jobs["test1"].Outputs["some_a_key"]) - assert.Equal(t, "${{ steps.test1_1.outputs.b-key }}", workflow.Jobs["test1"].Outputs["some-b-key"]) -} - -func TestReadWorkflow_Strategy(t *testing.T) { - w, err := NewWorkflowPlanner("testdata/strategy/push.yml", true) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - p, err := w.PlanJob("strategy-only-max-parallel") - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.Equal(t, len(p.Stages), 1) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, len(p.Stages[0].Runs), 1) //nolint:testifylint // pre-existing issue from nektos/act - - wf := p.Stages[0].Runs[0].Workflow - - job := wf.Jobs["strategy-only-max-parallel"] - matrixes, err := job.GetMatrixes() - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, matrixes, []map[string]any{{}}) //nolint:testifylint // pre-existing issue from nektos/act - matrix, err := job.Matrix() - require.NoError(t, err) - assert.Empty(t, matrix) - assert.Equal(t, job.Strategy.MaxParallel, 2) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, job.Strategy.FailFast, true) //nolint:testifylint // pre-existing issue from nektos/act - - job = wf.Jobs["strategy-only-fail-fast"] - matrixes, err = job.GetMatrixes() - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, matrixes, []map[string]any{{}}) //nolint:testifylint // pre-existing issue from nektos/act - matrix, err = job.Matrix() - require.NoError(t, err) - assert.Empty(t, matrix) - assert.Equal(t, job.Strategy.MaxParallel, 4) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, job.Strategy.FailFast, false) //nolint:testifylint // pre-existing issue from nektos/act - - job = wf.Jobs["strategy-no-matrix"] - matrixes, err = job.GetMatrixes() - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, matrixes, []map[string]any{{}}) //nolint:testifylint // pre-existing issue from nektos/act - matrix, err = job.Matrix() - require.NoError(t, err) - assert.Empty(t, matrix) - assert.Equal(t, job.Strategy.MaxParallel, 2) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, job.Strategy.FailFast, false) //nolint:testifylint // pre-existing issue from nektos/act - - job = wf.Jobs["strategy-all"] - matrixes, err = job.GetMatrixes() - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, matrixes, //nolint:testifylint // pre-existing issue from nektos/act - []map[string]any{ - {"datacenter": "site-c", "node-version": "14.x", "site": "staging"}, - {"datacenter": "site-c", "node-version": "16.x", "site": "staging"}, - {"datacenter": "site-d", "node-version": "16.x", "site": "staging"}, - {"php-version": 5.4}, - {"datacenter": "site-a", "node-version": "10.x", "site": "prod"}, - {"datacenter": "site-b", "node-version": "12.x", "site": "dev"}, - }, - ) - matrix, err = job.Matrix() - require.NoError(t, err) - assert.Equal(t, matrix, //nolint:testifylint // pre-existing issue from nektos/act - map[string][]any{ - "datacenter": {"site-c", "site-d"}, - "exclude": { - map[string]any{"datacenter": "site-d", "node-version": "14.x", "site": "staging"}, - }, - "include": { - map[string]any{"php-version": 5.4}, - map[string]any{"datacenter": "site-a", "node-version": "10.x", "site": "prod"}, - map[string]any{"datacenter": "site-b", "node-version": "12.x", "site": "dev"}, - }, - "node-version": {"14.x", "16.x"}, - "site": {"staging"}, - }, - ) - assert.Equal(t, job.Strategy.MaxParallel, 2) //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, job.Strategy.FailFast, false) //nolint:testifylint // pre-existing issue from nektos/act -} - -func TestStep_ShellCommand(t *testing.T) { - tests := []struct { - shell string - want string - }{ - {"pwsh -v '. {0}'", "pwsh -v '. {0}'"}, - {"pwsh", "pwsh -command . '{0}'"}, - {"powershell", "powershell -command . '{0}'"}, - } - for _, tt := range tests { - t.Run(tt.shell, func(t *testing.T) { - got := (&Step{Shell: tt.shell}).ShellCommand() - assert.Equal(t, got, tt.want) //nolint:testifylint // pre-existing issue from nektos/act - }) - } -} - -func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) { - yaml := ` - name: local-action-docker-url - ` - workflow, err := ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - workflowDispatch := workflow.WorkflowDispatchConfig() - assert.Nil(t, workflowDispatch) - - yaml = ` - name: local-action-docker-url - on: push - ` - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - workflowDispatch = workflow.WorkflowDispatchConfig() - assert.Nil(t, workflowDispatch) - - yaml = ` - name: local-action-docker-url - on: workflow_dispatch - ` - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - workflowDispatch = workflow.WorkflowDispatchConfig() - assert.NotNil(t, workflowDispatch) - assert.Nil(t, workflowDispatch.Inputs) - - yaml = ` - name: local-action-docker-url - on: [push, pull_request] - ` - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - workflowDispatch = workflow.WorkflowDispatchConfig() - assert.Nil(t, workflowDispatch) - - yaml = ` - name: local-action-docker-url - on: [push, workflow_dispatch] - ` - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - workflowDispatch = workflow.WorkflowDispatchConfig() - assert.NotNil(t, workflowDispatch) - assert.Nil(t, workflowDispatch.Inputs) - - yaml = ` - name: local-action-docker-url - on: - - push - - workflow_dispatch - ` - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - workflowDispatch = workflow.WorkflowDispatchConfig() - assert.NotNil(t, workflowDispatch) - assert.Nil(t, workflowDispatch.Inputs) - - yaml = ` - name: local-action-docker-url - on: - push: - pull_request: - ` - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - workflowDispatch = workflow.WorkflowDispatchConfig() - assert.Nil(t, workflowDispatch) - - yaml = ` - name: local-action-docker-url - on: - push: - pull_request: - workflow_dispatch: - inputs: - logLevel: - description: 'Log level' - required: true - default: 'warning' - type: choice - options: - - info - - warning - - debug - ` - workflow, err = ReadWorkflow(strings.NewReader(yaml)) - assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - workflowDispatch = workflow.WorkflowDispatchConfig() - assert.NotNil(t, workflowDispatch) - assert.Equal(t, WorkflowDispatchInput{ - Default: "warning", - Description: "Log level", - Options: []string{ - "info", - "warning", - "debug", - }, - Required: true, - Type: "choice", - }, workflowDispatch.Inputs["logLevel"]) -} - -func TestStep_UsesHash(t *testing.T) { - type fields struct { - Uses string - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "regular", - fields: fields{ - Uses: "https://gitea.com/testa/testb@v3", - }, - want: "ae437878e9f285bd7518c58664f9fabbb12d05feddd7169c01702a2a14322aa8", - }, - { - name: "empty", - fields: fields{ - Uses: "", - }, - want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Step{ - Uses: tt.fields.Uses, - } - assert.Equalf(t, tt.want, s.UsesHash(), "UsesHash()") - }) - } -} - -func TestNormalizeMatrixValue(t *testing.T) { - tests := []struct { - name string - key string - value any - wantResult []any - wantErr bool - errMsg string - }{ - { - name: "array_values_pass_through", - key: "version", - value: []any{"1.0", "2.0", "3.0"}, - wantResult: []any{"1.0", "2.0", "3.0"}, - wantErr: false, - }, - { - name: "string_scalar_wrapped", - key: "os", - value: "ubuntu-latest", - wantResult: []any{"ubuntu-latest"}, - wantErr: false, - }, - { - name: "template_expression_wrapped", - key: "version", - value: "${{ fromJson(needs.setup.outputs.versions) }}", - wantResult: []any{"${{ fromJson(needs.setup.outputs.versions) }}"}, - wantErr: false, - }, - { - name: "integer_scalar_wrapped", - key: "count", - value: 42, - wantResult: []any{42}, - wantErr: false, - }, - { - name: "float_scalar_wrapped", - key: "factor", - value: 3.14, - wantResult: []any{3.14}, - wantErr: false, - }, - { - name: "bool_scalar_wrapped", - key: "enabled", - value: true, - wantResult: []any{true}, - wantErr: false, - }, - { - name: "nil_scalar_wrapped", - key: "optional", - value: nil, - wantResult: []any{nil}, - wantErr: false, - }, - { - name: "nested_map_returns_error", - key: "config", - value: map[string]any{"nested": "value"}, - wantErr: true, - errMsg: "has invalid nested object value", - }, - { - name: "empty_array_passes_through", - key: "empty", - value: []any{}, - wantResult: []any{}, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := normalizeMatrixValue(tt.key, tt.value) - - if tt.wantErr { - assert.Error(t, err, "should return error") //nolint:testifylint // pre-existing issue from nektos/act - if tt.errMsg != "" { - assert.Contains(t, err.Error(), tt.errMsg) - } - } else { - assert.NoError(t, err, "should not return error") //nolint:testifylint // pre-existing issue from nektos/act - assert.Equal(t, tt.wantResult, result, "result should match expected") - } - }) - } -} - -func TestJobMatrix(t *testing.T) { - tests := []struct { - name string - yaml string - wantErr bool - wantLen int - checkFn func(*testing.T, map[string][]any) - }{ - { - name: "matrix_with_arrays", - yaml: ` -name: test -on: push -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - version: [1.18, 1.19] - steps: - - run: echo test -`, - wantErr: false, - wantLen: 2, - checkFn: func(t *testing.T, m map[string][]any) { - assert.Equal(t, []any{"ubuntu-latest", "windows-latest"}, m["os"]) - assert.Equal(t, []any{1.18, 1.19}, m["version"]) - }, - }, - { - name: "matrix_with_scalar_values", - yaml: ` -name: test -on: push -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - os: ubuntu-latest - version: 1.19 - steps: - - run: echo test -`, - wantErr: false, - wantLen: 2, - checkFn: func(t *testing.T, m map[string][]any) { - assert.Equal(t, []any{"ubuntu-latest"}, m["os"]) - assert.Equal(t, []any{1.19}, m["version"]) - }, - }, - { - name: "matrix_with_template_expression", - yaml: ` -name: test -on: push -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - versions: ${{ fromJson(needs.setup.outputs.versions) }} - steps: - - run: echo test -`, - wantErr: false, - wantLen: 1, - checkFn: func(t *testing.T, m map[string][]any) { - assert.Equal(t, []any{"${{ fromJson(needs.setup.outputs.versions) }}"}, m["versions"]) - }, - }, - { - name: "matrix_mixed_arrays_and_scalars", - yaml: ` -name: test -on: push -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - version: 1.19 - node: [14, 16] - steps: - - run: echo test -`, - wantErr: false, - wantLen: 3, - checkFn: func(t *testing.T, m map[string][]any) { - assert.Equal(t, []any{"ubuntu-latest", "windows-latest"}, m["os"]) - assert.Equal(t, []any{1.19}, m["version"]) - assert.Equal(t, []any{14, 16}, m["node"]) - }, - }, - { - name: "empty_matrix", - yaml: ` -name: test -on: push -jobs: - build: - runs-on: ubuntu-latest - steps: - - run: echo test -`, - wantErr: false, - wantLen: 0, - checkFn: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - workflow, err := ReadWorkflow(strings.NewReader(tt.yaml)) - assert.NoError(t, err, "reading workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act - - job := workflow.GetJob("build") - if job == nil { - // For empty matrix test - if tt.wantLen == 0 { - return - } - t.Fatal("job not found") - } - - matrix, err := job.Matrix() - - if tt.wantErr { - require.Error(t, err) - assert.Nil(t, matrix, "matrix should be nil on error") - } else { - require.NoError(t, err) - if tt.wantLen == 0 { - assert.Empty(t, matrix, "no matrix for jobs without strategy") - } else { - assert.NotNil(t, matrix, "matrix should not be nil") - assert.Len(t, matrix, tt.wantLen, "matrix should have expected number of keys") - if tt.checkFn != nil { - tt.checkFn(t, matrix) - } - } - } - }) - } -} - -func TestJobMatrixValidation(t *testing.T) { - // This test verifies that invalid nested map values are caught - t.Run("matrix_with_nested_map_fails", func(t *testing.T) { - // Manually construct a job with a problematic matrix containing a nested map - job := &Job{ - Strategy: &Strategy{ - RawMatrix: yaml.Node{ - Kind: yaml.MappingNode, - Content: []*yaml.Node{ - {Kind: yaml.ScalarNode, Tag: "!!str", Value: "config"}, - {Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{ - {Kind: yaml.ScalarNode, Tag: "!!str", Value: "nested"}, - {Kind: yaml.ScalarNode, Tag: "!!str", Value: "value"}, - }}, - }, - }, - }, - } - - matrix, err := job.Matrix() - require.ErrorContains(t, err, `matrix key "config" has invalid nested object value`) - assert.Nil(t, matrix) - }) -} - -func readJob(t *testing.T, content string) *Job { - t.Helper() - - var job Job - require.NoError(t, yaml.Unmarshal([]byte(content), &job)) - return &job -} - -func readStep(t *testing.T, content string) *Step { - t.Helper() - - var step Step - require.NoError(t, yaml.Unmarshal([]byte(content), &step)) - return &step -} diff --git a/act/runner/action.go b/act/runner/action.go index da2e811a..9cd9602e 100644 --- a/act/runner/action.go +++ b/act/runner/action.go @@ -23,8 +23,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/common/git" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/kballard/go-shellquote" ) diff --git a/act/runner/action_cache_test.go b/act/runner/action_cache_test.go index 5b185964..e5a61272 100644 --- a/act/runner/action_cache_test.go +++ b/act/runner/action_cache_test.go @@ -18,8 +18,8 @@ import ( "time" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/act/runner/action_composite.go b/act/runner/action_composite.go index 6dd12acd..202e7944 100644 --- a/act/runner/action_composite.go +++ b/act/runner/action_composite.go @@ -13,7 +13,8 @@ import ( "strings" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + + "gitea.dev/actions-proto-go/pkg/model" ) func evaluateCompositeInputAndEnv(ctx context.Context, parent *RunContext, step actionStep) map[string]string { diff --git a/act/runner/action_test.go b/act/runner/action_test.go index eb068881..1ae055fe 100644 --- a/act/runner/action_test.go +++ b/act/runner/action_test.go @@ -16,8 +16,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/common/git" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/act/runner/cancellation_test.go b/act/runner/cancellation_test.go index add340ce..f8bf434f 100644 --- a/act/runner/cancellation_test.go +++ b/act/runner/cancellation_test.go @@ -9,9 +9,9 @@ import ( "time" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/exprparser" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/exprparser" + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" diff --git a/act/runner/command_test.go b/act/runner/command_test.go index 5869456c..214115d1 100644 --- a/act/runner/command_test.go +++ b/act/runner/command_test.go @@ -12,8 +12,8 @@ import ( "testing" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" ) diff --git a/act/runner/expression.go b/act/runner/expression.go index 6a2c94a9..93e9770f 100644 --- a/act/runner/expression.go +++ b/act/runner/expression.go @@ -17,11 +17,11 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/exprparser" - "gitea.com/gitea/runner/act/model" _ "embed" + "gitea.dev/actions-proto-go/pkg/exprparser" + "gitea.dev/actions-proto-go/pkg/model" "go.yaml.in/yaml/v4" ) diff --git a/act/runner/expression_test.go b/act/runner/expression_test.go index e988e086..9a4d6482 100644 --- a/act/runner/expression_test.go +++ b/act/runner/expression_test.go @@ -9,9 +9,8 @@ import ( "strings" "testing" - "gitea.com/gitea/runner/act/exprparser" - "gitea.com/gitea/runner/act/model" - + "gitea.dev/actions-proto-go/pkg/exprparser" + "gitea.dev/actions-proto-go/pkg/model" assert "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" yaml "go.yaml.in/yaml/v4" diff --git a/act/runner/job_executor.go b/act/runner/job_executor.go index 753a1a60..69c35e39 100644 --- a/act/runner/job_executor.go +++ b/act/runner/job_executor.go @@ -23,8 +23,9 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/exprparser" - "gitea.com/gitea/runner/act/model" + + "gitea.dev/actions-proto-go/pkg/exprparser" + "gitea.dev/actions-proto-go/pkg/model" ) const maxJobSummaryBytes = 1024 * 1024 diff --git a/act/runner/job_executor_test.go b/act/runner/job_executor_test.go index 6247c6eb..aab5a3e9 100644 --- a/act/runner/job_executor_test.go +++ b/act/runner/job_executor_test.go @@ -22,8 +22,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" log "github.com/sirupsen/logrus" logrustest "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" diff --git a/act/runner/job_hooks_test.go b/act/runner/job_hooks_test.go index 51ec4fc8..3fc26d80 100644 --- a/act/runner/job_hooks_test.go +++ b/act/runner/job_hooks_test.go @@ -12,8 +12,8 @@ import ( "testing" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/act/runner/max_parallel_test.go b/act/runner/max_parallel_test.go index aba5d980..fcde6b31 100644 --- a/act/runner/max_parallel_test.go +++ b/act/runner/max_parallel_test.go @@ -6,8 +6,7 @@ package runner import ( "testing" - "gitea.com/gitea/runner/act/model" - + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "go.yaml.in/yaml/v4" ) diff --git a/act/runner/reusable_workflow.go b/act/runner/reusable_workflow.go index 00e98027..c032f710 100644 --- a/act/runner/reusable_workflow.go +++ b/act/runner/reusable_workflow.go @@ -16,7 +16,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/common/git" - "gitea.com/gitea/runner/act/model" + + "gitea.dev/actions-proto-go/pkg/model" ) func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor { diff --git a/act/runner/reusable_workflow_test.go b/act/runner/reusable_workflow_test.go index 401ee39d..5d44ff2b 100644 --- a/act/runner/reusable_workflow_test.go +++ b/act/runner/reusable_workflow_test.go @@ -14,8 +14,8 @@ import ( "time" "gitea.com/gitea/runner/act/common/git" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/require" ) diff --git a/act/runner/run_context.go b/act/runner/run_context.go index 29e53edf..6780bf85 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -26,10 +26,11 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/exprparser" - "gitea.com/gitea/runner/act/model" + "gitea.com/gitea/runner/act/ghcontext" "gitea.com/gitea/runner/internal/pkg/lock" + "gitea.dev/actions-proto-go/pkg/exprparser" + "gitea.dev/actions-proto-go/pkg/model" "github.com/docker/cli/cli/compose/loader" "github.com/docker/go-connections/nat" "github.com/moby/moby/api/types/mount" @@ -1154,12 +1155,12 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext ghc.SetBaseAndHeadRef() repoPath := rc.Config.Workdir - ghc.SetRepositoryAndOwner(ctx, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath) + ghcontext.SetRepositoryAndOwner(ctx, ghc, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath) if ghc.Ref == "" { - ghc.SetRef(ctx, rc.Config.DefaultBranch, repoPath) + ghcontext.SetRef(ctx, ghc, rc.Config.DefaultBranch, repoPath) } if ghc.Sha == "" { - ghc.SetSha(ctx, repoPath) + ghcontext.SetSha(ctx, ghc, repoPath) } ghc.SetRefTypeAndName() diff --git a/act/runner/run_context_test.go b/act/runner/run_context_test.go index 8179acfa..69e9a194 100644 --- a/act/runner/run_context_test.go +++ b/act/runner/run_context_test.go @@ -16,9 +16,9 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/exprparser" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/exprparser" + "gitea.dev/actions-proto-go/pkg/model" "github.com/docker/cli/cli/compose/loader" log "github.com/sirupsen/logrus" assert "github.com/stretchr/testify/assert" diff --git a/act/runner/runner.go b/act/runner/runner.go index e38e8dea..3e8a0d2b 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -16,8 +16,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" docker_container "github.com/moby/moby/api/types/container" log "github.com/sirupsen/logrus" ) diff --git a/act/runner/runner_test.go b/act/runner/runner_test.go index 922c0f02..818e697a 100644 --- a/act/runner/runner_test.go +++ b/act/runner/runner_test.go @@ -19,8 +19,8 @@ import ( "time" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/joho/godotenv" log "github.com/sirupsen/logrus" assert "github.com/stretchr/testify/assert" @@ -312,7 +312,7 @@ func TestRunEvent(t *testing.T) { {workdir, "workflow_dispatch-scalar", "workflow_dispatch", "", platforms, secrets}, {workdir, "workflow_dispatch-scalar-composite-action", "workflow_dispatch", "", platforms, secrets}, {workdir, "job-needs-context-contains-result", "push", "", platforms, secrets}, - {"../model/testdata", "container-volumes", "push", "", platforms, secrets}, + {workdir, "container-volumes", "push", "", platforms, secrets}, {workdir, "path-handling", "push", "", platforms, secrets}, {workdir, "do-not-leak-step-env-in-composite", "push", "", platforms, secrets}, {workdir, "set-env-step-env-override", "push", "", platforms, secrets}, diff --git a/act/runner/step.go b/act/runner/step.go index 01095862..4dbafe4f 100644 --- a/act/runner/step.go +++ b/act/runner/step.go @@ -15,8 +15,9 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/exprparser" - "gitea.com/gitea/runner/act/model" + + "gitea.dev/actions-proto-go/pkg/exprparser" + "gitea.dev/actions-proto-go/pkg/model" ) type step interface { diff --git a/act/runner/step_action_local.go b/act/runner/step_action_local.go index dd0884a6..d04a902d 100644 --- a/act/runner/step_action_local.go +++ b/act/runner/step_action_local.go @@ -16,7 +16,8 @@ import ( "path/filepath" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + + "gitea.dev/actions-proto-go/pkg/model" ) type stepActionLocal struct { diff --git a/act/runner/step_action_local_test.go b/act/runner/step_action_local_test.go index a17711ae..14d10e14 100644 --- a/act/runner/step_action_local_test.go +++ b/act/runner/step_action_local_test.go @@ -13,8 +13,8 @@ import ( "testing" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.yaml.in/yaml/v4" diff --git a/act/runner/step_action_remote.go b/act/runner/step_action_remote.go index d144049a..8ae158e4 100644 --- a/act/runner/step_action_remote.go +++ b/act/runner/step_action_remote.go @@ -18,8 +18,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/common/git" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" gogit "github.com/go-git/go-git/v5" ) diff --git a/act/runner/step_action_remote_test.go b/act/runner/step_action_remote_test.go index 473cbb62..c3dbf6ec 100644 --- a/act/runner/step_action_remote_test.go +++ b/act/runner/step_action_remote_test.go @@ -19,8 +19,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/common/git" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/act/runner/step_docker.go b/act/runner/step_docker.go index a0b9edc6..6fb31c76 100644 --- a/act/runner/step_docker.go +++ b/act/runner/step_docker.go @@ -11,8 +11,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/kballard/go-shellquote" ) diff --git a/act/runner/step_docker_test.go b/act/runner/step_docker_test.go index 2aac21f3..ba456a88 100644 --- a/act/runner/step_docker_test.go +++ b/act/runner/step_docker_test.go @@ -12,8 +12,8 @@ import ( "testing" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/act/runner/step_factory.go b/act/runner/step_factory.go index af77ca56..f7e81c9b 100644 --- a/act/runner/step_factory.go +++ b/act/runner/step_factory.go @@ -7,7 +7,7 @@ package runner import ( "fmt" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" ) type stepFactory interface { diff --git a/act/runner/step_factory_test.go b/act/runner/step_factory_test.go index f428820d..2b3995dd 100644 --- a/act/runner/step_factory_test.go +++ b/act/runner/step_factory_test.go @@ -7,8 +7,7 @@ package runner import ( "testing" - "gitea.com/gitea/runner/act/model" - + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" ) diff --git a/act/runner/step_run.go b/act/runner/step_run.go index 3a04b7ed..9b83e685 100644 --- a/act/runner/step_run.go +++ b/act/runner/step_run.go @@ -15,8 +15,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" "gitea.com/gitea/runner/act/lookpath" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/kballard/go-shellquote" yaml "go.yaml.in/yaml/v4" ) diff --git a/act/runner/step_run_print_test.go b/act/runner/step_run_print_test.go index 988e75d2..ee6ceebd 100644 --- a/act/runner/step_run_print_test.go +++ b/act/runner/step_run_print_test.go @@ -11,8 +11,8 @@ import ( "testing" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/act/runner/step_run_test.go b/act/runner/step_run_test.go index 17d569d6..2666b92c 100644 --- a/act/runner/step_run_test.go +++ b/act/runner/step_run_test.go @@ -11,8 +11,8 @@ import ( "testing" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) diff --git a/act/runner/step_test.go b/act/runner/step_test.go index fa9d95c5..4ce7725c 100644 --- a/act/runner/step_test.go +++ b/act/runner/step_test.go @@ -9,8 +9,8 @@ import ( "testing" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" + "gitea.dev/actions-proto-go/pkg/model" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/act/model/testdata/container-volumes/push.yml b/act/runner/testdata/container-volumes/push.yml similarity index 100% rename from act/model/testdata/container-volumes/push.yml rename to act/runner/testdata/container-volumes/push.yml diff --git a/act/runner/toolkit_patch.go b/act/runner/toolkit_patch.go index 3a3364d3..71f916bd 100644 --- a/act/runner/toolkit_patch.go +++ b/act/runner/toolkit_patch.go @@ -13,7 +13,8 @@ import ( "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/common/git" - "gitea.com/gitea/runner/act/model" + + "gitea.dev/actions-proto-go/pkg/model" ) // Actions bundle the @actions toolkit into their own JavaScript, and two of its lines keep it diff --git a/act/runner/toolkit_patch_test.go b/act/runner/toolkit_patch_test.go index 2b62b6d5..b24f4bce 100644 --- a/act/runner/toolkit_patch_test.go +++ b/act/runner/toolkit_patch_test.go @@ -12,8 +12,7 @@ import ( "strings" "testing" - "gitea.com/gitea/runner/act/model" - + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/go.mod b/go.mod index 3939b248..c5357dcd 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.0 require ( connectrpc.com/connect v1.20.0 dario.cat/mergo v1.0.2 - gitea.dev/actions-proto-go v0.6.0 + gitea.dev/actions-proto-go v0.6.1-0.20260804005058-906483894e64 github.com/Masterminds/semver v1.5.0 github.com/avast/retry-go/v5 v5.0.0 github.com/containerd/errdefs v1.0.0 @@ -30,7 +30,6 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.24.0 github.com/prometheus/client_model v0.6.2 - github.com/rhysd/actionlint v1.7.12 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -89,6 +88,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/common v0.70.0 // indirect github.com/prometheus/procfs v0.21.1 // indirect + github.com/rhysd/actionlint v1.7.12 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/skeema/knownhosts v1.3.2 // indirect diff --git a/go.sum b/go.sum index 02862ba5..49f2c11a 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o= cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -gitea.dev/actions-proto-go v0.6.0 h1:gjllYQ5vmwlkqOeofTQu5qKTZpmf7kWsafoHvoPCSzY= -gitea.dev/actions-proto-go v0.6.0/go.mod h1:p4RX+D9oqiEEzzkPMXscw2CmaGuYFPWFc6xIOmDNDqs= +gitea.dev/actions-proto-go v0.6.1-0.20260804005058-906483894e64 h1:ZD+XdGu5hbO8t3VIXMLHxADRUhLlZxSQoSxgKiktQE8= +gitea.dev/actions-proto-go v0.6.1-0.20260804005058-906483894e64/go.mod h1:u7ERHK63QBsVOcJ4nD0EFZsEmhlovEo1muSOlKYqIRY= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= diff --git a/internal/app/cmd/exec.go b/internal/app/cmd/exec.go index 4eb76cfc..bcf30ec1 100644 --- a/internal/app/cmd/exec.go +++ b/internal/app/cmd/exec.go @@ -20,10 +20,10 @@ import ( "gitea.com/gitea/runner/act/artifactcache" "gitea.com/gitea/runner/act/artifacts" "gitea.com/gitea/runner/act/common" - "gitea.com/gitea/runner/act/model" "gitea.com/gitea/runner/act/runner" "gitea.com/gitea/runner/internal/app/run" + "gitea.dev/actions-proto-go/pkg/model" "github.com/joho/godotenv" "github.com/moby/moby/api/types/container" log "github.com/sirupsen/logrus" diff --git a/internal/app/cmd/exec_test.go b/internal/app/cmd/exec_test.go index b3ce7c49..f2f6f6c6 100644 --- a/internal/app/cmd/exec_test.go +++ b/internal/app/cmd/exec_test.go @@ -12,8 +12,7 @@ import ( "strings" "testing" - "gitea.com/gitea/runner/act/model" - + "gitea.dev/actions-proto-go/pkg/model" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" ) diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index 4da04447..18323945 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -24,7 +24,6 @@ import ( "gitea.com/gitea/runner/act/artifactcache" "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/container" - "gitea.com/gitea/runner/act/model" "gitea.com/gitea/runner/act/runner" "gitea.com/gitea/runner/internal/pkg/client" "gitea.com/gitea/runner/internal/pkg/config" @@ -34,6 +33,7 @@ import ( "gitea.com/gitea/runner/internal/pkg/ver" "connectrpc.com/connect" + "gitea.dev/actions-proto-go/pkg/model" runnerv1 "gitea.dev/actions-proto-go/runner/v1" docker_container "github.com/moby/moby/api/types/container" log "github.com/sirupsen/logrus" diff --git a/internal/app/run/workflow.go b/internal/app/run/workflow.go index 519b7fa3..892948d1 100644 --- a/internal/app/run/workflow.go +++ b/internal/app/run/workflow.go @@ -9,8 +9,7 @@ import ( "sort" "strings" - "gitea.com/gitea/runner/act/model" - + "gitea.dev/actions-proto-go/pkg/model" runnerv1 "gitea.dev/actions-proto-go/runner/v1" "go.yaml.in/yaml/v4" ) diff --git a/internal/app/run/workflow_test.go b/internal/app/run/workflow_test.go index 52d3f9d4..2adbaca3 100644 --- a/internal/app/run/workflow_test.go +++ b/internal/app/run/workflow_test.go @@ -6,8 +6,7 @@ package run import ( "testing" - "gitea.com/gitea/runner/act/model" - + "gitea.dev/actions-proto-go/pkg/model" runnerv1 "gitea.dev/actions-proto-go/runner/v1" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" diff --git a/internal/pkg/client/header.go b/internal/pkg/client/header.go index ab29a558..64d43bb2 100644 --- a/internal/pkg/client/header.go +++ b/internal/pkg/client/header.go @@ -3,7 +3,11 @@ package client +import "gitea.dev/actions-proto-go/pkg/protocol" + +// The headers are defined in the shared protocol package so that Gitea and the +// runner cannot drift apart. const ( - UUIDHeader = "x-runner-uuid" - TokenHeader = "x-runner-token" + UUIDHeader = protocol.UUIDHeader + TokenHeader = protocol.TokenHeader )