My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

Add run history tracking and diff to results command

batch: writes runs/TIMESTAMP.json after each build with succeeded/
failed package lists and repo SHAs.

results: shows run history with counts, and diffs the last two runs
showing newly fixed packages, new failures, and net change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+105 -1
+35 -1
day11/bin/cmd_batch.ml
··· 396 396 match Day11_build.Build_layer.build env benv ?patches node () with 397 397 | Day11_build.Types.Success _ -> true 398 398 | _ -> false); 399 + let n_succeeded = Atomic.get succeeded in 400 + let n_failed = Atomic.get failed in 399 401 Printf.printf "\n=== Build: %d succeeded, %d failed ===\n%!" 400 - (Atomic.get succeeded) (Atomic.get failed); 402 + n_succeeded n_failed; 403 + (* Write run summary *) 404 + let runs_dir = Fpath.(cache_dir / "runs") in 405 + Bos.OS.Dir.create ~path:true runs_dir |> ignore; 406 + let timestamp = Day11_layer.Layer_meta.now_iso8601 () in 407 + let succeeded_pkgs = List.filter_map (fun (node : Day11_layer.Layer_type.build) -> 408 + let dir = Day11_layer.Layer_type.build_dir ~os_dir node in 409 + match Day11_layer.Layer_meta.load_build Fpath.(dir / "layer.json") with 410 + | Ok { exit_status = 0; _ } -> Some (OpamPackage.to_string node.pkg) 411 + | _ -> None 412 + ) nodes in 413 + let failed_pkgs = List.filter_map (fun (node : Day11_layer.Layer_type.build) -> 414 + let dir = Day11_layer.Layer_type.build_dir ~os_dir node in 415 + match Day11_layer.Layer_meta.load_build Fpath.(dir / "layer.json") with 416 + | Ok { exit_status; _ } when exit_status <> 0 -> 417 + Some (OpamPackage.to_string node.pkg) 418 + | _ -> None 419 + ) nodes in 420 + let run_json = `Assoc [ 421 + ("timestamp", `String timestamp); 422 + ("repos", `List (List.map (fun (repo, sha) -> 423 + `Assoc [ ("path", `String repo); ("commit", `String sha) ] 424 + ) repos_with_shas)); 425 + ("solved", `Int n_solved); 426 + ("succeeded", `Int (List.length succeeded_pkgs)); 427 + ("failed", `Int (List.length failed_pkgs)); 428 + ("succeeded_packages", `List (List.map (fun s -> `String s) 429 + (List.sort_uniq String.compare succeeded_pkgs))); 430 + ("failed_packages", `List (List.map (fun s -> `String s) 431 + (List.sort_uniq String.compare failed_pkgs))); 432 + ] in 433 + let run_file = Fpath.(runs_dir / (timestamp ^ ".json")) in 434 + ignore (Bos.OS.File.write run_file (Yojson.Safe.pretty_to_string run_json)); 401 435 (* Docs *) 402 436 if with_doc then begin 403 437 Printf.printf "\nBuilding doc tools...\n%!";
+70
day11/bin/cmd_results.ml
··· 174 174 if cascade_count > 0 then 175 175 Printf.printf "\nCascade failures: %d layers from duplicate hashes or cascades\n" 176 176 cascade_count; 177 + (* Show run history and diff *) 178 + let runs_dir = Fpath.(cache_dir / "runs") in 179 + (match Bos.OS.Dir.contents runs_dir with 180 + | Ok files -> 181 + let runs = files 182 + |> List.filter (fun f -> Fpath.has_ext ".json" f) 183 + |> List.sort (fun a b -> compare (Fpath.to_string b) (Fpath.to_string a)) 184 + in 185 + if runs <> [] then begin 186 + Printf.printf "\n=== Run History ===\n"; 187 + List.iter (fun f -> 188 + (try 189 + let data = In_channel.with_open_text (Fpath.to_string f) 190 + In_channel.input_all in 191 + let json = Yojson.Safe.from_string data in 192 + let open Yojson.Safe.Util in 193 + let ts = json |> member "timestamp" |> to_string in 194 + let ok = json |> member "succeeded" |> to_int in 195 + let fail = json |> member "failed" |> to_int in 196 + Printf.printf " %s: %d succeeded, %d failed\n" ts ok fail 197 + with _ -> ()) 198 + ) runs; 199 + (* Diff last two runs *) 200 + if List.length runs >= 2 then begin 201 + let load_pkg_list json key = 202 + let open Yojson.Safe.Util in 203 + json |> member key |> to_list |> List.map to_string 204 + |> List.sort String.compare 205 + in 206 + (try 207 + let latest = Yojson.Safe.from_file 208 + (Fpath.to_string (List.nth runs 0)) in 209 + let prev = Yojson.Safe.from_file 210 + (Fpath.to_string (List.nth runs 1)) in 211 + let latest_ok = load_pkg_list latest "succeeded_packages" in 212 + let prev_ok = load_pkg_list prev "succeeded_packages" in 213 + let latest_fail = load_pkg_list latest "failed_packages" in 214 + let prev_fail = load_pkg_list prev "failed_packages" in 215 + let newly_ok = List.filter (fun p -> 216 + not (List.mem p prev_ok)) latest_ok in 217 + let newly_fail = List.filter (fun p -> 218 + not (List.mem p prev_fail)) latest_fail in 219 + let fixed = List.filter (fun p -> 220 + List.mem p prev_fail) newly_ok in 221 + if newly_ok <> [] || newly_fail <> [] then begin 222 + Printf.printf "\n=== Changes (latest vs previous) ===\n"; 223 + if fixed <> [] then begin 224 + Printf.printf " Fixed (%d):\n" (List.length fixed); 225 + List.iter (fun p -> 226 + Printf.printf " + %s\n" p) (List.filteri (fun i _ -> i < 20) fixed); 227 + if List.length fixed > 20 then 228 + Printf.printf " ... and %d more\n" (List.length fixed - 20) 229 + end; 230 + if newly_fail <> [] then begin 231 + Printf.printf " New failures (%d):\n" (List.length newly_fail); 232 + List.iter (fun p -> 233 + Printf.printf " - %s\n" p) (List.filteri (fun i _ -> i < 20) newly_fail); 234 + if List.length newly_fail > 20 then 235 + Printf.printf " ... and %d more\n" (List.length newly_fail - 20) 236 + end; 237 + let net = List.length newly_ok - List.length newly_fail in 238 + if net > 0 then 239 + Printf.printf " Net: +%d packages\n" net 240 + else if net < 0 then 241 + Printf.printf " Net: %d packages\n" net 242 + end 243 + with _ -> ()) 244 + end 245 + end 246 + | Error _ -> ()); 177 247 0 178 248 179 249 let cmd =