1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase
4
5import (
6 "strings"
7
8 "bnema.dev/ermite-actions/template"
9 "bnema.dev/ermite-actions/workflow"
10 "bnema.dev/ermite/internal/domain"
11)
12
13// parseWorkflow is workflow.Parse plus the checks GitHub makes when it reads a
14// workflow that the parser of ermite-actions does not: a schedule timezone that
15// is no IANA time zone name, and queue: max with cancel-in-progress: true (CI-4
16// D6). A workflow with such an error is invalid like one that does not parse.
17func parseWorkflow(path string, content []byte) (*workflow.Workflow, []workflow.Error) {
18 wf, errs := workflow.Parse(path, content)
19 if len(errs) > 0 || wf == nil {
20 return wf, errs
21 }
22 return wf, validateWorkflow(path, wf)
23}
24
25func validateWorkflow(path string, wf *workflow.Workflow) []workflow.Error {
26 var errs []workflow.Error
27 if wf.On != nil {
28 if ev := wf.On.Event(eventSchedule); ev != nil {
29 for _, sc := range ev.Schedule {
30 if _, err := domain.ParseCronTimezone(sc.Timezone); err != nil {
31 errs = append(errs, workflow.Error{File: path, Raw: strings.TrimPrefix(err.Error(), domain.ErrInvalid.Error()+": ")})
32 }
33 }
34 }
35 }
36 check := func(tok template.Token) {
37 if pos, bad := queueMaxWithCancel(tok); bad {
38 errs = append(errs, workflow.Error{File: path, Pos: pos, Raw: errQueueMaxCancel.Error()})
39 }
40 }
41 check(wf.Concurrency)
42 for _, j := range wf.Jobs {
43 check(j.Concurrency)
44 }
45 return errs
46}
47
48// queueMaxWithCancel reports a concurrency mapping that writes queue: max and
49// cancel-in-progress: true as literals, the pair GitHub refuses as a workflow
50// validation error (docs.github.com, "Control the concurrency of workflows and
51// jobs": "The combination of `queue: max` and `cancel-in-progress: true` is not
52// allowed and will result in a workflow validation error"; languageservices
53// languageservice/src/validate.ts:702-743 checks the literal pair). An
54// expression is checked when it is evaluated (evalConcurrency).
55func queueMaxWithCancel(tok template.Token) (template.Pos, bool) {
56 m, ok := tok.(*template.Mapping)
57 if !ok {
58 return template.Pos{}, false
59 }
60 q, ok := m.Find("queue").(*template.String)
61 if !ok || q.Value != "max" {
62 return template.Pos{}, false
63 }
64 c, ok := m.Find("cancel-in-progress").(*template.Boolean)
65 if !ok || !c.Value {
66 return template.Pos{}, false
67 }
68 return c.Pos(), true
69}