Files
act_runner/pkg/model/workflow_test.go
silverwind b0ec3fa4fc fmt
2026-02-24 08:17:17 +01:00

639 lines
17 KiB
Go

package model
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
assert.Equal(t, []string{"ubuntu-latest"}, workflow.Jobs["test"].RunsOn())
}
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
assert.Equal(t, []string{"ubuntu-latest", "linux"}, workflow.Jobs["test"].RunsOn())
}
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
assert.Len(t, workflow.Jobs, 6)
jobType, err := workflow.Jobs["default-job"].Type()
require.NoError(t, err)
assert.Equal(t, JobTypeDefault, jobType)
jobType, err = workflow.Jobs["remote-reusable-workflow-yml"].Type()
require.NoError(t, err)
assert.Equal(t, JobTypeReusableWorkflowRemote, jobType)
jobType, err = workflow.Jobs["remote-reusable-workflow-yaml"].Type()
require.NoError(t, err)
assert.Equal(t, JobTypeReusableWorkflowRemote, jobType)
jobType, err = workflow.Jobs["remote-reusable-workflow-custom-path"].Type()
require.NoError(t, err)
assert.Equal(t, JobTypeReusableWorkflowRemote, jobType)
jobType, err = workflow.Jobs["local-reusable-workflow-yml"].Type()
require.NoError(t, err)
assert.Equal(t, JobTypeReusableWorkflowLocal, jobType)
jobType, err = workflow.Jobs["local-reusable-workflow-yaml"].Type()
require.NoError(t, err)
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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
assert.Len(t, workflow.Jobs, 4)
jobType, err := workflow.Jobs["remote-reusable-workflow-missing-version"].Type()
assert.Equal(t, JobTypeInvalid, jobType)
require.Error(t, err)
jobType, err = workflow.Jobs["remote-reusable-workflow-bad-extension"].Type()
assert.Equal(t, JobTypeInvalid, jobType)
require.Error(t, err)
jobType, err = workflow.Jobs["local-reusable-workflow-bad-extension"].Type()
assert.Equal(t, JobTypeInvalid, jobType)
require.Error(t, err)
jobType, err = workflow.Jobs["local-reusable-workflow-bad-path"].Type()
assert.Equal(t, JobTypeInvalid, jobType)
require.Error(t, err)
}
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
`
_, err := ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.Error(t, err, "read workflow should fail")
}
// 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), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
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", PlannerConfig{})
require.NoError(t, err)
p, err := w.PlanJob("strategy-only-max-parallel")
require.NoError(t, err)
assert.Len(t, p.Stages, 1)
assert.Len(t, p.Stages[0].Runs, 1)
wf := p.Stages[0].Runs[0].Workflow
job := wf.Jobs["strategy-only-max-parallel"]
matrixes, err := job.GetMatrixes()
require.NoError(t, err)
assert.Equal(t, []map[string]any{{}}, matrixes)
assert.Equal(t, job.Matrix(), map[string][]any(nil))
assert.Equal(t, 2, job.Strategy.MaxParallel)
assert.True(t, job.Strategy.FailFast)
job = wf.Jobs["strategy-only-fail-fast"]
matrixes, err = job.GetMatrixes()
require.NoError(t, err)
assert.Equal(t, []map[string]any{{}}, matrixes)
assert.Equal(t, job.Matrix(), map[string][]any(nil))
assert.Equal(t, 4, job.Strategy.MaxParallel)
assert.False(t, job.Strategy.FailFast)
job = wf.Jobs["strategy-no-matrix"]
matrixes, err = job.GetMatrixes()
require.NoError(t, err)
assert.Equal(t, []map[string]any{{}}, matrixes)
assert.Equal(t, job.Matrix(), map[string][]any(nil))
assert.Equal(t, 2, job.Strategy.MaxParallel)
assert.False(t, job.Strategy.FailFast)
job = wf.Jobs["strategy-all"]
matrixes, err = job.GetMatrixes()
require.NoError(t, err)
assert.Equal(t, []map[string]any{
{"datacenter": "site-c", "node-version": "14.x", "site": "staging", "php-version": 5.4},
{"datacenter": "site-c", "node-version": "16.x", "site": "staging", "php-version": 5.4},
{"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"},
}, matrixes,
)
assert.Equal(t, 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"},
}, job.Matrix(),
)
assert.Equal(t, 2, job.Strategy.MaxParallel)
assert.False(t, job.Strategy.FailFast)
}
func TestMatrixOnlyIncludes(t *testing.T) {
matrix := map[string][]any{
"include": {
map[string]any{"a": "1", "b": "2"},
map[string]any{"a": "3", "b": "4"},
},
}
rN := yaml.Node{}
err := rN.Encode(matrix)
require.NoError(t, err, "encoding matrix should succeed")
job := &Job{
Strategy: &Strategy{
RawMatrix: rN,
},
}
assert.Equal(t, job.Matrix(), matrix)
matrixes, err := job.GetMatrixes()
require.NoError(t, err)
assert.Equal(t, []map[string]any{
{"a": "1", "b": "2"},
{"a": "3", "b": "4"},
}, matrixes,
)
}
func TestStep_ShellCommand(t *testing.T) {
tests := []struct {
shell string
workflowShell string
want string
}{
{"pwsh -v '. {0}'", "", "pwsh -v '. {0}'"},
{"pwsh", "", "pwsh -command . '{0}'"},
{"powershell", "", "powershell -command . '{0}'"},
{"bash", "", "bash -e {0}"},
{"bash", "bash", "bash --noprofile --norc -e -o pipefail {0}"},
}
for _, tt := range tests {
t.Run(tt.shell, func(t *testing.T) {
got := (&Step{Shell: tt.shell, WorkflowShell: tt.workflowShell}).ShellCommand()
assert.Equal(t, tt.want, got)
})
}
}
func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
yaml := `
name: local-action-docker-url
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo Test
`
workflow, err := ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
workflowDispatch := workflow.WorkflowDispatchConfig()
assert.Nil(t, workflowDispatch)
yaml = `
name: local-action-docker-url
on: workflow_dispatch
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo Test
`
workflow, err = ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
workflowDispatch = workflow.WorkflowDispatchConfig()
assert.NotNil(t, workflowDispatch)
assert.Nil(t, workflowDispatch.Inputs)
yaml = `
name: local-action-docker-url
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo Test
`
workflow, err = ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
workflowDispatch = workflow.WorkflowDispatchConfig()
assert.Nil(t, workflowDispatch)
yaml = `
name: local-action-docker-url
on: [push, workflow_dispatch]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo Test
`
workflow, err = ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
workflowDispatch = workflow.WorkflowDispatchConfig()
assert.NotNil(t, workflowDispatch)
assert.Nil(t, workflowDispatch.Inputs)
yaml = `
name: local-action-docker-url
on:
- push
- workflow_dispatch
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo Test
`
workflow, err = ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
workflowDispatch = workflow.WorkflowDispatchConfig()
assert.NotNil(t, workflowDispatch)
assert.Nil(t, workflowDispatch.Inputs)
yaml = `
name: local-action-docker-url
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo Test
`
workflow, err = ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
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
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo Test
`
workflow, err = ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
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 TestReadWorkflow_InvalidStringEvent(t *testing.T) {
yaml := `
name: local-action-docker-url
on: push2
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: ./actions/docker-url
`
_, err := ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{Strict: true})
require.Error(t, err, "read workflow should succeed")
}
func TestReadWorkflow_AnchorStrict(t *testing.T) {
yaml := `
on: push
jobs:
test:
runs-on: &runner ubuntu-latest
steps:
- uses: &checkout actions/checkout@v5
test2:
runs-on: *runner
steps:
- uses: *checkout
`
w, err := ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{Strict: true})
require.NoError(t, err, "read workflow should succeed")
for _, job := range w.Jobs {
assert.Equal(t, []string{"ubuntu-latest"}, job.RunsOn())
assert.Equal(t, "actions/checkout@v5", job.Steps[0].Uses)
}
}
func TestReadWorkflow_Anchor(t *testing.T) {
yaml := `
jobs:
test:
runs-on: &runner ubuntu-latest
steps:
- uses: &checkout actions/checkout@v5
test2: &job
runs-on: *runner
steps:
- uses: *checkout
- run: echo $TRIGGER
env:
TRIGGER: &trigger push
test3: *job
on: push #*trigger
`
w, err := ReadWorkflow(strings.NewReader(yaml), WorkflowConfig{})
require.NoError(t, err, "read workflow should succeed")
for _, job := range w.Jobs {
assert.Equal(t, []string{"ubuntu-latest"}, job.RunsOn())
assert.Equal(t, "actions/checkout@v5", job.Steps[0].Uses)
}
}