-
Notifications
You must be signed in to change notification settings - Fork 0
298 lines (269 loc) · 10.9 KB
/
Copy pathci.yml
File metadata and controls
298 lines (269 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
name: CI
# Per-PR gate: two jobs. `test-fast` runs the pytest suite minus the
# @browser layer; `test-browser` runs only the @browser tests on its
# own runner so the Chromium binary can be cached independently of
# everything else. The 60-second soak smoke rides along in test-fast.
#
# Manual-only jobs (workflow_dispatch via the GitHub Actions "Run
# workflow" button): `nightly` runs the 8h soak; `fuzz-nightly`
# runs 30 min each of the cookie + robots LibFuzzer harnesses.
# Neither fires on a schedule — runner-minute cost outweighs the
# signal at this project's cadence; kick them off before a release
# or after a parser change.
on:
# Skip pushes/PRs that only touch docs — repeated `make docs`
# iterations or markdown tweaks shouldn't burn a runner. CI still
# fires the moment a code/test/config file is in the diff
# alongside the docs change. workflow_dispatch is always
# available for manual override.
pull_request:
branches: [ main ]
paths-ignore:
- 'docs/**'
- 'gh-pages/**'
- '**/*.md'
- 'LICENSE'
push:
branches: [ main ]
paths-ignore:
- 'docs/**'
- 'gh-pages/**'
- '**/*.md'
- 'LICENSE'
# Manual trigger only — used to fire the 8h soak and the fuzz
# campaigns (each can spend hours of runner time, so we don't run
# them on a cron). Also lets you re-run the whole CI on a docs-only
# commit when you're done iterating and want one final check. Hit
# the "Run workflow" button in GitHub Actions to invoke.
workflow_dispatch:
env:
# Shared env so provision + tests use the same paths.
BS_BASE: "https://localhost"
jobs:
# -------- Per-PR gate: fast tests + short soak smoke. --------
test-fast:
name: pytest (not browser) + short soak — Rocky
runs-on: ubuntu-24.04
# Rocky, because that is what production runs. This suite spent
# months red on Ubuntu for reasons that were purely about the
# platform rather than the module: the harness invoked the RHEL
# binary name against Debian paths, and nobody read the log closely
# enough to notice. Testing on the deployment target removes the
# whole class.
#
# A container, not a self-hosted runner: free, disposable, and no
# machine to expose. It has no init, so the harness signals httpd
# directly rather than through systemd -- see BS_SERVICE_MODE.
container:
image: rockylinux/rockylinux:8
options: --privileged
timeout-minutes: 25
# Live-provider tests need the secrets below. Without them the
# tests SKIP (provision.sh installs the publicly-documented
# always-pass keys for Turnstile / hCaptcha / reCAPTCHA v2, which
# is enough for the most critical integration paths to run). Real
# v3 / Friendly / GeeTest exercise only the plumbing branches
# unless the OK-branch env vars below are set.
env:
# By address, not by name. The container resolves localhost to ::1
# first, and an instance listening only on IPv4 then refuses every
# connection made by name -- which is how every test failed with
# connection refused right after the step's own curl check, which
# used the address, had succeeded.
BS_BASE: https://127.0.0.1:8443
BS_RECAPTCHA_V3_TOKEN: ${{ secrets.BS_RECAPTCHA_V3_TOKEN }}
BS_FRIENDLY_SOLUTION: ${{ secrets.BS_FRIENDLY_SOLUTION }}
BS_GEETEST_TOKEN: ${{ secrets.BS_GEETEST_TOKEN }}
steps:
- name: Container prerequisites
# The image is minimal: no git for checkout, no sudo, and the
# harness shells out to sudo everywhere. Running as root, a sudo
# that just execs is the smallest change that keeps one code path
# for both container and developer box.
run: |
dnf install -y --setopt=install_weak_deps=False \
git make gcc httpd httpd-devel openssl openssl-devel \
libcurl-devel json-c-devel python3 python3-pip diffutils \
procps-ng iproute redhat-rpm-config python3.11 mod_ssl >/dev/null
printf '#!/bin/sh\nexec "$@"\n' > /usr/bin/sudo
chmod 755 /usr/bin/sudo
- uses: actions/checkout@v7
- name: Provision the test instance
run: tests/setup/provision-rocky.sh
- name: Run fast tests (pytest, not browser, not slow)
# httpd is started here rather than in its own step. A daemon
# backgrounded by one step does not survive into the next one in
# a container job, and the symptom is every test failing with
# connection refused after a start step that looked fine --
# its own curl check passed before the process went away.
#
# Serial: the per-worker parallel instances are a separate
# provisioning step, and one container is one instance.
run: |
httpd -f /etc/httpd/bstest/httpd.conf -k start
sleep 2
curl -sk --max-time 5 -o /dev/null https://127.0.0.1:8443/ \
|| (tail -50 /var/log/httpd/bstest/error.log && exit 1)
tests/run --mark "not browser" --verbose
- name: Soak smoke (60s at 25 rps)
# The soak is a pytest test marked @slow + @serial. Running
# it via tests/run with --slow --match soak keeps it in the
# same framework + reports pipeline as every other test.
run: |
httpd -f /etc/httpd/bstest/httpd.conf -k start 2>/dev/null || true
sleep 2
tests/run --slow --match soak --verbose
- name: Upload reports
if: always()
uses: actions/upload-artifact@v7
with:
name: reports-fast
path: tests/reports/
retention-days: 14
- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: apache-logs-fast
# Rocky paths: this job runs in a Rocky container and was
# still pointing at the Debian log directory, so the one step
# whose whole purpose is to explain a failure uploaded
# nothing and the artifact never appeared. The state file and
# decision log come too, since the failures worth debugging
# here are about what survived a restart.
path: |
/var/log/httpd/bstest/
/var/lib/botshield-test/
if-no-files-found: warn
retention-days: 7
# -------- Per-PR gate: browser tests in an isolated job so the
# Chromium install + cache is independent of the fast lane. --------
test-browser:
name: pytest (browser) — headless Chromium
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
# Cache the Chromium binary across runs. provision.sh will
# `playwright install chromium` but it becomes a no-op when
# the cache hit populates ~/.cache/ms-playwright.
- name: Restore Playwright cache
id: pw-cache
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-v1-${{ hashFiles('tests/requirements-test.txt') }}
- name: Provision
run: sudo tests/setup/provision.sh
- name: Start Apache
run: |
sudo systemctl restart apache2
sleep 2
- name: Run browser tests (chromium)
run: tests/run --parallel --mark "browser" --verbose
- name: Upload reports
if: always()
uses: actions/upload-artifact@v7
with:
name: reports-browser
path: |
tests/reports/
tests/test-results/
retention-days: 14
- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: apache-logs-browser
path: |
/var/log/apache2/botshield-dev-error.log
/var/log/apache2/error.log
retention-days: 7
# -------- Manual: 8h soak. --------
# Available via the "Run workflow" button in GitHub Actions; not
# wired to any automatic trigger. 8 h × every-day burns runner
# minutes faster than the value warrants for a small project, so
# the cron was retired — kick it off manually before a release.
nightly:
name: 8h soak (manual)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 540 # 9h: 8h soak + provision overhead
steps:
- uses: actions/checkout@v7
- name: Provision
run: sudo tests/setup/provision.sh
- name: Start Apache
run: |
sudo systemctl restart apache2
sleep 2
- name: Full soak (8h at 50 rps)
env:
BS_SOAK_DURATION_SEC: "28800" # 8h
BS_SOAK_RPS: "50"
BS_SOAK_REPORT: /tmp/bs_soak_nightly.report
run: tests/run --slow --match soak --verbose
- name: Upload soak report
if: always()
uses: actions/upload-artifact@v7
with:
name: soak-nightly-report
path: |
/tmp/bs_soak_nightly.report
tests/reports/
retention-days: 30
- name: Upload Apache logs
if: always()
uses: actions/upload-artifact@v7
with:
name: apache-logs-nightly
path: |
/var/log/apache2/botshield-dev-error.log
/var/log/apache2/error.log
retention-days: 14
# -------- Manual: LibFuzzer campaigns. --------
# Two harnesses run sequentially within one job: fuzz_cookie
# (HMAC + GCM cookie parser via _fuzz_stubs.h) and fuzz_robots
# (robots.c parser, APR-only, no httpd dependency). 30 minutes
# each is enough to surface meaningful coverage on a mature
# corpus; longer campaigns belong on a dedicated runner. On a
# finding, the harness writes crash-/leak-/slow-unit-/timeout-
# <hash> reproducers next to itself in tests/fuzz/, which the
# artifact upload picks up.
#
# Manual-only — kick off via "Run workflow" before merging a
# parser change or before a release.
fuzz-nightly:
name: fuzz (cookie + robots, 30m each, manual)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
- name: Install clang + libfuzzer
run: |
sudo apt-get update
sudo apt-get install -y clang libclang-rt-dev pkg-config \
libapr1-dev libcrypto++-dev libssl-dev
- name: Build harnesses
run: |
make fuzz
make fuzz-robots
- name: Fuzz cookie parser (30 min)
run: tests/fuzz/run.sh --target cookie 1800
- name: Fuzz robots parser (30 min)
run: tests/fuzz/run.sh --target robots 1800
- name: Upload corpus + reproducers
if: always()
uses: actions/upload-artifact@v7
with:
name: fuzz-nightly-artifacts
path: |
tests/fuzz/corpus/
tests/fuzz/corpus-robots/
tests/fuzz/crash-*
tests/fuzz/leak-*
tests/fuzz/timeout-*
tests/fuzz/slow-unit-*
retention-days: 30
if-no-files-found: ignore