My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix four issues in failure classification and history recording

1. Extract compiler version from layer.json deps field instead of using
empty string - searches for ocaml-base-compiler or ocaml-variants
2. Record dependency_failure entries for packages that have no build
layer by comparing solutions against scanned layers
3. Deduplicate history entries by checking for existing run_id +
build_hash before appending
4. Record doc success entries for blessed packages, not just failures

Also extract matches_any helper to clean up classify_build_failure and
doc category detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+87 -22
+87 -22
day10/bin/main.ml
··· 267 267 268 268 let record_build_result ~packages_dir ~run_id ~pkg_str ~build_hash 269 269 ~compiler ~blessed ~status ~category ?error ?failed_dep ?failed_dep_hash () = 270 - let entry : Day10_lib.History.entry = { 271 - ts = Day10_lib.Run_log.format_time (Unix.gettimeofday ()); 272 - run = run_id; 273 - build_hash; 274 - status; 275 - category; 276 - compiler; 277 - blessed; 278 - error; 279 - failed_dep; 280 - failed_dep_hash; 281 - } in 282 - Day10_lib.History.append ~packages_dir ~pkg_str entry 270 + (* Skip if already recorded for this run and build_hash *) 271 + let existing = Day10_lib.History.read ~packages_dir ~pkg_str in 272 + let already_recorded = List.exists (fun (e : Day10_lib.History.entry) -> 273 + e.run = run_id && e.build_hash = build_hash 274 + ) existing in 275 + if already_recorded then () 276 + else begin 277 + let entry : Day10_lib.History.entry = { 278 + ts = Day10_lib.Run_log.format_time (Unix.gettimeofday ()); 279 + run = run_id; 280 + build_hash; 281 + status; 282 + category; 283 + compiler; 284 + blessed; 285 + error; 286 + failed_dep; 287 + failed_dep_hash; 288 + } in 289 + Day10_lib.History.append ~packages_dir ~pkg_str entry 290 + end 291 + 292 + (** Check if any pattern in the list matches the given text (case-insensitive). *) 293 + let matches_any patterns text = 294 + List.exists (fun pat -> 295 + try ignore (Str.search_forward (Str.regexp_case_fold pat) text 0); true 296 + with Not_found -> false 297 + ) patterns 298 + 299 + (** Extract the compiler version from a layer.json's deps list. 300 + Looks for packages starting with "ocaml-base-compiler" or "ocaml-variants". *) 301 + let extract_compiler_from_deps json = 302 + let open Yojson.Safe.Util in 303 + let deps = try json |> member "deps" |> to_list |> List.map to_string with _ -> [] in 304 + let compiler_pkg = List.find_opt (fun dep -> 305 + let name = try String.sub dep 0 (String.index dep '.') with Not_found -> dep in 306 + name = "ocaml-base-compiler" || name = "ocaml-variants" 307 + ) deps in 308 + match compiler_pkg with 309 + | Some pkg -> 310 + (try String.sub pkg (String.index pkg '.' + 1) (String.length pkg - String.index pkg '.' - 1) 311 + with Not_found -> pkg) 312 + | None -> "" 283 313 284 314 (** Classify a build failure by scanning the build log for known patterns. *) 285 315 let classify_build_failure build_log_path = ··· 300 330 "unmet dependencies"; 301 331 "dpkg: dependency problems"; 302 332 ] in 303 - if List.exists (fun pat -> try ignore (Str.search_forward (Str.regexp_case_fold pat) log_content 0); true with Not_found -> false) transient_patterns then 333 + if matches_any transient_patterns log_content then 304 334 ("failure", "transient_failure", Some "Transient infrastructure failure detected in build log") 305 - else if List.exists (fun pat -> try ignore (Str.search_forward (Str.regexp_case_fold pat) log_content 0); true with Not_found -> false) depext_patterns then 335 + else if matches_any depext_patterns log_content then 306 336 ("failure", "depext_unavailable", Some "Missing system dependency detected in build log") 307 337 else 308 338 ("failure", "build_failure", None) ··· 1313 1343 let doc_success = ref 0 in 1314 1344 let doc_fail = ref 0 in 1315 1345 let failures = ref [] in 1346 + (* Track which packages have build layers, for detecting dependency failures *) 1347 + let built_packages = Hashtbl.create 64 in 1348 + (* Track per-package build layer exit status and compiler, for dep failure reporting *) 1349 + let build_layer_info = Hashtbl.create 64 in 1316 1350 let () = 1317 1351 try 1318 1352 Sys.readdir layer_dir |> Array.iter (fun name -> ··· 1325 1359 (* Build layer *) 1326 1360 let pkg_name = json |> member "package" |> to_string in 1327 1361 let exit_status = json |> member "exit_status" |> to_int_option |> Option.value ~default:(-1) in 1362 + let compiler = extract_compiler_from_deps json in 1328 1363 (* Check if this build is blessed *) 1329 1364 let blessed_build_link = Path.(packages_dir / pkg_name / "blessed-build") in 1330 1365 let is_blessed = try 1331 1366 let target = Unix.readlink blessed_build_link in 1332 1367 Filename.basename target = name 1333 1368 with _ -> false in 1369 + Hashtbl.replace built_packages pkg_name true; 1370 + Hashtbl.replace build_layer_info pkg_name (name, exit_status, compiler); 1334 1371 if exit_status = 0 then begin 1335 1372 incr build_success; 1336 1373 (* Add build log to run *) ··· 1338 1375 Day10_lib.Run_log.add_build_log run_info ~package:pkg_name ~source_log:build_log; 1339 1376 (* Record success in history *) 1340 1377 record_build_result ~packages_dir ~run_id ~pkg_str:pkg_name 1341 - ~build_hash:name ~compiler:"" ~blessed:is_blessed 1378 + ~build_hash:name ~compiler ~blessed:is_blessed 1342 1379 ~status:"success" ~category:"success" () 1343 1380 end else begin 1344 1381 incr build_fail; ··· 1348 1385 (* Classify and record build failure in history *) 1349 1386 let (status, category, error) = classify_build_failure build_log in 1350 1387 record_build_result ~packages_dir ~run_id ~pkg_str:pkg_name 1351 - ~build_hash:name ~compiler:"" ~blessed:is_blessed 1388 + ~build_hash:name ~compiler ~blessed:is_blessed 1352 1389 ~status ~category ?error () 1353 1390 end 1354 1391 end else if String.length name > 4 && String.sub name 0 4 = "doc-" then begin ··· 1364 1401 Day10_lib.Run_log.add_doc_log run_info ~package:pkg_name ~source_log:doc_log ~layer_hash (); 1365 1402 (* Only count blessed docs in summary stats *) 1366 1403 if blessed then begin 1367 - if status = "success" then 1368 - incr doc_success 1369 - else begin 1404 + if status = "success" then begin 1405 + incr doc_success; 1406 + (* Record doc success for blessed packages *) 1407 + record_build_result ~packages_dir ~run_id ~pkg_str:pkg_name 1408 + ~build_hash:name ~compiler:"" ~blessed:true 1409 + ~status:"success" ~category:"success" () 1410 + end else begin 1370 1411 incr doc_fail; 1371 1412 let error_msg = doc |> member "error" |> to_string_option |> Option.value ~default:"unknown error" in 1372 1413 failures := (pkg_name, Printf.sprintf "doc: %s" error_msg) :: !failures; 1373 1414 (* Record blessed doc failure in history *) 1374 1415 let doc_category = 1375 - let lower = String.lowercase_ascii error_msg in 1376 - if try ignore (Str.search_forward (Str.regexp_string "link") lower 0); true with Not_found -> false then 1416 + if matches_any ["link"] (String.lowercase_ascii error_msg) then 1377 1417 "doc_link_failure" 1378 1418 else 1379 1419 "doc_compile_failure" ··· 1389 1429 ) 1390 1430 with _ -> () 1391 1431 in 1432 + (* Record dependency failures: packages in solutions that have no build layer *) 1433 + List.iter (fun (_target, solution) -> 1434 + OpamPackage.Map.iter (fun pkg deps -> 1435 + let pkg_str = OpamPackage.to_string pkg in 1436 + if not (Hashtbl.mem built_packages pkg_str) then begin 1437 + (* Find which dep failed by checking build_layer_info *) 1438 + let dep_pkgs = OpamPackage.Set.elements deps in 1439 + let failed_dep_info = List.find_map (fun dep -> 1440 + let dep_str = OpamPackage.to_string dep in 1441 + match Hashtbl.find_opt build_layer_info dep_str with 1442 + | Some (hash, exit_status, _) when exit_status <> 0 -> 1443 + Some (dep_str, hash) 1444 + | _ -> None 1445 + ) dep_pkgs in 1446 + let failed_dep, failed_dep_hash = match failed_dep_info with 1447 + | Some (dep, hash) -> (Some dep, Some hash) 1448 + | None -> (None, None) 1449 + in 1450 + record_build_result ~packages_dir ~run_id ~pkg_str 1451 + ~build_hash:"none" ~compiler:"" ~blessed:false 1452 + ~status:"failure" ~category:"dependency_failure" 1453 + ?failed_dep ?failed_dep_hash () 1454 + end 1455 + ) solution 1456 + ) solutions; 1392 1457 let html_versions = match config.html_output with 1393 1458 | None -> 0 1394 1459 | Some html_dir ->