diff --git a/db/builder_db.ml b/db/builder_db.ml index 4472b8e..f0e1257 100644 --- a/db/builder_db.ml +++ b/db/builder_db.ml @@ -325,6 +325,16 @@ module Build = struct ORDER BY b.start_d DESC, b.start_ps DESC |} + let get_all_artifact_sha_by_platform = + Caqti_request.collect + Caqti_type.(tup2 (id `job) string) + Rep.cstruct + {| SELECT DISTINCT a.sha256 + FROM build_artifact a, build b + WHERE b.job = ? AND b.platform = ? AND b.main_binary = a.id + ORDER BY b.start_d DESC, b.start_ps DESC + |} + let get_latest_failed = Caqti_request.find_opt (id `job) @@ -338,9 +348,22 @@ module Build = struct LIMIT 1 |} + let get_latest_failed_by_platform = + Caqti_request.find_opt + Caqti_type.(tup2 (id `job) string) + t + {| SELECT uuid, start_d, start_ps, finish_d, finish_ps, + result_code, result_msg, console, script, + platform, main_binary, input_id, user, job + FROM build + WHERE job = ? AND platform = ? AND result_code <> 0 + ORDER BY start_d DESC, start_ps DESC + LIMIT 1 + |} + let get_latest = Caqti_request.find_opt - (id `job) + Caqti_type.(tup2 (id `job) string) Caqti_type.(tup3 (id `build) t @@ -353,7 +376,7 @@ module Build = struct FROM build b LEFT JOIN build_artifact a ON b.main_binary = a.id - WHERE b.job = ? + WHERE b.job = ?1 AND b.platform = ?2 ORDER BY b.start_d DESC, b.start_ps DESC LIMIT 1 |} @@ -369,22 +392,46 @@ module Build = struct LIMIT 1 |} - let get_previous_successful = + let get_latest_successful_uuid_by_platform = + Caqti_request.find_opt + Caqti_type.(tup2 (id `job) string) + Rep.uuid + {| SELECT b.uuid + FROM build b + WHERE b.job = ?1 AND b.result_code = 0 AND b.platform = ?2 + ORDER BY b.start_d DESC, b.start_ps DESC + LIMIT 1 + |} + + + let get_previous_successful_uuid = Caqti_request.find_opt (id `build) - Caqti_type.(tup2 (id `build) t) - {| SELECT b.id, - b.uuid, b.start_d, b.start_ps, b.finish_d, b.finish_ps, - b.result_code, b.result_msg, b.console, b.script, - b.platform, b.main_binary, b.input_id, b.user, b.job + Rep.uuid + {| SELECT b.uuid FROM build b, build b0 WHERE b0.id = ? AND b0.job = b.job AND + b.platform = b0.platform AND b.result_code = 0 AND (b0.start_d > b.start_d OR b0.start_d = b.start_d AND b0.start_ps > b.start_ps) ORDER BY b.start_d DESC, b.start_ps DESC LIMIT 1 |} + let get_next_successful_uuid = + Caqti_request.find_opt + (id `build) + Rep.uuid + {| SELECT b.uuid + FROM build b, build b0 + WHERE b0.id = ? AND b0.job = b.job AND + b.platform = b0.platform AND + b.result_code = 0 AND + (b0.start_d < b.start_d OR b0.start_d = b.start_d AND b0.start_ps < b.start_ps) + ORDER BY b.start_d ASC, b.start_ps ASC + LIMIT 1 + |} + let get_same_input_same_output_builds = Caqti_request.collect (id `build) @@ -432,6 +479,12 @@ module Build = struct LIMIT 1 |} + let get_platforms_for_job = + Caqti_request.collect + (id `job) + Caqti_type.string + "SELECT DISTINCT platform FROM build WHERE job = ?" + let add = Caqti_request.exec t diff --git a/db/builder_db.mli b/db/builder_db.mli index d2b81a3..bae5eae 100644 --- a/db/builder_db.mli +++ b/db/builder_db.mli @@ -115,16 +115,26 @@ sig ([`job] id, [`build] id * t, [ `Many | `One | `Zero ]) Caqti_request.t val get_all_artifact_sha : ([`job] id, Cstruct.t, [ `Many | `One | `Zero ]) Caqti_request.t + val get_all_artifact_sha_by_platform : + ([`job] id * string, Cstruct.t, [ `Many | `One | `Zero ]) Caqti_request.t val get_latest : - ([`job] id, [`build] id * t * file option, [< `Many | `One | `Zero > `One `Zero ]) + ([`job] id * string, [`build] id * t * file option, [< `Many | `One | `Zero > `One `Zero ]) Caqti_request.t val get_latest_failed : ([`job] id, t, [< `Many | `One | `Zero > `One `Zero ]) Caqti_request.t + val get_latest_failed_by_platform : + ([`job] id * string, t, [< `Many | `One | `Zero > `One `Zero ]) Caqti_request.t val get_latest_successful_uuid : ([`job] id, Uuidm.t, [< `Many | `One | `Zero > `One `Zero ]) Caqti_request.t - val get_previous_successful : - ([`build] id, [`build] id * t, [< `Many | `One | `Zero > `One `Zero ]) + val get_latest_successful_uuid_by_platform : + ([`job] id * string, Uuidm.t, [< `Many | `One | `Zero > `One `Zero ]) + Caqti_request.t + val get_previous_successful_uuid : + ([`build] id, Uuidm.t, [< `Many | `One | `Zero > `One `Zero ]) + Caqti_request.t + val get_next_successful_uuid : + ([`build] id, Uuidm.t, [< `Many | `One | `Zero > `One `Zero ]) Caqti_request.t val get_same_input_same_output_builds : ([`build] id, t, [ `Many | `One | `Zero ]) Caqti_request.t @@ -134,6 +144,8 @@ sig ([`build] id, Cstruct.t, [ `Many | `One | `Zero ]) Caqti_request.t val get_one_by_input_id : (Cstruct.t, t, [< `Many | `One | `Zero > `One ]) Caqti_request.t + val get_platforms_for_job : + ([`job] id, string, [ `Many | `One | `Zero ]) Caqti_request.t val add : (t, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t val get_by_hash : (Cstruct.t, t, [< `Many | `One | `Zero > `One]) Caqti_request.t diff --git a/lib/builder_web.ml b/lib/builder_web.ml index 4d27caa..9536d6e 100644 --- a/lib/builder_web.ml +++ b/lib/builder_web.ml @@ -96,14 +96,19 @@ let add_routes datadir = List.fold_right (fun (job_id, job_name, section, synopsis) r -> r >>= fun acc -> - Dream.sql req (Model.build_with_main_binary job_id) >>= function - | Some (latest_build, latest_artifact) -> - let v = (job_name, synopsis, latest_build, latest_artifact) in - let section = Option.value ~default:"Failed" section in - Lwt_result.return (Utils.String_map.add_or_create section v acc) - | None -> - Log.warn (fun m -> m "Job without builds: %s" job_name); - Lwt_result.return acc) + Dream.sql req (Model.platforms_of_job job_id) >>= fun ps -> + List.fold_right (fun platform r -> + r >>= fun acc -> + Dream.sql req (Model.build_with_main_binary job_id platform) >>= function + | Some (build, artifact) -> + Lwt_result.return ((platform, build, artifact) :: acc) + | None -> + Log.warn (fun m -> m "Job without builds: %s" job_name); + Lwt_result.return acc) + ps (Lwt_result.return []) >>= fun platform_builds -> + let v = (job_name, synopsis, platform_builds) in + let section = Option.value ~default:"Uncategorized" section in + Lwt_result.return (Utils.String_map.add_or_create section v acc)) jobs (Lwt_result.return Utils.String_map.empty) |> if_error "Error getting jobs" @@ -114,7 +119,10 @@ let add_routes datadir = let job req = let job_name = Dream.param "job" req in - Dream.sql req (Model.job_and_readme job_name) + let platform = Dream.query "platform" req in + (Dream.sql req (Model.job_and_readme job_name) >>= fun (job_id, readme) -> + Dream.sql req (Model.builds_grouped_by_output job_id platform) >|= fun builds -> + (readme, builds)) |> if_error "Error getting job" ~log:(fun e -> Log.warn (fun m -> m "Error getting job: %a" pp_error e)) >>= fun (readme, builds) -> @@ -123,9 +131,10 @@ let add_routes datadir = let redirect_latest req = let job_name = Dream.param "job" req in + let platform = Dream.query "platform" req in let path = Dream.path req |> String.concat "/" in (Dream.sql req (Model.job_id job_name) >>= Model.not_found >>= fun job_id -> - Dream.sql req (Model.latest_successful_build_uuid job_id)) + Dream.sql req (Model.latest_successful_build_uuid job_id platform)) >>= Model.not_found |> if_error "Error getting job" >>= fun build -> Dream.redirect req @@ -162,12 +171,14 @@ let add_routes datadir = Dream.sql req (Model.builds_with_same_input_and_same_main_binary build_id) >>= fun same_input_same_output -> Dream.sql req (Model.builds_with_different_input_and_same_main_binary build_id) >>= fun different_input_same_output -> Dream.sql req (Model.builds_with_same_input_and_different_main_binary build_id) >>= fun same_input_different_output -> - Dream.sql req (Model.latest_successful_build_uuid build.job_id) >|= fun latest_uuid -> - (readme, build, artifacts, same_input_same_output, different_input_same_output, same_input_different_output, latest_uuid)) + Dream.sql req (Model.latest_successful_build_uuid build.job_id (Some build.Builder_db.Build.platform)) >>= fun latest_uuid -> + Dream.sql req (Model.next_successful_build_uuid build_id) >>= fun next_uuid -> + Dream.sql req (Model.previous_successful_build_uuid build_id) >|= fun previous_uuid -> + (readme, build, artifacts, same_input_same_output, different_input_same_output, same_input_different_output, latest_uuid, next_uuid, previous_uuid)) |> if_error "Error getting job build" ~log:(fun e -> Log.warn (fun m -> m "Error getting job build: %a" pp_error e)) - >>= fun (readme, build, artifacts, same_input_same_output, different_input_same_output, same_input_different_output, latest_uuid) -> - Views.job_build job_name readme build artifacts same_input_same_output different_input_same_output same_input_different_output latest_uuid + >>= fun (readme, build, artifacts, same_input_same_output, different_input_same_output, same_input_different_output, latest_uuid, next_uuid, previous_uuid) -> + Views.job_build job_name readme build artifacts same_input_same_output different_input_same_output same_input_different_output latest_uuid next_uuid previous_uuid |> string_of_html |> Dream.html |> Lwt_result.ok in @@ -343,6 +354,12 @@ let add_routes datadir = let w f req = or_error_response (f req) in + (* + /developer <- front page with failed builds (indication) + /job/:job/developer(?platform=XX) <- job list with failed builds + /failed-builds(?platform=XX) <- all failed builds across all jobs (limit by the most recent 10) + *) + Dream.router [ Dream.get "/" (w builder); Dream.get "/job/:job/" (w job); diff --git a/lib/model.ml b/lib/model.ml index fbe04cb..c25e822 100644 --- a/lib/model.ml +++ b/lib/model.ml @@ -50,12 +50,15 @@ let build_artifacts build (module Db : CONN) = Db.collect_list Builder_db.Build_artifact.get_all_by_build build >|= List.map snd +let platforms_of_job id (module Db : CONN) = + Db.collect_list Builder_db.Build.get_platforms_for_job id + let build uuid (module Db : CONN) = Db.find_opt Builder_db.Build.get_by_uuid uuid >>= not_found -let build_with_main_binary job (module Db : CONN) = - Db.find_opt Builder_db.Build.get_latest job >|= +let build_with_main_binary job platform (module Db : CONN) = + Db.find_opt Builder_db.Build.get_latest (job, platform) >|= Option.map (fun (_id, build, file) -> (build, file)) let build_hash hash (module Db : CONN) = @@ -65,12 +68,18 @@ let build_exists uuid (module Db : CONN) = Db.find_opt Builder_db.Build.get_by_uuid uuid >|= Option.is_some -let latest_successful_build_uuid job_id (module Db : CONN) = - Db.find_opt Builder_db.Build.get_latest_successful_uuid job_id +let latest_successful_build_uuid job_id platform (module Db : CONN) = + match platform with + | None -> + Db.find_opt Builder_db.Build.get_latest_successful_uuid job_id + | Some platform -> + Db.find_opt Builder_db.Build.get_latest_successful_uuid_by_platform (job_id, platform) -let previous_successful_build id (module Db : CONN) = - Db.find_opt Builder_db.Build.get_previous_successful id >|= - Option.map (fun (_id, meta) -> meta) +let previous_successful_build_uuid id (module Db : CONN) = + Db.find_opt Builder_db.Build.get_previous_successful_uuid id + +let next_successful_build_uuid id (module Db : CONN) = + Db.find_opt Builder_db.Build.get_next_successful_uuid id let builds_with_different_input_and_same_main_binary id (module Db : CONN) = Db.collect_list Builder_db.Build.get_different_input_same_output_input_ids id >>= fun ids -> @@ -114,9 +123,19 @@ let readme job (module Db : CONN) = let job_and_readme job (module Db : CONN) = job_id job (module Db) >>= not_found >>= fun job_id -> Db.find Builder_db.Tag.get_id_by_name "readme.md" >>= fun readme_id -> - Db.find_opt Builder_db.Job_tag.get_value (readme_id, job_id) >>= fun readme -> - Db.find_opt Builder_db.Build.get_latest_failed job_id >>= fun failed -> - Db.collect_list Builder_db.Build.get_all_artifact_sha job_id >>= fun sha -> + Db.find_opt Builder_db.Job_tag.get_value (readme_id, job_id) >|= fun readme -> + job_id, readme + +let builds_grouped_by_output job_id platform (module Db : CONN) = + (match platform with + | None -> + Db.find_opt Builder_db.Build.get_latest_failed job_id >>= fun failed -> + Db.collect_list Builder_db.Build.get_all_artifact_sha job_id >|= fun sha -> + (failed, sha) + | Some p -> + Db.find_opt Builder_db.Build.get_latest_failed_by_platform (job_id, p) >>= fun failed -> + Db.collect_list Builder_db.Build.get_all_artifact_sha_by_platform (job_id, p) >|= fun sha -> + (failed, sha)) >>= fun (failed, sha) -> Lwt_list.fold_left_s (fun acc hash -> match acc with | Error _ as e -> Lwt.return e @@ -126,8 +145,7 @@ let job_and_readme job (module Db : CONN) = | Some f when Ptime.is_later ~than:build.Builder_db.Build.start f.Builder_db.Build.start -> None, (build, file) :: (f, None) :: builds | x -> x, (build, file) :: builds) (Ok (failed, [])) sha >|= fun (x, builds) -> - let builds = match x with None -> builds | Some f -> (f, None) :: builds in - readme, List.rev builds + (match x with None -> builds | Some f -> (f, None) :: builds) |> List.rev let jobs_with_section_synopsis (module Db : CONN) = Db.collect_list Builder_db.Job.get_all_with_section_synopsis () @@ -220,64 +238,35 @@ let commit_files datadir staging_dir job_name uuid = Lwt.return (Bos.OS.Dir.create job_dir) >>= fun _ -> Lwt.return (Bos.OS.Path.move staging_dir dest) -let infer_section_and_synopsis platform name artifacts = - let opam_switch = - match List.find_opt (fun (p, _) -> String.equal (Fpath.basename p) "opam-switch") artifacts with - | None -> None - | Some (_, data) -> Some (OpamFile.SwitchExport.read_from_string data) +let infer_section_and_synopsis artifacts = + let infer_synopsis_and_descr switch root = + match OpamPackage.Name.Map.find_opt root.OpamPackage.name switch.OpamFile.SwitchExport.overlays with + | None -> None, None + | Some opam -> OpamFile.OPAM.synopsis opam, OpamFile.OPAM.descr_body opam in - let infer_synopsis_and_descr switch = - let root = switch.OpamFile.SwitchExport.selections.OpamTypes.sel_roots in - if OpamPackage.Set.cardinal root <> 1 then - None, None - else - let root = OpamPackage.Set.choose root in - match OpamPackage.Name.Map.find_opt root.OpamPackage.name switch.OpamFile.SwitchExport.overlays with - | None -> None, None - | Some opam -> OpamFile.OPAM.synopsis opam, OpamFile.OPAM.descr_body opam - in - let infer_section_from_packages switch = - let root = switch.OpamFile.SwitchExport.selections.OpamTypes.sel_roots in - if OpamPackage.Set.cardinal root <> 1 then - None - else - let root = OpamPackage.Set.choose root in - let root_pkg_name = OpamPackage.Name.to_string root.OpamPackage.name in - if Astring.String.is_prefix ~affix:"mirage-unikernel-" root_pkg_name then - let influx = OpamPackage.Name.of_string "metrics-influx" in - if OpamPackage.Set.exists (fun p -> OpamPackage.Name.equal p.OpamPackage.name influx) - switch.OpamFile.SwitchExport.selections.OpamTypes.sel_installed - then - Some "Unikernels (with metrics reported to Influx)" - else - Some "Unikernels" + let infer_section switch root = + let root_pkg_name = OpamPackage.Name.to_string root.OpamPackage.name in + if Astring.String.is_prefix ~affix:"mirage-unikernel-" root_pkg_name then + let influx = OpamPackage.Name.of_string "metrics-influx" in + if OpamPackage.Set.exists (fun p -> OpamPackage.Name.equal p.OpamPackage.name influx) + switch.OpamFile.SwitchExport.selections.OpamTypes.sel_installed + then + "Unikernels (with metrics reported to Influx)" else - None - in - let infer_section_from_platform_or_name = - if String.equal platform "no-platform" then - let map = [ - "-freebsd", "FreeBSD" ; - "-debian", "Debian" ; - "-ubuntu", "Ubuntu" ; - ] in - match - List.find_opt (fun (affix, _) -> Astring.String.is_infix ~affix name) map - with - | None -> None - | Some (_, os) -> Some (os ^ " Packages") + "Unikernels" else - Some (platform ^ " Packages") + "Packages" in - match opam_switch with + match List.find_opt (fun (p, _) -> String.equal (Fpath.basename p) "opam-switch") artifacts with | None -> None, (None, None) - | Some opam_switch -> - let section = - match infer_section_from_packages opam_switch with - | None -> infer_section_from_platform_or_name - | Some x -> Some x - in - section, infer_synopsis_and_descr opam_switch + | Some (_, data) -> + try + let switch = OpamFile.SwitchExport.read_from_string data in + let root = switch.OpamFile.SwitchExport.selections.OpamTypes.sel_roots in + assert (OpamPackage.Set.cardinal root = 1); + let root = OpamPackage.Set.choose root in + Some (infer_section switch root), infer_synopsis_and_descr switch root + with _ -> None, (None, None) let compute_input_id artifacts = let get_hash filename = @@ -363,7 +352,7 @@ let add_build console; script; platform; main_binary = None; input_id; user_id; job_id } >>= fun () -> Db.find last_insert_rowid () >>= fun id -> - let sec_syn = infer_section_and_synopsis platform job_name raw_artifacts in + let sec_syn = infer_section_and_synopsis raw_artifacts in let add_or_update tag_id tag_value = Db.find_opt Job_tag.get_value (tag_id, job_id) >>= function | None -> Db.exec Job_tag.add (tag_id, tag_value, job_id) diff --git a/lib/model.mli b/lib/model.mli index 277991b..3a8ce8b 100644 --- a/lib/model.mli +++ b/lib/model.mli @@ -21,10 +21,13 @@ val build_artifact_data : Fpath.t -> Builder_db.file -> val build_artifacts : [`build] Builder_db.id -> Caqti_lwt.connection -> (Builder_db.file list, [> Caqti_error.call_or_retrieve ]) result Lwt.t +val platforms_of_job : [`job] Builder_db.id -> Caqti_lwt.connection -> + (string list, [> Caqti_error.call_or_retrieve ]) result Lwt.t + val build : Uuidm.t -> Caqti_lwt.connection -> ([`build] Builder_db.id * Builder_db.Build.t, [> error ]) result Lwt.t -val build_with_main_binary : [`job] Builder_db.id -> Caqti_lwt.connection -> +val build_with_main_binary : [`job] Builder_db.id -> string -> Caqti_lwt.connection -> ((Builder_db.Build.t * Builder_db.file option) option, [> Caqti_error.call_or_retrieve ]) result Lwt.t val build_hash : Cstruct.t -> Caqti_lwt.connection -> @@ -33,11 +36,14 @@ val build_hash : Cstruct.t -> Caqti_lwt.connection -> val build_exists : Uuidm.t -> Caqti_lwt.connection -> (bool, [> Caqti_error.call_or_retrieve ]) result Lwt.t -val latest_successful_build_uuid : [`job] Builder_db.id -> Caqti_lwt.connection -> +val latest_successful_build_uuid : [`job] Builder_db.id -> string option -> Caqti_lwt.connection -> (Uuidm.t option, [> Caqti_error.call_or_retrieve ]) result Lwt.t -val previous_successful_build : [`build] Builder_db.id -> Caqti_lwt.connection -> - (Builder_db.Build.t option, [> Caqti_error.call_or_retrieve ]) result Lwt.t +val previous_successful_build_uuid : [`build] Builder_db.id -> Caqti_lwt.connection -> + (Uuidm.t option, [> Caqti_error.call_or_retrieve ]) result Lwt.t + +val next_successful_build_uuid : [`build] Builder_db.id -> Caqti_lwt.connection -> + (Uuidm.t option, [> Caqti_error.call_or_retrieve ]) result Lwt.t val builds_with_different_input_and_same_main_binary : [`build] Builder_db.id -> Caqti_lwt.connection -> (Builder_db.Build.t list, [> Caqti_error.call_or_retrieve ]) result Lwt.t @@ -58,7 +64,10 @@ val readme : string -> Caqti_lwt.connection -> (string option, [> error ]) result Lwt.t val job_and_readme : string -> Caqti_lwt.connection -> - (string option * (Builder_db.Build.t * Builder_db.file option) list, [> error ]) result Lwt.t + ([`job] Builder_db.id * string option, [> error ]) result Lwt.t + +val builds_grouped_by_output : [`job] Builder_db.id -> string option -> Caqti_lwt.connection -> + ((Builder_db.Build.t * Builder_db.file option) list, [> error ]) result Lwt.t val job_id : string -> Caqti_lwt.connection -> ([`job] Builder_db.id option, [> Caqti_error.call_or_retrieve ]) result Lwt.t diff --git a/lib/views.ml b/lib/views.ml index e4be281..64fa0b3 100644 --- a/lib/views.ml +++ b/lib/views.ml @@ -127,27 +127,35 @@ let builder section_job_map = Utils.String_map.fold (fun section jobs acc -> acc @ [ h2 [ txt section ]; - ul (List.map (fun (job_name, synopsis, latest_build, latest_artifact) -> + ul (List.map (fun (job_name, synopsis, platform_builds) -> li ([ - a ~a:[a_href ("job/" ^ job_name ^ "/")] - [txt job_name]; - txt " "; - check_icon latest_build.Builder_db.Build.result; + a ~a:[a_href ("job/" ^ job_name ^ "/")] [txt job_name]; br (); txt (Option.value ~default:"" synopsis); - br (); - a ~a:[a_href (Fmt.str "job/%s/build/%a/" job_name Uuidm.pp - latest_build.Builder_db.Build.uuid)] - [txtf "%a" (Ptime.pp_human ()) latest_build.Builder_db.Build.start]; - txt " "; - ] @ match latest_artifact with - | Some main_binary -> - artifact ~basename:true job_name latest_build main_binary - | None -> - [ - txtf "Build failed"; - ])) jobs) - ]) + br () + ] @ + List.flatten + (List.map (fun (platform, latest_build, latest_artifact) -> + [ + check_icon latest_build.Builder_db.Build.result; + txt " "; + a ~a:[a_href (Fmt.str "job/%s/?platform=%s" job_name platform)][txt platform]; + txt " "; + a ~a:[a_href (Fmt.str "job/%s/build/%a/" job_name Uuidm.pp + latest_build.Builder_db.Build.uuid)] + [txtf "%a" (Ptime.pp_human ()) latest_build.Builder_db.Build.start]; + txt " "; + ] @ (match latest_artifact with + | Some main_binary -> + artifact ~basename:true job_name latest_build main_binary + | None -> + [ txtf "Build failure: %a" Builder.pp_execution_result + latest_build.Builder_db.Build.result ] + ) @ [ br () ]) + platform_builds) + )) + jobs) + ]) section_job_map []) @@ -165,26 +173,21 @@ let job name readme builds = [ h2 ~a:[a_id "builds"] [txt "Builds"]; a ~a:[a_href "#readme"] [txt "Back to readme"]; - p [ - txtf "Currently %d builds." - (List.length builds) - ]; ul (List.map (fun (build, main_binary) -> li ([ + check_icon build.Builder_db.Build.result; + txtf " %s " build.platform; a ~a:[a_href Fpath.(to_string (v "build" / Uuidm.to_string build.Builder_db.Build.uuid / ""))] [ txtf "%a" (Ptime.pp_human ()) build.Builder_db.Build.start; ]; txt " "; - check_icon build.result; - br (); ] @ match main_binary with | Some main_binary -> artifact ~basename:true name build main_binary | None -> - [ - txtf "Build failed"; - ])) + [ txtf "Build failure: %a" Builder.pp_execution_result + build.Builder_db.Build.result ])) builds); ]) @@ -195,10 +198,9 @@ let job_build { Builder_db.Build.uuid; start; finish; result; platform; _ } artifacts same_input_same_output different_input_same_output same_input_different_output - latest_uuid + latest_uuid next_uuid previous_uuid = let delta = Ptime.diff finish start in - let successful_build = match result with Builder.Exited 0 -> true | _ -> false in layout ~title:(Fmt.str "Job %s %a" name pp_ptime start) ((h1 [txtf "Job %s" name] :: (match readme with @@ -214,14 +216,7 @@ let job_build a ~a:[a_href "#readme"] [txt "Back to readme"]; p [txtf "Built on platform %s" platform ]; p [txtf "Build took %a." Ptime.Span.pp delta ]; - p [txtf "Execution result: %a." Builder.pp_execution_result result]; ] @ - (match same_input_same_output with [] -> [] | xs -> [ - h3 [ txt "Reproduced by builds"] ; - p (List.concat_map (fun { Builder_db.Build.start ; uuid ; _ } -> - [ a ~a:[Fmt.kstr a_href "/job/%s/build/%a" name Uuidm.pp uuid] - [txtf "%a" pp_ptime start] ; - txt ", " ]) - xs) ] ) @ [ + p [txtf "Execution result: %a." Builder.pp_execution_result result]; h3 [txt "Build info"]; ul [ li [ a ~a:[Fmt.kstr a_href "/job/%s/build/%a/console" name Uuidm.pp uuid] @@ -231,26 +226,6 @@ let job_build [txt "Build script"]; ] ]; - h3 [txt "Comparisons with other builds"]; - p - ((match latest_uuid with - | Some latest_uuid when successful_build && not (Uuidm.equal latest_uuid uuid) -> - [ a ~a:[Fmt.kstr a_href "/compare/%a/%a/opam-switch" - Uuidm.pp uuid Uuidm.pp latest_uuid] - [txt "With latest build"] ; br () ] - | _ -> []) @ - List.concat_map (fun { Builder_db.Build.start = other_start ; uuid = other_uuid ; _ } -> - let fst, snd = if Ptime.is_later ~than:start other_start then uuid, other_uuid else other_uuid, uuid in - [ a ~a:[Fmt.kstr a_href "/compare/%a/%a/opam-switch" - Uuidm.pp fst Uuidm.pp snd] - [txtf "With build %a (output is identical binary)" pp_ptime other_start] ; br () ]) - different_input_same_output @ - List.concat_map (fun { Builder_db.Build.start = other_start ; uuid = other_uuid ; _ } -> - let fst, snd = if Ptime.is_later ~than:start other_start then uuid, other_uuid else other_uuid, uuid in - [ a ~a:[Fmt.kstr a_href "/compare/%a/%a/opam-switch" - Uuidm.pp fst Uuidm.pp snd] - [txtf "With build %a (same input, different output)" pp_ptime other_start] ; br () ]) - same_input_different_output); h3 [txt "Build artifacts"]; dl (List.concat_map (fun { Builder_db.filepath; localpath=_; sha256; size } -> @@ -265,30 +240,51 @@ let job_build ]; ]) artifacts); - (* - (* FIXME *) - h3 [txt "Job script"]; - toggleable "job-script" "Show/hide" - [ pre [txt script] ]; - h3 [txt "Build log"]; - toggleable ~hidden:false "build-log" "Show/hide build log" - [ - table - (List.mapi (fun idx (ts, line) -> - let ts_id = "L" ^ string_of_int idx in - tr [ - td ~a:[ - a_class ["output-ts"]; - a_id ts_id; - ] - [a ~a:[a_href ("#"^ts_id); a_class ["output-ts-anchor"]] - [code [txtf "%#d ms" (Duration.to_ms (Int64.of_int ts))]]]; - td ~a:[a_class ["output-code"]] - [code [txt line]]; - ]) - (List.rev console)); - ]; - *) + h3 [ txtf "Reproduced by %d builds" (List.length (same_input_same_output @ different_input_same_output))] ; + ul + ((List.map (fun { Builder_db.Build.start ; uuid ; platform ; _ } -> + li [ + txtf "on %s, same input, " platform; + a ~a:[Fmt.kstr a_href "/job/%s/build/%a/" name Uuidm.pp uuid] + [txtf "%a" pp_ptime start] + ]) + same_input_same_output) @ + List.map (fun { Builder_db.Build.start = other_start ; uuid = other_uuid ; platform ; _ } -> + let fst, snd = if Ptime.is_later ~than:start other_start then uuid, other_uuid else other_uuid, uuid in + li [ + txtf "on %s, different input, " platform; + a ~a:[Fmt.kstr a_href "/compare/%a/%a/opam-switch" + Uuidm.pp fst Uuidm.pp snd] + [txtf "%a" pp_ptime other_start] + ]) + different_input_same_output) + ] @ + (if same_input_different_output = [] then + [] + else + [ h3 [txt "Same input, different output (not reproducible!)"]; + ul ( + List.map (fun { Builder_db.Build.start = other_start ; uuid = other_uuid ; platform ; _ } -> + let fst, snd = if Ptime.is_later ~than:start other_start then uuid, other_uuid else other_uuid, uuid in + li [ + txtf "on %s, " platform ; + a ~a:[Fmt.kstr a_href "/compare/%a/%a/opam-switch" Uuidm.pp fst Uuidm.pp snd] + [txtf "%a" pp_ptime other_start] + ]) + same_input_different_output) + ]) @ + [ h3 [txt "Comparisons with other builds on the same platform"]; + let opt_build (ctx, uu) = + match uu with + | Some uu when not (Uuidm.equal uuid uu) -> + [ li [ a ~a:[Fmt.kstr a_href "/compare/%a/%a/opam-switch" + Uuidm.pp uuid Uuidm.pp uu] + [txtf "With %s build" ctx]] + ] + | _ -> [] + in + ul + (List.concat_map opt_build [ ("latest", latest_uuid) ; ("next", next_uuid) ; ("previous", previous_uuid) ]) ]) let key_values xs = diff --git a/test/builder_db.ml b/test/builder_db.ml index 9748d62..64bb168 100644 --- a/test/builder_db.ml +++ b/test/builder_db.ml @@ -220,7 +220,7 @@ let test_build_get_latest (module Db : CONN) = add_second_build (module Db) >>= fun () -> (* Test *) Db.find_opt Builder_db.Job.get_id_by_name job_name >>= fail_if_none >>= fun job_id -> - Db.find_opt Builder_db.Build.get_latest job_id + Db.find_opt Builder_db.Build.get_latest (job_id, platform) >>| get_opt "no latest build" >>| fun (_id, meta, main_binary') -> Alcotest.(check (option Testable.file)) "same main binary" main_binary' (Some main_binary); Alcotest.(check Testable.uuid) "same uuid" meta.uuid uuid' @@ -229,17 +229,17 @@ let test_build_get_previous (module Db : CONN) = add_second_build (module Db) >>= fun () -> Db.find_opt Builder_db.Build.get_by_uuid uuid' >>| get_opt "no build" >>= fun (id, _build) -> - Db.find_opt Builder_db.Build.get_previous_successful id - >>| get_opt "no previous build" >>| fun (_id, meta) -> - Alcotest.(check Testable.uuid) "same uuid" meta.uuid uuid + Db.find_opt Builder_db.Build.get_previous_successful_uuid id + >>| get_opt "no previous build" >>| fun uuid' -> + Alcotest.(check Testable.uuid) "same uuid" uuid' uuid let test_build_get_previous_none (module Db : CONN) = Db.find_opt Builder_db.Build.get_by_uuid uuid >>| get_opt "no build" >>= fun (id, _build) -> - Db.find_opt Builder_db.Build.get_previous_successful id >>| function + Db.find_opt Builder_db.Build.get_previous_successful_uuid id >>| function | None -> () - | Some (_id, meta) -> - Alcotest.failf "Got unexpected result %a" Uuidm.pp meta.uuid + | Some uuid -> + Alcotest.failf "Got unexpected result %a" Uuidm.pp uuid let test_build_get_with_jobname_by_hash (module Db : CONN) = add_second_build (module Db) >>= fun () ->