Skip to content

Commit d1a6a29

Browse files
authored
Merge pull request #1306 from PyThaiNLP/copilot/improve-fuzz-testing-workflow
feat: split ClusterFuzzLite into PR, batch, and corpus-pruning jobs
2 parents 0852423 + 8d4837c commit d1a6a29

2 files changed

Lines changed: 222 additions & 26 deletions

File tree

.github/workflows/clusterfuzzlite.yml

Lines changed: 205 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,56 @@ on:
2525
- '**.txt'
2626
- 'docs/**'
2727
schedule:
28-
- cron: '0 6 * * *' # Daily at 06:00 UTC
29-
30-
# Avoid duplicate runs for the same source branch and repository.
31-
# For pull_request events, uses the source repo name from
32-
# github.event.pull_request.head.repo.full_name; otherwise uses github.repository.
33-
# For push events, uses the branch name from github.ref_name.
34-
# For pull_request events, uses the source branch name from github.head_ref.
35-
# This ensures events for the same repo and branch share the same group,
36-
# and avoids cross-fork collisions when branch names are reused.
37-
concurrency:
38-
group: >-
39-
${{ github.workflow }}-${{
40-
github.event.pull_request.head.repo.full_name || github.repository
41-
}}-${{ github.head_ref || github.ref_name }}
42-
cancel-in-progress: true
28+
# Batch Fuzzing: 01:30 AM UTC+7 = 18:30 UTC
29+
- cron: '30 18 * * *'
30+
# Corpus Pruning: 04:00 AM UTC+7 = 21:00 UTC (2.5 h after Batch Fuzzing)
31+
- cron: '0 21 * * *'
4332

33+
# Restrict default permissions to read-only at the workflow level.
34+
# Each job that needs write access declares it explicitly.
4435
permissions:
45-
contents: write
46-
issues: write
36+
contents: read
4737

4838
jobs:
49-
fuzzing:
39+
# -------------------------------------------------------------------------
40+
# 1. PR Fuzzing
41+
# Quick check for "shallow" bugs introduced by new code.
42+
# Target: under 10 minutes. Triggered by pull_request or push.
43+
# -------------------------------------------------------------------------
44+
pr-fuzzing:
45+
name: PR Fuzzing
46+
if: github.event_name == 'pull_request' || github.event_name == 'push'
5047
runs-on: ubuntu-latest
48+
permissions:
49+
contents: write # Push corpus updates to gh-pages
50+
issues: write # Allow run_fuzzers to file issues on crashes
51+
# Cancel in-progress runs for the same branch to avoid wasted resources.
52+
# Uses the source repo name to avoid cross-fork collisions.
53+
concurrency:
54+
group: >-
55+
pr-fuzzing-${{
56+
github.event.pull_request.head.repo.full_name || github.repository
57+
}}-${{ github.head_ref || github.ref_name }}
58+
cancel-in-progress: true
5159
strategy:
5260
fail-fast: false
5361
matrix:
5462
sanitizer: [address]
5563
steps:
64+
- name: Summarize job parameters
65+
run: |
66+
{
67+
echo "## PR Fuzzing"
68+
echo ""
69+
echo "| Property | Value |"
70+
echo "| --- | --- |"
71+
echo "| Sanitizer | \`${{ matrix.sanitizer }}\` |"
72+
echo "| Mode | \`code-change\` |"
73+
echo "| Fuzz seconds | 300 |"
74+
echo "| Trigger | \`${{ github.event_name }}\` |"
75+
echo "| Ref | \`${{ github.head_ref || github.ref_name }}\` |"
76+
} >> "$GITHUB_STEP_SUMMARY"
77+
5678
- name: Build Fuzzers (${{ matrix.sanitizer }})
5779
id: build
5880
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
@@ -66,15 +88,178 @@ jobs:
6688
with:
6789
github-token: ${{ secrets.GITHUB_TOKEN }}
6890
fuzz-seconds: 300
69-
mode: ${{ github.event_name == 'pull_request' && 'code-change' || 'batch' }}
91+
mode: code-change
92+
sanitizer: ${{ matrix.sanitizer }}
93+
storage-repo: https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
94+
storage-repo-branch: gh-pages
95+
storage-repo-branch-coverage: gh-pages
96+
97+
- name: Report results
98+
if: always()
99+
run: |
100+
{
101+
echo ""
102+
if [ "${{ steps.build.outcome }}" != "success" ]; then
103+
echo ":x: **Build failed.** Check the build step log for details."
104+
elif [ "${{ steps.run.outcome }}" = "success" ]; then
105+
echo ":white_check_mark: **PR fuzzing completed — no new crashes found.**"
106+
else
107+
echo ":x: **PR fuzzing found a crash or encountered an error.**"
108+
echo "Download the \`${{ matrix.sanitizer }}-pr-artifacts\` artifact for crash inputs."
109+
fi
110+
} >> "$GITHUB_STEP_SUMMARY"
111+
112+
- name: Upload crash artifacts
113+
if: failure() && steps.run.outcome == 'failure'
114+
uses: actions/upload-artifact@v4
115+
with:
116+
name: ${{ matrix.sanitizer }}-pr-artifacts
117+
path: ./out/artifacts
118+
119+
# -------------------------------------------------------------------------
120+
# 2. Batch Fuzzing
121+
# Deep, long-running session to build corpus and find "deep" edge cases.
122+
# Duration: ~2 hour. Scheduled daily at 01:30 AM UTC+7 (18:30 UTC).
123+
# -------------------------------------------------------------------------
124+
batch-fuzzing:
125+
name: Batch Fuzzing
126+
if: >-
127+
github.event_name == 'schedule' &&
128+
github.event.schedule == '30 18 * * *'
129+
runs-on: ubuntu-latest
130+
permissions:
131+
contents: write # Push corpus updates to gh-pages
132+
issues: write # Allow run_fuzzers to file issues on crashes
133+
# Do not cancel in-progress batch runs; let them finish naturally.
134+
concurrency:
135+
group: batch-fuzzing-${{ github.repository }}
136+
cancel-in-progress: false
137+
strategy:
138+
fail-fast: false
139+
matrix:
140+
sanitizer: [address]
141+
steps:
142+
- name: Summarize job parameters
143+
run: |
144+
{
145+
echo "## Batch Fuzzing"
146+
echo ""
147+
echo "| Property | Value |"
148+
echo "| --- | --- |"
149+
echo "| Sanitizer | \`${{ matrix.sanitizer }}\` |"
150+
echo "| Mode | \`batch\` |"
151+
echo "| Fuzz seconds | 7200 |"
152+
echo "| Schedule | 01:30 AM UTC+7 (18:30 UTC) |"
153+
} >> "$GITHUB_STEP_SUMMARY"
154+
155+
- name: Build Fuzzers (${{ matrix.sanitizer }})
156+
id: build
157+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
158+
with:
159+
sanitizer: ${{ matrix.sanitizer }}
160+
language: python
161+
162+
- name: Run Fuzzers (${{ matrix.sanitizer }})
163+
id: run
164+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
165+
with:
166+
github-token: ${{ secrets.GITHUB_TOKEN }}
167+
fuzz-seconds: 7200
168+
mode: batch
70169
sanitizer: ${{ matrix.sanitizer }}
71170
storage-repo: https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
72171
storage-repo-branch: gh-pages
73172
storage-repo-branch-coverage: gh-pages
74173

174+
- name: Report results
175+
if: always()
176+
run: |
177+
{
178+
echo ""
179+
if [ "${{ steps.build.outcome }}" != "success" ]; then
180+
echo ":x: **Build failed.** Check the build step log for details."
181+
elif [ "${{ steps.run.outcome }}" = "success" ]; then
182+
echo ":white_check_mark: **Batch fuzzing completed — no new crashes found.**"
183+
echo ""
184+
echo "Corpus has been updated in the \`gh-pages\` branch."
185+
else
186+
echo ":x: **Batch fuzzing found a crash or encountered an error.**"
187+
echo "Download the \`${{ matrix.sanitizer }}-batch-artifacts\` artifact for crash inputs."
188+
fi
189+
} >> "$GITHUB_STEP_SUMMARY"
190+
75191
- name: Upload crash artifacts
76192
if: failure() && steps.run.outcome == 'failure'
77193
uses: actions/upload-artifact@v4
78194
with:
79-
name: ${{ matrix.sanitizer }}-artifacts
195+
name: ${{ matrix.sanitizer }}-batch-artifacts
80196
path: ./out/artifacts
197+
198+
# -------------------------------------------------------------------------
199+
# 3. Corpus Pruning
200+
# Housekeeping: removes redundant test cases to keep the fuzzer efficient.
201+
# Scheduled daily at 04:00 AM UTC+7 (21:00 UTC), 2.5 h after Batch Fuzzing.
202+
# -------------------------------------------------------------------------
203+
corpus-pruning:
204+
name: Corpus Pruning
205+
if: >-
206+
github.event_name == 'schedule' &&
207+
github.event.schedule == '0 21 * * *'
208+
runs-on: ubuntu-latest
209+
permissions:
210+
contents: write # Push pruned corpus back to gh-pages
211+
# Do not cancel in-progress pruning runs.
212+
concurrency:
213+
group: corpus-pruning-${{ github.repository }}
214+
cancel-in-progress: false
215+
strategy:
216+
fail-fast: false
217+
matrix:
218+
sanitizer: [address]
219+
steps:
220+
- name: Summarize job parameters
221+
run: |
222+
{
223+
echo "## Corpus Pruning"
224+
echo ""
225+
echo "| Property | Value |"
226+
echo "| --- | --- |"
227+
echo "| Sanitizer | \`${{ matrix.sanitizer }}\` |"
228+
echo "| Mode | \`prune\` |"
229+
echo "| Schedule | 04:00 AM UTC+7 (21:00 UTC) |"
230+
} >> "$GITHUB_STEP_SUMMARY"
231+
232+
- name: Build Fuzzers (${{ matrix.sanitizer }})
233+
id: build
234+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
235+
with:
236+
sanitizer: ${{ matrix.sanitizer }}
237+
language: python
238+
239+
- name: Prune Corpus (${{ matrix.sanitizer }})
240+
id: prune
241+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
242+
with:
243+
github-token: ${{ secrets.GITHUB_TOKEN }}
244+
fuzz-seconds: 600
245+
mode: prune
246+
sanitizer: ${{ matrix.sanitizer }}
247+
storage-repo: https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
248+
storage-repo-branch: gh-pages
249+
storage-repo-branch-coverage: gh-pages
250+
251+
- name: Report results
252+
if: always()
253+
run: |
254+
{
255+
echo ""
256+
if [ "${{ steps.build.outcome }}" != "success" ]; then
257+
echo ":x: **Build failed.** Check the build step log for details."
258+
elif [ "${{ steps.prune.outcome }}" = "success" ]; then
259+
echo ":white_check_mark: **Corpus pruning completed successfully.**"
260+
echo ""
261+
echo "The corpus in the \`gh-pages\` branch has been pruned."
262+
else
263+
echo ":x: **Corpus pruning encountered an error.** Check the prune step log."
264+
fi
265+
} >> "$GITHUB_STEP_SUMMARY"

fuzz/README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,21 @@ python fuzz/fuzz_tokenize.py corpus_dir/ -max_total_time=60
7070

7171
## CI/CD integration
7272

73-
Fuzzing runs automatically via GitHub Actions:
73+
Fuzzing runs automatically via GitHub Actions in three separate jobs:
7474

75-
- On pull requests to `dev` branch (focuses on code changes)
76-
- On push to `dev` branch
77-
- Daily at 06:00 UTC (full fuzzing run)
75+
| Job | Trigger | Mode | Fuzz seconds | Purpose |
76+
| --- | --- | --- | --- | --- |
77+
| **PR Fuzzing** | Pull request / push to `dev` | `code-change` | 300 s (5 minutes) | Quick check for shallow bugs in new code |
78+
| **Batch Fuzzing** | Daily at 01:30 AM UTC+7 (18:30 UTC) | `batch` | 7200 s (2 hours) | Deep session to build corpus and find edge cases |
79+
| **Corpus Pruning** | Daily at 04:00 AM UTC+7 (21:00 UTC) | `prune` | 600 s | Remove redundant corpus entries; runs 2.5 h after Batch Fuzzing |
80+
81+
Total job wall-clock time is longer than the fuzz seconds value because it
82+
includes building the fuzz targets (Docker image pull + compile, typically
83+
3–5 minutes). For example, PR Fuzzing targets a total wall-clock time under
84+
10 minutes with 300 seconds of actual fuzzing.
85+
86+
Each job writes a summary to the GitHub Actions Job Summary panel so that
87+
the status and any required actions are immediately visible.
7888

7989
Configuration: `.github/workflows/clusterfuzzlite.yml`
8090

@@ -182,8 +192,9 @@ If a fuzzer finds a crash:
182192
### Performance issues
183193

184194
- Adjust fuzzing time in `.github/workflows/clusterfuzzlite.yml`
185-
- Default is 300 seconds (5 minutes) per fuzzer
186-
- For longer sessions, increase the value
195+
- PR Fuzzing default: 300 seconds (5 minutes)
196+
- Batch Fuzzing default: 7200 seconds (2 hours)
197+
- For corpus pruning, the `fuzz-seconds` value is 600 seconds
187198

188199
### Known warnings
189200

0 commit comments

Comments
 (0)