-
Notifications
You must be signed in to change notification settings - Fork 0
575 lines (494 loc) · 22.4 KB
/
test.yml
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
name: test
on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
FOUNDRY_PROFILE: 'ci'
SEPOLIA_RPC_URL: 'https://sepolia.base.org' # TODO: Throw this in repo env once permissioned to do so
BASE_RPC_URL: 'https://mainnet.base.org' # TODO: Throw this in repo env once permissioned to do so
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run fmt
run: npm run fmt
- name: Run fmt check
run: npm run fmt:check
test:
name: Foundry Tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Cache Foundry dependencies
uses: actions/cache@v3
with:
path: |
~/.foundry/cache
~/.foundry/rpc-cache
cache/
out/
key: foundry-${{ runner.os }}-${{ hashFiles('foundry.toml') }}-${{ hashFiles('**/foundry.lock') }}
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: |
npm ci
forge install
- name: Run Forge tests
id: forge-test
run: |
mkdir -p artifacts/forge-test-results
forge test -vvv 2>&1 | tee artifacts/forge-test-results/forge-test-results.txt
exit ${PIPESTATUS[0]}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: artifacts/forge-test-results/forge-test-results.txt
security:
name: Security Analysis
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
cache: 'pip'
- name: Install Slither
run: |
python -m pip install --upgrade pip
pip install slither-analyzer
- name: Run Slither analysis
id: slither
continue-on-error: true
run: |
# Create a slither.config.json file with correct format
echo '{
"filter_paths": "lib/,node_modules/",
"exclude_informational": true,
"exclude_low": true,
"exclude_optimization": true
}' > slither.config.json
slither . --json slither-output.json
- name: Process Slither results
if: always()
run: |
if [ -f slither-output.json ]; then
echo "Analyzing Slither results..."
# Format and display high severity issues from src/ files
echo "=== HIGH SEVERITY ISSUES ===" > slither-report.txt
jq -r '.results.detectors[] |
select(.impact == "High") |
select(any(.elements[].source_mapping.filename_short; startswith("src/"))) |
"- [\(.check)]: \(.description | gsub("\n\t"; " ") | gsub("\n"; " "))"' slither-output.json | sort -u >> slither-report.txt
# Format and display medium severity issues from src/ files
echo -e "\n=== MEDIUM SEVERITY ISSUES ===" >> slither-report.txt
jq -r '.results.detectors[] |
select(.impact == "Medium") |
select(any(.elements[].source_mapping.filename_short; startswith("src/"))) |
"- [\(.check)]: \(.description | gsub("\n\t"; " ") | gsub("\n"; " "))"' slither-output.json | sort -u >> slither-report.txt
# Count issues (unique)
HIGH_SEVERITY=$(jq -r '[.results.detectors[] |
select(.impact == "High") |
select(any(.elements[].source_mapping.filename_short; startswith("src/")))] | length' slither-output.json)
MEDIUM_SEVERITY=$(jq -r '[.results.detectors[] |
select(.impact == "Medium") |
select(any(.elements[].source_mapping.filename_short; startswith("src/")))] | length' slither-output.json)
# Display summary
echo -e "\n=== SUMMARY ==="
echo "High Severity Issues: $HIGH_SEVERITY"
echo "Medium Severity Issues: $MEDIUM_SEVERITY"
# Output the report
cat slither-report.txt
# Fail on high severity issues
if [ "$HIGH_SEVERITY" -gt 0 ]; then
echo "❌ Found $HIGH_SEVERITY high severity issues"
echo "Review the detailed report above"
exit 1
else
echo "✅ No high severity issues found"
if [ "$MEDIUM_SEVERITY" -gt 0 ]; then
echo "⚠️ Found $MEDIUM_SEVERITY medium severity issues to review"
fi
fi
else
echo "❌ Slither analysis failed to produce output"
exit 1
fi
- name: Upload Slither results
if: always()
uses: actions/upload-artifact@v4
with:
name: slither-results
path: |
slither-output.json
slither-report.txt
gas:
name: Gas Analysis
runs-on: ubuntu-latest
needs: test
if: success() && needs.test.result == 'success'
timeout-minutes: 30
env:
FOUNDRY_FUZZ_RUNS: 256
FOUNDRY_FUZZ_MAX_TEST_REJECTS: 65536
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Cache Foundry dependencies
uses: actions/cache@v3
with:
path: |
~/.foundry/cache
~/.foundry/rpc-cache
cache/
out/
key: foundry-${{ runner.os }}-${{ hashFiles('foundry.toml') }}-${{ hashFiles('**/foundry.lock') }}
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: |
npm ci
forge install
- name: Generate gas report
id: gas-report
continue-on-error: true
run: |
mkdir -p snapshots
# Run tests and capture gas reports for successful tests
forge test --gas-report > snapshots/current-gas-report.txt
# Generate snapshot (Forge automatically includes only successful tests)
forge snapshot --snap snapshots/current-gas.snap
- name: Compare gas snapshots
if: always()
run: |
if [ ! -f "snapshots/current-gas.snap" ]; then
echo "⚠️ No gas snapshot generated - skipping comparison"
exit 0
fi
if [ -f ".gas-snapshot" ]; then
echo "📊 Gas Comparison:"
forge snapshot --diff .gas-snapshot snapshots/current-gas.snap || (
echo "🔍 Gas changes detected!"
exit 0
)
else
echo "Creating first snapshot for future comparisons"
cp snapshots/current-gas.snap .gas-snapshot
fi
- name: Upload gas reports
if: always()
uses: actions/upload-artifact@v4
with:
name: gas-reports
path: snapshots/
if-no-files-found: warn
summary:
name: Create Summary
if: always()
needs: [test, security, gas]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create Test Summary
if: always()
run: |
echo "=== Debug Information ==="
echo "Listing current directory:"
ls -la
echo
echo "Listing artifacts directory:"
ls -la artifacts || echo "artifacts directory does not exist"
echo
echo "Listing test-results directory (if it exists):"
ls -la artifacts/test-results || echo "test-results directory does not exist"
echo
echo "Checking for forge test results:"
ls -la artifacts/test-results/forge-test-results.txt || echo "forge-test-results.txt does not exist"
echo
if [ -f "artifacts/test-results/forge-test-results.txt" ]; then
# Get total test counts from the final summary line
TOTAL_LINE=$(grep "^Ran .* test suites" "artifacts/test-results/forge-test-results.txt" | tail -n 1)
if [[ $TOTAL_LINE =~ ([0-9]+)[[:space:]]tests[[:space:]]passed,[[:space:]]([0-9]+)[[:space:]]failed,[[:space:]]([0-9]+)[[:space:]]skipped[[:space:]]\(([0-9]+)[[:space:]]total[[:space:]]tests\) ]]; then
PASSED_TESTS="${BASH_REMATCH[1]}"
mapfile -t FAILING_TESTS < <(sed -n '/^Failing tests:/,/^Encountered a total of/p' "artifacts/test-results/forge-test-results.txt" | grep '^\[FAIL:')
FAILED_TESTS=${#FAILING_TESTS[@]}
SKIPPED_TESTS="${BASH_REMATCH[3]}"
TOTAL_TESTS=$((PASSED_TESTS + FAILED_TESTS + SKIPPED_TESTS))
# Write main status with emoji
if [ "$FAILED_TESTS" -gt 0 ]; then
echo "❌ $FAILED_TESTS tests failed, $PASSED_TESTS passed, $SKIPPED_TESTS skipped (Total: $TOTAL_TESTS)" >> test-summary.md
else
echo "✅ All $PASSED_TESTS tests passed! ($SKIPPED_TESTS skipped, Total: $TOTAL_TESTS)" >> test-summary.md
fi
# If there are failing tests, show them first
if [ "$FAILED_TESTS" -gt 0 ]; then
echo -e "\n### Failing Tests in this PR Branch\n" >> test-summary.md
# Process each failing test
while IFS= read -r line; do
if [[ $line =~ ^\[FAIL:[[:space:]]([^]]+)\][[:space:]]([^[:space:]]+) ]]; then
# Get the full test name including parameters
full_test=$(echo "$line" | sed -E 's/.*\][[:space:]](.*) \(gas:.*/\1/')
# Get the test name without parameters for the awk pattern
test_pattern=$(echo "$full_test" | sed -E 's/\(.*\)//')
# Write test name header
echo -e "\`$full_test\`\n" >> test-summary.md
echo -e "<details><summary>Stack Trace</summary>\n\n" >> test-summary.md
echo -e "\`\`\`\n" >> test-summary.md
# Extract stack trace for this test
awk -v test="$test_pattern" '
/^Traces:/ { p = 1 }
p == 1 { print }
/^$/ { p = 0 }
' "artifacts/test-results/forge-test-results.txt" >> test-summary.md
echo -e "\n\`\`\`\n\n</details>\n\n" >> test-summary.md
fi
done < <(sed -n '/^Failing tests:/,/^Encountered a total of/p' "artifacts/test-results/forge-test-results.txt" | grep '^\[FAIL:')
fi
# Add test suite details section
echo -e "\n### Test Results for Merge" >> test-summary.md
echo -e "\n| Test Suite | Status | Coverage | Time |" >> test-summary.md
echo "|------------|--------|----------|------|" >> test-summary.md
# Process each test suite
current_suite=""
while IFS= read -r line; do
if [[ $line =~ Ran[[:space:]][0-9]+[[:space:]]tests[[:space:]]for[[:space:]]([^:]+) ]]; then
current_suite="${BASH_REMATCH[1]}"
elif [[ $line =~ Suite[[:space:]]result:[[:space:]](ok|FAILED)\.[[:space:]]([0-9]+)[[:space:]]passed\;[[:space:]]([0-9]+)[[:space:]]failed\;[[:space:]]([0-9]+)[[:space:]]skipped\;[[:space:]]finished[[:space:]]in[[:space:]]([0-9.]+)(m?s) ]]; then
status="${BASH_REMATCH[1]}"
passed="${BASH_REMATCH[2]}"
failed="${BASH_REMATCH[3]}"
skipped="${BASH_REMATCH[4]}"
time="${BASH_REMATCH[5]}"
time_unit="${BASH_REMATCH[6]}"
if [ ! -z "$current_suite" ]; then
status_emoji="✅"
[ "$status" = "FAILED" ] && status_emoji="❌"
[ "$skipped" -gt 0 ] && [ "$failed" -eq 0 ] && status_emoji="⚠️"
total=$((passed + failed + skipped))
coverage=0
[ "$total" -gt 0 ] && coverage=$((passed * 100 / total))
# Convert time to seconds if in milliseconds
if [ "$time_unit" = "ms" ]; then
time=$(echo "scale=3; $time/1000" | bc)
fi
printf "| \`%s\` | %s | %d%% (%d/%d) | %.3fs |\n" \
"$current_suite" "$status_emoji" "$coverage" "$passed" "$total" "$time" >> test-summary.md
current_suite=""
fi
fi
done < "artifacts/test-results/forge-test-results.txt"
else
echo "⚠️ Could not parse test results" >> test-summary.md
fi
else
echo "⚠️ No test results found" >> test-summary.md
fi
- name: Create Slither Summary
id: slither-summary
run: |
echo "### 🔒 Security Analysis" > slither-summary.md
if [ -f "artifacts/slither-results/slither-output.json" ]; then
# Count vulnerabilities by severity
HIGH_COUNT=$(jq -r '[.results.detectors[] | select(.impact == "High")] | length' "artifacts/slither-results/slither-output.json")
MEDIUM_COUNT=$(jq -r '[.results.detectors[] | select(.impact == "Medium")] | length' "artifacts/slither-results/slither-output.json")
if [ "$HIGH_COUNT" -gt 0 ] || [ "$MEDIUM_COUNT" -gt 0 ]; then
echo "⚠️ Found **$HIGH_COUNT High** and **$MEDIUM_COUNT Medium** severity issues" >> slither-summary.md
# Process high severity issues
if [ "$HIGH_COUNT" -gt 0 ]; then
echo -e "\n#### High Severity Issues" >> slither-summary.md
# Group findings by check type
jq -r '
def clean_description:
gsub("\n\t"; " ") | gsub("\n"; " ");
def format_location:
. as $loc |
if contains("#") then split("#")[0]
else . end;
.results.detectors
| map(select(.impact == "High"))
| group_by(.check)[]
| {
check: .[0].check,
description: (.[0].description | clean_description),
files: ([.[].elements[].source_mapping.filename_short] | unique | sort),
findings: map({
file: .elements[0].source_mapping.filename_short,
function: (.elements[0].name | format_location),
line: .elements[0].source_mapping.lines[0]
})
}
| "##### \(.check)\n**Impact**: \(.description)\n\n**Affected Files**:\n\(.files | map("- `" + . + "`") | join("\n"))\n\n<details>\n<summary>View Detailed Findings</summary>\n\n\(.findings | map("- `" + .file + ":" + (.line|tostring) + "` in `" + .function + "`") | join("\n"))\n</details>\n"
' "artifacts/slither-results/slither-output.json" >> slither-summary.md
fi
# Process medium severity issues
if [ "$MEDIUM_COUNT" -gt 0 ]; then
echo -e "\n#### Medium Severity Issues" >> slither-summary.md
echo "<details><summary>View Medium Severity Issues</summary>" >> slither-summary.md
jq -r '
def clean_description:
gsub("\n\t"; " ") | gsub("\n"; " ");
def format_location:
. as $loc |
if contains("#") then split("#")[0]
else . end;
.results.detectors
| map(select(.impact == "Medium"))
| group_by(.check)[]
| {
check: .[0].check,
description: (.[0].description | clean_description),
files: ([.[].elements[].source_mapping.filename_short] | unique | sort),
findings: map({
file: .elements[0].source_mapping.filename_short,
function: (.elements[0].name | format_location),
line: .elements[0].source_mapping.lines[0]
})
}
| "##### \(.check)\n**Impact**: \(.description)\n\n**Affected Files**:\n\(.files | map("- `" + . + "`") | join("\n"))\n\n\(.findings | map("- `" + .file + ":" + (.line|tostring) + "` in `" + .function + "`") | join("\n"))\n"
' "artifacts/slither-results/slither-output.json" >> slither-summary.md
echo "</details>" >> slither-summary.md
fi
# Add recommendations section
echo -e "\n#### Recommended Actions" >> slither-summary.md
echo "1. Review and fix all high severity issues before deployment" >> slither-summary.md
echo "2. Implement thorough testing for affected components" >> slither-summary.md
echo "3. Consider additional security measures:" >> slither-summary.md
echo " - Access controls" >> slither-summary.md
echo " - Input validation" >> slither-summary.md
echo " - Invariant checks" >> slither-summary.md
else
echo "✅ No high or medium severity issues found!" >> slither-summary.md
fi
else
echo "⚠️ No security analysis results found" >> slither-summary.md
fi
- name: Create Gas Summary
id: gas-summary
run: |
echo "### ⛽ Gas Analysis" > gas-summary.md
if [ -f "artifacts/gas-reports/current-gas.snap" ] && [ -f ".gas-snapshot" ]; then
# Compare snapshots and capture the diff
DIFF_OUTPUT=$(forge snapshot --diff .gas-snapshot "artifacts/gas-reports/current-gas.snap" 2>&1 || true)
if echo "$DIFF_OUTPUT" | grep -q "Different"; then
echo "🔄 Gas changes detected" >> gas-summary.md
# Extract and format the changes
echo -e "\n**Changes:**" >> gas-summary.md
echo "\`\`\`" >> gas-summary.md
echo "$DIFF_OUTPUT" | grep -A 1 "Different" >> gas-summary.md || true
echo "\`\`\`" >> gas-summary.md
# Add full diff in collapsible section
echo -e "\n<details>" >> gas-summary.md
echo "<summary>📋 Full Gas Comparison</summary>" >> gas-summary.md
echo -e "\n\`\`\`" >> gas-summary.md
echo "$DIFF_OUTPUT" >> gas-summary.md
echo "\`\`\`" >> gas-summary.md
echo "</details>" >> gas-summary.md
else
echo "✅ No gas changes detected" >> gas-summary.md
fi
elif [ -f "artifacts/gas-reports/current-gas.snap" ]; then
echo "📊 First gas snapshot created" >> gas-summary.md
else
echo "⚠️ No gas snapshot generated" >> gas-summary.md
fi
- name: Combine Summaries
id: combine-summaries
run: |
{
echo "## Summary of Test Results if Merged To Main:"
echo
echo "- Full logs & artifacts are available in the [Actions tab](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
echo "- This comment will update automatically with new CI runs"
echo
cat test-summary.md
echo
cat slither-summary.md
echo
cat gas-summary.md
echo
} > combined-summary.md
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: CI Results Summary
- name: Create or Update Comment
uses: peter-evans/create-or-update-comment@v3
if: github.event_name == 'pull_request'
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-file: combined-summary.md
edit-mode: replace
permissions:
pull-requests: write
contents: read