Skip to content

Commit 89a8f9b

Browse files
authored
Merge branch 'master' into fix/readonlyadmin-cascade-delete
2 parents 7c737f9 + 00bdd53 commit 89a8f9b

18 files changed

Lines changed: 275 additions & 39 deletions

.github/actions/bot-ci-failure/analyze_failure.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,23 @@
66
from google import genai
77
from google.genai import types
88

9-
# Keywords that indicate an automated test failure (as opposed to
10-
# QA-only / commit-message-only failures).
11-
TEST_FAILURE_MARKERS = (
9+
# Strict markers that unequivocally indicate a failing test or assertion.
10+
STRICT_TEST_FAILURE_MARKERS = (
1211
"FAIL:",
13-
"ERROR:",
1412
"FAILED (",
15-
"Traceback (most recent call last):",
1613
"AssertionError",
1714
)
1815

16+
# Generic markers that could be from a test failure (e.g., TypeError)
17+
# OR from a transient infrastructure crash.
18+
GENERIC_TEST_FAILURE_MARKERS = (
19+
"ERROR:",
20+
"Traceback (most recent call last):",
21+
)
22+
23+
# Combined for functions that need to extract blocks of failed tests
24+
TEST_FAILURE_MARKERS = STRICT_TEST_FAILURE_MARKERS + GENERIC_TEST_FAILURE_MARKERS
25+
1926
# Patterns that indicate transient / infrastructure failures which are
2027
# not caused by the contributor's code.
2128
TRANSIENT_FAILURE_MARKERS = (
@@ -31,6 +38,7 @@
3138
"Posting coverage data to https://coveralls.io",
3239
"OperationalError: database is locked",
3340
"ERROR: Could not install packages due to an OSError",
41+
"about:neterror?e=connectionFailure",
3442
)
3543

3644

@@ -148,10 +156,25 @@ def process_error_logs(content):
148156
continue
149157
seen_bodies.add(body_key)
150158
total_unique_jobs += 1
151-
job_has_test_failure = any(m in body for m in TEST_FAILURE_MARKERS)
152-
if _is_transient_failure(body) and not job_has_test_failure:
159+
is_transient = _is_transient_failure(body)
160+
# 1. Strict markers (e.g., "FAIL:") ALWAYS mean a real test broke.
161+
has_strict_failure = any(m in body for m in STRICT_TEST_FAILURE_MARKERS)
162+
# 2. Generic markers (e.g., "Traceback") could be a code bug OR an infrastructure crash.
163+
has_generic_failure = any(m in body for m in GENERIC_TEST_FAILURE_MARKERS)
164+
if has_strict_failure:
165+
# Genuine test failures (AssertionError, FAIL:) always block auto-retry.
166+
job_has_test_failure = True
167+
elif is_transient:
168+
# If we detected a known transient error (like a network drop), we assume
169+
# the generic "ERROR:" and "Traceback" strings belong to that crash.
170+
# We safely forgive them to allow the auto-retry to trigger.
171+
job_has_test_failure = False
172+
else:
173+
# There was no transient error. Therefore, if we found generic failures
174+
# (like a SyntaxError or TypeError in the code), it is a real test failure.
175+
job_has_test_failure = has_generic_failure
176+
if is_transient and not job_has_test_failure:
153177
transient_jobs += 1
154-
# Detect real test failures and keep only the failing parts.
155178
if job_has_test_failure:
156179
tests_failed = True
157180
body = _extract_failed_tests(body)
@@ -367,6 +390,9 @@ def main():
367390
issue" and mention the CI has been restarted automatically if applicable.
368391
- For Coveralls failures specifically, mention it is a known flaky
369392
service and not actionable by the contributor.
393+
- If there are ALSO real test failures (like AssertionErrors) in the logs,
394+
tell the contributor to fix the real test failures first and push a new commit.
395+
Do NOT tell them the CI restarted automatically if real test failures exist.
370396
371397
5. **Build/Infrastructure/Other** (missing dependencies, Docker errors,
372398
setup failures that are NOT transient)

.github/actions/bot-ci-failure/test_analyze_failure.py

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ def test_detects_connection_refused(self):
197197
)
198198
)
199199

200+
def test_detects_about_neterror_connection_failure(self):
201+
self.assertTrue(
202+
_is_transient_failure(
203+
"Selenium loaded about:neterror?e=connectionFailure during test run"
204+
)
205+
)
206+
200207
def test_normal_test_failure_not_transient(self):
201208
self.assertFalse(_is_transient_failure("FAIL: test_login"))
202209

@@ -252,15 +259,55 @@ def test_single_block_no_job_headers(self):
252259
self.assertFalse(tests_failed)
253260
self.assertFalse(transient)
254261

255-
def test_transient_only_true_when_all_transient(self):
262+
def test_pure_transient_error_allows_retry(self):
263+
"""Ensure a purely transient error correctly flags for a retry."""
256264
content = (
257-
"===== JOB 100 =====\n"
258-
"marionette.errors.MarionetteException: connection lost\n"
259-
"===== JOB 200 =====\n"
260-
"NS_ERROR_ABORT in browser\n"
265+
"===== JOB 1 =====\n"
266+
"Setting up Selenium...\n"
267+
"selenium.common.exceptions.InvalidSessionIdException: Message: Session is not active\n"
261268
)
262-
_, _, transient = process_error_logs(content)
263-
self.assertTrue(transient)
269+
text, tests_failed, transient_only = process_error_logs(content)
270+
self.assertFalse(tests_failed)
271+
self.assertTrue(transient_only)
272+
273+
def test_transient_forgives_generic_traceback(self):
274+
"""Ensure transient crashes with generic tracebacks don't falsely trigger test failures."""
275+
content = (
276+
"===== JOB 2 =====\n"
277+
"Traceback (most recent call last):\n"
278+
" File 'urllib3/connectionpool.py', line 703\n"
279+
"ERROR: Could not install packages due to an OSError: HTTPSConnectionPool\n"
280+
"Network is unreachable\n"
281+
)
282+
text, tests_failed, transient_only = process_error_logs(content)
283+
self.assertFalse(tests_failed)
284+
self.assertTrue(transient_only)
285+
286+
def test_strict_failure_blocks_transient_retry(self):
287+
"""Ensure a strict failure (AssertionError) blocks retry even if a transient error exists."""
288+
content = (
289+
"===== JOB 3 =====\n"
290+
"FAIL: test_user_login\n"
291+
"AssertionError: True is not False\n"
292+
"...\n"
293+
"Connection reset by peer\n"
294+
)
295+
text, tests_failed, transient_only = process_error_logs(content)
296+
self.assertTrue(tests_failed)
297+
self.assertFalse(transient_only)
298+
299+
def test_pure_generic_bug_blocks_retry(self):
300+
"""Ensure a generic traceback without a transient error is flagged as a real code bug."""
301+
content = (
302+
"===== JOB 4 =====\n"
303+
"Traceback (most recent call last):\n"
304+
" File 'app/models.py', line 45, in save\n"
305+
"TypeError: 'NoneType' object is not subscriptable\n"
306+
"ERROR: Process exited with code 1\n"
307+
)
308+
text, tests_failed, transient_only = process_error_logs(content)
309+
self.assertTrue(tests_failed)
310+
self.assertFalse(transient_only)
264311

265312
def test_transient_only_false_when_mixed(self):
266313
content = (
@@ -281,6 +328,16 @@ def test_coveralls_only_is_transient(self):
281328
_, _, transient = process_error_logs(content)
282329
self.assertTrue(transient)
283330

331+
def test_transient_marker_containing_error_keyword(self):
332+
content = (
333+
"===== JOB 100 =====\n"
334+
"ERROR: Could not install packages due to an OSError: HTTPSConnectionPool\n"
335+
"Network is unreachable\n"
336+
)
337+
text, tests_failed, transient_only = process_error_logs(content)
338+
self.assertTrue(transient_only)
339+
self.assertFalse(tests_failed)
340+
284341

285342
class TestNormalizeForDedup(unittest.TestCase):
286343
"""Tests for _normalize_for_dedup."""

.github/workflows/bot-autoassign-issue.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
steps:
2020
- name: Generate GitHub App token
2121
id: generate-token
22-
uses: actions/create-github-app-token@v2
22+
uses: actions/create-github-app-token@v3
2323
with:
2424
app-id: ${{ secrets.OPENWISP_BOT_APP_ID }}
2525
private-key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}

.github/workflows/bot-autoassign-pr-issue-link.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
steps:
2121
- name: Generate GitHub App token
2222
id: generate-token
23-
uses: actions/create-github-app-token@v2
23+
uses: actions/create-github-app-token@v3
2424
with:
2525
app-id: ${{ secrets.OPENWISP_BOT_APP_ID }}
2626
private-key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}

.github/workflows/bot-autoassign-pr-reopen.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
steps:
2323
- name: Generate GitHub App token
2424
id: generate-token
25-
uses: actions/create-github-app-token@v2
25+
uses: actions/create-github-app-token@v3
2626
with:
2727
app-id: ${{ secrets.OPENWISP_BOT_APP_ID }}
2828
private-key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
@@ -53,7 +53,7 @@ jobs:
5353
steps:
5454
- name: Generate GitHub App token
5555
id: generate-token
56-
uses: actions/create-github-app-token@v2
56+
uses: actions/create-github-app-token@v3
5757
with:
5858
app-id: ${{ secrets.OPENWISP_BOT_APP_ID }}
5959
private-key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}

.github/workflows/bot-autoassign-stale-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
steps:
2121
- name: Generate GitHub App token
2222
id: generate-token
23-
uses: actions/create-github-app-token@v2
23+
uses: actions/create-github-app-token@v3
2424
with:
2525
app-id: ${{ secrets.OPENWISP_BOT_APP_ID }}
2626
private-key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}

.github/workflows/bot-changelog-runner.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
steps:
2222
- name: Download PR metadata
2323
id: download
24-
uses: actions/download-artifact@v4
24+
uses: actions/download-artifact@v8
2525
with:
2626
name: changelog-metadata
2727
github-token: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/bot-changelog-trigger.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030

3131
- name: Upload PR metadata
3232
if: steps.check.outputs.has_noteworthy == 'true'
33-
uses: actions/upload-artifact@v4
33+
uses: actions/upload-artifact@v7
3434
with:
3535
name: changelog-metadata
3636
path: pr_number

.github/workflows/reusable-backport.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
steps:
7373
- name: Generate GitHub App Token
7474
id: generate-token
75-
uses: actions/create-github-app-token@v2
75+
uses: actions/create-github-app-token@v3
7676
with:
7777
app-id: ${{ secrets.app_id }}
7878
private-key: ${{ secrets.private_key }}

.github/workflows/reusable-bot-autoassign.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
steps:
2020
- name: Generate GitHub App token
2121
id: generate-token
22-
uses: actions/create-github-app-token@v2
22+
uses: actions/create-github-app-token@v3
2323
with:
2424
app-id: ${{ secrets.OPENWISP_BOT_APP_ID }}
2525
private-key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}

0 commit comments

Comments
 (0)