SymptomExtractor
Reads symptoms from normalized log text; never throws.
The text is GitHub Actions output, either raw (<timestamp> <text> per line) or as printed by gh run view --log (<job>\t<step>\t<timestamp> <text>). Lines end at \n, \r\n or a lone \r, and only the first MAX_LINE characters of a line are read. A job<TAB>step prefix counts from the first timestamped line that carries it (at most 64 distinct prefixes), so a tab inside ordinary output is not taken for one. Color codes (ESC or the literal ^[[) are dropped, and lines the runner echoes in cyan (script bodies) are skipped by every matcher below.
Matching is plain string scanning on ASCII-lowercased text, no regex:
SIGNATURE: the first line of a
##[error]annotation, exceptProcess completed with exit code; with no annotation in the whole text, lines shaped likeXError: ...,XException: ...,error: ...orfatal: ...instead. Both go through Signature.normalize.EXIT: a non-zero number after
exit code,exited with codeorexit status.SIGNAL: a
SIGplus uppercase token on a line that also says signal, killed or terminated.STEP and DURATION: for each step (a
##[group]Runheader up to the next header orPost job cleanup.) that holds an annotation, the header command and the milliseconds from the header's timestamp to the step's last timestamped line.TRANSPORT: the phrases in
transport, each mapped to one canonical value.
Equal job, kind and value merge into one symptom with a count (a STEP or DURATION also needs the same header line, so two steps stay two symptoms). At most MAX_SYMPTOMS distinct symptoms are kept, in line order.