1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6 "reflect"
7 "testing"
8)
9
10// TestRecordedInvalidWorkflows parses the workflow files of the recorded
11// scenarios expr-status-in-env, expr-status-in-run, invalid-needs and
12// invalid-schema, which GitHub refused (a failed run without jobs; the
13// recordings do not hold the messages), and checks that this package refuses
14// them, with the reference parser's messages, except for the status functions
15// whose message was observed on the github.com run page. The valid sibling of
16// each scenario is in TestRecordedValid.
17func TestRecordedInvalidWorkflows(t *testing.T) {
18 const on = "on:\n push:\n branches: ['scn/**']\n"
19 tests := []struct {
20 name, src string
21 want []string
22 }{
23 {"expr-status-in-env", on + `jobs:
24 main:
25 runs-on: ubuntu-24.04
26 steps:
27 - name: status functions in env
28 env:
29 ST_SUCCESS: ${{ success() }}
30 ST_FAILURE: ${{ failure() }}
31 ST_ALWAYS: ${{ always() }}
32 ST_CANCELLED: ${{ cancelled() }}
33 run: env | grep '^ST_' | sort
34`, []string{
35 "w.yml (Line: 10, Col: 23): Unrecognized function: 'success'. Located at position 1 within expression: success()",
36 "w.yml (Line: 11, Col: 23): Unrecognized function: 'failure'. Located at position 1 within expression: failure()",
37 "w.yml (Line: 12, Col: 22): Unrecognized function: 'always'. Located at position 1 within expression: always()",
38 "w.yml (Line: 13, Col: 25): Unrecognized function: 'cancelled'. Located at position 1 within expression: cancelled()",
39 }},
40 {"expr-status-in-run", on + `jobs:
41 main:
42 runs-on: ubuntu-24.04
43 steps:
44 - name: status functions in run
45 run: |
46 echo "success=${{ success() }}"
47 echo "failure=${{ failure() }}"
48`, []string{
49 "w.yml (Line: 9, Col: 14): Unrecognized function: 'success'. Located at position 1 within expression: success()",
50 "w.yml (Line: 9, Col: 14): Unrecognized function: 'failure'. Located at position 1 within expression: failure()",
51 }},
52 {"invalid-needs unknown-need", on + `jobs:
53 main:
54 needs: nowhere
55 runs-on: ubuntu-24.04
56 steps:
57 - run: echo SHOULD-NOT-RUN
58`, []string{"w.yml (Line: 5, Col: 3): The workflow must contain at least one job with no dependencies."}},
59 {"invalid-needs unknown-need-with-root", on + `jobs:
60 root:
61 runs-on: ubuntu-24.04
62 steps:
63 - run: echo SHOULD-NOT-RUN
64 dependent:
65 needs: [root, nowhere]
66 runs-on: ubuntu-24.04
67 steps:
68 - run: echo SHOULD-NOT-RUN
69`, []string{"w.yml (Line: 10, Col: 19): Job 'dependent' depends on unknown job 'nowhere'."}},
70 {"invalid-needs cycle", on + `jobs:
71 a:
72 needs: b
73 runs-on: ubuntu-24.04
74 steps:
75 - run: echo SHOULD-NOT-RUN
76 b:
77 needs: a
78 runs-on: ubuntu-24.04
79 steps:
80 - run: echo SHOULD-NOT-RUN
81`, []string{"w.yml (Line: 5, Col: 3): The workflow must contain at least one job with no dependencies."}},
82 {"invalid-needs cycle-with-root", on + `jobs:
83 root:
84 runs-on: ubuntu-24.04
85 steps:
86 - run: echo SHOULD-NOT-RUN
87 a:
88 needs: [root, b]
89 runs-on: ubuntu-24.04
90 steps:
91 - run: echo SHOULD-NOT-RUN
92 b:
93 needs: a
94 runs-on: ubuntu-24.04
95 steps:
96 - run: echo SHOULD-NOT-RUN
97`, []string{
98 "w.yml (Line: 10, Col: 19): Job 'a' depends on job 'b' which creates a cycle in the dependency graph.",
99 "w.yml (Line: 15, Col: 12): Job 'b' depends on job 'a' which creates a cycle in the dependency graph.",
100 }},
101 {"invalid-schema unknown-key", on + `jobs:
102 main:
103 runs-on: ubuntu-24.04
104 unknown-key: true
105 steps:
106 - run: echo SHOULD-NOT-RUN
107`, []string{"w.yml (Line: 7, Col: 5): Unexpected value 'unknown-key'"}},
108 {"invalid-schema no-runs-on", on + `jobs:
109 main:
110 steps:
111 - run: echo SHOULD-NOT-RUN
112`, []string{"w.yml (Line: 6, Col: 5): Required property is missing: runs-on"}},
113 {"invalid-schema empty-step", on + `jobs:
114 main:
115 runs-on: ubuntu-24.04
116 steps:
117 - name: nothing to run
118`, []string{"w.yml (Line: 8, Col: 9): There's not enough info to determine what you meant. Add one of these properties: run, shell, uses, with, working-directory"}},
119 }
120 for _, tc := range tests {
121 t.Run(tc.name, func(t *testing.T) {
122 _, errs := Parse("w.yml", []byte(tc.src))
123 var got []string
124 for _, e := range errs {
125 got = append(got, e.Error())
126 }
127 if !reflect.DeepEqual(got, tc.want) {
128 t.Errorf("errors:\n got %q\n want %q", got, tc.want)
129 }
130 })
131 }
132}
133
134// TestRecordedValid checks that the valid siblings of the invalid scenarios
135// and the status functions in conditions, which GitHub accepted, parse.
136func TestRecordedValid(t *testing.T) {
137 for name, src := range map[string]string{
138 "valid sibling": "name: valid-sibling\non:\n push:\n branches: ['scn/**']\n tags: ['scn-*']\njobs:\n main:\n runs-on: ubuntu-24.04\n steps:\n - run: echo valid-sibling-ran\n",
139 "status functions in if": "on: push\njobs:\n main:\n runs-on: x\n if: success() || failure()\n steps:\n - if: always() && !cancelled()\n run: echo\n",
140 } {
141 if _, errs := Parse("w.yml", []byte(src)); len(errs) > 0 {
142 t.Errorf("%s: %v", name, errs)
143 }
144 }
145}