1212
1313import json
1414import os
15+ import re
1516import sys
1617import time
1718import traceback
@@ -240,7 +241,62 @@ def classify(run_id, attempt, retry_note):
240241 obs [eid ] = store .FLAKE_ONLY
241242 else :
242243 obs [eid ] = store .PASS
243- return obs , {k : sorted (v ) for k , v in fail_refs .items ()}
244+ # every host dir came from examples.yml, which nightly.yml calls as `host`
245+ targets = {eid : "examples" for eid in obs }
246+
247+ jobs_obs , jobs_refs , jobs_targets = classify_jobs (run_id , attempt , retry_note )
248+ obs .update (jobs_obs )
249+ fail_refs .update (jobs_refs )
250+ targets .update (jobs_targets )
251+ return obs , {k : sorted (v ) for k , v in fail_refs .items ()}, targets
252+
253+
254+ # Jobs the nightly fans out to that emit no results-*.json. Without this they
255+ # ran every night, went red, and told nobody -- 12 of the 13 targets.
256+ HOST_JOBS = {"host" , "refs" , "gate" }
257+ REF_RE = re .compile (r"wolfSSL (\S+)|\((master|v[\d.]+-stable)\)" )
258+
259+
260+ def job_unit (name ):
261+ """`esp32 / Build / ESP32 (...) wolfSSL master` -> ('esp32', 'master').
262+
263+ The prefix is nightly.yml's job name, which is deliberately the target name
264+ and therefore the ci:<target> label.
265+ """
266+ if " / " not in name :
267+ return None , None
268+ target , rest = name .split (" / " , 1 )
269+ if target in HOST_JOBS :
270+ return None , None
271+ m = REF_RE .search (rest )
272+ return target , (m .group (1 ) or m .group (2 )) if m else "unknown"
273+
274+
275+ def classify_jobs (run_id , attempt , retry_note ):
276+ def by_unit (jobs ):
277+ out = defaultdict (list )
278+ for j in jobs :
279+ t , ref = job_unit (j .get ("name" , "" ))
280+ if t :
281+ out [t ].append ((j .get ("conclusion" ), ref ))
282+ return out
283+
284+ a1 = by_unit (all_jobs (run_id , attempt = 1 ))
285+ final = by_unit (all_jobs (run_id )) if attempt >= 2 else a1
286+
287+ obs , refs , targets = {}, defaultdict (set ), {}
288+ for t in set (a1 ) | set (final ):
289+ failed_now = any (c == "failure" for c , _ in final .get (t , []))
290+ failed_before = any (c == "failure" for c , _ in a1 .get (t , []))
291+ if failed_now :
292+ obs [t ] = store .UNCONFIRMED if retry_note else store .REAL
293+ refs [t ] = {r for c , r in final [t ] if c == "failure" }
294+ elif failed_before and attempt >= 2 :
295+ obs [t ] = store .FLAKE_ONLY
296+ else :
297+ obs [t ] = store .PASS
298+ targets [t ] = t
299+ return obs , refs , targets
244300
245301
246302# --- AI: advisory only -----------------------------------------------------
@@ -401,10 +457,21 @@ def open_issues():
401457 page += 1
402458
403459
460+ NIGHTLY_TARGETS = ("examples" , "esp32" , "android" , "rt1060" , "uefi" , "puf" ,
461+ "rpi-pico" , "ebpf" , "fullstack" , "java" , "cmake" , "csharp" ,
462+ "emulated" )
463+
464+
404465def ensure_labels ():
466+ for t in NIGHTLY_TARGETS :
467+ try :
468+ gh (f"/repos/{ REPO } /labels" , method = "POST" , retry = False ,
469+ body = {"name" : store .target_label (t ), "color" : "1d76db" ,
470+ "description" : f"Nightly { t } target" })
471+ except urllib .error .HTTPError :
472+ pass # 422 = already exists
405473 for name , color , desc in [
406474 (store .LABEL , "b60205" , "Filed by the nightly examples triage" ),
407- (store .LABEL_FLAKY , "fbca04" , "Flaps between pass and fail" ),
408475 (store .LABEL_MUTE , "ededed" , "Triage will not touch this issue" ),
409476 (store .LABEL_UNCONFIRMED , "d4c5f9" , "Retry did not run; not double-confirmed" ),
410477 ]:
@@ -658,7 +725,7 @@ def post_report(obs, fail_refs, verdicts, filed, results, attempt, retry_note):
658725 post_slack (color , fallback , blocks )
659726
660727
661- def apply (eid , observation , issue , ai , state , rows , close_allowed ):
728+ def apply (eid , observation , issue , ai , state , rows , close_allowed , target ):
662729 """Returns the issue number now tracking this dir, or None."""
663730 now = datetime .now (timezone .utc ).strftime ("%Y-%m-%d" )
664731 action , why = store .decide (observation , issue , close_allowed )
@@ -674,16 +741,21 @@ def apply(eid, observation, issue, ai, state, rows, close_allowed):
674741 body = store .render_body (eid , state , ai , rows , run_url (), now )
675742
676743 if action == "open" :
677- created = gh (
678- f"/repos/{ REPO } /issues" ,
679- method = "POST" ,
680- body = {
744+ body_kw = {
681745 "title" : f"CI: `{ eid } ` failing in the nightly" ,
682746 "body" : body ,
683- "labels" : [store .LABEL ]
747+ "labels" : [store .LABEL , store . target_label ( target ) ]
684748 + ([store .LABEL_UNCONFIRMED ] if observation == store .UNCONFIRMED else []),
685- },
686- )
749+ # wolfHSM assigns its nightly issues to the bot; retried
750+ # without it below if the account is not assignable here.
751+ "assignees" : ["wolfSSL-Bot" ],
752+ }
753+ try :
754+ created = gh (f"/repos/{ REPO } /issues" , method = "POST" , body = body_kw )
755+ except urllib .error .HTTPError :
756+ # not assignable in this repo; file it anyway rather than lose it
757+ body_kw .pop ("assignees" , None )
758+ created = gh (f"/repos/{ REPO } /issues" , method = "POST" , body = body_kw )
687759 num = created ["number" ]
688760 print (f" { eid } : opened #{ num } ({ why } )" )
689761 elif action == "reopen" :
@@ -699,19 +771,20 @@ def apply(eid, observation, issue, ai, state, rows, close_allowed):
699771 )
700772 print (f" { eid } : reopened #{ issue ['number' ]} ({ why } )" )
701773 elif action == "update" :
702- # Silent: issues.update sends no notification. A comment every night on a
703- # long-standing break is how a bot gets muted.
704774 gh (f"/repos/{ REPO } /issues/{ issue ['number' ]} " , method = "PATCH" , body = {"body" : body })
775+ # A comment every night, so the thread IS the history. wolfHSM #412 does
776+ # this with a bare "Still failing as of <url>" x15; carry the verdict,
777+ # the retry outcome and what changed instead, so a reader can skim the
778+ # thread and see how the failure evolved.
705779 reason = store .material_change (store .parse_state (issue .get ("body" , "" )), state )
706- if reason :
707- gh (
708- f"/repos/{ REPO } /issues/{ issue ['number' ]} /comments" ,
709- method = "POST" ,
710- body = {"body" : f"{ reason } . [Run]({ run_url ()} )" },
711- )
712- print (f" { eid } : updated #{ issue ['number' ]} + comment ({ reason } )" )
713- else :
714- print (f" { eid } : updated #{ issue ['number' ]} silently ({ why } )" )
780+ gh (
781+ f"/repos/{ REPO } /issues/{ issue ['number' ]} /comments" ,
782+ method = "POST" ,
783+ body = {"body" : store .render_comment (state , ai , observation , reason ,
784+ run_url (), now )},
785+ )
786+ print (f" { eid } : updated #{ issue ['number' ]} + comment"
787+ + (f" ({ reason } )" if reason else "" ))
715788 elif action == "close" :
716789 gh (
717790 f"/repos/{ REPO } /issues/{ issue ['number' ]} /comments" ,
@@ -731,7 +804,7 @@ def apply(eid, observation, issue, ai, state, rows, close_allowed):
731804
732805def report (attempt , retry_note ):
733806 results = merged_results (RUN_ID , attempt )
734- obs , fail_refs = classify (RUN_ID , attempt , retry_note )
807+ obs , fail_refs , targets = classify (RUN_ID , attempt , retry_note )
735808
736809 logs = defaultdict (str )
737810 for r in results :
@@ -804,7 +877,7 @@ def report(attempt, retry_note):
804877 # collected, not read back from `issues`: a dir filed for the first time
805878 # tonight is not in that map, and its card entry would have no link
806879 filed [eid ] = apply (eid , observation , issue , verdicts .get (eid ), state , rows ,
807- close_allowed )
880+ close_allowed , targets . get ( eid , "examples" ) )
808881
809882 # Last, so the card can link the issues opened above. Never let a reporting
810883 # failure lose the triage that already succeeded.
0 commit comments