fix(ci-artifacts): sign lint failures by their finding and ignore steps after the failure
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b9ca192c51
commit
90b0a2ee88
2 files changed
+40
-5
No files matched your search
@@ -23,9 +23,13 @@ from pathlib import Path
|
|||||||
FAILED = {"failure", "cancelled", "timed_out"}
|
FAILED = {"failure", "cancelled", "timed_out"}
|
||||||
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
||||||
STAMP = re.compile(r"^\d{4}-\d\d-\d\dT[\d:.]+Z\s?")
|
STAMP = re.compile(r"^\d{4}-\d\d-\d\dT[\d:.]+Z\s?")
|
||||||
GENERIC = re.compile(r"Process completed with exit code|failed=0\b|FAILED - RETRYING")
|
GENERIC = re.compile(r"failed=0\b|FAILED - RETRYING|^make(\[\d+\])?: \*\*\* \[.*\] Error \d+")
|
||||||
|
PROCESS_END = re.compile(r"##\[error\]Process completed with exit code")
|
||||||
ANNOTATION = re.compile(r"##\[error\]")
|
ANNOTATION = re.compile(r"##\[error\]")
|
||||||
FATAL = re.compile(r"fatal: \[|FAILED!|Traceback|AssertionError|panic:")
|
FATAL = re.compile(r"fatal: \[|FAILED!|Traceback|AssertionError|panic:")
|
||||||
|
LINT_AT = re.compile(r"\S+:\d+:\d+:? ((error|warning) )?[A-Z]{1,4}\d{3,4}\b")
|
||||||
|
LINT_CODE = re.compile(r"^[A-Z]{1,4}\d{3,4}\b")
|
||||||
|
LINT_ARROW = re.compile(r"^-->\s+\S+:\d+:\d+")
|
||||||
SIGNAL = re.compile(r"\b(ERROR|Error|FAILED|FAIL|fatal|Exception|failed)\b")
|
SIGNAL = re.compile(r"\b(ERROR|Error|FAILED|FAIL|fatal|Exception|failed)\b")
|
||||||
TASK = re.compile(r"^TASK \[")
|
TASK = re.compile(r"^TASK \[")
|
||||||
REFUSAL = "terminal escape sequences"
|
REFUSAL = "terminal escape sequences"
|
||||||
@@ -49,6 +53,8 @@ def output_lines(text: str) -> list[tuple[str, str]]:
|
|||||||
kept, echo, task = [], False, ""
|
kept, echo, task = [], False, ""
|
||||||
for raw in text.splitlines():
|
for raw in text.splitlines():
|
||||||
line = clean(raw)
|
line = clean(raw)
|
||||||
|
if PROCESS_END.search(line):
|
||||||
|
break
|
||||||
if line.startswith("##[group]Run "):
|
if line.startswith("##[group]Run "):
|
||||||
echo = True
|
echo = True
|
||||||
elif line.startswith("##[endgroup]"):
|
elif line.startswith("##[endgroup]"):
|
||||||
@@ -60,13 +66,28 @@ def output_lines(text: str) -> list[tuple[str, str]]:
|
|||||||
return kept
|
return kept
|
||||||
|
|
||||||
|
|
||||||
|
def is_lint(lines: list[tuple[str, str]], index: int) -> bool:
|
||||||
|
line = lines[index][1]
|
||||||
|
if LINT_AT.search(line):
|
||||||
|
return True
|
||||||
|
following = lines[index + 1][1] if index + 1 < len(lines) else ""
|
||||||
|
return bool(LINT_CODE.match(line) and LINT_ARROW.match(following))
|
||||||
|
|
||||||
|
|
||||||
def signature(text: str) -> str:
|
def signature(text: str) -> str:
|
||||||
lines = output_lines(text)
|
lines = output_lines(text)
|
||||||
for pattern in (ANNOTATION, FATAL, SIGNAL):
|
tiers = (
|
||||||
for task, line in reversed(lines):
|
(lambda i: ANNOTATION.search(lines[i][1]), False),
|
||||||
if pattern.search(line):
|
(lambda i: FATAL.search(lines[i][1]), True),
|
||||||
|
(lambda i: is_lint(lines, i), False),
|
||||||
|
(lambda i: SIGNAL.search(lines[i][1]), False),
|
||||||
|
)
|
||||||
|
for matches, with_task in tiers:
|
||||||
|
for index in reversed(range(len(lines))):
|
||||||
|
if matches(index):
|
||||||
|
task, line = lines[index]
|
||||||
found = ANNOTATION.sub("", line).strip()
|
found = ANNOTATION.sub("", line).strip()
|
||||||
return normalise(f"{task} {found}" if pattern is FATAL and task else found)
|
return normalise(f"{task} {found}" if with_task and task else found)
|
||||||
return normalise(lines[-1][1]) if lines else "<empty log>"
|
return normalise(lines[-1][1]) if lines else "<empty log>"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,20 @@ class TestCiFailures(unittest.TestCase):
|
|||||||
'TASK [web-app-x : Wait for health] **** fatal: [localhost]: FAILED! => {"msg": "timeout <n>"}',
|
'TASK [web-app-x : Wait for health] **** fatal: [localhost]: FAILED! => {"msg": "timeout <n>"}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_lint_finding_beats_make_error_and_later_steps(self):
|
||||||
|
text = (
|
||||||
|
"Ran 41 tests in 1.0s\n"
|
||||||
|
"OK\n"
|
||||||
|
"UP017 [*] Use `datetime.UTC` alias\n"
|
||||||
|
" --> skills/democracy/scripts/council.py:92:40\n"
|
||||||
|
"Found 1 error.\n"
|
||||||
|
"make: *** [Makefile:15: test] Error 1\n"
|
||||||
|
"##[error]Process completed with exit code 2.\n"
|
||||||
|
"Post job cleanup.\n"
|
||||||
|
"SHA256 digest of uploaded artifact is 0123456789abcdef\n"
|
||||||
|
)
|
||||||
|
self.assertEqual(self.ci.signature(text), "UP<n> [*] Use `datetime.UTC` alias")
|
||||||
|
|
||||||
def test_generic_exit_code_line_is_not_the_signature(self):
|
def test_generic_exit_code_line_is_not_the_signature(self):
|
||||||
text = "Error: disk full\n##[error]Process completed with exit code 1.\n"
|
text = "Error: disk full\n##[error]Process completed with exit code 1.\n"
|
||||||
self.assertEqual(self.ci.signature(text), "Error: disk full")
|
self.assertEqual(self.ci.signature(text), "Error: disk full")
|
||||||
|
|||||||
Reference in new issue
Block a user