File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/workflow/workflow_test.go
555 lines · 17kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package workflow
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"errors"
133728e barerepo 1mo
5
"os/exec"
133728e barerepo 1mo
6
"strings"
133728e barerepo 1mo
7
"testing"
133728e barerepo 1mo
8
)
133728e barerepo 1mo
9
133728e barerepo 1mo
10
// The common case: a go repository whose whole CI is three run steps behind a checkout.
133728e barerepo 1mo
11
func TestParseTheOrdinaryWorkflow(t *testing.T) {
133728e barerepo 1mo
12
jobs, err := Parse(Dir+"/ci.yml", `
133728e barerepo 1mo
13
name: ci
133728e barerepo 1mo
14
on: [push]
133728e barerepo 1mo
15
env:
133728e barerepo 1mo
16
CGO_ENABLED: "0"
133728e barerepo 1mo
17
jobs:
133728e barerepo 1mo
18
test:
133728e barerepo 1mo
19
runs-on: ubuntu-latest
133728e barerepo 1mo
20
steps:
133728e barerepo 1mo
21
- uses: actions/checkout@v4
133728e barerepo 1mo
22
- name: build
133728e barerepo 1mo
23
run: go build ./...
133728e barerepo 1mo
24
- name: test
133728e barerepo 1mo
25
run: |
133728e barerepo 1mo
26
go vet ./...
133728e barerepo 1mo
27
go test ./...
133728e barerepo 1mo
28
`, Context{Repo: "john/bot", Ref: "refs/heads/master", SHA: "a3f9c2d"})
133728e barerepo 1mo
29
if err != nil {
133728e barerepo 1mo
30
t.Fatal(err)
133728e barerepo 1mo
31
}
133728e barerepo 1mo
32
if len(jobs) != 1 {
133728e barerepo 1mo
33
t.Fatalf("got %d jobs, want 1", len(jobs))
133728e barerepo 1mo
34
}
133728e barerepo 1mo
35
j := jobs[0]
133728e barerepo 1mo
36
if j.Workflow != "ci" || j.ID != "test" {
133728e barerepo 1mo
37
t.Errorf("job identity is %+v", j)
133728e barerepo 1mo
38
}
133728e barerepo 1mo
39
if len(j.RunsOn) != 1 || j.RunsOn[0] != "ubuntu-latest" {
133728e barerepo 1mo
40
t.Errorf("runs-on = %v", j.RunsOn)
133728e barerepo 1mo
41
}
133728e barerepo 1mo
42
for _, want := range []string{
133728e barerepo 1mo
43
"export CGO_ENABLED='0'",
133728e barerepo 1mo
44
"go build ./...",
133728e barerepo 1mo
45
"go vet ./...",
133728e barerepo 1mo
46
"go test ./...",
133728e barerepo 1mo
47
// Checkout is answered rather than skipped, because barerepo has already cloned.
133728e barerepo 1mo
48
"barerepo cloned this repository",
133728e barerepo 1mo
49
} {
133728e barerepo 1mo
50
if !strings.Contains(j.Script, want) {
133728e barerepo 1mo
51
t.Errorf("the script is missing %q\n%s", want, j.Script)
133728e barerepo 1mo
52
}
133728e barerepo 1mo
53
}
133728e barerepo 1mo
54
if len(j.Skipped) != 0 {
133728e barerepo 1mo
55
t.Errorf("nothing here is untranslatable, yet it skipped %+v", j.Skipped)
133728e barerepo 1mo
56
}
133728e barerepo 1mo
57
if !j.Runnable() {
133728e barerepo 1mo
58
t.Error("a job with three run steps is not runnable")
133728e barerepo 1mo
59
}
133728e barerepo 1mo
60
}
133728e barerepo 1mo
61
133728e barerepo 1mo
62
// Rule: never skip quietly. Every step barerepo does not run has to be named and explained.
133728e barerepo 1mo
63
func TestEverySkipIsNamedAndExplained(t *testing.T) {
133728e barerepo 1mo
64
jobs, err := Parse(Dir+"/ci.yml", `
133728e barerepo 1mo
65
jobs:
133728e barerepo 1mo
66
build:
133728e barerepo 1mo
67
runs-on: ubuntu-latest
133728e barerepo 1mo
68
steps:
133728e barerepo 1mo
69
- uses: actions/setup-node@v4
133728e barerepo 1mo
70
with: {node-version: 20}
133728e barerepo 1mo
71
- name: only on main
133728e barerepo 1mo
72
if: github.ref == 'refs/heads/main'
133728e barerepo 1mo
73
run: ./deploy.sh
133728e barerepo 1mo
74
- name: powershell
133728e barerepo 1mo
75
shell: pwsh
133728e barerepo 1mo
76
run: Write-Host hi
133728e barerepo 1mo
77
- name: real work
133728e barerepo 1mo
78
run: make ci
133728e barerepo 1mo
79
`, Context{Repo: "john/bot", Ref: "refs/heads/master", SHA: "a3f9c2d"})
133728e barerepo 1mo
80
if err != nil {
133728e barerepo 1mo
81
t.Fatal(err)
133728e barerepo 1mo
82
}
133728e barerepo 1mo
83
j := jobs[0]
133728e barerepo 1mo
84
if !strings.Contains(j.Script, "make ci") {
133728e barerepo 1mo
85
t.Error("the one runnable step did not survive the skipping")
133728e barerepo 1mo
86
}
133728e barerepo 1mo
87
// setup-node is answered with a check, not skipped, so it is not in the list below.
133728e barerepo 1mo
88
if !strings.Contains(j.Script, "command -v node") {
133728e barerepo 1mo
89
t.Errorf("setup-node did not become a check\n%s", j.Script)
133728e barerepo 1mo
90
}
133728e barerepo 1mo
91
want := map[string]string{
133728e barerepo 1mo
92
"only on main": "cannot evaluate",
133728e barerepo 1mo
93
"powershell": "pwsh shell",
133728e barerepo 1mo
94
}
133728e barerepo 1mo
95
if len(j.Skipped) != len(want) {
133728e barerepo 1mo
96
t.Fatalf("skipped %d steps, want %d: %+v", len(j.Skipped), len(want), j.Skipped)
133728e barerepo 1mo
97
}
133728e barerepo 1mo
98
for _, s := range j.Skipped {
133728e barerepo 1mo
99
frag, ok := want[s.Step]
133728e barerepo 1mo
100
if !ok {
133728e barerepo 1mo
101
t.Errorf("unexpected skip of %q", s.Step)
133728e barerepo 1mo
102
continue
133728e barerepo 1mo
103
}
133728e barerepo 1mo
104
if !strings.Contains(s.Reason, frag) {
133728e barerepo 1mo
105
t.Errorf("%s was skipped because %q, want it to mention %q", s.Step, s.Reason, frag)
133728e barerepo 1mo
106
}
133728e barerepo 1mo
107
}
133728e barerepo 1mo
108
}
133728e barerepo 1mo
109
133728e barerepo 1mo
110
// sh reads ${{ as a bad substitution, so an expression barerepo knows is filled, not passed through.
133728e barerepo 1mo
111
func TestKnownExpressionsAreFilledIn(t *testing.T) {
133728e barerepo 1mo
112
jobs, err := Parse(Dir+"/ci.yml", `
133728e barerepo 1mo
113
jobs:
133728e barerepo 1mo
114
b:
133728e barerepo 1mo
115
runs-on: [self-hosted, linux]
133728e barerepo 1mo
116
steps:
133728e barerepo 1mo
117
- run: echo building ${{ github.sha }} of ${{ github.repository }} on ${{ runner.os }}
133728e barerepo 1mo
118
`, Context{Repo: "john/bot", Ref: "refs/heads/master", SHA: "a3f9c2d"})
133728e barerepo 1mo
119
if err != nil {
133728e barerepo 1mo
120
t.Fatal(err)
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
j := jobs[0]
133728e barerepo 1mo
123
if len(j.RunsOn) != 2 || j.RunsOn[1] != "linux" {
133728e barerepo 1mo
124
t.Errorf("a runs-on list did not survive: %v", j.RunsOn)
133728e barerepo 1mo
125
}
133728e barerepo 1mo
126
if len(j.Skipped) != 0 {
133728e barerepo 1mo
127
t.Errorf("expressions barerepo knows were reported instead of filled: %+v", j.Skipped)
133728e barerepo 1mo
128
}
133728e barerepo 1mo
129
if strings.Contains(j.Script, "${{") {
133728e barerepo 1mo
130
t.Errorf("an expression reached the script, where sh calls it a bad substitution\n%s", j.Script)
133728e barerepo 1mo
131
}
133728e barerepo 1mo
132
133728e barerepo 1mo
133
// Run it the way the runner does, and the values have to arrive.
133728e barerepo 1mo
134
out, err := exec.Command("sh", "-c", j.Script).CombinedOutput()
133728e barerepo 1mo
135
if err != nil {
133728e barerepo 1mo
136
t.Fatalf("the script does not run: %v\n%s", err, out)
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
for _, want := range []string{"building a3f9c2d", "of john/bot"} {
133728e barerepo 1mo
139
if !strings.Contains(string(out), want) {
133728e barerepo 1mo
140
t.Errorf("the output is missing %q\n%s", want, out)
133728e barerepo 1mo
141
}
133728e barerepo 1mo
142
}
133728e barerepo 1mo
143
// runner.os is the machine's own answer, so it is whatever this machine is.
133728e barerepo 1mo
144
if !strings.Contains(string(out), "on ") {
133728e barerepo 1mo
145
t.Errorf("runner.os produced nothing\n%s", out)
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
}
133728e barerepo 1mo
148
133728e barerepo 1mo
149
// A workflow reading the environment directly must work too, because GitHub sets these.
133728e barerepo 1mo
150
func TestGitHubEnvironmentIsSet(t *testing.T) {
133728e barerepo 1mo
151
jobs, err := Parse(Dir+"/ci.yml", `
133728e barerepo 1mo
152
name: ci
133728e barerepo 1mo
153
jobs:
133728e barerepo 1mo
154
unit:
133728e barerepo 1mo
155
steps:
133728e barerepo 1mo
156
- run: echo "$GITHUB_REPOSITORY $GITHUB_REF_NAME $GITHUB_SHA $GITHUB_WORKFLOW $GITHUB_JOB $CI"
133728e barerepo 1mo
157
`, Context{Repo: "john/bot", Ref: "refs/heads/master", SHA: "a3f9c2d"})
133728e barerepo 1mo
158
if err != nil {
133728e barerepo 1mo
159
t.Fatal(err)
133728e barerepo 1mo
160
}
133728e barerepo 1mo
161
out, err := exec.Command("sh", "-c", jobs[0].Script).CombinedOutput()
133728e barerepo 1mo
162
if err != nil {
133728e barerepo 1mo
163
t.Fatalf("the script does not run: %v\n%s", err, out)
133728e barerepo 1mo
164
}
133728e barerepo 1mo
165
// ref_name is the branch alone, which is what a workflow means by it.
133728e barerepo 1mo
166
want := "john/bot master a3f9c2d ci unit true"
133728e barerepo 1mo
167
if !strings.Contains(string(out), want) {
133728e barerepo 1mo
168
t.Errorf("the environment is wrong.\n got: %s\nwant: %s", out, want)
133728e barerepo 1mo
169
}
133728e barerepo 1mo
170
}
133728e barerepo 1mo
171
133728e barerepo 1mo
172
// An expression barerepo cannot fill is left alone and reported, rather than guessed at.
133728e barerepo 1mo
173
func TestUnknownExpressionsAreStillReported(t *testing.T) {
133728e barerepo 1mo
174
jobs, err := Parse(Dir+"/ci.yml", `
133728e barerepo 1mo
175
jobs:
133728e barerepo 1mo
176
b:
133728e barerepo 1mo
177
steps:
133728e barerepo 1mo
178
- run: deploy --key ${{ secrets.DEPLOY_KEY }}
133728e barerepo 1mo
179
`, Context{Repo: "john/bot"})
133728e barerepo 1mo
180
if err != nil {
133728e barerepo 1mo
181
t.Fatal(err)
133728e barerepo 1mo
182
}
133728e barerepo 1mo
183
j := jobs[0]
133728e barerepo 1mo
184
if len(j.Skipped) != 1 || !strings.Contains(j.Skipped[0].Reason, "${{secrets.DEPLOY_KEY}}") {
133728e barerepo 1mo
185
t.Fatalf("an unfillable expression was not reported: %+v", j.Skipped)
133728e barerepo 1mo
186
}
133728e barerepo 1mo
187
if !strings.Contains(j.Script, "${{ secrets.DEPLOY_KEY }}") {
133728e barerepo 1mo
188
t.Error("an expression barerepo cannot fill was rewritten instead of left alone")
133728e barerepo 1mo
189
}
133728e barerepo 1mo
190
}
133728e barerepo 1mo
191
133728e barerepo 1mo
192
// A matrix builds several times, so barerepo queues one job per combination. Chapter 15A.
133728e barerepo 1mo
193
func TestAMatrixBecomesOneJobPerCombination(t *testing.T) {
133728e barerepo 1mo
194
jobs, err := Parse(Dir+"/ci.yml", `
133728e barerepo 1mo
195
jobs:
133728e barerepo 1mo
196
m:
133728e barerepo 1mo
197
runs-on: ${{ matrix.os }}
133728e barerepo 1mo
198
strategy:
133728e barerepo 1mo
199
matrix:
133728e barerepo 1mo
200
os: [ubuntu-latest, macos-latest]
133728e barerepo 1mo
201
go: ["1.25", "1.26"]
133728e barerepo 1mo
202
exclude:
133728e barerepo 1mo
203
- os: macos-latest
133728e barerepo 1mo
204
go: "1.25"
133728e barerepo 1mo
205
steps:
133728e barerepo 1mo
206
- run: echo testing go ${{ matrix.go }} on ${{ matrix.os }}
133728e barerepo 1mo
207
`, Context{Repo: "john/bot", SHA: "a3f9c2d"})
133728e barerepo 1mo
208
if err != nil {
133728e barerepo 1mo
209
t.Fatal(err)
133728e barerepo 1mo
210
}
133728e barerepo 1mo
211
// Two by two is four, less the one excluded.
133728e barerepo 1mo
212
if len(jobs) != 3 {
133728e barerepo 1mo
213
t.Fatalf("got %d jobs, want 3: %+v", len(jobs), jobs)
133728e barerepo 1mo
214
}
133728e barerepo 1mo
215
133728e barerepo 1mo
216
seen := map[string][]string{}
133728e barerepo 1mo
217
for _, j := range jobs {
133728e barerepo 1mo
218
if !j.Runnable() {
133728e barerepo 1mo
219
t.Errorf("%s is not runnable", j.Name)
219 of 555 lines · the whole file
history · rawbarerepo 0.1.0