detached 21505b0

test(workflow): pin the invalid workflow messages of the recorded scenarios

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close