Keep the order of `on` when parsing workflow (#46)

Keep the order of `on` when parsing workflow, and fix the occasional unit test failure of `actions` like https://gitea.com/gitea/act/actions/runs/68

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/46
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
This commit is contained in:
sillyguodong 2023-04-24 23:16:41 +08:00 committed by Lunny Xiao
parent 0c1f2edb99
commit 518d8c96f3
2 changed files with 133 additions and 30 deletions

View File

@ -25,35 +25,21 @@ func (w *SingleWorkflow) Job() (string, *Job) {
} }
func (w *SingleWorkflow) jobs() ([]string, []*Job, error) { func (w *SingleWorkflow) jobs() ([]string, []*Job, error) {
var ids []string ids, jobs, err := parseMappingNode[*Job](&w.RawJobs)
var jobs []*Job if err != nil {
expectKey := true return nil, nil, err
for _, item := range w.RawJobs.Content { }
if expectKey {
if item.Kind != yaml.ScalarNode { for _, job := range jobs {
return nil, nil, fmt.Errorf("invalid job id: %v", item.Value) steps := make([]*Step, 0, len(job.Steps))
for _, s := range job.Steps {
if s != nil {
steps = append(steps, s)
} }
ids = append(ids, item.Value)
expectKey = false
} else {
job := &Job{}
if err := item.Decode(job); err != nil {
return nil, nil, fmt.Errorf("yaml.Unmarshal: %w", err)
}
steps := make([]*Step, 0, len(job.Steps))
for _, s := range job.Steps {
if s != nil {
steps = append(steps, s)
}
}
job.Steps = steps
jobs = append(jobs, job)
expectKey = true
} }
job.Steps = steps
} }
if len(ids) != len(jobs) {
return nil, nil, fmt.Errorf("invalid jobs: %v", w.RawJobs.Value)
}
return ids, jobs, nil return ids, jobs, nil
} }
@ -232,13 +218,13 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
} }
return res, nil return res, nil
case yaml.MappingNode: case yaml.MappingNode:
var val map[string]interface{} events, triggers, err := parseMappingNode[interface{}](rawOn)
err := rawOn.Decode(&val)
if err != nil { if err != nil {
return nil, err return nil, err
} }
res := make([]*Event, 0, len(val)) res := make([]*Event, 0, len(events))
for k, v := range val { for i, k := range events {
v := triggers[i]
if v == nil { if v == nil {
res = append(res, &Event{ res = append(res, &Event{
Name: k, Name: k,
@ -312,3 +298,36 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
return nil, fmt.Errorf("unknown on type: %v", rawOn.Kind) return nil, fmt.Errorf("unknown on type: %v", rawOn.Kind)
} }
} }
// parseMappingNode parse a mapping node and preserve order.
func parseMappingNode[T any](node *yaml.Node) ([]string, []T, error) {
if node.Kind != yaml.MappingNode {
return nil, nil, fmt.Errorf("input node is not a mapping node")
}
var scalars []string
var datas []T
expectKey := true
for _, item := range node.Content {
if expectKey {
if item.Kind != yaml.ScalarNode {
return nil, nil, fmt.Errorf("not a valid scalar node: %v", item.Value)
}
scalars = append(scalars, item.Value)
expectKey = false
} else {
var val T
if err := item.Decode(&val); err != nil {
return nil, nil, err
}
datas = append(datas, val)
expectKey = true
}
}
if len(scalars) != len(datas) {
return nil, nil, fmt.Errorf("invalid definition of on: %v", node.Value)
}
return scalars, datas, nil
}

View File

@ -220,3 +220,87 @@ func TestSingleWorkflow_SetJob(t *testing.T) {
assert.Equal(t, string(want), builder.String()) assert.Equal(t, string(want), builder.String())
}) })
} }
func TestParseMappingNode(t *testing.T) {
tests := []struct {
input string
scalars []string
datas []interface{}
}{
{
input: "on:\n push:\n branches:\n - master",
scalars: []string{"push"},
datas: []interface {
}{
map[string]interface{}{
"branches": []interface{}{"master"},
},
},
},
{
input: "on:\n branch_protection_rule:\n types: [created, deleted]",
scalars: []string{"branch_protection_rule"},
datas: []interface{}{
map[string]interface{}{
"types": []interface{}{"created", "deleted"},
},
},
},
{
input: "on:\n project:\n types: [created, deleted]\n milestone:\n types: [opened, deleted]",
scalars: []string{"project", "milestone"},
datas: []interface{}{
map[string]interface{}{
"types": []interface{}{"created", "deleted"},
},
map[string]interface{}{
"types": []interface{}{"opened", "deleted"},
},
},
},
{
input: "on:\n pull_request:\n types:\n - opened\n branches:\n - 'releases/**'",
scalars: []string{"pull_request"},
datas: []interface{}{
map[string]interface{}{
"types": []interface{}{"opened"},
"branches": []interface{}{"releases/**"},
},
},
},
{
input: "on:\n push:\n branches:\n - main\n pull_request:\n types:\n - opened\n branches:\n - '**'",
scalars: []string{"push", "pull_request"},
datas: []interface{}{
map[string]interface{}{
"branches": []interface{}{"main"},
},
map[string]interface{}{
"types": []interface{}{"opened"},
"branches": []interface{}{"**"},
},
},
},
{
input: "on:\n schedule:\n - cron: '20 6 * * *'",
scalars: []string{"schedule"},
datas: []interface{}{
[]interface{}{map[string]interface{}{
"cron": "20 6 * * *",
}},
},
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
workflow, err := model.ReadWorkflow(strings.NewReader(test.input))
assert.NoError(t, err)
scalars, datas, err := parseMappingNode[interface{}](&workflow.RawOn)
assert.NoError(t, err)
assert.EqualValues(t, test.scalars, scalars, fmt.Sprintf("%#v", scalars))
assert.EqualValues(t, test.datas, datas, fmt.Sprintf("%#v", datas))
})
}
}