// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2020 The nektos/act Authors. All rights reserved. // SPDX-License-Identifier: MIT package model import ( "strings" "testing" "github.com/stretchr/testify/assert" "go.yaml.in/yaml/v4" ) 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.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act 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.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act 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.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act } 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 assert.Equal(t, job.Matrix(), map[string][]any(nil)) 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 assert.Equal(t, job.Matrix(), map[string][]any(nil)) 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 assert.Equal(t, job.Matrix(), map[string][]any(nil)) 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"}, }, ) assert.Equal(t, job.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 := job.Matrix() if tt.wantErr { assert.Nil(t, matrix, "matrix should be nil on error") } else { if tt.wantLen == 0 { assert.Nil(t, matrix, "matrix should be nil for jobs without strategy") } else { assert.NotNil(t, matrix, "matrix should not be nil") assert.Equal(t, tt.wantLen, len(matrix), "matrix should have expected number of keys") //nolint:testifylint // pre-existing issue from nektos/act 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"}, }}, }, }, }, } // Attempt to get matrix matrix := job.Matrix() // Should return nil due to validation error assert.Nil(t, matrix, "matrix with nested map should return nil") }) }