Compare commits

..

1 commit

Author SHA1 Message Date
3a7e58abca Add builder-db access-list command 2021-11-08 16:51:16 +01:00
78 changed files with 2938 additions and 6857 deletions

3
.gitignore vendored
View file

@ -1,4 +1 @@
_build
*~
*#

View file

@ -1,130 +0,0 @@
# -*- conf -*-
# This is an example configuration file for ocp-indent
#
# Copy to the root of your project with name ".ocp-indent", customise, and
# transparently get consistent indentation on all your ocaml source files.
# Starting the configuration file with a preset ensures you won't fallback to
# definitions from "~/.ocp/ocp-indent.conf".
# These are `normal`, `apprentice` and `JaneStreet` and set different defaults.
normal
#
# INDENTATION VALUES
#
# Number of spaces used in all base cases, for example:
# let foo =
# ^^bar
base = 2
# Indent for type definitions:
# type t =
# ^^int
type = 2
# Indent after `let in` (unless followed by another `let`):
# let foo = () in
# ^^bar
in = 0
# Indent after `match/try with` or `function`:
# match foo with
# ^^| _ -> bar
with = 0
# Indent for clauses inside a pattern-match (after the arrow):
# match foo with
# | _ ->
# ^^^^bar
# the default is 2, which aligns the pattern and the expression
match_clause = 2
# Indentation for items inside extension nodes:
# [%% id.id
# ^^^^contents ]
# [@@id
# ^^^^foo
# ]
ppx_stritem_ext = 2
# When nesting expressions on the same line, their indentation are in
# some cases stacked, so that it remains correct if you close them one
# at a line. This may lead to large indents in complex code though, so
# this parameter can be used to set a maximum value. Note that it only
# affects indentation after function arrows and opening parens at end
# of line.
#
# for example (left: `none`; right: `4`)
# let f = g (h (i (fun x -> # let f = g (h (i (fun x ->
# x) # x)
# ) # )
# ) # )
max_indent = 4
#
# INDENTATION TOGGLES
#
# Wether the `with` parameter should be applied even when in a sub-block.
# Can be `always`, `never` or `auto`.
# if `always`, there are no exceptions
# if `auto`, the `with` parameter is superseded when seen fit (most of the time,
# but not after `begin match` for example)
# if `never`, `with` is only applied if the match block starts a line.
#
# For example, the following is not indented if set to `always`:
# let f = function
# ^^| Foo -> bar
strict_with = never
# Controls indentation after the `else` keyword. `always` indents after the
# `else` keyword normally, like after `then`.
# If set to `never', the `else` keyword won't indent when followed by a newline.
# `auto` indents after `else` unless in a few "unclosable" cases (`let in`,
# `match`...).
#
# For example, with `strict_else=never`:
# if cond then
# foo
# else
# bar;
# baz
# `never` is discouraged if you may encounter code like this example,
# because it hides the scoping error (`baz` is always executed)
strict_else = always
# Ocp-indent will normally try to preserve your in-comment indentation, as long
# as it respects the left-margin or starts with `(*\n`. Setting this to `true`
# forces alignment within comments.
strict_comments = false
# Toggles preference of column-alignment over line indentation for most
# of the common operators and after mid-line opening parentheses.
#
# for example (left: `false'; right: `true')
# let f x = x # let f x = x
# + y # + y
align_ops = true
# Function parameters are normally indented one level from the line containing
# the function. This option can be used to have them align relative to the
# column of the function body instead.
# if set to `always`, always align below the function
# if `auto`, only do that when seen fit (mainly, after arrows)
# if `never`, no alignment whatsoever
#
# for example (left: `never`; right: `always or `auto)
# match foo with # match foo with
# | _ -> some_fun # | _ -> some_fun
# ^^parameter # ^^parameter
align_params = auto
#
# SYNTAX EXTENSIONS
#
# You can also add syntax extensions (as per the --syntax command-line option):
# syntax = mll lwt

View file

@ -1,27 +0,0 @@
## v0.2.0 (2024-09-05)
A whole slew of changes. Internally, we made a lot of incremental changes and improvements without doing a release. Thus this release is rather big. There is a lot of database migrations to apply, and unfortunately they have to be applied one at a time.
* Add a /failed-builds/ endpoint that lists the most recent failed builds.
* By default don't display failed builds on the front page.
* Times are printed with the 'Z' time zone offset indicator.
* Link to comparisons of builds take into account whether the "input", among others the list of dependencies, is different.
* New subcommand `builder-db extract-build` takes a build UUID and extracts the builder "full" file.
* Add /job/<job>/build/<build>/all.tar.gz endpoint with a gzip compressed tar archive of all build artifacts.
* Visual overhaul.
* Add (optional) visualizations displaying package dependencies ("opam-graph") and for unikernels a "modulectomy" view of how much each OCaml module is contributing to the final binary size. The visualizations are read from a cache on disk and can be generated from a script.
* A script hook is added on file upload. It may be used to generate visualizations or publish system packages to a repository.
* The 404 file not found page tries to be more informative.
* The build page for a unikernel build displays the solo5 device manifest, e.g. `with block devices "storage", and net devices "service"`.
* URLs with trailing slash redirect to without the trailing slash.
* Builder-web will try to be more helpful if its database doesn't exist or the database version is wrong.
* The opam diff works for mirage 4 unikernels taking into account the opam-monorepo/duniverse packages.
* Markdown rendering is now done using cmarkit instead of omd.
* Builder-web doesn't display jobs older than 30 days (customizable with `--expired-jobs` command line argument) on the front page.
* Build artifacts are stored by their content, and artifacts are automatically deduplicated. This makes builder-web much more space efficient on deployments that don't use deduplication on the filesystem level.
* New subcommands `builder-db vacuum *` to remove older builds. Can be called from a cron job to keep disk usage bounded.
* Lots of other improvements and bug fixes.
## v0.1.0 (2021-11-12)
* Initial public release

View file

@ -1,26 +1,20 @@
# Builder-web - a web frontend for reproducible builds
Builder-web takes in submissions of builds, typically from [builder](https://github.com/robur-coop/builder/), and displays the produced artifacts in a way that makes it easy to compare checksums and build status.
Produced binaries can be downloaded and executed.
[builds.robur.coop](https://builds.robur.coop/) itself runs builder-web.
Builder-web takes in submissions of builds, typically from [builder](https://github.com/roburio/builder/), and displays the produced artifacts in a way that makes it easy to compare checksums and build status.
Produced binaries can be downloaded and run by users.
## Overview
Builder-web is a single binary web server using a sqlite3 database with versioned schemas.
Finished builds from [builder](https://github.com/robur-coop/builder/) are uploaded to builder-web, stored and indexed in the database and presented in the web interface to the user.
Finished builds from [builder](https://github.com/roburio/builder/) are uploaded to builder-web, stored and indexed in the database and presented in the web interface to the user.
Users can:
* Get an overview of *jobs* - a job is typically script or opam package that is run and builds an artifact,
* Get an overview of *jobs* - a job is typically script or opam package that is run and builds an artifact,
* Browse all *builds* of a job - each job may have multiple builds, that are executed periodically by builder
* Browse individual *build* and download artifacts and build information for reproducing the same binary.
* Compare two builds, observing the differences in used opam packages, environment variables, and system packages.
* Search for the SHA-256 hash of a binary to view a build that resulted in that binary.
* Browse individual *build* and download artifacts and build information for reproducing the same binary.
Each build has a single binary as output - which checksum is recorded and can be searched for. The build information (opam acpackges, environment variables, host system packages) can be compared, and they may differ even if the checksum of the binary is identical.
## Installation
Installing from source can be done with opam: `opam install builder-web`.
We also provide [reproducible binary packages](https://builds.robur.coop/job/builder-web/).
The build artifacts are stored on the filesystem, its metadata is stored in the database.
## Setup
@ -28,33 +22,11 @@ Builder-web consists of a binary `builder-web` that runs a web server on port 30
These values can be changed with the `--port` and `--host` flags respectively.
See `builder-web --help` for more information.
Service scripts for FreeBSD and systemd are provided.
The web server expects a sqlite3 database in its data directory.
An empty database can be created with `builder-db migrate`.
## Database migrations
The sqlite3 database builder-web uses contains versioning information.
On every schema change the database schema version is updated, and migration and rollback scripts are provided.
The tool for database migrations is `builder-migrations`.
See the `builder-migrations --help` output for each migration for further details.
## Less common workflows
Here are listed some less common but useful workflows:
### Extracting builds from one server to another
This is useful for development on a separate machine that doesn't run the build jobs itself.
On the source server:
```ocaml
builder-db extract-build <build-hash> --dest <build-hash>.full
```
After copying the file over the destination server (you need a user first,
see `builder-db user-add --help`):
```ocaml
curl --data-binary @<build-hash>.full http://<user>:<passwd>@localhost:<builder-web-port>/upload
```
See the `--help` output for each migration for further details.

View file

@ -12,9 +12,9 @@ let scrypt_params ?(scrypt_n = 16384) ?(scrypt_r = 8) ?(scrypt_p = 1) () =
{ scrypt_n; scrypt_r; scrypt_p }
type pbkdf2_sha256 =
[ `Pbkdf2_sha256 of string * string * pbkdf2_sha256_params ]
[ `Pbkdf2_sha256 of Cstruct.t * Cstruct.t * pbkdf2_sha256_params ]
type scrypt = [ `Scrypt of string * string * scrypt_params ]
type scrypt = [ `Scrypt of Cstruct.t * Cstruct.t * scrypt_params ]
type password_hash = [ pbkdf2_sha256 | scrypt ]
@ -25,10 +25,10 @@ type 'a user_info = {
}
let pbkdf2_sha256 ~params:{ pbkdf2_sha256_iter = count } ~salt ~password =
Pbkdf.pbkdf2 ~prf:`SHA256 ~count ~dk_len:32l ~salt ~password
Pbkdf.pbkdf2 ~prf:`SHA256 ~count ~dk_len:32l ~salt ~password:(Cstruct.of_string password)
let scrypt ~params:{ scrypt_n = n; scrypt_r = r; scrypt_p = p } ~salt ~password =
Scrypt.scrypt ~n ~r ~p ~dk_len:32l ~salt ~password
Scrypt_kdf.scrypt_kdf ~n ~r ~p ~dk_len:32l ~salt ~password:(Cstruct.of_string password)
let hash ?(scrypt_params=scrypt_params ())
~username ~password ~restricted () =
@ -43,10 +43,10 @@ let hash ?(scrypt_params=scrypt_params ())
let verify_password password user_info =
match user_info.password_hash with
| `Pbkdf2_sha256 (password_hash, salt, params) ->
String.equal
Cstruct.equal
(pbkdf2_sha256 ~params ~salt ~password)
password_hash
| `Scrypt (password_hash, salt, params) ->
String.equal
Cstruct.equal
(scrypt ~params ~salt ~password)
password_hash

View file

@ -1,3 +1,3 @@
(library
(name builder_web_auth)
(libraries kdf.pbkdf kdf.scrypt mirage-crypto-rng))
(libraries pbkdf scrypt-kdf mirage-crypto-rng))

488
bin/builder_db.ml Normal file
View file

@ -0,0 +1,488 @@
let ( let* ) = Result.bind
let ( let+ ) x f = Result.map f x
let or_die exit_code = function
| Ok r -> r
| Error (`Msg msg) ->
Format.eprintf "Error: %s" msg;
exit exit_code
| Error (#Caqti_error.t as e) ->
Format.eprintf "Database error: %a" Caqti_error.pp e;
exit exit_code
let foreign_keys =
Caqti_request.exec
Caqti_type.unit
"PRAGMA foreign_keys = ON"
let defer_foreign_keys =
Caqti_request.exec
Caqti_type.unit
"PRAGMA defer_foreign_keys = ON"
let connect uri =
let* (module Db : Caqti_blocking.CONNECTION) = Caqti_blocking.connect uri in
let* () = Db.exec foreign_keys () in
let* () = Db.exec defer_foreign_keys () in
Ok (module Db : Caqti_blocking.CONNECTION)
let do_migrate dbpath =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ())
in
List.fold_left
(fun r migrate ->
let* () = r in
Logs.debug (fun m -> m "Executing migration query: %a" Caqti_request.pp migrate);
Db.exec migrate ())
(Ok ())
Builder_db.migrate
let migrate () dbpath =
or_die 1 (do_migrate dbpath)
let user_mod action dbpath scrypt_n scrypt_r scrypt_p username unrestricted =
let scrypt_params = Builder_web_auth.scrypt_params ?scrypt_n ?scrypt_r ?scrypt_p () in
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
print_string "Password: ";
flush stdout;
(* FIXME: getpass *)
let password = read_line () in
let restricted = not unrestricted in
let user_info = Builder_web_auth.hash ~scrypt_params ~username ~password ~restricted () in
match action with
| `Add ->
Db.exec Builder_db.User.add user_info
| `Update ->
Db.exec Builder_db.User.update_user user_info
in
or_die 1 r
let user_add () dbpath = user_mod `Add dbpath
let user_update () dbpath = user_mod `Update dbpath
let user_list () dbpath =
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
Db.iter_s Builder_db.User.get_all
(fun (_id, username) -> Ok (print_endline username))
()
in
or_die 1 r
let user_remove () dbpath username =
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
let* () = Db.exec Builder_db.Access_list.remove_all_by_username username in
Db.exec Builder_db.User.remove_user username
in
or_die 1 r
let user_disable () dbpath username =
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
let* () = Db.exec Builder_db.Access_list.remove_all_by_username username in
let* user = Db.find_opt Builder_db.User.get_user username in
match user with
| None -> Error (`Msg "user not found")
| Some (_, user_info) ->
let password_hash = `Scrypt (Cstruct.empty, Cstruct.empty, Builder_web_auth.scrypt_params ()) in
let user_info = { user_info with password_hash ; restricted = true } in
Db.exec Builder_db.User.update_user user_info
in
or_die 1 r
let access_add () dbpath username jobname =
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
let* (user_id, _) =
Result.bind (Db.find_opt Builder_db.User.get_user username)
(Option.to_result ~none:(`Msg "unknown user"))
in
let* job_id =
Result.bind (Db.find_opt Builder_db.Job.get_id_by_name jobname)
(Option.to_result ~none:(`Msg "job not found"))
in
Db.exec Builder_db.Access_list.add (user_id, job_id)
in
or_die 1 r
let access_remove () dbpath username jobname =
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
let* (user_id, _) =
Result.bind (Db.find_opt Builder_db.User.get_user username)
(Option.to_result ~none:(`Msg "unknown user"))
in
let* (job_id) =
Result.bind (Db.find_opt Builder_db.Job.get_id_by_name jobname)
(Option.to_result ~none:(`Msg "job not found"))
in
Db.exec Builder_db.Access_list.remove (user_id, job_id)
in
or_die 1 r
let access_list () dbpath =
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
Db.iter_s Builder_db.Access_list.get_all_names
(fun (username, job) -> Ok (Printf.printf "%s:%s\n" username job))
()
in
or_die 1 r
let job_remove () datadir jobname =
let dbpath = datadir ^ "/builder.sqlite3" in
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
let* job_id_opt = Db.find_opt Builder_db.Job.get_id_by_name jobname in
match job_id_opt with
| None ->
Logs.info (fun m -> m "Job %S doesn't exist or has already been removed." jobname);
Ok ()
| Some job_id ->
let* () = Db.start () in
let* () = Db.exec defer_foreign_keys () in
let r =
let* builds = Db.collect_list Builder_db.Build.get_all job_id in
let* () =
List.fold_left (fun r (build_id, build) ->
let* () = r in
let dir = Fpath.(v datadir / jobname / Uuidm.to_string build.Builder_db.Build.uuid) in
(match Bos.OS.Dir.delete ~recurse:true dir with
| Ok _ -> ()
| Error `Msg e -> Logs.warn (fun m -> m "failed to remove build directory %a: %s" Fpath.pp dir e));
let* () = Db.exec Builder_db.Build_artifact.remove_by_build build_id in
Db.exec Builder_db.Build.remove build_id)
(Ok ())
builds
in
let* () = Db.exec Builder_db.Job.remove job_id in
Db.commit ()
in
match r with
| Ok () -> Ok ()
| Error _ as e ->
Logs.warn (fun m -> m "Error: rolling back...");
let* () = Db.rollback () in
e
in
or_die 1 r
let input_ids =
Caqti_request.collect
Caqti_type.unit
Builder_db.Rep.cstruct
"SELECT DISTINCT input_id FROM build WHERE input_id IS NOT NULL"
let main_artifact_hash =
Caqti_request.collect
Builder_db.Rep.cstruct
(Caqti_type.tup3 Builder_db.Rep.cstruct Builder_db.Rep.uuid Caqti_type.string)
{|
SELECT a.sha256, b.uuid, j.name FROM build_artifact a, build b, job j
WHERE b.input_id = ? AND a.id = b.main_binary AND b.job = j.id
|}
let verify_input_id () dbpath =
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
let* input_ids = Db.collect_list input_ids () in
List.fold_left (fun acc input_id ->
let* () = acc in
let+ hashes = Db.collect_list main_artifact_hash input_id in
match hashes with
| (h, uuid, jobname) :: tl ->
List.iter (fun (h', uuid', _) ->
if Cstruct.equal h h' then
()
else
Logs.warn (fun m -> m "job %s input id %a with two different hashes (%a, %a), build %a and %a"
jobname Cstruct.hexdump_pp input_id
Cstruct.hexdump_pp h Cstruct.hexdump_pp h'
Uuidm.pp uuid Uuidm.pp uuid'))
tl
| [] -> ())
(Ok ()) input_ids
in
or_die 1 r
let num_build_artifacts =
Caqti_request.find
Caqti_type.unit
Caqti_type.int
"SELECT count(*) FROM build_artifact"
let build_artifacts : (unit, string * Uuidm.t * (Fpath.t * Fpath.t * Cstruct.t * int64), [ `One | `Zero | `Many ]) Caqti_request.t =
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup3 string Builder_db.Rep.uuid (tup4 Builder_db.Rep.fpath Builder_db.Rep.fpath Builder_db.Rep.cstruct int64))
{| SELECT job.name, b.uuid, a.filepath, a.localpath, a.sha256, a.size
FROM build_artifact a, build b, job
WHERE a.build = b.id AND b.job = job.id |}
let script_and_console : (unit, _, [`One | `Zero | `Many ]) Caqti_request.t =
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup4 string Builder_db.Rep.uuid Builder_db.Rep.fpath Builder_db.Rep.fpath)
{| SELECT job.name, b.uuid, b.console, b.script
FROM build b, job
WHERE job.id = b.job |}
module FpathSet = Set.Make(Fpath)
let files_in_dir dir =
Bos.OS.Dir.fold_contents ~elements:`Files ~dotfiles:true
(fun f acc ->
let f = Option.get (Fpath.rem_prefix dir f) in
FpathSet.add f acc)
FpathSet.empty
dir
let verify_data_dir () datadir =
let files_in_filesystem = or_die 1 (files_in_dir (Fpath.v datadir)) in
Logs.info (fun m -> m "files in filesystem: %d" (FpathSet.cardinal files_in_filesystem));
let files_tracked = ref (FpathSet.singleton (Fpath.v "builder.sqlite3")) in
let dbpath = datadir ^ "/builder.sqlite3" in
Logs.info (fun m -> m "connecting to %s" dbpath);
let r =
let* (module Db : Caqti_blocking.CONNECTION) =
connect (Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
in
let* num_build_artifacts = Db.find num_build_artifacts () in
Logs.info (fun m -> m "total: %d artifacts" num_build_artifacts);
let progress =
let idx = ref 0 in
fun () -> incr idx; if !idx mod 100 = 0 then Logs.info (fun m -> m "%d" !idx);
in
let verify_job_and_uuid ?fpath job uuid path =
match Fpath.segs path with
| job' :: uuid' :: tl ->
if String.equal job job' then () else Logs.warn (fun m -> m "job names do not match: %s vs %s" job job');
if String.equal (Uuidm.to_string uuid) uuid' then () else Logs.warn (fun m -> m "uuid does not match: %s vs %s" (Uuidm.to_string uuid) uuid');
(match fpath, tl with
| None, _ -> ()
| Some f, "output" :: tl ->
if Fpath.equal (Fpath.v (String.concat "/" tl)) f then
()
else
Logs.err (fun m -> m "path (%a) and fpath (%a) do not match" Fpath.pp path Fpath.pp f)
| Some _, _ ->
Logs.err (fun m -> m "path is not of form <job>/<uuid>/output/<filename>: %a" Fpath.pp path))
| _ -> Logs.err (fun m -> m "path is not of form <job>/<uuid>/...: %a" Fpath.pp path)
in
let* () =
Db.iter_s build_artifacts (fun (job, uuid, (fpath, lpath, sha, size)) ->
progress ();
verify_job_and_uuid ~fpath job uuid lpath;
let abs_path = Fpath.(v datadir // lpath) in
(match Bos.OS.File.read abs_path with
| Error (`Msg msg) -> Logs.err (fun m -> m "file %a not present: %s" Fpath.pp abs_path msg)
| Ok data ->
files_tracked := FpathSet.add lpath !files_tracked;
let s = Int64.of_int (String.length data) in
if s <> size then Logs.err (fun m -> m "File %a has different size (in DB %Lu on disk %Lu)" Fpath.pp abs_path size s);
let sh = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
if not (Cstruct.equal sha sh) then Logs.err (fun m -> m "File %a has different hash (in DB %a on disk %a" Fpath.pp abs_path Cstruct.hexdump_pp sha Cstruct.hexdump_pp sh)) ;
Ok ()
) ()
in
Db.iter_s script_and_console (fun (job, uuid, console, script) ->
verify_job_and_uuid job uuid console;
verify_job_and_uuid job uuid script;
let console_file = Fpath.(v datadir // console)
and script_file = Fpath.(v datadir // script)
in
let* _ = Bos.OS.File.must_exist console_file in
let* _ = Bos.OS.File.must_exist script_file in
files_tracked := FpathSet.add console (FpathSet.add script !files_tracked);
Ok ()) ()
in
let files_untracked = FpathSet.diff files_in_filesystem !files_tracked in
FpathSet.iter (fun f ->
Logs.warn (fun m -> m "untracked file in filesystem: %a" Fpath.pp f))
files_untracked;
or_die 1 r
let help man_format cmds = function
| None -> `Help (man_format, None)
| Some cmd ->
if List.mem cmd cmds
then `Help (man_format, Some cmd)
else `Error (true, "Unknown command: " ^ cmd)
let dbpath =
let doc = "sqlite3 database path" in
Cmdliner.Arg.(value &
opt non_dir_file "/var/db/builder-web/builder.sqlite3" &
info ~doc ["dbpath"])
let dbpath_new =
let doc = "sqlite3 database path" in
Cmdliner.Arg.(value &
opt string "/var/db/builder-web/builder.sqlite3" &
info ~doc ["dbpath"])
let datadir =
let doc = "data directory" in
Cmdliner.Arg.(value &
opt dir "/var/db/builder-web/" &
info ~doc ["datadir"])
let jobname =
let doc = "jobname" in
Cmdliner.Arg.(required &
pos 0 (some string) None &
info ~doc ~docv:"JOBNAME" [])
let username =
let doc = "username" in
Cmdliner.Arg.(required &
pos 0 (some string) None &
info ~doc ~docv:"USERNAME" [])
let password_iter =
let doc = "password hash count" in
Cmdliner.Arg.(value &
opt (some int) None &
info ~doc ["hash-count"])
let scrypt_n =
let doc = "scrypt n parameter" in
Cmdliner.Arg.(value &
opt (some int) None &
info ~doc ["scrypt-n"])
let scrypt_r =
let doc = "scrypt r parameter" in
Cmdliner.Arg.(value &
opt (some int) None &
info ~doc ["scrypt-r"])
let scrypt_p =
let doc = "scrypt p parameter" in
Cmdliner.Arg.(value &
opt (some int) None &
info ~doc ["scrypt-p"])
let unrestricted =
let doc = "unrestricted user" in
Cmdliner.Arg.(value & flag & info ~doc [ "unrestricted" ])
let job =
let doc = "job" in
Cmdliner.Arg.(required &
pos 1 (some string) None &
info ~doc ~docv:"JOB" [])
let setup_log =
let setup_log level =
Logs.set_level level;
Logs.set_reporter (Logs_fmt.reporter ~dst:Format.std_formatter ());
Logs.debug (fun m -> m "Set log level %s" (Logs.level_to_string level))
in
Cmdliner.Term.(const setup_log $ Logs_cli.level ())
let migrate_cmd =
let doc = "create database and add tables" in
Cmdliner.Term.(pure migrate $ setup_log $ dbpath_new),
Cmdliner.Term.info ~doc "migrate"
let user_add_cmd =
let doc = "add a user" in
(Cmdliner.Term.(pure user_add $ setup_log $ dbpath $ scrypt_n $ scrypt_r $ scrypt_p $ username $ unrestricted),
Cmdliner.Term.info ~doc "user-add")
let user_update_cmd =
let doc = "update a user password" in
(Cmdliner.Term.(pure user_update $ setup_log $ dbpath $ scrypt_n $ scrypt_r $ scrypt_p $ username $ unrestricted),
Cmdliner.Term.info ~doc "user-update")
let user_remove_cmd =
let doc = "remove a user" in
(Cmdliner.Term.(pure user_remove $ setup_log $ dbpath $ username),
Cmdliner.Term.info ~doc "user-remove")
let user_disable_cmd =
let doc = "disable a user" in
(Cmdliner.Term.(pure user_disable $ setup_log $ dbpath $ username),
Cmdliner.Term.info ~doc "user-disable")
let user_list_cmd =
let doc = "list all users" in
(Cmdliner.Term.(pure user_list $ setup_log $ dbpath),
Cmdliner.Term.info ~doc "user-list")
let access_add_cmd =
let doc = "grant access to user and job" in
(Cmdliner.Term.(pure access_add $ setup_log $ dbpath $ username $ job),
Cmdliner.Term.info ~doc "access-add")
let access_remove_cmd =
let doc = "remove access to user and job" in
(Cmdliner.Term.(pure access_remove $ setup_log $ dbpath $ username $ job),
Cmdliner.Term.info ~doc "access-remove")
let access_list_cmd =
let doc = "list user access" in
(Cmdliner.Term.(pure access_list $ setup_log $ dbpath),
Cmdliner.Term.info ~doc "access-list")
let job_remove_cmd =
let doc = "remove job and its associated builds and artifacts" in
(Cmdliner.Term.(pure job_remove $ setup_log $ datadir $ jobname),
Cmdliner.Term.info ~doc "job-remove")
let verify_input_id_cmd =
let doc = "verify that the main binary hash of all builds with the same input are equal" in
(Cmdliner.Term.(pure verify_input_id $ setup_log $ dbpath),
Cmdliner.Term.info ~doc "verify-input-id")
let verify_data_dir_cmd =
let doc = "verify that the data directory is consistent with the build_artifact table" in
(Cmdliner.Term.(pure verify_data_dir $ setup_log $ datadir),
Cmdliner.Term.info ~doc "verify-data-dir")
let help_cmd =
let topic =
let doc = "Command to get help on" in
Cmdliner.Arg.(value & pos 0 (some string) None & info ~doc ~docv:"COMMAND" [])
in
let doc = "Builder database help" in
Cmdliner.Term.(ret (const help $ man_format $ choice_names $ topic)),
Cmdliner.Term.info ~doc "help"
let default_cmd =
let doc = "Builder database command" in
Cmdliner.Term.(ret (const help $ man_format $ choice_names $ const None)),
Cmdliner.Term.info ~doc "builder-db"
let () =
Mirage_crypto_rng_unix.initialize ();
Cmdliner.Term.eval_choice
default_cmd
[help_cmd; migrate_cmd;
user_add_cmd; user_update_cmd; user_remove_cmd; user_list_cmd; user_disable_cmd;
access_add_cmd; access_remove_cmd; access_list_cmd; job_remove_cmd;
verify_input_id_cmd; verify_data_dir_cmd ]
|> Cmdliner.Term.exit

File diff suppressed because it is too large Load diff

View file

@ -1,17 +0,0 @@
let uname =
let cmd = Bos.Cmd.(v "uname" % "-s") in
lazy (match Bos.OS.Cmd.(run_out cmd |> out_string |> success) with
| Ok s when s = "FreeBSD" -> `FreeBSD
| Ok s when s = "Linux" -> `Linux
| Ok s -> invalid_arg (Printf.sprintf "OS %s not supported" s)
| Error (`Msg m) -> invalid_arg m)
let default_datadir =
match Lazy.force uname with
| `FreeBSD -> "/var/db/builder-web"
| `Linux -> "/var/lib/builder-web"
let default_configdir =
match Lazy.force uname with
| `FreeBSD -> "/usr/local/etc/builder-web"
| `Linux -> "/etc/builder-web"

View file

@ -30,7 +30,7 @@ let write_raw s buf =
safe_close s >|= fun () ->
Error `Exception)
in
(* Logs.debug (fun m -> m "writing %a" (Ohex.pp_hexdump ()) (Bytes.unsafe_to_string buf)) ; *)
(* Logs.debug (fun m -> m "writing %a" Cstruct.hexdump_pp (Cstruct.of_bytes buf)) ; *)
w 0 (Bytes.length buf)
let process =
@ -78,101 +78,30 @@ let init_influx name data =
in
Lwt.async report
let run_batch_viz ~cachedir ~datadir ~configdir =
let open Rresult.R.Infix in
begin
let script = Fpath.(configdir / "batch-viz.sh")
and script_log = Fpath.(cachedir / "batch-viz.log")
and viz_script = Fpath.(configdir / "upload-hooks" / "visualizations.sh")
in
Bos.OS.File.exists script >>= fun script_exists ->
if not script_exists then begin
Logs.warn (fun m -> m "Didn't find %s" (Fpath.to_string script));
Ok ()
end else
let args =
[ "--cache-dir=" ^ Fpath.to_string cachedir;
"--data-dir=" ^ Fpath.to_string datadir;
"--viz-script=" ^ Fpath.to_string viz_script ]
|> List.map (fun s -> "\"" ^ String.escaped s ^ "\"")
|> String.concat " "
in
(*> Note: The reason for appending, is that else a new startup could
overwrite an existing running batch's log*)
(Fpath.to_string script ^ " " ^ args
^ " 2>&1 >> " ^ Fpath.to_string script_log
^ " &")
|> Sys.command
|> ignore
|> Result.ok
end
|> function
| Ok () -> ()
| Error err ->
Logs.warn (fun m ->
m "Error while starting batch-viz.sh: %a"
Rresult.R.pp_msg err)
let setup_app level influx port host datadir cachedir configdir run_batch_viz_flag expired_jobs =
let setup_app level influx port host datadir =
let dbpath = Printf.sprintf "%s/builder.sqlite3" datadir in
let datadir = Fpath.v datadir in
let cachedir =
cachedir |> Option.fold ~none:Fpath.(datadir / "_cache") ~some:Fpath.v
in
let configdir = Fpath.v configdir in
let () = Mirage_crypto_rng_unix.initialize (module Mirage_crypto_rng.Fortuna) in
let () = init_influx "builder-web" influx in
let () =
if run_batch_viz_flag then
run_batch_viz ~cachedir ~datadir ~configdir
in
match Builder_web.init dbpath datadir with
| exception Sqlite3.Error e ->
Format.eprintf "Error: @[@,%s.\
@,Does the database file exist? Create with `builder-db migrate`.@]\n%!"
e;
exit 2
| Error (#Caqti_error.load as e) ->
Format.eprintf "Error: %a\n%!" Caqti_error.pp e;
exit 2
| Error (`Wrong_version _ as e) ->
Format.eprintf "Error: @[@,%a.\
@,Migrate database version with `builder-migrations`,\
@,or start with a fresh database with `builder-db migrate`.@]\n%!"
Builder_web.pp_error e;
| Error (
#Caqti_error.connect
| #Caqti_error.call_or_retrieve
| `Msg _ as e
) ->
| Error (#Caqti_error.connect | #Caqti_error.call_or_retrieve | `Msg _ | `Wrong_version _ as e) ->
Format.eprintf "Error: %a\n%!" Builder_web.pp_error e;
exit 1
| Ok () ->
let level = match level with
| None -> None
| Some Logs.Debug -> Some `Debug
| Some Info -> Some `Info
| Some Warning -> Some `Warning
| Some Error -> Some `Error
| Some App -> None
in
let error_handler = Dream.error_template Builder_web.error_template in
let level = match level with None -> None | Some Logs.Debug -> Some `Debug | Some Info -> Some `Info | Some Warning -> Some `Warning | Some Error -> Some `Error | Some App -> None in
Dream.initialize_log ?level ();
let dream_routes = Builder_web.(
routes ~datadir ~cachedir ~configdir ~expired_jobs
|> to_dream_routes
)
in
Dream.run ~port ~interface:host ~tls:false ~error_handler
Dream.run ~port ~interface:host ~https:false
@@ Dream.logger
@@ Dream.sql_pool ("sqlite3:" ^ dbpath)
@@ Http_status_metrics.handle
@@ Builder_web.Middleware.remove_trailing_url_slash
@@ Dream.router dream_routes
@@ Builder_web.add_routes datadir
@@ Dream.not_found
open Cmdliner
let ip_port : (Ipaddr.V4.t * int) Arg.conv =
let ip_port : (Ipaddr.V4.t * int) Arg.converter =
let default_port = 8094 in
let parse s =
match
@ -184,40 +113,16 @@ let ip_port : (Ipaddr.V4.t * int) Arg.conv =
end
| _ -> Error "multiple : found"
with
| Error msg -> Error (`Msg msg)
| Error msg -> `Error msg
| Ok (ip, port) -> match Ipaddr.V4.of_string ip with
| Ok ip -> Ok (ip, port)
| Error `Msg msg -> Error (`Msg msg)
| Ok ip -> `Ok (ip, port)
| Error `Msg msg -> `Error msg
in
let printer ppf (ip, port) =
Format.fprintf ppf "%a:%d" Ipaddr.V4.pp ip port in
Arg.conv (parse, printer)
parse, fun ppf (ip, port) -> Format.fprintf ppf "%a:%d" Ipaddr.V4.pp ip port
let datadir =
let doc = "data directory" in
let docv = "DATA_DIR" in
let env = Cmdliner.Cmd.Env.info "BUILDER_WEB_DATADIR" in
Arg.(
value &
opt dir Builder_system.default_datadir &
info ~env [ "d"; "datadir" ] ~doc ~docv
)
let cachedir =
let doc = "cache directory" in
let docv = "CACHE_DIR" in
Arg.(
value
& opt (some ~none:"DATADIR/_cache" dir) None
& info [ "cachedir" ] ~doc ~docv)
let configdir =
let doc = "config directory" in
let docv = "CONFIG_DIR" in
Arg.(
value &
opt dir Builder_system.default_configdir &
info [ "c"; "configdir" ] ~doc ~docv)
Arg.(value & opt dir "/var/db/builder-web/" & info [ "d"; "datadir" ] ~doc)
let port =
let doc = "port" in
@ -228,31 +133,13 @@ let host =
Arg.(value & opt string "0.0.0.0" & info [ "h"; "host" ] ~doc)
let influx =
let doc = "IP address and port (default: 8094) to report metrics to \
influx line protocol" in
Arg.(
value &
opt (some ip_port) None &
info [ "influx" ] ~doc ~docv:"INFLUXHOST[:PORT]")
let run_batch_viz =
let doc = "Run CONFIG_DIR/batch-viz.sh on startup. \
Note that this is started in the background - so the user \
is in charge of not running several instances of this. A \
log is written to CACHE_DIR/batch-viz.log" in
Arg.(value & flag & info [ "run-batch-viz" ] ~doc)
let expired_jobs =
let doc = "Amount of days after which a job is considered to be inactive if \
no successful build has been achieved (use 0 for infinite)" in
Arg.(value & opt int 30 & info [ "expired-jobs" ] ~doc)
let doc = "IP address and port (default: 8094) to report metrics to in influx line protocol" in
Arg.(value & opt (some ip_port) None & info [ "influx" ] ~doc ~docv:"INFLUXHOST[:PORT]")
let () =
let term =
Term.(const setup_app $ Logs_cli.level () $ influx $ port $ host $ datadir $
cachedir $ configdir $ run_batch_viz $ expired_jobs)
in
let info = Cmd.info "Builder web" ~doc:"Builder web" ~man:[] in
Cmd.v info term
|> Cmd.eval
|> exit
let term = Term.(pure setup_app $ Logs_cli.level () $ influx $ port $ host $ datadir) in
let info = Term.info "Builder web" ~doc:"Builder web" ~man:[] in
match Term.eval (term, info) with
| `Ok () -> exit 0
| `Error _ -> exit 1
| _ -> exit 0

View file

@ -1,19 +1,11 @@
(library
(name builder_system)
(modules builder_system)
(libraries bos))
(executable
(public_name builder-web)
(name builder_web_app)
(modules builder_web_app)
(libraries builder_web builder_system mirage-crypto-rng.unix cmdliner
logs.cli metrics metrics-lwt metrics-influx metrics-rusage ipaddr
ipaddr.unix http_status_metrics))
(libraries builder_web mirage-crypto-rng.unix cmdliner logs.cli metrics metrics-lwt metrics-influx metrics-rusage ipaddr ipaddr.unix http_status_metrics))
(executable
(public_name builder-db)
(name builder_db_app)
(modules builder_db_app)
(libraries builder_web builder_db builder_system caqti.blocking uri bos fmt
logs logs.cli logs.fmt cmdliner mirage-crypto-rng.unix builder))
(name builder_db)
(modules builder_db)
(libraries builder_db caqti.blocking uri bos fmt logs logs.cli logs.fmt cmdliner mirage-crypto-rng.unix))

View file

@ -24,7 +24,7 @@ let pp_error ppf = function
let or_die exit_code = function
| Ok r -> r
| Error e ->
Format.eprintf "Database error: %a\n" pp_error e;
Format.eprintf "Database error: %a" pp_error e;
exit exit_code
let do_database_action action () datadir =
@ -61,10 +61,9 @@ let help man_format migrations = function
let datadir =
let doc = "data directory containing builder.sqlite3 and data files" in
let env = Cmdliner.Cmd.Env.info "BUILDER_WEB_DATADIR" in
Cmdliner.Arg.(value &
opt dir Builder_system.default_datadir &
info ~env ~doc ["datadir"; "d"])
opt dir "/var/db/builder-web/" &
info ~doc ["datadir"])
let setup_log =
let setup_log level =
@ -73,69 +72,53 @@ let setup_log =
in
Cmdliner.Term.(const setup_log $ Logs_cli.level ())
open Cmdliner
let actions (module M : MIGRATION) =
let c s = s ^ "-" ^ M.identifier in
let v doc from_ver to_ver = Printf.sprintf "%s (DB version %Ld -> %Ld)" doc from_ver to_ver in
let migrate_cmd =
let term = Term.(
const do_database_action $ const M.migrate $ setup_log $ datadir) in
let info = Cmd.info ~doc:(v M.migrate_doc M.old_version M.new_version)
(c "migrate") in
Cmd.v info term
in
let rollback_cmd =
let term = Term.(
const do_database_action $ const M.rollback $ setup_log $ datadir) in
let info = Cmd.info ~doc:(v M.rollback_doc M.new_version M.old_version)
(c "rollback") in
Cmd.v info term
in
[ migrate_cmd; rollback_cmd ]
[
(Cmdliner.Term.(const do_database_action $ const M.migrate $ setup_log $ datadir),
Cmdliner.Term.info ~doc:(v M.migrate_doc M.old_version M.new_version)
(c "migrate"));
(Cmdliner.Term.(const do_database_action $ const M.rollback $ setup_log $ datadir),
Cmdliner.Term.info ~doc:(v M.rollback_doc M.new_version M.old_version)
(c "rollback"));
]
let f20210308 =
let doc = "Remove broken builds as fixed in commit a57798f4c02eb4d528b90932ec26fb0b718f1a13. \
Note that the files on disk have to be removed manually." in
let term = Term.(const do_database_action $ const M20210308.fixup $ setup_log $ datadir) in
let info = Cmd.info ~doc "fixup-2021-03-08" in
Cmd.v info term
Cmdliner.Term.(const do_database_action $ const M20210308.fixup $ setup_log $ datadir),
Cmdliner.Term.info ~doc "fixup-2021-03-08"
let f20210707a =
let doc = "Remove orb.deb and orb.txz that ended up in the build." in
let term = Term.(const do_database_action $ const M20210707a.fixup $ setup_log $ datadir) in
let info = Cmd.info ~doc "fixup-2021-07-07a" in
Cmd.v info term
Cmdliner.Term.(const do_database_action $ const M20210707a.fixup $ setup_log $ datadir),
Cmdliner.Term.info ~doc "fixup-2021-07-07a"
let f20210707b =
let doc = "Move *.deb.debug to bin/*.deb and remove the earlier bin/*.deb. Adjust main_binary of build." in
let term = Term.(const do_database_action $ const M20210707b.fixup $ setup_log $ datadir) in
let info = Cmd.info ~doc "fixup-2021-07-07b" in
Cmd.v info term
Cmdliner.Term.(const do_database_action $ const M20210707b.fixup $ setup_log $ datadir),
Cmdliner.Term.info ~doc "fixup-2021-07-07b"
let f20210707c =
let doc = "Strip bin/*.{hvt,xen} if no *.{hvt,xen} exists. Adjust build_artifact table and main_binary of build." in
let term = Term.(const do_database_action $ const M20210707c.fixup $ setup_log $ datadir) in
let info = Cmd.info ~doc "fixup-2021-07-07c" in
Cmd.v info term
Cmdliner.Term.(const do_database_action $ const M20210707c.fixup $ setup_log $ datadir),
Cmdliner.Term.info ~doc "fixup-2021-07-07c"
let f20210707d =
let doc = "Remove ./ from filepath." in
let term = Term.(const do_database_action $ const M20210707d.fixup $ setup_log $ datadir) in
let info = Cmd.info ~doc "fixup-2021-07-07d" in
Cmd.v info term
Cmdliner.Term.(const do_database_action $ const M20210707d.fixup $ setup_log $ datadir),
Cmdliner.Term.info ~doc "fixup-2021-07-07d"
let f20210712b =
let doc = "Remove build-hashes and README from artifacts." in
let term = Term.(const do_database_action $ const M20210712b.fixup $ setup_log $ datadir) in
let info = Cmd.info ~doc "fixup-2021-07-12b" in
Cmd.v info term
Cmdliner.Term.(const do_database_action $ const M20210712b.fixup $ setup_log $ datadir),
Cmdliner.Term.info ~doc "fixup-2021-07-12b"
let f20210910 =
let doc = "Undo builds with script and console mixed up." in
let term = Term.(const do_database_action $ const M20210910.fixup $ setup_log $ datadir) in
let info = Cmd.info ~doc "fixup-2021-09-10" in
Cmd.v info term
Cmdliner.Term.(const do_database_action $ const M20210910.fixup $ setup_log $ datadir),
Cmdliner.Term.info ~doc "fixup-2021-09-10"
let help_cmd =
let topic =
@ -143,16 +126,17 @@ let help_cmd =
Cmdliner.Arg.(value & pos 0 (some string) None & info ~doc ~docv:"MIGRATION" [])
in
let doc = "Builder migration help" in
let term = Term.(ret (const help $ Arg.man_format $ choice_names $ topic)) in
let info = Cmd.info ~doc "help" in
Cmd.v info term
Cmdliner.Term.(ret (const help $ man_format $ choice_names $ topic)),
Cmdliner.Term.info ~doc "help"
let default_cmd =
let doc = "Builder migration command" in
Cmdliner.Term.(ret (const help $ man_format $ choice_names $ const None)),
Cmdliner.Term.info ~doc "builder-migrations"
let () =
let doc = "Builder migration command" in
let default_term = Term.(ret (const help $ Arg.man_format $ choice_names $ const None)) in
let default_info = Cmd.info ~doc "builder-migrations" in
Cmd.group
~default:default_term default_info
Cmdliner.Term.eval_choice
default_cmd
(List.concat [
[ help_cmd ];
actions (module M20210126);
@ -179,9 +163,5 @@ let () =
actions (module M20210712c);
[ f20210910 ];
actions (module M20211105);
actions (module M20220509);
actions (module M20230911);
actions (module M20230914);
])
|> Cmd.eval
|> exit
|> Cmdliner.Term.exit

View file

@ -1,5 +1,4 @@
(executable
(public_name builder-migrations)
(name builder_migrations)
(libraries builder_system builder_db caqti caqti-driver-sqlite3
caqti.blocking cmdliner logs logs.cli logs.fmt opam-format bos duration))
(libraries builder_db caqti caqti-driver-sqlite3 caqti.blocking cmdliner logs logs.cli logs.fmt opam-format bos duration))

View file

@ -1,29 +1,20 @@
(* Grej is utilities *)
module Syntax = struct
open Caqti_request.Infix
let ( let* ) = Result.bind
let ( let+ ) x f = Result.map f x
let ( ->. ) = ( ->. ) ~oneshot:true
let ( ->! ) = ( ->! ) ~oneshot:true
let ( ->? ) = ( ->? ) ~oneshot:true
let ( ->* ) = ( ->* ) ~oneshot:true
end
module Infix = struct
open Caqti_request.Infix
let ( >>= ) = Result.bind
let ( >>| ) x f = Result.map f x
let ( ->. ) = ( ->. ) ~oneshot:true
let ( ->! ) = ( ->! ) ~oneshot:true
let ( ->? ) = ( ->? ) ~oneshot:true
let ( ->* ) = ( ->* ) ~oneshot:true
end
open Syntax
let set_version version =
Caqti_type.unit ->. Caqti_type.unit @@
Printf.sprintf "PRAGMA user_version = %Ld" version
Caqti_request.exec ~oneshot:true
Caqti_type.unit
(Printf.sprintf "PRAGMA user_version = %Ld" version)
let check_version
?application_id:(desired_application_id=Builder_db.application_id)
@ -43,5 +34,6 @@ let list_iter_result f xs =
let foreign_keys on =
let on = if on then "ON" else "OFF" in
Caqti_type.unit ->. Caqti_type.unit @@
Printf.sprintf "PRAGMA foreign_keys = %s" on
Caqti_request.exec
Caqti_type.unit
(Printf.sprintf "PRAGMA foreign_keys = %s" on)

View file

@ -3,27 +3,32 @@ let identifier = "2021-01-26"
let migrate_doc = "add column main_binary to build"
let rollback_doc = "remove column main_binary from build"
open Grej.Infix
let set_application_id =
Caqti_type.unit ->. Caqti_type.unit @@
Printf.sprintf "PRAGMA application_id = %ld" Builder_db.application_id
Caqti_request.exec
Caqti_type.unit
(Printf.sprintf "PRAGMA application_id = %ld" Builder_db.application_id)
let alter_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE build ADD COLUMN main_binary TEXT"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"ALTER TABLE build ADD COLUMN main_binary TEXT"
let all_builds =
Caqti_type.unit ->* Caqti_type.int64 @@
"SELECT id FROM build"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
Caqti_type.int64
"SELECT id FROM build"
let bin_artifact =
Caqti_type.int64 ->* Caqti_type.(t2 int64 string) @@
"SELECT id, filepath FROM build_artifact WHERE build = ? AND filepath LIKE 'bin/%'"
Caqti_request.collect ~oneshot:true
Caqti_type.int64
Caqti_type.(tup2 int64 string)
"SELECT id, filepath FROM build_artifact WHERE build = ? AND filepath LIKE 'bin/%'"
let set_main_binary =
Caqti_type.(t2 int64 (option string)) ->. Caqti_type.unit @@
"UPDATE build SET main_binary = $2 WHERE id = $1"
Caqti_request.exec ~oneshot:true
Caqti_type.(tup2 int64 (option string))
"UPDATE build SET main_binary = ?2 WHERE id = ?1"
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
@ -47,36 +52,39 @@ let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
Db.exec (Grej.set_version new_version) ()
let rename_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE build RENAME TO __tmp_build"
Caqti_request.exec
Caqti_type.unit
"ALTER TABLE build RENAME TO __tmp_build"
let create_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let rollback_data =
Caqti_type.unit ->. Caqti_type.unit @@
{| INSERT INTO build
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps,
result_kind, result_code, result_msg, console, script, job
FROM __tmp_build
|}
Caqti_request.exec
Caqti_type.unit
{| INSERT INTO build
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps,
result_kind, result_code, result_msg, console, script, job
FROM __tmp_build
|}
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in

View file

@ -7,16 +7,18 @@ open Grej.Infix
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let job_build_idx =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX job_build_idx ON build(job)";
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"CREATE INDEX job_build_idx ON build(job)";
in
Grej.check_version ~user_version:1L (module Db) >>= fun () ->
Db.exec job_build_idx ()
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let q =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP INDEX IF EXISTS job_build_idx"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"DROP INDEX IF EXISTS job_build_idx"
in
Grej.check_version ~user_version:1L (module Db) >>= fun () ->
Db.exec q ()

View file

@ -4,43 +4,46 @@ let identifier = "2021-02-16"
let migrate_doc = "change to scrypt hashed passwords (NB: destructive!!)"
let rollback_doc = "rollback scrypt hashed passwords (NB: destructive!!)"
open Grej.Infix
let drop_user =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE user"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"DROP TABLE user"
let new_user =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
scrypt_n INTEGER NOT NULL,
scrypt_r INTEGER NOT NULL,
scrypt_p INTEGER NOT NULL
)
|}
Caqti_request.exec ~oneshot:true
Caqti_type.unit
{| CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
scrypt_n INTEGER NOT NULL,
scrypt_r INTEGER NOT NULL,
scrypt_p INTEGER NOT NULL
)
|}
let old_user =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
password_iter INTEGER NOT NULL
)
|}
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
password_iter INTEGER NOT NULL
)
|}
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.exec drop_user () >>= fun () ->
Db.exec new_user () >>= fun () ->
Db.exec (Grej.set_version new_version) ()
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:new_version (module Db) >>= fun () ->
Db.exec drop_user () >>= fun () ->
Db.exec old_user () >>= fun () ->

View file

@ -4,75 +4,86 @@ let identifier = "2021-02-18"
let migrate_doc = "add column size to build_file and build_artifact"
let rollback_doc = "remove column size to build_file and build_artifact"
open Grej.Infix
let new_build_artifact =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build_artifact (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
size INTEGER NOT NULL,
build INTEGER NOT NULL,
Caqti_request.exec ~oneshot:true
Caqti_type.unit
{| CREATE TABLE new_build_artifact (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
size INTEGER NOT NULL,
build INTEGER NOT NULL,
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
let new_build_file =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
size INTEGER NOT NULL,
build INTEGER NOT NULL,
Caqti_request.exec ~oneshot:true
Caqti_type.unit
{| CREATE TABLE new_build_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
size INTEGER NOT NULL,
build INTEGER NOT NULL,
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
let collect_build_artifact =
Caqti_type.unit ->* Caqti_type.(t3 int64 (t3 string string octets) int64) @@
"SELECT id, filepath, localpath, sha256, build FROM build_artifact"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
Caqti_type.(tup3 int64 (tup3 string string octets) int64)
"SELECT id, filepath, localpath, sha256, build FROM build_artifact"
let collect_build_file =
Caqti_type.unit ->* Caqti_type.(t3 int64 (t3 string string octets) int64) @@
"SELECT id, filepath, localpath, sha256, build FROM build_file"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
Caqti_type.(tup3 int64 (tup3 string string octets) int64)
"SELECT id, filepath, localpath, sha256, build FROM build_file"
let insert_new_build_artifact =
Caqti_type.(t3 int64 (t4 string string octets int64) int64) ->. Caqti_type.unit @@
{| INSERT INTO new_build_artifact (id, filepath, localpath, sha256, size, build)
VALUES (?, ?, ?, ?, ?, ?)
|}
Caqti_request.exec ~oneshot:true
Caqti_type.(tup3 int64 (tup4 string string octets int64) int64)
{| INSERT INTO new_build_artifact (id, filepath, localpath, sha256, size, build)
VALUES (?, ?, ?, ?, ?, ?)
|}
let insert_new_build_file =
Caqti_type.(t3 int64 (t4 string string octets int64) int64) ->. Caqti_type.unit @@
{| INSERT INTO new_build_file (id, filepath, localpath, sha256, size, build)
VALUES (?, ?, ?, ?, ?, ?)
|}
Caqti_request.exec ~oneshot:true
Caqti_type.(tup3 int64 (tup4 string string octets int64) int64)
{| INSERT INTO new_build_file (id, filepath, localpath, sha256, size, build)
VALUES (?, ?, ?, ?, ?, ?)
|}
let drop_build_artifact =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build_artifact"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"DROP TABLE build_artifact"
let drop_build_file =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build_file"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"DROP TABLE build_file"
let rename_build_artifact =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE new_build_artifact RENAME TO build_artifact"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"ALTER TABLE new_build_artifact RENAME TO build_artifact"
let rename_build_file =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE new_build_file RENAME TO build_file"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"ALTER TABLE new_build_file RENAME TO build_file"
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.exec new_build_artifact () >>= fun () ->
Db.rev_collect_list collect_build_artifact () >>= fun build_artifacts ->
@ -99,42 +110,47 @@ let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
Db.exec (Grej.set_version new_version) ()
let old_build_artifact =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build_artifact (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
build INTEGER NOT NULL,
Caqti_request.exec ~oneshot:true
Caqti_type.unit
{| CREATE TABLE new_build_artifact (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
build INTEGER NOT NULL,
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
let old_build_file =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
build INTEGER NOT NULL,
Caqti_request.exec ~oneshot:true
Caqti_type.unit
{| CREATE TABLE new_build_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
build INTEGER NOT NULL,
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
let copy_build_artifact =
Caqti_type.unit ->. Caqti_type.unit @@
"INSERT INTO new_build_artifact SELECT id, filepath, localpath, sha256, build FROM build_artifact"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"INSERT INTO new_build_artifact SELECT id, filepath, localpath, sha256, build FROM build_artifact"
let copy_build_file =
Caqti_type.unit ->. Caqti_type.unit @@
"INSERT INTO new_build_file SELECT id, filepath, localpath, sha256, build FROM build_file"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"INSERT INTO new_build_file SELECT id, filepath, localpath, sha256, build FROM build_file"
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:new_version (module Db) >>= fun () ->
Db.exec old_build_artifact () >>= fun () ->
Db.exec copy_build_artifact () >>= fun () ->

View file

@ -1,21 +1,22 @@
module Rep = Builder_db.Rep
open Grej.Infix
let broken_builds =
Caqti_type.unit ->* Caqti_type.t3 (Rep.id `build) Rep.uuid Caqti_type.string @@
{| SELECT b.id, b.uuid, job.name FROM build b, job
WHERE result_kind = 0 AND result_code = 0 AND main_binary IS NOT NULL AND job.id = b.job AND
(SELECT COUNT( * ) FROM build_artifact a
WHERE a.build = b.id and a.filepath = b.main_binary) = 0
|}
Caqti_request.collect ~oneshot:true
Caqti_type.unit
(Caqti_type.tup3 (Rep.id `build) Rep.uuid Caqti_type.string)
{| SELECT b.id, b.uuid, job.name FROM build b, job
WHERE result_kind = 0 AND result_code = 0 AND main_binary IS NOT NULL AND job.id = b.job AND
(SELECT COUNT( * ) FROM build_artifact a
WHERE a.build = b.id and a.filepath = b.main_binary) = 0
|}
let fixup datadir (module Db : Caqti_blocking.CONNECTION) =
let fixup _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:3L (module Db) >>= fun () ->
Db.rev_collect_list broken_builds () >>= fun broken_builds ->
Grej.list_iter_result
(fun ((build, uuid, job_name) : [`build] Rep.id * Uuidm.t * string) ->
Format.printf "Removing job %a.\nPlease clean up data files in %a/%s/%a\n"
Uuidm.pp uuid Fpath.pp datadir job_name Uuidm.pp uuid;
Format.printf "Removing job %a.\nPlease clean up data files in /var/db/builder-web/%s/%a\n"
Uuidm.pp uuid job_name Uuidm.pp uuid;
Db.exec Builder_db.Build.remove build)
broken_builds

View file

@ -7,12 +7,14 @@ open Grej.Infix
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let idx_build_job_start =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
in
let rm_job_build_idx =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP INDEX IF EXISTS job_build_idx"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"DROP INDEX IF EXISTS job_build_idx"
in
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.exec rm_job_build_idx () >>= fun () ->
@ -20,12 +22,14 @@ let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let job_build_idx =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX job_build_idx ON build(job)"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"CREATE INDEX job_build_idx ON build(job)"
in
let rm_idx_build_job_start =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP INDEX IF EXISTS idx_build_job_start"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"DROP INDEX IF EXISTS idx_build_job_start"
in
Grej.check_version ~user_version:new_version (module Db) >>= fun () ->
Db.exec rm_idx_build_job_start () >>= fun () ->

View file

@ -4,19 +4,21 @@ let identifier = "2021-05-31"
let migrate_doc = "remove datadir prefix from build_artifact.localpath"
let rollback_doc = "add datadir prefix to build_artifact.localpath"
open Grej.Infix
let build_artifacts =
Caqti_type.unit ->* Caqti_type.t2 Builder_db.Rep.untyped_id Builder_db.Rep.fpath @@
"SELECT id, localpath FROM build_artifact"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
Caqti_type.(tup2 Builder_db.Rep.untyped_id Builder_db.Rep.fpath)
"SELECT id, localpath FROM build_artifact"
let build_artifact_update_localpath =
Caqti_type.t2 Builder_db.Rep.untyped_id Builder_db.Rep.fpath ->. Caqti_type.unit @@
"UPDATE build_artifact SET localpath = $2 WHERE id = $1"
Caqti_request.exec ~oneshot:true
Caqti_type.(tup2 Builder_db.Rep.untyped_id Builder_db.Rep.fpath)
"UPDATE build_artifact SET localpath = ?2 WHERE id = ?1"
(* We are not migrating build_file because it is unused *)
let migrate datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.collect_list build_artifacts () >>= fun artifacts ->
Grej.list_iter_result (fun (id, localpath) ->
@ -27,6 +29,7 @@ let migrate datadir (module Db : Caqti_blocking.CONNECTION) =
Db.exec (Grej.set_version new_version) ()
let rollback datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:new_version (module Db) >>= fun () ->
Db.collect_list build_artifacts () >>= fun artifacts ->
Grej.list_iter_result (fun (id, localpath) ->

View file

@ -3,114 +3,117 @@ let identifier = "2021-06-02"
let migrate_doc = "build.main_binary foreign key"
let rollback_doc = "build.main_binary filepath"
open Grej.Infix
let idx_build_job_start =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
let new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
job INTEGER NOT NULL,
FOREIGN KEY(main_binary) REFERENCES build_artifact(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary TEXT,
job INTEGER NOT NULL,
let old_build =
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary TEXT,
job INTEGER NOT NULL,
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let collect_old_build =
Caqti_type.unit ->*
Caqti_type.(t3 Builder_db.Rep.untyped_id
(t3 (t4 string int64 int64 int64)
(t4 int64 int (option int) (option string))
(t3 octets string (option string)))
Builder_db.Rep.untyped_id) @@
{| SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind, result_code, result_msg,
console, script, main_binary, job
FROM build |}
Caqti_request.collect ~oneshot:true
Caqti_type.unit
Caqti_type.(tup3 Builder_db.Rep.untyped_id
(tup3 (tup4 string int64 int64 int64) (tup4 int64 int (option int) (option string)) (tup3 octets string (option string)))
Builder_db.Rep.untyped_id)
{| SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind, result_code, result_msg,
console, script, main_binary, job
FROM build |}
let insert_new_build =
Caqti_type.(t3 Builder_db.Rep.untyped_id
(t3 (t4 string int64 int64 int64)
(t4 int64 int (option int) (option string))
(t3 octets string (option Builder_db.Rep.untyped_id)))
Builder_db.Rep.untyped_id) ->. Caqti_type.unit @@
{| INSERT INTO new_build (id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind,
result_code, result_msg, console, script, main_binary, job)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |}
Caqti_request.exec ~oneshot:true
Caqti_type.(tup3 Builder_db.Rep.untyped_id
(tup3 (tup4 string int64 int64 int64) (tup4 int64 int (option int) (option string)) (tup3 octets string (option Builder_db.Rep.untyped_id)))
Builder_db.Rep.untyped_id)
{| INSERT INTO new_build (id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind,
result_code, result_msg, console, script, main_binary, job)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |}
let drop_build =
Caqti_type.unit ->. Caqti_type.unit @@
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"DROP TABLE build"
let rename_build =
Caqti_type.unit ->. Caqti_type.unit @@
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"ALTER TABLE new_build RENAME TO build"
let find_main_artifact_id =
Caqti_type.(t2 Builder_db.Rep.untyped_id string) ->! Builder_db.Rep.untyped_id @@
"SELECT id FROM build_artifact WHERE build = $1 AND filepath = $2"
Caqti_request.find ~oneshot:true
Caqti_type.(tup2 Builder_db.Rep.untyped_id string)
Builder_db.Rep.untyped_id
"SELECT id FROM build_artifact WHERE build = ?1 AND filepath = ?2"
let find_main_artifact_filepath =
Builder_db.Rep.untyped_id ->! Caqti_type.string @@
Caqti_request.find ~oneshot:true
Builder_db.Rep.untyped_id
Caqti_type.string
"SELECT filepath FROM build_artifact WHERE id = ?"
let collect_new_build =
Caqti_type.unit ->*
Caqti_type.(t3 Builder_db.Rep.untyped_id
(t3 (t4 string int64 int64 int64)
(t4 int64 int (option int) (option string))
(t3 octets string (option Builder_db.Rep.untyped_id)))
Builder_db.Rep.untyped_id) @@
{| SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind, result_code, result_msg,
console, script, main_binary, job
FROM build |}
Caqti_request.collect ~oneshot:true
Caqti_type.unit
Caqti_type.(tup3 Builder_db.Rep.untyped_id
(tup3 (tup4 string int64 int64 int64) (tup4 int64 int (option int) (option string)) (tup3 octets string (option Builder_db.Rep.untyped_id)))
Builder_db.Rep.untyped_id)
{| SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind, result_code, result_msg,
console, script, main_binary, job
FROM build |}
let insert_old_build =
Caqti_type.(t3 Builder_db.Rep.untyped_id
(t3 (t4 string int64 int64 int64)
(t4 int64 int (option int) (option string))
(t3 octets string (option string)))
Builder_db.Rep.untyped_id) ->. Caqti_type.unit @@
{| INSERT INTO new_build (id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind,
result_code, result_msg, console, script, main_binary, job)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |}
Caqti_request.exec ~oneshot:true
Caqti_type.(tup3 Builder_db.Rep.untyped_id
(tup3 (tup4 string int64 int64 int64) (tup4 int64 int (option int) (option string)) (tup3 octets string (option string)))
Builder_db.Rep.untyped_id)
{| INSERT INTO new_build (id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind,
result_code, result_msg, console, script, main_binary, job)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |}
let migrate _ (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.exec new_build () >>= fun () ->
Db.rev_collect_list collect_old_build () >>= fun builds ->
@ -121,15 +124,16 @@ let migrate _ (module Db : Caqti_blocking.CONNECTION) =
| Some path -> Db.find find_main_artifact_id (id, path) >>| fun id -> Some id)
>>= fun main_binary_id ->
Db.exec insert_new_build
(id, ((uuid, start_d, start_ps, finish_d), (finish_ps, result_kind, result_code, result_msg), (console, script, main_binary_id)), job))
(id, ((uuid, start_d, start_ps, finish_d), (finish_ps, result_kind, result_code, result_msg), (console, script, main_binary_id)), job))
builds >>= fun () ->
Db.exec drop_build () >>= fun () ->
Db.exec rename_build () >>= fun () ->
Db.exec idx_build_job_start () >>= fun () ->
Db.exec (Grej.set_version new_version) ()
let rollback _ (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:new_version (module Db) >>= fun () ->
Db.exec old_build () >>= fun () ->
Db.rev_collect_list collect_new_build () >>= fun builds ->
@ -140,7 +144,7 @@ let rollback _ (module Db : Caqti_blocking.CONNECTION) =
| Some main_binary_id -> Db.find find_main_artifact_filepath main_binary_id >>| fun filepath -> Some filepath)
>>= fun filepath ->
Db.exec insert_old_build
(id, ((uuid, start_d, start_ps, finish_d), (finish_ps, result_kind, result_code, result_msg), (console, script, filepath)), job))
(id, ((uuid, start_d, start_ps, finish_d), (finish_ps, result_kind, result_code, result_msg), (console, script, filepath)), job))
builds >>= fun () ->
Db.exec drop_build () >>= fun () ->
Db.exec rename_build () >>= fun () ->

View file

@ -3,79 +3,87 @@ let identifier = "2021-06-08"
let migrate_doc = "add access list"
let rollback_doc = "remove access list"
open Grej.Infix
let new_user =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_user (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
scrypt_n INTEGER NOT NULL,
scrypt_r INTEGER NOT NULL,
scrypt_p INTEGER NOT NULL,
restricted BOOLEAN NOT NULL
)
|}
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_user (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
scrypt_n INTEGER NOT NULL,
scrypt_r INTEGER NOT NULL,
scrypt_p INTEGER NOT NULL,
restricted BOOLEAN NOT NULL
)
|}
let old_user =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_user (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
scrypt_n INTEGER NOT NULL,
scrypt_r INTEGER NOT NULL,
scrypt_p INTEGER NOT NULL
)
|}
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_user (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash BLOB NOT NULL,
password_salt BLOB NOT NULL,
scrypt_n INTEGER NOT NULL,
scrypt_r INTEGER NOT NULL,
scrypt_p INTEGER NOT NULL
)
|}
let collect_old_user =
Caqti_type.unit ->*
Caqti_type.(t4 int64 string (t2 octets octets) (t3 int64 int64 int64)) @@
"SELECT id, username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p FROM user"
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup4 int64 string (tup2 octets octets) (tup3 int64 int64 int64))
"SELECT id, username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p FROM user"
let collect_new_user =
Caqti_type.unit ->*
Caqti_type.(t4 int64 string (t2 octets octets) (t4 int64 int64 int64 bool)) @@
"SELECT id, username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p, restricted FROM user"
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup4 int64 string (tup2 octets octets) (tup4 int64 int64 int64 bool))
"SELECT id, username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p, restricted FROM user"
let insert_new_user =
Caqti_type.(t4 int64 string (t2 octets octets) (t4 int64 int64 int64 bool)) ->.
Caqti_type.unit @@
"INSERT INTO new_user (id, username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p, restricted) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
Caqti_request.exec
Caqti_type.(tup4 int64 string (tup2 octets octets) (tup4 int64 int64 int64 bool))
"INSERT INTO new_user (id, username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p, restricted) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
let insert_old_user =
Caqti_type.(t4 int64 string (t2 octets octets) (t3 int64 int64 int64)) ->.
Caqti_type.unit @@
"INSERT INTO new_user (id, username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p) VALUES (?, ?, ?, ?, ?, ?, ?)"
Caqti_request.exec
Caqti_type.(tup4 int64 string (tup2 octets octets) (tup3 int64 int64 int64))
"INSERT INTO new_user (id, username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p) VALUES (?, ?, ?, ?, ?, ?, ?)"
let drop_user =
Caqti_type.unit ->. Caqti_type.unit @@
Caqti_request.exec
Caqti_type.unit
"DROP TABLE user"
let rename_new_user =
Caqti_type.unit ->. Caqti_type.unit @@
Caqti_request.exec
Caqti_type.unit
"ALTER TABLE new_user RENAME TO user"
let access_list =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE access_list (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE access_list (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id),
UNIQUE(user, job)
)
|}
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id),
UNIQUE(user, job)
)
|}
let rollback_access_list =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE IF EXISTS access_list"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE IF EXISTS access_list"
open Grej.Infix
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->

View file

@ -3,92 +3,100 @@ let identifier = "2021-06-09"
let migrate_doc = "add user column to build"
let rollback_doc = "remove user column from build"
open Grej.Infix
let idx_build_job_start =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
let nologin_user =
Caqti_type.unit ->. Caqti_type.unit @@
"INSERT INTO user (username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p, restricted) \
VALUES ('nologin', x'', x'', 16384, 8, 1, true)"
Caqti_request.exec
Caqti_type.unit
"INSERT INTO user (username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p, restricted) \
VALUES ('nologin', x'', x'', 16384, 8, 1, true)"
let remove_nologin_user =
Caqti_type.unit ->. Caqti_type.unit @@
"DELETE FROM user WHERE username = 'nologin'"
Caqti_request.exec
Caqti_type.unit
"DELETE FROM user WHERE username = 'nologin'"
let new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(main_binary) REFERENCES build_artifact(id),
FOREIGN KEY(job) REFERENCES job(id),
FOREIGN KEY(user) REFERENCES user(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id),
FOREIGN KEY(job) REFERENCES job(id),
FOREIGN KEY(user) REFERENCES user(id)
)
|}
let old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
job INTEGER NOT NULL,
FOREIGN KEY(main_binary) REFERENCES build_artifact(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let insert_from_old_build =
Builder_db.Rep.id (`user : [`user]) ->. Caqti_type.unit @@
{| INSERT INTO new_build (id, uuid, start_d, start_ps, finish_d, finish_ps,
result_kind, result_code, result_msg, console,
script, main_binary, job, user)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind,
result_code, result_msg, console, script, main_binary, job, ?
FROM build |}
Caqti_request.exec ~oneshot:true
(Builder_db.Rep.id (`user : [`user]))
{| INSERT INTO new_build (id, uuid, start_d, start_ps, finish_d, finish_ps,
result_kind, result_code, result_msg, console,
script, main_binary, job, user)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind,
result_code, result_msg, console, script, main_binary, job, ?
FROM build |}
let insert_from_new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| INSERT INTO new_build (id, uuid, start_d, start_ps, finish_d, finish_ps,
result_kind, result_code, result_msg, console,
script, main_binary, job)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind,
result_code, result_msg, console, script, main_binary, job
FROM build |}
Caqti_request.exec ~oneshot:true
Caqti_type.unit
{| INSERT INTO new_build (id, uuid, start_d, start_ps, finish_d, finish_ps,
result_kind, result_code, result_msg, console,
script, main_binary, job)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind,
result_code, result_msg, console, script, main_binary, job
FROM build |}
let drop_build =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"DROP TABLE build"
let rename_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE new_build RENAME TO build"
Caqti_request.exec ~oneshot:true
Caqti_type.unit
"ALTER TABLE new_build RENAME TO build"
open Grej.Infix
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->

View file

@ -3,26 +3,28 @@ let identifier = "2021-06-25"
let migrate_doc = "drop build_file table"
let rollback_doc = "recreate build_file table"
open Grej.Infix
let build_file =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE build_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
size INTEGER NOT NULL,
build INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE build_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
size INTEGER NOT NULL,
build INTEGER NOT NULL,
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
let drop_build_file =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build_file"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE build_file"
open Grej.Infix
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->

View file

@ -3,50 +3,55 @@ let identifier = "2021-06-29"
let migrate_doc = "add tag and job_tag table"
let rollback_doc = "remove tag and job tag table"
open Grej.Infix
let tag =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
tag VARCHAR(255) NOT NULL UNIQUE
)
|}
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
tag VARCHAR(255) NOT NULL UNIQUE
)
|}
let job_tag =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE job_tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
tag INTEGER NOT NULL,
value TEXT NOT NULL,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE job_tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
tag INTEGER NOT NULL,
value TEXT NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(job) REFERENCES job(id),
FOREIGN KEY(tag) REFERENCES tag(id),
UNIQUE(tag, job)
)
|}
FOREIGN KEY(job) REFERENCES job(id),
FOREIGN KEY(tag) REFERENCES tag(id),
UNIQUE(tag, job)
)
|}
let jobs =
Caqti_type.unit ->* Builder_db.Rep.untyped_id @@
"SELECT id FROM job"
Caqti_request.collect
Caqti_type.unit
Builder_db.Rep.untyped_id
"SELECT id FROM job"
let latest_successful_build =
Builder_db.Rep.untyped_id ->? Builder_db.Rep.untyped_id @@
{| SELECT b.id
FROM build b
WHERE b.job = ? AND b.result_kind = 0 AND b.result_code = 0
ORDER BY b.start_d DESC, b.start_ps DESC
LIMIT 1
|}
Caqti_request.find_opt
Builder_db.Rep.untyped_id
Builder_db.Rep.untyped_id
{| SELECT b.id
FROM build b
WHERE b.job = ? AND b.result_kind = 0 AND b.result_code = 0
ORDER BY b.start_d DESC, b.start_ps DESC
LIMIT 1
|}
let build_artifacts =
Builder_db.Rep.untyped_id ->*
Caqti_type.t2 Builder_db.Rep.fpath Builder_db.Rep.fpath @@
{| SELECT a.filepath, a.localpath
FROM build_artifact a
WHERE a.build = ?
|}
Caqti_request.collect
Builder_db.Rep.untyped_id
Caqti_type.(tup2 Builder_db.Rep.fpath Builder_db.Rep.fpath)
{| SELECT a.filepath, a.localpath
FROM build_artifact a
WHERE a.build = ?
|}
let infer_section_and_synopsis artifacts =
@ -92,27 +97,34 @@ let infer_section_and_synopsis artifacts =
| None -> infer_section_from_packages opam_switch
in
Some section, infer_synopsis_and_descr opam_switch
let remove_tag =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE tag"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE tag"
let remove_job_tag =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE job_tag"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE job_tag"
let insert_tag =
Caqti_type.string ->. Caqti_type.unit @@
"INSERT INTO tag (tag) VALUES (?)"
Caqti_request.exec
Caqti_type.string
"INSERT INTO tag (tag) VALUES (?)"
let insert_job_tag =
Caqti_type.(t3 Builder_db.Rep.untyped_id string Builder_db.Rep.untyped_id) ->.
Caqti_type.unit @@
"INSERT INTO job_tag (tag, value, job) VALUES (?, ?, ?)"
Caqti_request.exec
Caqti_type.(tup3 Builder_db.Rep.untyped_id string Builder_db.Rep.untyped_id)
"INSERT INTO job_tag (tag, value, job) VALUES (?, ?, ?)"
let find_tag =
Caqti_type.string ->! Builder_db.Rep.untyped_id @@
"SELECT id FROM tag where tag = ?"
Caqti_request.find
Caqti_type.string
Builder_db.Rep.untyped_id
"SELECT id FROM tag where tag = ?"
open Grej.Infix
let migrate datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->

View file

@ -3,49 +3,59 @@ let identifier = "2021-06-30"
let migrate_doc = "add readme.md tag"
let rollback_doc = "remove readme.md tag"
open Grej.Infix
let jobs =
Caqti_type.unit ->* Builder_db.Rep.untyped_id @@
"SELECT id FROM job"
Caqti_request.collect
Caqti_type.unit
Builder_db.Rep.untyped_id
"SELECT id FROM job"
let latest_successful_build =
Builder_db.Rep.untyped_id ->? Builder_db.Rep.untyped_id @@
{| SELECT b.id
FROM build b
WHERE b.job = ? AND b.result_kind = 0 AND b.result_code = 0
ORDER BY b.start_d DESC, b.start_ps DESC
LIMIT 1
|}
Caqti_request.find_opt
Builder_db.Rep.untyped_id
Builder_db.Rep.untyped_id
{| SELECT b.id
FROM build b
WHERE b.job = ? AND b.result_kind = 0 AND b.result_code = 0
ORDER BY b.start_d DESC, b.start_ps DESC
LIMIT 1
|}
let build_artifacts =
Builder_db.Rep.untyped_id ->*
Caqti_type.t2 Builder_db.Rep.fpath Builder_db.Rep.fpath @@
{| SELECT a.filepath, a.localpath
FROM build_artifact a
WHERE a.build = ?
|}
Caqti_request.collect
Builder_db.Rep.untyped_id
Caqti_type.(tup2 Builder_db.Rep.fpath Builder_db.Rep.fpath)
{| SELECT a.filepath, a.localpath
FROM build_artifact a
WHERE a.build = ?
|}
let insert_tag =
Caqti_type.string ->. Caqti_type.unit @@
"INSERT INTO tag (tag) VALUES (?)"
Caqti_request.exec
Caqti_type.string
"INSERT INTO tag (tag) VALUES (?)"
let insert_job_tag =
Caqti_type.(t3 Builder_db.Rep.untyped_id string Builder_db.Rep.untyped_id) ->.
Caqti_type.unit @@
"INSERT INTO job_tag (tag, value, job) VALUES (?, ?, ?)"
Caqti_request.exec
Caqti_type.(tup3 Builder_db.Rep.untyped_id string Builder_db.Rep.untyped_id)
"INSERT INTO job_tag (tag, value, job) VALUES (?, ?, ?)"
let find_tag =
Caqti_type.string ->! Builder_db.Rep.untyped_id @@
"SELECT id FROM tag where tag = ?"
Caqti_request.find
Caqti_type.string
Builder_db.Rep.untyped_id
"SELECT id FROM tag where tag = ?"
let remove_job_tag =
Builder_db.Rep.untyped_id ->. Caqti_type.unit @@
"DELETE FROM job_tag where tag = ?"
Caqti_request.exec
Builder_db.Rep.untyped_id
"DELETE FROM job_tag where tag = ?"
let remove_tag =
Builder_db.Rep.untyped_id ->. Caqti_type.unit @@
"DELETE FROM tag where id = ?"
Caqti_request.exec
Builder_db.Rep.untyped_id
"DELETE FROM tag where id = ?"
open Grej.Infix
let migrate datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->

View file

@ -3,71 +3,76 @@ let identifier = "2021-07-01"
let migrate_doc = "build.main_binary deferred foreign key constraint"
let rollback_doc = "build.main_binary immediate foreign key constraint"
open Grej.Infix
let idx_build_job_start =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
let new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(main_binary) REFERENCES build_artifact(id),
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id),
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let copy_build =
Caqti_type.unit ->. Caqti_type.unit @@
"INSERT INTO new_build SELECT * from build"
Caqti_request.exec
Caqti_type.unit
"INSERT INTO new_build SELECT * from build"
let drop_build =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE build"
let rename_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE new_build RENAME TO build"
Caqti_request.exec
Caqti_type.unit
"ALTER TABLE new_build RENAME TO build"
open Grej.Infix
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->

View file

@ -3,80 +3,88 @@ let identifier = "2021-07-06"
let migrate_doc = "add a input_id column to the build table"
let rollback_doc = "remove the input_id column from the build table"
open Grej.Infix
let add_input_id_to_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE build ADD COLUMN input_id BLOB"
Caqti_request.exec
Caqti_type.unit
{| ALTER TABLE build ADD COLUMN input_id BLOB |}
let idx_build_job_start =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
let old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind TINYINT NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let copy_build =
Caqti_type.unit ->. Caqti_type.unit @@
"INSERT INTO new_build SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind, result_code, result_msg, console, script, main_binary, user, job FROM build"
Caqti_request.exec
Caqti_type.unit
"INSERT INTO new_build SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_kind, result_code, result_msg, console, script, main_binary, user, job FROM build"
let drop_build =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE build"
let rename_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE new_build RENAME TO build"
Caqti_request.exec
Caqti_type.unit
"ALTER TABLE new_build RENAME TO build"
let drop_input_id_from_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| ALTER TABLE build DROP COLUMN input_id |}
Caqti_request.exec
Caqti_type.unit
{| ALTER TABLE build DROP COLUMN input_id |}
let builds =
Caqti_type.unit ->*
Caqti_type.t4
Builder_db.Rep.untyped_id
Caqti_type.octets
Caqti_type.octets
Caqti_type.octets @@
{| SELECT b.id, opam.sha256, env.sha256, system.sha256
FROM build b, build_artifact opam, build_artifact env, build_artifact system
WHERE opam.filepath = 'opam-switch' AND env.filepath = 'build-environment'
AND system.filepath = 'system-packages'
AND opam.build = b.id AND env.build = b.id AND system.build = b.id
|}
Caqti_request.collect
Caqti_type.unit
(Caqti_type.tup4
Builder_db.Rep.untyped_id
Builder_db.Rep.cstruct
Builder_db.Rep.cstruct
Builder_db.Rep.cstruct)
{| SELECT b.id, opam.sha256, env.sha256, system.sha256
FROM build b, build_artifact opam, build_artifact env, build_artifact system
WHERE opam.filepath = 'opam-switch' AND env.filepath = 'build-environment'
AND system.filepath = 'system-packages'
AND opam.build = b.id AND env.build = b.id AND system.build = b.id
|}
let set_input_id =
Caqti_type.t2 Builder_db.Rep.untyped_id Caqti_type.octets ->. Caqti_type.unit @@
"UPDATE build SET input_id = $2 WHERE id = $1"
Caqti_request.exec
(Caqti_type.tup2 Builder_db.Rep.untyped_id Builder_db.Rep.cstruct)
"UPDATE build SET input_id = ?2 WHERE id = ?1"
open Grej.Infix
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.exec add_input_id_to_build () >>= fun () ->
Db.collect_list builds () >>= fun builds ->
Grej.list_iter_result (fun (id, opam_sha, env_sha, pkg_sha) ->
let input_id = Digestif.SHA256.(to_raw_string (digestv_string [ opam_sha ; env_sha ; pkg_sha ])) in
let input_id = Mirage_crypto.Hash.SHA256.digest (Cstruct.concat [ opam_sha ; env_sha ; pkg_sha ]) in
Db.exec set_input_id (id, input_id))
builds >>= fun () ->
Db.exec (Grej.set_version new_version) ()

View file

@ -1,13 +1,13 @@
open Grej.Infix
let orb_left_in_builds =
Caqti_type.unit ->*
Caqti_type.t2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath @@
{| SELECT id, localpath FROM build_artifact
WHERE filepath = 'orb.deb' OR filepath = 'orb.txz'
|}
Caqti_request.collect ~oneshot:true
Caqti_type.unit
(Caqti_type.tup2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath)
{| SELECT id, localpath FROM build_artifact
WHERE filepath = 'orb.deb' OR filepath = 'orb.txz'
|}
let fixup datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:12L (module Db) >>= fun () ->
Db.rev_collect_list orb_left_in_builds () >>= fun leftover_orb ->
Grej.list_iter_result

View file

@ -1,28 +1,30 @@
open Grej.Infix
let deb_debug_left_in_builds =
Caqti_type.unit ->*
Caqti_type.t4 (Builder_db.Rep.id `build_artifact) (Builder_db.Rep.id `build)
Builder_db.Rep.fpath Builder_db.Rep.fpath @@
{| SELECT id, build, localpath, filepath FROM build_artifact
WHERE filepath LIKE '%.deb.debug'
|}
Caqti_request.collect ~oneshot:true
Caqti_type.unit
(Caqti_type.tup4 (Builder_db.Rep.id `build_artifact) (Builder_db.Rep.id `build) Builder_db.Rep.fpath Builder_db.Rep.fpath)
{| SELECT id, build, localpath, filepath FROM build_artifact
WHERE filepath LIKE '%.deb.debug'
|}
let get_main_binary =
Builder_db.Rep.id `build ->? Builder_db.Rep.id `build_artifact @@
"SELECT main_binary FROM build WHERE id = ?"
Caqti_request.find_opt
(Builder_db.Rep.id `build)
(Builder_db.Rep.id `build_artifact)
"SELECT main_binary FROM build WHERE id = ?"
let get_localpath =
Builder_db.Rep.id `build_artifact ->! Builder_db.Rep.fpath @@
"SELECT localpath FROM build_artifact WHERE id = ?"
Caqti_request.find
(Builder_db.Rep.id `build_artifact)
Builder_db.Rep.fpath
"SELECT localpath FROM build_artifact WHERE id = ?"
let update_paths =
Caqti_type.t3 (Builder_db.Rep.id `build_artifact)
Builder_db.Rep.fpath Builder_db.Rep.fpath ->.
Caqti_type.unit @@
"UPDATE build_artifact SET localpath = $2, filepath = $3 WHERE id = $1"
Caqti_request.exec
(Caqti_type.tup3 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath Builder_db.Rep.fpath)
"UPDATE build_artifact SET localpath = ?2, filepath = ?3 WHERE id = ?1"
let fixup datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:12L (module Db) >>= fun () ->
Db.rev_collect_list deb_debug_left_in_builds () >>= fun leftover_debug ->
Grej.list_iter_result

View file

@ -1,28 +1,28 @@
open Grej.Infix
let all_builds_with_binary : (unit, [`build] Builder_db.Rep.id * [`build_artifact] Builder_db.Rep.id * Fpath.t * Fpath.t, [ `Zero | `One | `Many ]) Caqti_request.t =
Caqti_type.unit ->*
Caqti_type.t4 (Builder_db.Rep.id `build) (Builder_db.Rep.id `build_artifact)
Builder_db.Rep.fpath Builder_db.Rep.fpath @@
"SELECT b.id, b.main_binary, a.filepath, a.localpath FROM build b, build_artifact a WHERE b.main_binary = a.id AND b.main_binary IS NOT NULL"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
(Caqti_type.tup4 (Builder_db.Rep.id `build) (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath Builder_db.Rep.fpath)
"SELECT b.id, b.main_binary, a.filepath, a.localpath FROM build b, build_artifact a WHERE b.main_binary = a.id AND b.main_binary IS NOT NULL"
let build_not_stripped : ([`build] Builder_db.Rep.id, [`build_artifact] Builder_db.Rep.id, [ `Zero | `One ]) Caqti_request.t =
Builder_db.Rep.id `build ->? Builder_db.Rep.id `build_artifact @@
"SELECT id FROM build_artifact WHERE build = ? AND filepath LIKE '%.debug'"
let build_not_stripped : ([`build] Builder_db.Rep.id, [`build_artifact] Builder_db.Rep.id, [< `Zero | `One | `Many > `Zero `One ]) Caqti_request.t =
Caqti_request.find_opt
(Builder_db.Rep.id `build)
(Builder_db.Rep.id `build_artifact)
{| SELECT id FROM build_artifact WHERE build = ? AND filepath LIKE '%.debug' |}
let update_paths : ([`build_artifact] Builder_db.Rep.id * Fpath.t * Fpath.t, unit, [ `Zero ]) Caqti_request.t =
Caqti_type.t3 (Builder_db.Rep.id `build_artifact)
Builder_db.Rep.fpath Builder_db.Rep.fpath ->.
Caqti_type.unit @@
"UPDATE build_artifact SET localpath = $2, filepath = $3 WHERE id = $1"
let update_paths : ([`build_artifact] Builder_db.Rep.id * Fpath.t * Fpath.t, unit, [< `Zero | `One | `Many > `Zero ]) Caqti_request.t =
Caqti_request.exec
(Caqti_type.tup3 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath Builder_db.Rep.fpath)
"UPDATE build_artifact SET localpath = ?2, filepath = ?3 WHERE id = ?1"
let add_artifact : ((Fpath.t * Fpath.t * string) * (int64 * [`build] Builder_db.Rep.id), unit, [ `Zero]) Caqti_request.t =
Caqti_type.(t2 (t3 Builder_db.Rep.fpath Builder_db.Rep.fpath Caqti_type.octets)
(t2 Caqti_type.int64 (Builder_db.Rep.id `build))) ->.
Caqti_type.unit @@
"INSERT INTO build_artifact (filepath, localpath, sha256, size, build) VALUES (?, ?, ?, ?, ?)"
let add_artifact : ((Fpath.t * Fpath.t * Cstruct.t) * (int64 * [`build] Builder_db.Rep.id), unit, [< `Zero | `One | `Many > `Zero]) Caqti_request.t =
Caqti_request.exec
Caqti_type.(tup2 (tup3 Builder_db.Rep.fpath Builder_db.Rep.fpath Builder_db.Rep.cstruct) (tup2 Caqti_type.int64 (Builder_db.Rep.id `build)))
{| INSERT INTO build_artifact (filepath, localpath, sha256, size, build) VALUES (?, ?, ?, ?, ?) |}
let fixup datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:12L (module Db) >>= fun () ->
Db.rev_collect_list all_builds_with_binary () >>= fun builds ->
Grej.list_iter_result
@ -48,8 +48,7 @@ let fixup datadir (module Db : Caqti_blocking.CONNECTION) =
in
assert (r = 0);
Bos.OS.File.read (Fpath.append datadir artifact_lpath) >>= fun data ->
let size = Int64.of_int (String.length data)
and sha256 = Digestif.SHA256.(to_raw_string (digest_string data)) in
let size = Int64.of_int (String.length data) and sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
Db.exec update_paths (artifact_id, new_artifact_lpath, path artifact_fpath) >>= fun () ->
Db.exec add_artifact ((artifact_fpath, artifact_lpath, sha256), (size, build_id)) >>= fun () ->
Db.find Builder_db.last_insert_rowid () >>= fun new_build_artifact_id ->

View file

@ -1,16 +1,17 @@
open Grej.Infix
let all_build_artifacts_with_dot_slash : (unit, [`build_artifact] Builder_db.Rep.id * Fpath.t, [ `Zero | `One | `Many ]) Caqti_request.t =
Caqti_type.unit ->*
Caqti_type.t2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath @@
"SELECT id, filepath FROM build_artifact WHERE filepath LIKE './%'"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
(Caqti_type.tup2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath)
"SELECT id, filepath FROM build_artifact WHERE filepath LIKE './%'"
let update_path : ([`build_artifact] Builder_db.Rep.id * Fpath.t, unit, [< `Zero | `One | `Many > `Zero ]) Caqti_request.t =
Caqti_type.t2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath ->.
Caqti_type.unit @@
"UPDATE build_artifact SET filepath = $2 WHERE id = $1"
Caqti_request.exec
(Caqti_type.tup2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath)
"UPDATE build_artifact SET filepath = ?2 WHERE id = ?1"
let fixup _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:12L (module Db) >>= fun () ->
Db.rev_collect_list all_build_artifacts_with_dot_slash () >>= fun build_artifacts ->
Grej.list_iter_result

View file

@ -3,101 +3,108 @@ and identifier = "2021-07-12a"
and migrate_doc = "remove result_kind from build, add indexes idx_build_failed and idx_build_artifact_sha256"
and rollback_doc = "add result_kind to build, remove indexes idx_build_failed and idx_build_artifact_sha256"
open Grej.Infix
let new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let copy_old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, 0, result_msg,
console, script, main_binary, user, job, input_id
FROM build
|}
Caqti_request.exec
Caqti_type.unit
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, 0, result_msg,
console, script, main_binary, user, job, input_id
FROM build
|}
let old_build_execution_result =
Caqti_type.unit ->*
Caqti_type.(t3 (Builder_db.Rep.id (`build : [ `build ])) int (option int)) @@
"SELECT id, result_kind, result_code FROM build"
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup3 (Builder_db.Rep.id (`build : [ `build ])) int (option int))
"SELECT id, result_kind, result_code FROM build"
let update_new_build_execution_result =
Caqti_type.(t2 (Builder_db.Rep.id (`build : [ `build ])) int) ->. Caqti_type.unit @@
"UPDATE new_build SET result_code = $2 WHERE id = $1"
Caqti_request.exec
Caqti_type.(tup2 (Builder_db.Rep.id (`build : [ `build ])) int)
"UPDATE new_build SET result_code = ?2 WHERE id = ?1"
let old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind INTEGER NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_kind INTEGER NOT NULL,
result_code INTEGER,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let copy_new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_kind, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, 0, result_msg,
console, script, main_binary, user, job, input_id
FROM build
|}
Caqti_request.exec
Caqti_type.unit
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_kind, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, 0, result_msg,
console, script, main_binary, user, job, input_id
FROM build
|}
let new_build_execution_result =
Caqti_type.unit ->*
Caqti_type.(t2 (Builder_db.Rep.id (`build : [ `build ])) int) @@
"SELECT id, result_code FROM build"
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup2 (Builder_db.Rep.id (`build : [ `build ])) int)
"SELECT id, result_code FROM build"
let update_old_build_execution_result =
Caqti_type.(t3 (Builder_db.Rep.id (`build : [ `build ])) int (option int)) ->.
Caqti_type.unit @@
"UPDATE new_build SET result_kind = $2, result_code = $3 WHERE id = $1"
Caqti_request.exec
Caqti_type.(tup3 (Builder_db.Rep.id (`build : [ `build ])) int (option int))
"UPDATE new_build SET result_kind = ?2, result_code = ?3 WHERE id = ?1"
let drop_build =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE build"
let rename_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE new_build RENAME TO build"
Caqti_request.exec
Caqti_type.unit
"ALTER TABLE new_build RENAME TO build"
let execution_new_of_old kind code =
match kind, code with
@ -119,6 +126,7 @@ let execution_old_of_new code =
else Error (`Msg "bad encoding")
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.exec new_build () >>= fun () ->
Db.exec copy_old_build () >>= fun () ->
@ -129,25 +137,25 @@ let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
results >>= fun () ->
Db.exec drop_build () >>= fun () ->
Db.exec rename_build () >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) \
WHERE result_code <> 0")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) WHERE result_code <> 0")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_input_id ON build(input_id)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_input_id ON build(input_id)")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_main_binary ON build(main_binary)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_main_binary ON build(main_binary)")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_artifact_sha256 ON build_artifact(sha256)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_artifact_sha256 ON build_artifact(sha256)")
() >>= fun () ->
Db.exec (Grej.set_version new_version) ()
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:new_version (module Db) >>= fun () ->
Db.exec old_build () >>= fun () ->
Db.exec copy_new_build () >>= fun () ->
@ -158,9 +166,8 @@ let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
results >>= fun () ->
Db.exec drop_build () >>= fun () ->
Db.exec rename_build () >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"DROP INDEX idx_build_artifact_sha256") () >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
Db.exec (Caqti_request.exec Caqti_type.unit "DROP INDEX idx_build_artifact_sha256") () >>= fun () ->
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
() >>= fun () ->
Db.exec (Grej.set_version old_version) ()

View file

@ -1,16 +1,18 @@
open Grej.Infix
let all_build_artifacts_like_hashes : (unit, [`build_artifact] Builder_db.Rep.id * Fpath.t, [ `Zero | `One | `Many ]) Caqti_request.t =
Caqti_type.unit ->*
Caqti_type.t2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath @@
"SELECT id, localpath FROM build_artifact WHERE filepath LIKE '%.build-hashes'"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
(Caqti_type.tup2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath)
"SELECT id, localpath FROM build_artifact WHERE filepath LIKE '%.build-hashes'"
let all_build_artifacts_like_readme : (unit, [`build_artifact] Builder_db.Rep.id * Fpath.t, [ `Zero | `One | `Many ]) Caqti_request.t =
Caqti_type.unit ->*
Caqti_type.t2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath @@
"SELECT id, localpath FROM build_artifact WHERE filepath LIKE '%README.md'"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
(Caqti_type.tup2 (Builder_db.Rep.id `build_artifact) Builder_db.Rep.fpath)
"SELECT id, localpath FROM build_artifact WHERE filepath LIKE '%README.md'"
let fixup datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:13L (module Db) >>= fun () ->
Db.rev_collect_list all_build_artifacts_like_hashes () >>= fun build_artifacts_build_hashes ->
Db.rev_collect_list all_build_artifacts_like_readme () >>= fun build_artifacts_readme ->

View file

@ -8,8 +8,8 @@ open Grej.Infix
module Asn = struct
let decode_strict codec cs =
match Asn.decode codec cs with
| Ok (a, rest) ->
if String.length rest = 0
| Ok (a, cs) ->
if Cstruct.length cs = 0
then Ok a
else Error "trailing bytes"
| Error (`Parse msg) -> Error ("parse error: " ^ msg)
@ -28,101 +28,107 @@ module Asn = struct
end
let new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console TEXT NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console TEXT NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console BLOB NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let copy_from_old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_code, result_msg,
'', '', main_binary, user, job, input_id
FROM build
|}
Caqti_request.exec
Caqti_type.unit
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_code, result_msg,
'', '', main_binary, user, job, input_id
FROM build
|}
let copy_from_new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_code, result_msg,
x'', '', main_binary, user, job, input_id
FROM build
|}
Caqti_request.exec
Caqti_type.unit
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_code, result_msg,
x'', '', main_binary, user, job, input_id
FROM build
|}
let old_build_console_script =
Caqti_type.unit ->*
Caqti_type.(t4 (Builder_db.Rep.id (`build : [ `build ]))
(t2 string Builder_db.Rep.uuid) octets string) @@
"SELECT b.id, job.name, b.uuid, b.console, b.script FROM build b, job WHERE b.job = job.id"
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup4 (Builder_db.Rep.id (`build : [ `build ]))
(tup2 string Builder_db.Rep.uuid) Builder_db.Rep.cstruct string)
"SELECT b.id, job.name, b.uuid, b.console, b.script FROM build b, job WHERE b.job = job.id"
let update_new_build_console_script =
Caqti_type.(t3 (Builder_db.Rep.id (`build : [ `build ]))
Builder_db.Rep.fpath Builder_db.Rep.fpath) ->.
Caqti_type.unit @@
"UPDATE new_build SET console = $2, script = $3 WHERE id = $1"
Caqti_request.exec
Caqti_type.(tup3 (Builder_db.Rep.id (`build : [ `build ])) Builder_db.Rep.fpath Builder_db.Rep.fpath)
"UPDATE new_build SET console = ?2, script = ?3 WHERE id = ?1"
let new_build_console_script =
Caqti_type.unit ->*
Caqti_type.t3 (Builder_db.Rep.id (`build : [ `build ]))
Builder_db.Rep.fpath Builder_db.Rep.fpath @@
"SELECT id, console, script FROM build"
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup3 (Builder_db.Rep.id (`build : [ `build ])) Builder_db.Rep.fpath Builder_db.Rep.fpath)
"SELECT id, console, script FROM build"
let update_old_build_console_script =
Caqti_type.(t3 (Builder_db.Rep.id (`build : [ `build ])) octets string) ->.
Caqti_type.unit @@
"UPDATE new_build SET console = $2, script = $3 WHERE id = $1"
Caqti_request.exec
Caqti_type.(tup3 (Builder_db.Rep.id (`build : [ `build ])) Builder_db.Rep.cstruct string)
"UPDATE new_build SET console = ?2, script = ?3 WHERE id = ?1"
let drop_build =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE build"
let rename_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE new_build RENAME TO build"
Caqti_request.exec
Caqti_type.unit
"ALTER TABLE new_build RENAME TO build"
let console_to_string console =
Asn.console_of_cs console
@ -181,18 +187,17 @@ let migrate datadir (module Db : Caqti_blocking.CONNECTION) =
console_scripts >>= fun () ->
Db.exec drop_build () >>= fun () ->
Db.exec rename_build () >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) \
WHERE result_code <> 0")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) WHERE result_code <> 0")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_input_id ON build(input_id)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_input_id ON build(input_id)")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_main_binary ON build(main_binary)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_main_binary ON build(main_binary)")
() >>= fun () ->
Db.exec (Grej.set_version new_version) ()
@ -207,17 +212,16 @@ let rollback datadir (module Db : Caqti_blocking.CONNECTION) =
console_scripts >>= fun () ->
Db.exec drop_build () >>= fun () ->
Db.exec rename_build () >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) \
WHERE result_code <> 0")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) WHERE result_code <> 0")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_input_id ON build(input_id)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_input_id ON build(input_id)")
() >>= fun () ->
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_main_binary ON build(main_binary)")
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_main_binary ON build(main_binary)")
() >>= fun () ->
Db.exec (Grej.set_version old_version) ()

View file

@ -1,19 +1,16 @@
open Grej.Infix
let mixups =
Caqti_type.unit ->*
Caqti_type.t3 (Builder_db.Rep.id (`build : [`build]))
Builder_db.Rep.fpath Builder_db.Rep.fpath @@
"SELECT id, console, script FROM build \
WHERE console LIKE '%/script.txt' AND script LIKE '%/console.txt'"
Caqti_request.collect ~oneshot:true
Caqti_type.unit
(Caqti_type.tup3 (Builder_db.Rep.id (`build : [`build])) Builder_db.Rep.fpath Builder_db.Rep.fpath)
"SELECT id, console, script FROM build WHERE console LIKE '%/script.txt' AND script LIKE '%/console.txt'"
let fixup =
Caqti_type.t3 (Builder_db.Rep.id (`build : [`build]))
Builder_db.Rep.fpath Builder_db.Rep.fpath ->.
Caqti_type.unit @@
"UPDATE build SET console = $2, script = $3 WHERE id = $1"
Caqti_request.exec ~oneshot:true
(Caqti_type.tup3 (Builder_db.Rep.id (`build : [`build])) Builder_db.Rep.fpath Builder_db.Rep.fpath)
"UPDATE build SET console = ?2, script = ?3 WHERE id = ?1"
let fixup _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:14L (module Db) >>= fun () ->
Db.collect_list mixups () >>= fun mixups ->
Grej.list_iter_result (fun (id, console, script) ->

View file

@ -6,87 +6,96 @@ and rollback_doc = "remove platform from build"
open Grej.Syntax
let new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console TEXT NOT NULL,
script TEXT NOT NULL,
platform TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console TEXT NOT NULL,
script TEXT NOT NULL,
platform TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console TEXT NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE new_build (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uuid VARCHAR(36) NOT NULL UNIQUE,
start_d INTEGER NOT NULL,
start_ps INTEGER NOT NULL,
finish_d INTEGER NOT NULL,
finish_ps INTEGER NOT NULL,
result_code INTEGER NOT NULL,
result_msg TEXT,
console TEXT NOT NULL,
script TEXT NOT NULL,
main_binary INTEGER,
user INTEGER NOT NULL,
job INTEGER NOT NULL,
input_id BLOB, -- sha256 (sha256<opam-switch> || sha256<build-environment> || sha256<system-packages>)
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
FOREIGN KEY(main_binary) REFERENCES build_artifact(id) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY(user) REFERENCES user(id),
FOREIGN KEY(job) REFERENCES job(id)
)
|}
let copy_from_old_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, platform, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_code, result_msg,
console, script, 'PLACEHOLDER-PLATFORM', main_binary, user, job, input_id
FROM build
|}
Caqti_request.exec
Caqti_type.unit
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, platform, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_code, result_msg,
console, script, 'PLACEHOLDER-PLATFORM', main_binary, user, job, input_id
FROM build
|}
let copy_from_new_build =
Caqti_type.unit ->. Caqti_type.unit @@
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_code, result_msg,
console, script, main_binary, user, job, input_id
FROM build
|}
Caqti_request.exec
Caqti_type.unit
{| INSERT INTO new_build(id, uuid, start_d, start_ps, finish_d, finish_ps,
result_code, result_msg, console, script, main_binary, user, job, input_id)
SELECT id, uuid, start_d, start_ps, finish_d, finish_ps, result_code, result_msg,
console, script, main_binary, user, job, input_id
FROM build
|}
let build_id_and_user =
Caqti_type.unit ->* Caqti_type.(t2 (Builder_db.Rep.id (`build : [ `build ])) int64) @@
"SELECT id, user FROM build"
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup2 (Builder_db.Rep.id (`build : [ `build ])) int64)
"SELECT id, user FROM build"
let update_new_build_platform =
Caqti_type.(t2 (Builder_db.Rep.id (`build : [ `build ])) string) ->. Caqti_type.unit @@
"UPDATE new_build SET platform = $2 WHERE id = $1"
Caqti_request.exec
Caqti_type.(tup2 (Builder_db.Rep.id (`build : [ `build ])) string)
"UPDATE new_build SET platform = ?2 WHERE id = ?1"
let drop_build =
Caqti_type.unit ->. Caqti_type.unit @@
"DROP TABLE build"
Caqti_request.exec
Caqti_type.unit
"DROP TABLE build"
let rename_build =
Caqti_type.unit ->. Caqti_type.unit @@
"ALTER TABLE new_build RENAME TO build"
Caqti_request.exec
Caqti_type.unit
"ALTER TABLE new_build RENAME TO build"
(*
1|reynir
@ -105,22 +114,6 @@ let platform_of_user_id = function
| 7L -> "debian-11"
| _ -> assert false
let idx_build_job_start =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"
let idx_build_failed =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) WHERE result_code <> 0"
let idx_build_input_id =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_input_id ON build(input_id)"
let idx_build_main_binary =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_main_binary ON build(main_binary)"
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let* () = Grej.check_version ~user_version:old_version (module Db) in
let* () = Db.exec new_build () in
@ -134,10 +127,26 @@ let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
in
let* () = Db.exec drop_build () in
let* () = Db.exec rename_build () in
let* () = Db.exec idx_build_job_start () in
let* () = Db.exec idx_build_failed () in
let* () = Db.exec idx_build_input_id () in
let* () = Db.exec idx_build_main_binary () in
let* () =
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
()
in
let* () =
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) WHERE result_code <> 0")
()
in
let* () =
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_input_id ON build(input_id)")
()
in
let* () =
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_main_binary ON build(main_binary)")
()
in
Db.exec (Grej.set_version new_version) ()
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
@ -146,9 +155,25 @@ let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let* () = Db.exec copy_from_new_build () in
let* () = Db.exec drop_build () in
let* () = Db.exec rename_build () in
let* () = Db.exec idx_build_job_start () in
let* () = Db.exec idx_build_failed () in
let* () = Db.exec idx_build_input_id () in
let* () = Db.exec idx_build_main_binary () in
let* () =
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)")
()
in
let* () =
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) WHERE result_code <> 0")
()
in
let* () =
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_input_id ON build(input_id)")
()
in
let* () =
Db.exec (Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_main_binary ON build(main_binary)")
()
in
Db.exec (Grej.set_version old_version) ()

View file

@ -1,56 +0,0 @@
let new_version = 16L and old_version = 15L
and identifier = "2022-05-09"
and migrate_doc = "switch uuid encoding to hex"
and rollback_doc = "switch uuid encoding back to binary"
open Grej.Infix
let old_uuid_rep =
let encode uuid = Ok (Uuidm.to_bytes uuid) in
let decode s =
Uuidm.of_bytes s
|> Option.to_result ~none:"failed to decode uuid"
in
Caqti_type.custom ~encode ~decode Caqti_type.string
let new_uuid_rep =
let encode uuid = Ok (Uuidm.to_string uuid) in
let decode s =
Uuidm.of_string s
|> Option.to_result ~none:"failed to decode uuid"
in
Caqti_type.custom ~encode ~decode Caqti_type.string
let uuids_byte_encoded_q =
Caqti_type.unit ->*
Caqti_type.t2 (Builder_db.Rep.id (`build : [`build])) old_uuid_rep @@
"SELECT id, uuid FROM build"
let uuids_hex_encoded_q =
Caqti_type.unit ->*
Caqti_type.t2 (Builder_db.Rep.id (`build : [`build])) new_uuid_rep @@
"SELECT id, uuid FROM build"
let migrate_q =
Caqti_type.t2 (Builder_db.Rep.id (`build : [`build])) new_uuid_rep ->.
Caqti_type.unit @@
"UPDATE build SET uuid = $2 WHERE id = $1"
let rollback_q =
Caqti_type.t2 (Builder_db.Rep.id (`build : [`build])) old_uuid_rep ->.
Caqti_type.unit @@
"UPDATE build SET uuid = $2 WHERE id = $1"
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.collect_list uuids_byte_encoded_q () >>= fun old_uuids ->
Grej.list_iter_result (Db.exec migrate_q) old_uuids >>= fun () ->
Db.exec (Grej.set_version new_version) ()
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let open Grej.Infix in
Grej.check_version ~user_version:new_version (module Db) >>= fun () ->
Db.collect_list uuids_hex_encoded_q () >>= fun new_uuids ->
Grej.list_iter_result (Db.exec rollback_q) new_uuids >>= fun () ->
Db.exec (Grej.set_version old_version) ()

View file

@ -1,32 +0,0 @@
let new_version = 17L and old_version = 16L
and identifier = "2023-09-11"
and migrate_doc = "index failed builds on main binary is null"
and rollback_doc = "index failed builds on exit code"
open Grej.Syntax
let drop_idx_build_failed =
Caqti_type.(unit ->. unit) @@
"DROP INDEX idx_build_failed"
let migrate _datadir (module Db : Caqti_blocking.CONNECTION) =
let* () = Grej.check_version ~user_version:old_version (module Db) in
let* () = Db.exec drop_idx_build_failed () in
let* () =
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) \
WHERE main_binary IS NULL")
()
in
Db.exec (Grej.set_version new_version) ()
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
let* () = Grej.check_version ~user_version:new_version (module Db) in
let* () = Db.exec drop_idx_build_failed () in
let* () =
Db.exec (Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_failed ON build(job, start_d DESC, start_ps DESC) \
WHERE result_code <> 0")
()
in
Db.exec (Grej.set_version old_version) ()

View file

@ -1,162 +0,0 @@
let new_version = 18L and old_version = 17L
and identifier = "2023-09-14"
and migrate_doc = "Artifacts are stored content-addressed in the filesystem"
and rollback_doc = "Artifacts are stored under their build's job name and uuid"
open Grej.Syntax
let new_build_artifact =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build_artifact (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL,
sha256 BLOB NOT NULL,
size INTEGER NOT NULL,
build INTEGER NOT NULL,
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
let old_build_artifact =
Caqti_type.unit ->. Caqti_type.unit @@
{| CREATE TABLE new_build_artifact (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
filepath TEXT NOT NULL, -- the path as in the build
localpath TEXT NOT NULL, -- local path to the file on disk
sha256 BLOB NOT NULL,
size INTEGER NOT NULL,
build INTEGER NOT NULL,
FOREIGN KEY(build) REFERENCES build(id),
UNIQUE(build, filepath)
)
|}
let idx_build_artifact_sha256 =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_artifact_sha256 ON build_artifact(sha256)"
let idx_build_artifact_build =
Caqti_type.unit ->. Caqti_type.unit @@
"CREATE INDEX idx_build_artifact_build ON build_artifact(build)"
let copy_new_build_artifact =
Caqti_type.(unit ->. unit) @@
{| INSERT INTO new_build_artifact(id, filepath, sha256, size, build)
SELECT id, filepath, sha256, size, build
FROM build_artifact
|}
let copy_old_build_artifact =
Caqti_type.(unit ->. unit) @@
{| INSERT INTO new_build_artifact(id, filepath, localpath, sha256, size, build)
SELECT a.id, a.filepath,
j.name || '/' || b.uuid || '/output/' || a.filepath,
a.sha256, a.size, a.build
FROM build_artifact a, job j, build b
WHERE b.id = a.build AND j.id = b.job
|}
let new_build_artifact_paths =
Caqti_type.unit ->* Caqti_type.(t2 string string) @@
{| SELECT localpath, '_artifacts/' || substr(lower(hex(sha256)), 1, 2) || '/' || lower(hex(sha256))
FROM build_artifact
|}
let old_build_artifact_paths =
Caqti_type.unit ->* Caqti_type.(t2 string string) @@
{| SELECT '_artifacts/' || substr(lower(hex(a.sha256)), 1, 2) || '/' || lower(hex(a.sha256)),
j.name || '/' || b.uuid || '/output/' || a.filepath
FROM build_artifact a, job j, build b
WHERE b.id = a.build AND j.id = b.job
|}
let drop_build_artifact =
Caqti_type.(unit ->. unit) @@
"DROP TABLE build_artifact"
let rename_build_artifact =
Caqti_type.(unit ->. unit) @@
"ALTER TABLE new_build_artifact RENAME TO build_artifact"
let move_paths ?force datadir (old_path, new_path) =
let old_path = Fpath.(datadir // v old_path) and new_path = Fpath.(datadir // v new_path) in
let* _created = Bos.OS.Dir.create (Fpath.parent new_path) in
Bos.OS.Path.move ?force old_path new_path
let copy_paths datadir (old_path, new_path) =
let old_path = Fpath.(datadir // v old_path) and new_path = Fpath.(datadir // v new_path) in
let new_path_tmp = Fpath.(new_path + "tmp") in
let* _created = Bos.OS.Dir.create (Fpath.parent new_path) in
let cmd = Bos.Cmd.(v "cp" % p old_path % p new_path_tmp) in
let* () =
match Bos.OS.Cmd.run_status cmd with
| Ok `Exited 0 ->
Ok ()
| Ok status ->
let _ = Bos.OS.Path.delete new_path_tmp in
Error (`Msg (Fmt.str "cp failed: %a" Bos.OS.Cmd.pp_status status))
| Error _ as e ->
let _ = Bos.OS.Path.delete new_path_tmp in
e
in
Bos.OS.Path.move ~force:true new_path_tmp new_path
let migrate datadir (module Db : Caqti_blocking.CONNECTION) =
let* () = Grej.check_version ~user_version:old_version (module Db) in
let* () = Db.exec new_build_artifact () in
let* () = Db.exec copy_new_build_artifact () in
let* () = Db.iter_s new_build_artifact_paths (move_paths ~force:true datadir) () in
let* () = Db.exec drop_build_artifact () in
let* () = Db.exec rename_build_artifact () in
let* () = Db.exec idx_build_artifact_sha256 () in
let* () = Db.exec idx_build_artifact_build () in
Db.exec (Grej.set_version new_version) ()
let rollback datadir (module Db : Caqti_blocking.CONNECTION) =
let* () = Grej.check_version ~user_version:new_version (module Db) in
let* () = Db.exec old_build_artifact () in
let* () = Db.exec copy_old_build_artifact () in
let* () = Db.iter_s old_build_artifact_paths (copy_paths datadir) () in
let* () =
Db.iter_s old_build_artifact_paths
(fun (old_path, _new_path) ->
Bos.OS.Path.delete Fpath.(datadir // v old_path))
()
in
let* () = Db.exec drop_build_artifact () in
let* () = Db.exec rename_build_artifact () in
let* () = Db.exec idx_build_artifact_sha256 () in
Db.exec (Grej.set_version old_version) ()
(* migration failed but managed to move *some* files *)
let fixup_migrate datadir (module Db : Caqti_blocking.CONNECTION) =
let* () = Grej.check_version ~user_version:old_version (module Db) in
let* () =
Db.iter_s new_build_artifact_paths
(fun (old_path, new_path) ->
let* old_exists = Bos.OS.Path.exists Fpath.(datadir // v old_path) in
let* new_exists = Bos.OS.Path.exists Fpath.(datadir // v new_path) in
if new_exists && not old_exists then
copy_paths datadir (new_path, old_path)
else Ok ())
()
in
Db.iter_s new_build_artifact_paths
(fun (_old_path, new_path) ->
Bos.OS.Path.delete Fpath.(datadir // v new_path))
()
(* rollback failed but some or all artifacts were copied *)
let fixup_rollback datadir (module Db : Caqti_blocking.CONNECTION) =
let* () = Grej.check_version ~user_version:new_version (module Db) in
Db.iter_s old_build_artifact_paths
(fun (old_path, new_path) ->
let* old_exists = Bos.OS.Path.exists Fpath.(datadir // v old_path) in
if old_exists then
Bos.OS.Path.delete Fpath.(datadir // v new_path)
else
move_paths datadir (new_path, old_path))
()

View file

@ -1,9 +1,9 @@
opam-version: "2.0"
maintainer: "Reynir Björnsson <reynir@reynir.dk>"
authors: ["Reynir Björnsson <reynir@reynir.dk>"]
homepage: "https://github.com/robur-coop/builder-web"
dev-repo: "git+https://github.com/robur-coop/builder-web.git"
bug-reports: "https://github.com/robur-coop/builder-web/issues"
homepage: "https://github.com/roburio/builder-web"
dev-repo: "git+https://github.com/roburio/builder-web.git"
bug-reports: "https://github.com/roburio/builder-web/issues"
license: "ISC"
build: [
@ -15,49 +15,35 @@ build: [
]
depends: [
"ocaml" {>= "4.13.0"}
"dune" {>= "2.7.0"}
"builder" {>= "0.4.0"}
"dream" {>= "1.0.0~alpha7"}
"builder"
"dream" {>= "1.0.0~alpha2"}
"cstruct" {>= "6.0.0"}
"bos"
"ohex" {>= "0.2.0"}
"lwt" {>= "5.7.0"}
"caqti" {>= "2.1.2"}
"hex"
"caqti"
"caqti-lwt"
"caqti-driver-sqlite3"
"mirage-crypto-rng" {>= "0.11.0"}
"kdf"
"pbkdf"
"mirage-crypto-rng"
"scrypt-kdf"
"alcotest" {with-test}
"opam-core"
"opam-format" {>= "2.1.0"}
"metrics" {>= "0.3.0"}
"metrics-lwt" {>= "0.3.0"}
"metrics-influx" {>= "0.3.0"}
"metrics-rusage" {>= "0.3.0"}
"opam-format"
"metrics"
"metrics-lwt"
"metrics-influx"
"metrics-rusage"
"ipaddr"
"tyxml" {>= "4.3.0"}
"tyxml"
"ptime"
"duration"
"asn1-combinators" {>= "0.3.0"}
"mirage-crypto"
"asn1-combinators"
"logs"
"cmdliner" {>= "1.1.0"}
"cmdliner"
"uri"
"fmt" {>= "0.8.7"}
"cmarkit" {>= "0.3.0"}
"tar" {>= "3.0.0"}
"tar-unix" {>= "3.0.0"}
"owee"
"solo5-elftool" {>= "0.3.0"}
"decompress" {>= "1.5.0"}
"digestif" {>= "1.2.0"}
"alcotest" {>= "1.2.0" & with-test}
"ppx_deriving" {with-test}
"ppx_deriving_yojson" {with-test}
"yojson" {with-test}
"omd"
]
synopsis: "Web interface for builder"
description: """
Builder-web takes in submissions of builds, typically from [builder](https://github.com/robur-coop/builder/), and displays the produced artifacts in a way that makes it easy to compare checksums and build status.
Produced binaries can be downloaded and executed.
[builds.robur.coop](https://builds.robur.coop/) itself runs builder-web.
"""

File diff suppressed because it is too large Load diff

View file

@ -3,16 +3,17 @@ module Rep : sig
type 'a id
type file = {
filepath : Fpath.t;
sha256 : string;
localpath : Fpath.t;
sha256 : Cstruct.t;
size : int;
}
val id_to_int64 : 'a id -> int64
val untyped_id : untyped_id Caqti_type.t
val id : 'a -> 'a id Caqti_type.t
val uuid : Uuidm.t Caqti_type.t
val ptime : Ptime.t Caqti_type.t
val fpath : Fpath.t Caqti_type.t
val cstruct : Cstruct.t Caqti_type.t
val file : file Caqti_type.t
val execution_result : Builder.execution_result Caqti_type.t
val console : (int * string) list Caqti_type.t
@ -21,7 +22,8 @@ type 'a id = 'a Rep.id
type file = Rep.file = {
filepath : Fpath.t;
sha256 : string;
localpath : Fpath.t;
sha256 : Cstruct.t;
size : int;
}
@ -30,67 +32,64 @@ val application_id : int32
val current_version : int64
val get_application_id :
(unit, int32, [ `One ]) Caqti_request.t
(unit, int32, [< `Many | `One | `Zero > `One ]) Caqti_request.t
val set_application_id :
(unit, unit, [ `Zero ]) Caqti_request.t
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val get_version :
(unit, int64, [ `One ]) Caqti_request.t
(unit, int64, [< `Many | `One | `Zero > `One ]) Caqti_request.t
val set_current_version :
(unit, unit, [ `Zero ]) Caqti_request.t
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val last_insert_rowid :
(unit, 'a id, [ `One ]) Caqti_request.t
(unit, 'a id, [< `Many | `One | `Zero > `One ]) Caqti_request.t
module Job : sig
val get :
([`job] id, string, [ `One ])
([`job] id, string, [< `Many | `One | `Zero > `One ])
Caqti_request.t
val get_id_by_name :
(string, [`job] id, [ `One | `Zero ]) Caqti_request.t
(string, [`job] id, [< `Many | `One | `Zero > `One `Zero ]) Caqti_request.t
val get_all_with_section_synopsis :
(unit, [`job] id * string * string option * string option, [ `Many | `One | `Zero ]) Caqti_request.t
val try_add :
(string, unit, [ `Zero ]) Caqti_request.t
(string, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val remove :
([`job] id, unit, [ `Zero ]) Caqti_request.t
([`job] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
end
module Tag : sig
val get_id_by_name :
(string, [`tag] id, [ `One ]) Caqti_request.t
(string, [`tag] id, [< `Many | `One | `Zero > `One ]) Caqti_request.t
val try_add :
(string, unit, [ `Zero ]) Caqti_request.t
(string, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
end
module Job_tag : sig
val add :
([`tag] id * string * [`job] id, unit, [ `Zero ]) Caqti_request.t
([`tag] id * string * [`job] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val update :
([`tag] id * string * [`job] id, unit, [ `Zero ]) Caqti_request.t
([`tag] id * string * [`job] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val get_value :
([`tag] id * [`job] id, string, [ `One | `Zero ]) Caqti_request.t
val remove_by_job :
([`job] id, unit, [ `Zero ]) Caqti_request.t
([`tag] id * [`job] id, string, [< `Many | `One | `Zero > `Zero `One ]) Caqti_request.t
end
module Build_artifact : sig
val get : ([`build_artifact] id, file, [ `One]) Caqti_request.t
val get : ([`build_artifact] id, file, [< `Many | `One | `Zero > `One]) Caqti_request.t
val get_by_build_uuid :
(Uuidm.t * Fpath.t, [`build_artifact] id * file,
[ `One | `Zero ])
[< `Many | `One | `Zero > `One `Zero ])
Caqti_request.t
val get_all_by_build :
([`build] id, [`build_artifact] id * file, [ `Many | `One | `Zero ]) Caqti_request.t
val exists : (string, bool, [ `One ]) Caqti_request.t
val add :
(file * [`build] id, unit, [ `Zero ]) Caqti_request.t
(file * [`build] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val remove_by_build :
([`build] id, unit, [ `Zero ]) Caqti_request.t
([`build] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val remove :
([`build_artifact] id, unit, [ `Zero ]) Caqti_request.t
([`build_artifact] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
end
module Build :
@ -104,94 +103,91 @@ sig
script : Fpath.t;
platform : string;
main_binary : [`build_artifact] id option;
input_id : string option;
input_id : Cstruct.t option;
user_id : [`user] id;
job_id : [`job] id;
}
val pp : t Fmt.t
val get_by_uuid :
(Uuidm.t, [`build] id * t, [ `One | `Zero ])
(Uuidm.t, [`build] id * t, [< `Many | `One | `Zero > `One `Zero ])
Caqti_request.t
val get_all :
([`job] id, [`build] id * t, [ `Many | `One | `Zero ]) Caqti_request.t
val get_all_failed :
(int * int * string option, string * t, [ `Many | `One | `Zero ]) Caqti_request.t
val get_all_artifact_sha :
([`job] id * string option, string, [ `Many | `One | `Zero ]) Caqti_request.t
val get_latest_successful_with_binary :
([`job] id * string, [`build] id * t * file, [ `One | `Zero ])
([`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 * string, [`build] id * t * file option, [< `Many | `One | `Zero > `One `Zero ])
Caqti_request.t
val get_failed_builds :
([`job] id * string option, t, [ `Many | `One | `Zero ]) Caqti_request.t
val get_latest_successful :
([`job] id * string option, t, [ `One | `Zero ])
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_builds_older_than :
([`job] id * string option * Ptime.t, [`build] id * t, [ `Many | `One | `Zero ]) Caqti_request.t
val get_builds_excluding_latest_n :
([`job] id * string option * int, [`build] id * t, [ `Many | `One | `Zero ]) Caqti_request.t
val get_nth_latest_successful :
([`job] id * string option * int, [`build] id * t, [ `One | `Zero ]) Caqti_request.t
val get_previous_successful_different_output :
([`build] id, t, [ `One | `Zero ])
val get_latest_successful_uuid_by_platform :
([`job] id * string, Uuidm.t, [< `Many | `One | `Zero > `One `Zero ])
Caqti_request.t
val get_next_successful_different_output :
([`build] id, t, [ `One | `Zero ])
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
val get_same_input_different_output_hashes :
([`build] id, string, [ `Many | `One | `Zero ]) Caqti_request.t
([`build] id, Cstruct.t, [ `Many | `One | `Zero ]) Caqti_request.t
val get_different_input_same_output_input_ids :
([`build] id, string, [ `Many | `One | `Zero ]) Caqti_request.t
([`build] id, Cstruct.t, [ `Many | `One | `Zero ]) Caqti_request.t
val get_one_by_input_id :
(string, t, [ `One ]) Caqti_request.t
(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, [ `Zero ]) Caqti_request.t
val add : (t, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val get_by_hash :
(string, t, [ `One]) Caqti_request.t
(Cstruct.t, t, [< `Many | `One | `Zero > `One]) Caqti_request.t
val get_with_main_binary_by_hash :
(string, t * file option, [ `One]) Caqti_request.t
(Cstruct.t, t * file option, [< `Many | `One | `Zero > `One]) Caqti_request.t
val get_with_jobname_by_hash :
(string, string * t, [ `One | `Zero]) Caqti_request.t
val set_main_binary : ([`build] id * [`build_artifact] id, unit, [ `Zero ]) Caqti_request.t
val remove : ([`build] id, unit, [ `Zero]) Caqti_request.t
(Cstruct.t, string * t, [< `Many | `One | `Zero > `One `Zero]) Caqti_request.t
val set_main_binary : ([`build] id * [`build_artifact] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val remove : ([`build] id, unit, [< `Many | `One | `Zero > `Zero]) Caqti_request.t
end
module User : sig
val get_user :
(string, [`user] id * Builder_web_auth.scrypt Builder_web_auth.user_info,
[ `One | `Zero ])
[< `Many | `One | `Zero > `One `Zero ])
Caqti_request.t
val get_all :
(unit, string, [ `Many | `One | `Zero ]) Caqti_request.t
(unit, [`user] id * string, [ `Many | `One | `Zero ]) Caqti_request.t
val add :
(Builder_web_auth.scrypt Builder_web_auth.user_info, unit, [ `Zero ])
(Builder_web_auth.scrypt Builder_web_auth.user_info, unit, [< `Many | `One | `Zero > `Zero ])
Caqti_request.t
val remove_user :
(string, unit, [ `Zero ]) Caqti_request.t
(string, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val update_user :
(Builder_web_auth.scrypt Builder_web_auth.user_info, unit, [ `Zero ])
(Builder_web_auth.scrypt Builder_web_auth.user_info, unit, [< `Many | `One | `Zero > `Zero ])
Caqti_request.t
end
module Access_list : sig
val get :
([`user] id * [`job] id, [`access_list] id, [ `One ]) Caqti_request.t
([`user] id * [`job] id, [`access_list] id, [< `Many | `One | `Zero > `One ]) Caqti_request.t
val get_all_names :
(unit, string * string, [ `Many | `One | `Zero ]) Caqti_request.t
val add :
([`user] id * [`job] id, unit, [ `Zero ]) Caqti_request.t
([`user] id * [`job] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val remove :
([`user] id * [`job] id, unit, [ `Zero ]) Caqti_request.t
val remove_by_job :
([`job] id, unit, [ `Zero ]) Caqti_request.t
([`user] id * [`job] id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val remove_all_by_username :
(string, unit, [ `Zero ]) Caqti_request.t
(string, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
end
val migrate :
(unit, unit, [ `Zero ]) Caqti_request.t list
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t list
val rollback :
(unit, unit, [ `Zero ]) Caqti_request.t list
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t list

View file

@ -1,3 +1,3 @@
(library
(name builder_db)
(libraries builder caqti caqti-driver-sqlite3 asn1-combinators builder_web_auth))
(libraries builder caqti caqti-driver-sqlite3 asn1-combinators mirage-crypto builder_web_auth))

View file

@ -1,8 +1,8 @@
module Asn = struct
let decode_strict codec cs =
match Asn.decode codec cs with
| Ok (a, rest) ->
if String.length rest = 0
| Ok (a, cs) ->
if Cstruct.length cs = 0
then Ok a
else Error "trailing bytes"
| Error (`Parse msg) -> Error ("parse error: " ^ msg)
@ -17,7 +17,7 @@ module Asn = struct
(required ~label:"delta" int)
(required ~label:"data" utf8_string)))
let console_of_str, console_to_str = projections_of console
let console_of_cs, console_to_cs = projections_of console
end
type untyped_id = int64
@ -26,18 +26,17 @@ type 'a id = untyped_id
let id (_ : 'a) : 'a id Caqti_type.t = untyped_id
let any_id : 'a id Caqti_type.t = untyped_id
let id_to_int64 (id : 'a id) : int64 = id
type file = {
filepath : Fpath.t;
sha256 : string;
localpath : Fpath.t;
sha256 : Cstruct.t;
size : int;
}
let uuid =
let encode uuid = Ok (Uuidm.to_string uuid) in
let encode uuid = Ok (Uuidm.to_bytes uuid) in
let decode s =
Uuidm.of_string s
Uuidm.of_bytes s
|> Option.to_result ~none:"failed to decode uuid"
in
Caqti_type.custom ~encode ~decode Caqti_type.string
@ -47,7 +46,7 @@ let ptime =
let encode t = Ok (Ptime.Span.to_d_ps (Ptime.to_span t)) in
let decode (d, ps) = Ok (Ptime.v (d, ps))
in
let rep = Caqti_type.(t2 int int64) in
let rep = Caqti_type.(tup2 int int64) in
Caqti_type.custom ~encode ~decode rep
let fpath =
@ -56,25 +55,30 @@ let fpath =
|> Result.map_error (fun (`Msg s) -> s) in
Caqti_type.custom ~encode ~decode Caqti_type.string
let cstruct =
let encode t = Ok (Cstruct.to_string t) in
let decode s = Ok (Cstruct.of_string s) in
Caqti_type.custom ~encode ~decode Caqti_type.octets
let file =
let encode { filepath; sha256; size } =
Ok (filepath, sha256, size) in
let decode (filepath, sha256, size) =
Ok { filepath; sha256; size } in
Caqti_type.custom ~encode ~decode Caqti_type.(t3 fpath octets int)
let encode { filepath; localpath; sha256; size } =
Ok (filepath, localpath, sha256, size) in
let decode (filepath, localpath, sha256, size) =
Ok { filepath; localpath; sha256; size } in
Caqti_type.custom ~encode ~decode Caqti_type.(tup4 fpath fpath cstruct int)
let file_opt =
let rep = Caqti_type.(t3 (option fpath) (option octets) (option int)) in
let rep = Caqti_type.(tup4 (option fpath) (option fpath) (option cstruct) (option int)) in
let encode = function
| Some { filepath; sha256; size } ->
Ok (Some filepath, Some sha256, Some size)
| Some { filepath; localpath; sha256; size } ->
Ok (Some filepath, Some localpath, Some sha256, Some size)
| None ->
Ok (None, None, None)
Ok (None, None, None, None)
in
let decode = function
| (Some filepath, Some sha256, Some size) ->
Ok (Some { filepath; sha256; size })
| (None, None, None) ->
| (Some filepath, Some localpath, Some sha256, Some size) ->
Ok (Some { filepath; localpath; sha256; size })
| (None, None, None, None) ->
Ok None
| _ ->
(* This should not happen if the database is well-formed *)
@ -103,25 +107,25 @@ let execution_result =
else
Error "bad encoding (unknown number)"
in
let rep = Caqti_type.(t2 int (option string)) in
let rep = Caqti_type.(tup2 int (option string)) in
Caqti_type.custom ~encode ~decode rep
let console =
let encode console = Ok (Asn.console_to_str console) in
let decode data = Asn.console_of_str data in
Caqti_type.(custom ~encode ~decode octets)
let encode console = Ok (Asn.console_to_cs console) in
let decode data = Asn.console_of_cs data in
Caqti_type.custom ~encode ~decode cstruct
let user_info =
let rep = Caqti_type.(t7 string octets octets int int int bool) in
let rep = Caqti_type.(tup4 string cstruct cstruct (tup4 int int int bool)) in
let encode { Builder_web_auth.username;
password_hash = `Scrypt (password_hash, password_salt, {
Builder_web_auth.scrypt_n; scrypt_r; scrypt_p
});
restricted; }
=
Ok (username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p, restricted)
Ok (username, password_hash, password_salt, (scrypt_n, scrypt_r, scrypt_p, restricted))
in
let decode (username, password_hash, password_salt, scrypt_n, scrypt_r, scrypt_p, restricted) =
let decode (username, password_hash, password_salt, (scrypt_n, scrypt_r, scrypt_p, restricted)) =
Ok { Builder_web_auth.username;
password_hash =
`Scrypt (password_hash, password_salt,
@ -132,6 +136,7 @@ let user_info =
(* this doesn't really belong in this module, but we need access to the type of [id] *)
let last_insert_rowid =
let open Caqti_request.Infix in
Caqti_type.unit ->! any_id @@
Caqti_request.find
Caqti_type.unit
any_id
"SELECT last_insert_rowid()"

View file

@ -1,3 +1,2 @@
(lang dune 2.7)
(name builder-web)
(formatting disabled)

View file

@ -6,14 +6,14 @@ open Lwt.Syntax
let realm = "builder-web"
let user_info_field = Dream.new_field ~name:"user_info" ()
let user_info_local = Dream.new_local ~name:"user_info" ()
let authenticate handler = fun req ->
let unauthorized () =
let headers = ["WWW-Authenticate", Printf.sprintf "Basic realm=\"%s\"" realm] in
Dream.respond ~headers ~status:`Unauthorized "Forbidden!"
in
match Dream.header req "Authorization" with
match Dream.header "Authorization" req with
| None -> unauthorized ()
| Some data -> match String.split_on_char ' ' data with
| [ "Basic" ; user_pass ] ->
@ -31,7 +31,7 @@ let authenticate handler = fun req ->
match user_info with
| Ok (Some (id, user_info)) ->
if Builder_web_auth.verify_password pass user_info
then (Dream.set_field req user_info_field (id, user_info); handler req)
then handler (Dream.with_local user_info_local (id, user_info) req)
else unauthorized ()
| Ok None ->
let _ : _ Builder_web_auth.user_info =
@ -45,7 +45,7 @@ let authenticate handler = fun req ->
Dream.respond ~status:`Bad_Request "Couldn't decode authorization"
let authorized req job_name =
match Dream.field req user_info_field with
match Dream.local user_info_local req with
| None -> Lwt.return (Error (`Msg "not authenticated"))
| Some (id, user) ->
if user.restricted then

View file

@ -9,8 +9,8 @@ let pp_error ppf = function
| #Model.error as e -> Model.pp_error ppf e
| `Wrong_version (application_id, version) ->
if application_id = Builder_db.application_id
then Format.fprintf ppf "Wrong database version: %Ld, expected %Ld" version Builder_db.current_version
else Format.fprintf ppf "Wrong database application id: %ld, expected %ld" application_id Builder_db.application_id
then Format.fprintf ppf "Wrong database version: %Ld" version
else Format.fprintf ppf "Wrong database application id: %ld" application_id
let init_datadir datadir =
let ( let* ) = Result.bind and ( let+ ) x f = Result.map f x in
@ -26,7 +26,7 @@ let init_datadir datadir =
let init dbpath datadir =
Result.bind (init_datadir datadir) @@ fun () ->
Lwt_main.run (
Caqti_lwt_unix.connect
Caqti_lwt.connect
(Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
>>= fun (module Db : Caqti_lwt.CONNECTION) ->
Db.find Builder_db.get_application_id () >>= fun application_id ->
@ -46,29 +46,16 @@ let safe_seg path =
else Fmt.kstr (fun s -> Error (`Msg s)) "unsafe path %S" path
(* mime lookup with orb knowledge *)
let append_charset = function
(* mime types from nginx:
http://nginx.org/en/docs/http/ngx_http_charset_module.html#charset_types *)
| "text/html" | "text/xml" | "text/plain" | "text/vnd.wap.wml"
| "application/javascript" | "application/rss+xml" | "application/atom+xml"
as content_type ->
content_type ^ "; charset=utf-8" (* default to utf-8 *)
| content_type -> content_type
let mime_lookup path =
append_charset
(match Fpath.to_string path with
| "build-environment" | "opam-switch" | "system-packages" ->
"text/plain"
| _ ->
if Fpath.has_ext "build-hashes" path
then "text/plain"
else if Fpath.is_prefix Fpath.(v "bin/") path
then "application/octet-stream"
else Magic_mime.lookup (Fpath.to_string path))
let string_of_html =
Format.asprintf "%a" (Tyxml.Html.pp ())
match Fpath.to_string path with
| "build-environment" | "opam-switch" | "system-packages" ->
"text/plain"
| _ ->
if Fpath.has_ext "build-hashes" path
then "text/plain"
else if Fpath.is_prefix Fpath.(v "bin/") path
then "application/octet-stream"
else Magic_mime.lookup (Fpath.to_string path)
let or_error_response r =
let* r = r in
@ -76,13 +63,7 @@ let or_error_response r =
| Ok response -> Lwt.return response
| Error (text, status) -> Dream.respond ~status text
let default_log_warn ~status e =
Log.warn (fun m -> m "%s: %a" (Dream.status_to_string status) pp_error e)
let if_error
?(status = `Internal_Server_Error)
?(log = default_log_warn ~status)
message r =
let if_error ?(status = `Internal_Server_Error) ?(log=(fun e -> Log.warn (fun m -> m "%s: %a" (Dream.status_to_string status) pp_error e))) message r =
let* r = r in
match r with
| Error `Not_found ->
@ -92,12 +73,8 @@ let if_error
Lwt_result.fail (message, status)
| Ok _ as r -> Lwt.return r
let not_found_error r =
let* r = r in
match r with
| Error `Not_found ->
Lwt_result.fail ("Resource not found", `Not_Found)
| Ok _ as r -> Lwt.return r
let string_of_html =
Format.asprintf "%a" (Tyxml.Html.pp ())
let get_uuid s =
Lwt.return
@ -107,164 +84,11 @@ let get_uuid s =
| None -> Error ("Bad uuid", `Bad_Request)
else Error ("Bad uuid", `Bad_Request))
let add_routes datadir =
let datadir_global = Dream.new_global ~name:"datadir" (fun () -> datadir) in
let main_binary_of_uuid uuid db =
Model.build uuid db
|> if_error "Error getting job build"
~log:(fun e -> Log.warn (fun m -> m "Error getting job build: %a" pp_error e))
>>= fun (_id, build) ->
Model.not_found build.Builder_db.Build.main_binary
|> not_found_error
>>= fun main_binary ->
Model.build_artifact_by_id main_binary db
|> if_error "Error getting main binary"
module Viz_aux = struct
let viz_type_to_string = function
| `Treemap -> "treemap"
| `Dependencies -> "dependencies"
let viz_dir ~cachedir ~viz_typ ~version =
let typ_str = viz_type_to_string viz_typ in
Fpath.(cachedir / Fmt.str "%s_%d" typ_str version)
let viz_path ~cachedir ~viz_typ ~version ~input_hash =
Fpath.(
viz_dir ~cachedir ~viz_typ ~version
/ input_hash + "html"
)
let choose_versioned_viz_path
~cachedir
~viz_typ
~viz_input_hash
~current_version =
let ( >>= ) = Result.bind in
let rec aux current_version =
let path =
viz_path ~cachedir
~viz_typ
~version:current_version
~input_hash:viz_input_hash in
Bos.OS.File.exists path >>= fun path_exists ->
if path_exists then Ok path else (
if current_version = 1 then
Error (`Msg (Fmt.str "viz '%s': There exist no version of the requested \
visualization"
(viz_type_to_string viz_typ)))
else
aux @@ pred current_version
)
in
aux current_version
let get_viz_version_from_dirs ~cachedir ~viz_typ =
let ( >>= ) = Result.bind in
Bos.OS.Dir.contents cachedir >>= fun versioned_dirs ->
let max_cached_version =
let viz_typ_str = viz_type_to_string viz_typ ^ "_" in
versioned_dirs
|> List.filter_map (fun versioned_dir ->
match Bos.OS.Dir.exists versioned_dir with
| Error (`Msg err) ->
Logs.warn (fun m -> m "%s" err);
None
| Ok false -> None
| Ok true ->
let dir_str = Fpath.filename versioned_dir in
if not (String.starts_with ~prefix:viz_typ_str dir_str) then
None
else
try
String.(sub dir_str
(length viz_typ_str)
(length dir_str - length viz_typ_str))
|> int_of_string
|> Option.some
with Failure _ ->
Logs.warn (fun m ->
m "Failed to read visualization-version from directory: '%s'"
(Fpath.to_string versioned_dir));
None
)
|> List.fold_left Int.max (-1)
in
if max_cached_version = -1 then
Result.error @@
`Msg (Fmt.str "Couldn't find any visualization-version of %s"
(viz_type_to_string viz_typ))
else
Result.ok max_cached_version
let hash_viz_input ~uuid typ db =
let open Builder_db in
main_binary_of_uuid uuid db >>= fun main_binary ->
Model.build uuid db
|> if_error "Error getting build" >>= fun (build_id, _build) ->
Model.build_artifacts build_id db
|> if_error "Error getting build artifacts" >>= fun artifacts ->
match typ with
| `Treemap ->
let debug_binary =
let bin = Fpath.base main_binary.filepath in
List.find_opt
(fun p -> Fpath.(equal (bin + "debug") (base p.filepath)))
artifacts
in
begin
Model.not_found debug_binary
|> not_found_error >>= fun debug_binary ->
debug_binary.sha256
|> Ohex.encode
|> Lwt_result.return
end
| `Dependencies ->
let opam_switch =
List.find_opt
(fun p -> Fpath.(equal (v "opam-switch") (base p.filepath)))
artifacts
in
Model.not_found opam_switch
|> not_found_error >>= fun opam_switch ->
opam_switch.sha256
|> Ohex.encode
|> Lwt_result.return
let try_load_cached_visualization ~cachedir ~uuid viz_typ db =
Lwt.return (get_viz_version_from_dirs ~cachedir ~viz_typ)
|> if_error "Error getting visualization version" >>= fun latest_viz_version ->
hash_viz_input ~uuid viz_typ db >>= fun viz_input_hash ->
(choose_versioned_viz_path
~cachedir
~current_version:latest_viz_version
~viz_typ
~viz_input_hash
|> Lwt.return
|> if_error "Error finding a version of the requested visualization")
>>= fun viz_path ->
Lwt_result.catch (fun () ->
Lwt_io.with_file ~mode:Lwt_io.Input
(Fpath.to_string viz_path)
Lwt_io.read
)
|> Lwt_result.map_error (fun exn -> `Msg (Printexc.to_string exn))
|> if_error "Error getting cached visualization"
end
let routes ~datadir ~cachedir ~configdir ~expired_jobs =
let builds ~all ?(filter_builds_later_than = 0) req =
let than =
if filter_builds_later_than = 0 then
Ptime.epoch
else
let n = Ptime.Span.v (filter_builds_later_than, 0L) in
let now = Ptime_clock.now () in
Ptime.Span.sub (Ptime.to_span now) n |> Ptime.of_span |>
Option.fold ~none:Ptime.epoch ~some:Fun.id
in
let builder req =
(* TODO filter unsuccessful builds, ?failed=true *)
Dream.sql req Model.jobs_with_section_synopsis
|> if_error "Error getting jobs"
~log:(fun e -> Log.warn (fun m -> m "Error getting jobs: %a" pp_error e))
@ -277,145 +101,93 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
r >>= fun acc ->
Dream.sql req (Model.build_with_main_binary job_id platform) >>= function
| Some (build, artifact) ->
if Ptime.is_later ~than build.finish then
Lwt_result.return ((platform, build, artifact) :: acc)
else
Lwt_result.return acc
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 ->
if platform_builds = [] then
Lwt_result.return acc
else
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))
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"
~log:(fun e -> Log.warn (fun m -> m "Error getting jobs: %a" pp_error e))
>>= fun jobs ->
Views.Builds.make ~all jobs |> string_of_html |> Dream.html |> Lwt_result.ok
Views.builder jobs |> string_of_html |> Dream.html |> Lwt_result.ok
in
let job req =
let job_name = Dream.param req "job" in
let platform = Dream.query req "platform" in
let job_name = Dream.param "job" req in
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) ->
Views.Job.make ~failed:false ~job_name ~platform ~readme builds
|> string_of_html |> Dream.html |> Lwt_result.ok
Views.job job_name readme builds |> string_of_html |> Dream.html |> Lwt_result.ok
in
let job_with_failed req =
let job_name = Dream.param req "job" in
let platform = Dream.query req "platform" in
(Dream.sql req (Model.job_and_readme job_name) >>= fun (job_id, readme) ->
Dream.sql req (Model.builds_grouped_by_output_with_failed 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) ->
Views.Job.make ~failed:true ~job_name ~platform ~readme builds
|> string_of_html |> Dream.html |> Lwt_result.ok
in
let redirect_latest req ~job_name ~platform ~artifact =
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 platform))
>>= Model.not_found
|> if_error "Error getting job" >>= fun build ->
Dream.redirect req
(Link.Job_build_artifact.make_from_string ~job_name ~build ~artifact ())
(Fmt.str "/job/%s/build/%a/%s" job_name Uuidm.pp build path)
|> Lwt_result.ok
in
let redirect_latest req =
let job_name = Dream.param req "job" in
let platform = Dream.query req "platform" in
let artifact =
(* FIXME Dream.path deprecated *)
let path = begin[@alert "-deprecated"] Dream.path req end in
if path = [] then
"" (* redirect without trailing slash *)
else
"/" ^ (List.map Uri.pct_encode path |> String.concat "/")
in
redirect_latest req ~job_name ~platform ~artifact
and redirect_latest_no_slash req =
let job_name = Dream.param req "job" in
let platform = Dream.query req "platform" in
redirect_latest req ~job_name ~platform ~artifact:""
in
let redirect_main_binary req =
let job_name = Dream.param req "job"
and build = Dream.param req "build" in
let job_name = Dream.param "job" req
and build = Dream.param "build" req in
get_uuid build >>= fun uuid ->
Dream.sql req (main_binary_of_uuid uuid) >>= fun main_binary ->
let artifact = `File main_binary.Builder_db.filepath in
Link.Job_build_artifact.make ~job_name ~build:uuid ~artifact ()
|> Dream.redirect req
|> Lwt_result.ok
in
let job_build_viz viz_typ req =
let _job_name = Dream.param req "job"
and build = Dream.param req "build" in
get_uuid build >>= fun uuid ->
Dream.sql req (Viz_aux.try_load_cached_visualization ~cachedir ~uuid viz_typ)
>>= fun svg_html ->
Lwt_result.ok (Dream.html svg_html)
Dream.sql req (Model.build uuid)
|> if_error "Error getting job build"
~log:(fun e -> Log.warn (fun m -> m "Error getting job build: %a" pp_error e))
>>= fun (_id, build) ->
match build.Builder_db.Build.main_binary with
| None -> Lwt_result.fail ("Resource not found", `Not_Found)
| Some main_binary ->
Dream.sql req (Model.build_artifact_by_id main_binary)
|> if_error "Error getting main binary" >>= fun main_binary ->
Dream.redirect req
(Fmt.str "/job/%s/build/%a/f/%a" job_name Uuidm.pp uuid
Fpath.pp main_binary.Builder_db.filepath)
|> Lwt_result.ok
in
let job_build req =
let job_name = Dream.param req "job"
and build = Dream.param req "build" in
let job_name = Dream.param "job" req
and build = Dream.param "build" req in
get_uuid build >>= fun uuid ->
Dream.sql req (fun conn ->
Model.build uuid conn >>= fun (build_id, build) ->
(match build.Builder_db.Build.main_binary with
| Some main_binary ->
Model.build_artifact_by_id main_binary conn |> Lwt_result.map Option.some
| None -> Lwt_result.return None) >>= fun main_binary ->
Model.build_artifacts build_id conn >>= fun artifacts ->
Model.builds_with_same_input_and_same_main_binary build_id conn >>= fun same_input_same_output ->
Model.builds_with_different_input_and_same_main_binary build_id conn >>= fun different_input_same_output ->
Model.builds_with_same_input_and_different_main_binary build_id conn >>= fun same_input_different_output ->
Model.latest_successful_build build.job_id (Some build.Builder_db.Build.platform) conn >>= fun latest ->
Model.next_successful_build_different_output build_id conn >>= fun next ->
Model.previous_successful_build_different_output build_id conn >|= fun previous ->
(build, main_binary, artifacts, same_input_same_output, different_input_same_output, same_input_different_output, latest, next, previous)
)
(Dream.sql req (Model.readme job_name) >>= fun readme ->
Dream.sql req (Model.build uuid) >>= fun (build_id, build) ->
Dream.sql req (Model.build_artifacts build_id) >>= fun artifacts ->
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 (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 (build, main_binary, artifacts, same_input_same_output, different_input_same_output, same_input_different_output, latest, next, previous) ->
let solo5_manifest = Option.bind main_binary (Model.solo5_manifest datadir) in
Views.Job_build.make
~job_name
~build
~artifacts
~main_binary
~solo5_manifest
~same_input_same_output
~different_input_same_output
~same_input_different_output
~latest ~next ~previous
>>= 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
let job_build_file req =
let _job_name = Dream.param req "job"
and build = Dream.param req "build"
(* FIXME *)
and filepath = begin[@alert "-deprecated"] Dream.path req |> String.concat "/" end in
let if_none_match = Dream.header req "if-none-match" in
let datadir = Dream.global datadir_global req in
let _job_name = Dream.param "job" req
and build = Dream.param "build" req
and filepath = Dream.path req |> String.concat "/" in
let if_none_match = Dream.header "if-none-match" req in
(* XXX: We don't check safety of [file]. This should be fine however since
* we don't use [file] for the filesystem but is instead used as a key for
* lookup in the data table of the 'full' file. *)
@ -424,15 +196,15 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
|> if_error ~status:`Not_Found "File not found" >>= fun filepath ->
Dream.sql req (Model.build_artifact build filepath)
|> if_error "Error getting build artifact" >>= fun file ->
let etag = Base64.encode_string file.Builder_db.sha256 in
let etag = Base64.encode_string (Cstruct.to_string file.Builder_db.sha256) in
match if_none_match with
| Some etag' when etag = etag' ->
Dream.empty `Not_Modified |> Lwt_result.ok
| _ ->
Model.build_artifact_data datadir file
|> if_error "Error getting build artifact"
~log:(fun e -> Log.warn (fun m -> m "Error getting build artifact data for file %a: %a"
Fpath.pp file.Builder_db.filepath
~log:(fun e -> Log.warn (fun m -> m "Error getting build artifact data for file %a in %a: %a"
Fpath.pp file.Builder_db.filepath Fpath.pp file.Builder_db.localpath
pp_error e)) >>= fun data ->
let headers = [
"Content-Type", mime_lookup file.Builder_db.filepath;
@ -442,8 +214,9 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
in
let job_build_static_file (file : [< `Console | `Script ]) req =
let _job_name = Dream.param req "job"
and build = Dream.param req "build" in
let datadir = Dream.global datadir_global req in
let _job_name = Dream.param "job" req
and build = Dream.param "build" req in
get_uuid build >>= fun build ->
(match file with
| `Console ->
@ -453,52 +226,16 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
|> if_error "Error getting data"
~log:(fun e -> Log.warn (fun m -> m "Error getting script or console data for build %a: %a"
Uuidm.pp build pp_error e)) >>= fun data ->
let headers = [ "Content-Type", "text/plain; charset=utf-8" ] in
let headers = [ "Content-Type", "text/plain" ] in
Dream.respond ~headers data |> Lwt_result.ok
in
let failed_builds req =
let platform = Dream.query req "platform" in
let to_int default s = Option.(value ~default (bind s int_of_string_opt)) in
let start = to_int 0 (Dream.query req "start") in
let count = to_int 10 (Dream.query req "count") in
Dream.sql req (Model.failed_builds ~start ~count platform)
|> if_error "Error getting data"
~log:(fun e -> Log.warn (fun m -> m "Error getting failed builds: %a"
pp_error e)) >>= fun builds ->
Views.failed_builds ~start ~count builds
|> string_of_html |> Dream.html |> Lwt_result.ok
in
let job_build_targz req =
let _job_name = Dream.param req "job"
and build = Dream.param req "build" in
get_uuid build >>= fun build ->
Dream.sql req (Model.build build)
|> if_error "Error getting build" >>= fun (build_id, build) ->
Dream.sql req (Model.build_artifacts build_id)
|> if_error "Error getting artifacts" >>= fun artifacts ->
Ptime.diff build.finish Ptime.epoch |> Ptime.Span.to_int_s
|> Option.to_result ~none:(`Msg "bad finish time") |> Result.map Int64.of_int
|> Lwt.return |> if_error "Internal server error" >>= fun finish ->
Dream.stream ~headers:["Content-Type", "application/tar+gzip"]
(fun stream ->
let+ r = Dream_tar.targz_response datadir finish artifacts stream in
match r with
| Ok () -> ()
| Error _ ->
Log.warn (fun m -> m "error assembling gzipped tar archive");
())
|> Lwt_result.ok
in
let upload req =
let* body = Dream.body req in
Builder.Asn.exec_of_str body |> Lwt.return
Builder.Asn.exec_of_cs (Cstruct.of_string body) |> Lwt.return
|> if_error ~status:`Bad_Request "Bad request"
~log:(fun e ->
Log.warn (fun m -> m "Received bad builder ASN.1");
Log.debug (fun m -> m "Bad builder ASN.1: %a" pp_error e))
Log.warn (fun m -> m "Received bad builder ASN.1: %a" pp_error e))
>>= fun ((({ name ; _ } : Builder.script_job), uuid, _, _, _, _, _) as exec) ->
Log.debug (fun m -> m "Received build %a" pp_exec exec);
Authorization.authorized req name
@ -514,54 +251,54 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
(Fmt.str "Build with same uuid exists: %a\n" Uuidm.pp uuid)
|> Lwt_result.ok
| false ->
(Lwt.return (Dream.field req Authorization.user_info_field |>
let datadir = Dream.global datadir_global req in
(Lwt.return (Dream.local Authorization.user_info_local req |>
Option.to_result ~none:(`Msg "no authenticated user")) >>= fun (user_id, _) ->
Dream.sql req (Model.add_build ~datadir ~cachedir ~configdir user_id exec))
Dream.sql req (Model.add_build datadir user_id exec))
|> if_error "Internal server error"
~log:(fun e -> Log.warn (fun m -> m "Error saving build %a: %a" pp_exec exec pp_error e))
>>= fun () -> Dream.respond "" |> Lwt_result.ok
in
let hash req =
Dream.query req "sha256" |> Option.to_result ~none:(`Msg "Missing sha256 query parameter")
|> Lwt.return
Dream.query "sha256" req |> Option.to_result ~none:(`Msg "Missing sha256 query parameter") |> Lwt.return
|> if_error ~status:`Bad_Request "Bad request" >>= fun hash_hex ->
begin try Ohex.decode hash_hex |> Lwt_result.return
begin try Hex.to_cstruct (`Hex hash_hex) |> Lwt_result.return
with Invalid_argument e -> Lwt_result.fail (`Msg ("Bad hex: " ^ e))
end
|> if_error ~status:`Bad_Request "Bad request" >>= fun hash ->
Dream.sql req (Model.build_hash hash) >>= Model.not_found
|> if_error "Internal server error" >>= fun (job_name, build) ->
Dream.redirect req
(Link.Job_build.make ~job_name ~build:build.Builder_db.Build.uuid ())
(Fmt.str "/job/%s/build/%a/" job_name Uuidm.pp build.Builder_db.Build.uuid)
|> Lwt_result.ok
in
let compare_builds req =
let build_left = Dream.param req "build_left" in
let build_right = Dream.param req "build_right" in
let compare_opam req =
let datadir = Dream.global datadir_global req in
let build_left = Dream.param "build_left" req in
let build_right = Dream.param "build_right" req in
get_uuid build_left >>= fun build_left ->
get_uuid build_right >>= fun build_right ->
Dream.sql req (fun conn ->
Model.build build_left conn >>= fun (_id, build_left) ->
Model.build build_right conn >>= fun (_id, build_right) ->
Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "opam-switch") conn >>=
Model.build_artifact_data datadir >>= fun switch_left ->
Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "build-environment") conn >>=
Model.build_artifact_data datadir >>= fun build_env_left ->
Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "system-packages") conn >>=
Model.build_artifact_data datadir >>= fun system_packages_left ->
Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "opam-switch") conn >>=
Model.build_artifact_data datadir >>= fun switch_right ->
Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "build-environment") conn >>=
Model.build_artifact_data datadir >>= fun build_env_right ->
Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "system-packages") conn >>=
Model.build_artifact_data datadir >>= fun system_packages_right ->
Model.job_name build_left.job_id conn >>= fun job_left ->
Model.job_name build_right.job_id conn >|= fun job_right ->
(job_left, job_right, build_left, build_right,
switch_left, build_env_left, system_packages_left,
switch_right, build_env_right, system_packages_right))
(Dream.sql req (Model.build build_left) >>= fun (_id, build_left) ->
Dream.sql req (Model.build build_right) >>= fun (_id, build_right) ->
Dream.sql req (Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "opam-switch")) >>=
Model.build_artifact_data datadir >>= fun switch_left ->
Dream.sql req (Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "build-environment")) >>=
Model.build_artifact_data datadir >>= fun build_env_left ->
Dream.sql req (Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "system-packages")) >>=
Model.build_artifact_data datadir >>= fun system_packages_left ->
Dream.sql req (Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "opam-switch")) >>=
Model.build_artifact_data datadir >>= fun switch_right ->
Dream.sql req (Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "build-environment")) >>=
Model.build_artifact_data datadir >>= fun build_env_right ->
Dream.sql req (Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "system-packages")) >>=
Model.build_artifact_data datadir >>= fun system_packages_right ->
Dream.sql req (Model.job_name build_left.job_id) >>= fun job_left ->
Dream.sql req (Model.job_name build_right.job_id) >|= fun job_right ->
(job_left, job_right, build_left, build_right,
switch_left, build_env_left, system_packages_left,
switch_right, build_env_right, system_packages_right))
|> if_error "Internal server error"
>>= fun (job_left, job_right, build_left, build_right,
switch_left, build_env_left, system_packages_left,
@ -571,21 +308,16 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
in
let switch_left = OpamFile.SwitchExport.read_from_string switch_left
and switch_right = OpamFile.SwitchExport.read_from_string switch_right in
let opam_diff = Opamdiff.compare switch_left switch_right in
Views.compare_builds
~job_left ~job_right
~build_left ~build_right
~env_diff
~pkg_diff
~opam_diff
Opamdiff.compare switch_left switch_right
|> Views.compare_opam job_left job_right build_left build_right env_diff pkg_diff
|> string_of_html |> Dream.html |> Lwt_result.ok
in
let upload_binary req =
let job = Dream.param req "job" in
let platform = Dream.param req "platform" in
let job = Dream.param "job" req in
let platform = Dream.param "platform" req in
let binary_name =
Dream.query req "binary_name"
Dream.query "binary_name" req
|> Option.map Fpath.of_string
|> Option.value ~default:(Ok Fpath.(v job + "bin"))
in
@ -606,14 +338,15 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
(Fmt.str "Build with same uuid exists: %a\n" Uuidm.pp uuid)
|> Lwt_result.ok
| false ->
let datadir = Dream.global datadir_global req in
let exec =
let now = Ptime_clock.now () in
({ Builder.name = job ; platform ; script = "" }, uuid, [], now, now, Builder.Exited 0,
[ (Fpath.(v "bin" // binary_name), body) ])
in
(Lwt.return (Dream.field req Authorization.user_info_field |>
(Lwt.return (Dream.local Authorization.user_info_local req |>
Option.to_result ~none:(`Msg "no authenticated user")) >>= fun (user_id, _) ->
Dream.sql req (Model.add_build ~datadir ~cachedir ~configdir user_id exec))
Dream.sql req (Model.add_build datadir user_id exec))
|> if_error "Internal server error"
~log:(fun e -> Log.warn (fun m -> m "Error saving build %a: %a" pp_exec exec pp_error e))
>>= fun () -> Dream.respond "" |> Lwt_result.ok
@ -621,87 +354,23 @@ let routes ~datadir ~cachedir ~configdir ~expired_jobs =
let w f req = or_error_response (f req) in
[
`Get, "/", (w (builds ~all:false ~filter_builds_later_than:expired_jobs));
`Get, "/job/:job", (w job);
`Get, "/job/:job/failed", (w job_with_failed);
`Get, "/job/:job/build/latest/**", (w redirect_latest);
`Get, "/job/:job/build/latest", (w redirect_latest_no_slash);
`Get, "/job/:job/build/:build", (w job_build);
`Get, "/job/:job/build/:build/f/**", (w job_build_file);
`Get, "/job/:job/build/:build/main-binary", (w redirect_main_binary);
`Get, "/job/:job/build/:build/viztreemap", (w @@ job_build_viz `Treemap);
`Get, "/job/:job/build/:build/vizdependencies", (w @@ job_build_viz `Dependencies);
`Get, "/job/:job/build/:build/script", (w (job_build_static_file `Script));
`Get, "/job/:job/build/:build/console", (w (job_build_static_file `Console));
`Get, "/job/:job/build/:build/all.tar.gz", (w job_build_targz);
`Get, "/failed-builds", (w failed_builds);
`Get, "/all-builds", (w (builds ~all:true));
`Get, "/hash", (w hash);
`Get, "/compare/:build_left/:build_right", (w compare_builds);
`Post, "/upload", (Authorization.authenticate (w upload));
`Post, "/job/:job/platform/:platform/upload", (Authorization.authenticate (w upload_binary));
(*
/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);
Dream.get "/job/:job/build/latest/**" (w redirect_latest);
Dream.get "/job/:job/build/:build/" (w job_build);
Dream.get "/job/:job/build/:build/f/**" (w job_build_file);
Dream.get "/job/:job/build/:build/main-binary" (w redirect_main_binary);
Dream.get "/job/:job/build/:build/script" (w (job_build_static_file `Script));
Dream.get "/job/:job/build/:build/console" (w (job_build_static_file `Console));
Dream.get "/hash" (w hash);
Dream.get "/compare/:build_left/:build_right/opam-switch" (w compare_opam);
Dream.post "/upload" (Authorization.authenticate (w upload));
Dream.post "/job/:job/platform/:platform/upload" (Authorization.authenticate (w upload_binary));
]
let to_dream_route = function
| `Get, path, handler -> Dream.get path handler
| `Post, path, handler -> Dream.post path handler
let to_dream_routes l = List.map to_dream_route l
let routeprefix_ignorelist_when_removing_trailing_slash = [
"/job/:job/build/:build/f";
"/job/:job/build/latest";
]
module Middleware = struct
let remove_trailing_url_slash : Dream.middleware =
fun handler req ->
let path = Dream.target req |> Utils.Path.of_url in
let is_ignored =
routeprefix_ignorelist_when_removing_trailing_slash
|> List.exists (Utils.Path.matches_dreamroute ~path)
in
if not (List.mem (Dream.method_ req) [`GET; `HEAD]) || is_ignored then
handler req
else match List.rev path with
| "" :: [] (* / *) -> handler req
| "" :: path (* /.../ *) ->
let path = List.rev path in
let queries = Dream.all_queries req in
let url = Utils.Path.to_url ~path ~queries in
(*> Note: See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location*)
Dream.redirect ~status:`Moved_Permanently req url
| _ (* /... *) -> handler req
end
let is_iframe_page ~req =
match Option.bind req (fun r -> Dream.header r "Sec-Fetch-Dest") with
| Some "iframe" -> true
| _ -> false
let error_template error _debug_info suggested_response =
let target =
match error.Dream.request with
| None -> "?"
| Some req -> Dream.target req in
let referer =
Option.bind error.Dream.request (fun req -> Dream.header req "referer")
in
match Dream.status suggested_response with
| `Not_Found ->
let html =
if is_iframe_page ~req:error.Dream.request then
Views.viz_not_found
else
Views.page_not_found ~target ~referer
in
Dream.set_header suggested_response "Content-Type" Dream.text_html;
Dream.set_body suggested_response @@ string_of_html html;
Lwt.return suggested_response
| _ ->
Lwt.return suggested_response
module Link = Link

View file

@ -1,92 +0,0 @@
module High : sig
type t
type 'a s = 'a Lwt.t
external inj : 'a s -> ('a, t) Tar.io = "%identity"
external prj : ('a, t) Tar.io -> 'a s = "%identity"
end = struct
type t
type 'a s = 'a Lwt.t
external inj : 'a -> 'b = "%identity"
external prj : 'a -> 'b = "%identity"
end
let value v = Tar.High (High.inj v)
let ok_value v = value (Lwt_result.ok v)
let run t stream =
let rec run : type a. (a, 'err, High.t) Tar.t -> (a, 'err) result Lwt.t =
function
| Tar.Write str ->
(* Can this not fail?!? Obviously, it can, but we never know?? *)
Lwt_result.ok (Dream.write stream str)
| Tar.Seek _ | Tar.Read _ | Tar.Really_read _ -> assert false
| Tar.Return value -> Lwt.return value
| Tar.High value -> High.prj value
| Tar.Bind (x, f) ->
let open Lwt_result.Syntax in
let* v = run x in
run (f v)
in
run t
let header_of_file mod_time (file : Builder_db.file) =
let file_mode = if Fpath.is_prefix Fpath.(v "bin/") file.filepath then
0o755
else
0o644
in
Tar.Header.make ~file_mode ~mod_time (Fpath.to_string file.filepath) (Int64.of_int file.size)
let contents datadir file : unit -> (string option, _, _) Tar.t =
let state = ref `Initial in
let dispenser () =
let ( let* ) = Tar.( let* ) in
let src = Fpath.append datadir (Model.artifact_path file) in
let* state' =
match !state with
| `Initial ->
let* fd = ok_value (Lwt_io.open_file ~mode:Lwt_io.Input (Fpath.to_string src)) in
let s = `Active fd in
state := s; Tar.return (Ok s)
| `Active _ | `Closed as s -> Tar.return (Ok s)
in
match state' with
| `Closed -> Tar.return (Ok None)
| `Active fd ->
let* data = ok_value (Lwt_io.read ~count:65536 fd) in
if String.length data = 0 then begin
state := `Closed;
let* () = ok_value (Lwt_io.close fd) in
Tar.return (Ok None)
end else
Tar.return (Ok (Some data))
in
dispenser
let entries datadir finish files =
let files =
List.map (fun file ->
let hdr = header_of_file finish file in
let level = Some Tar.Header.Posix in
(level, hdr, contents datadir file)
)
files
in
let files = ref files in
fun () -> match !files with
| [] -> Tar.return (Ok None)
| f :: fs -> files := fs; Tar.return (Ok (Some f))
let targz_response datadir finish files stream =
let entries : (_, _) Tar.entries = entries datadir finish files in
let global_hdr =
Tar.Header.Extended.make
~comment:"Tar file produced by builder-web.%%VERSION_NUM%%"
()
in
let finish32 = Int64.to_int32 finish in
Logs.err (fun m -> m "finished at %ld (%Ld)" finish32 finish);
run (Tar_gz.out_gzipped ~level:9 ~mtime:finish32 Gz.Unix (Tar.out ~global_hdr entries)) stream

View file

@ -1,5 +1,3 @@
(library
(name builder_web)
(libraries builder builder_db dream tyxml bos duration ohex caqti-lwt
opamdiff ptime.clock.os cmarkit tar tar.gz tar-unix owee solo5-elftool decompress.de
decompress.gz uri digestif))
(libraries builder builder_db dream tyxml bos duration hex caqti-lwt opamdiff ptime.clock.os omd))

View file

@ -1,89 +0,0 @@
let fpath_url_pp ppf f =
Fpath.segs f
|> List.map Uri.pct_encode
|> Fmt.(list ~sep:(any "/") string) ppf
module Queries_aux = struct
let flatten = Option.value ~default:[]
let add_raw url_str queries =
let uri = Uri.of_string url_str in
let uri = Uri.add_query_params uri queries in
Uri.to_string uri
let add ~encode_query queries url_str =
queries |> flatten |> List.map encode_query |> add_raw url_str
end
let pctencode fmt str = Format.fprintf fmt "%s" (Uri.pct_encode str)
module Root = struct
let make () = "/"
end
module Job = struct
let encode_query = function
| `Platform p -> "platform", [ p ]
let make ?queries ~job_name () =
Fmt.str "/job/%a" pctencode job_name
|> Queries_aux.add ~encode_query queries
let make_failed ?queries ~job_name () =
Fmt.str "/job/%a/failed" pctencode job_name
|> Queries_aux.add ~encode_query queries
end
module Job_build = struct
let make ~job_name ~build () =
Fmt.str "/job/%a/build/%a"
pctencode job_name
Uuidm.pp build
end
module Job_build_artifact = struct
let encode_artifact = function
| `Main_binary -> "/main-binary"
| `Viz_treemap -> "/viztreemap"
| `Viz_dependencies -> "/vizdependencies"
| `Script -> "/script"
| `Console -> "/console"
| `All_targz -> "/all.tar.gz"
| `File f -> "/f/" ^ Fmt.to_to_string fpath_url_pp f
let make_from_string ~job_name ~build ~artifact () =
Fmt.str "/job/%a/build/%a%s"
pctencode job_name
Uuidm.pp build
artifact
let make ~job_name ~build ~artifact () =
let artifact = encode_artifact artifact in
make_from_string ~job_name ~build ~artifact ()
end
module Compare_builds = struct
let make ~left ~right () =
Fmt.str "/compare/%a/%a"
Uuidm.pp left
Uuidm.pp right
end
module Failed_builds = struct
let make ~count ~start () =
Fmt.str "/failed-builds?count=%d&start=%d" count start
end

View file

@ -15,18 +15,10 @@ let pp_error ppf = function
Caqti_error.pp ppf e
let not_found = function
| None -> Lwt_result.fail `Not_found
| None -> Lwt.return (Error `Not_found :> (_, [> error ]) result)
| Some v -> Lwt_result.return v
let staging datadir = Fpath.(datadir / "_staging")
let artifact_path artifact =
let sha256 = Ohex.encode artifact.Builder_db.sha256 in
(* NOTE: [sha256] is 64 characters when it's a hex sha256 checksum *)
(* NOTE: We add the prefix to reduce the number of files in a directory - a
workaround for inferior filesystems. We can easily revert this by changing
this function and adding a migration. *)
let prefix = String.sub sha256 0 2 in
Fpath.(v "_artifacts" / prefix / sha256)
let read_file datadir filepath =
let filepath = Fpath.(datadir // filepath) in
@ -39,10 +31,10 @@ let read_file datadir filepath =
Lwt_result.return data)
(function
| Unix.Unix_error (e, _, _) ->
Log.warn (fun m -> m "Error reading local file %a: %s"
Logs.warn (fun m -> m "Error reading local file %a: %s"
Fpath.pp filepath (Unix.error_message e));
Lwt.return_error (`File_error filepath)
| e -> Lwt.reraise e)
| e -> Lwt.fail e)
let build_artifact build filepath (module Db : CONN) =
Db.find_opt Builder_db.Build_artifact.get_by_build_uuid (build, filepath)
@ -52,16 +44,12 @@ let build_artifact_by_id id (module Db : CONN) =
Db.find Builder_db.Build_artifact.get id
let build_artifact_data datadir file =
read_file datadir (artifact_path file)
read_file datadir file.Builder_db.localpath
let build_artifacts build (module Db : CONN) =
Db.collect_list Builder_db.Build_artifact.get_all_by_build build >|=
List.map snd
let solo5_manifest datadir file =
let buf = Owee_buf.map_binary Fpath.(to_string (datadir // artifact_path file)) in
Solo5_elftool.query_manifest buf |> Result.to_option
let platforms_of_job id (module Db : CONN) =
Db.collect_list Builder_db.Build.get_platforms_for_job id
@ -70,7 +58,7 @@ let build uuid (module Db : CONN) =
not_found
let build_with_main_binary job platform (module Db : CONN) =
Db.find_opt Builder_db.Build.get_latest_successful_with_binary (job, platform) >|=
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) =
@ -80,21 +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 job_id platform (module Db : CONN) =
Db.find_opt Builder_db.Build.get_latest_successful (job_id, platform)
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 latest_successful_build_uuid job_id platform db =
latest_successful_build job_id platform db >|= fun build ->
Option.map (fun build -> build.Builder_db.Build.uuid) build
let previous_successful_build_uuid id (module Db : CONN) =
Db.find_opt Builder_db.Build.get_previous_successful_uuid id
let previous_successful_build_different_output id (module Db : CONN) =
Db.find_opt Builder_db.Build.get_previous_successful_different_output id
let next_successful_build_different_output id (module Db : CONN) =
Db.find_opt Builder_db.Build.get_next_successful_different_output id
let failed_builds ~start ~count platform (module Db : CONN) =
Db.collect_list Builder_db.Build.get_all_failed (start, count, platform)
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 ->
@ -142,21 +127,25 @@ let job_and_readme job (module Db : CONN) =
job_id, readme
let builds_grouped_by_output job_id platform (module Db : CONN) =
Db.collect_list Builder_db.Build.get_all_artifact_sha (job_id, platform) >>= fun sha ->
(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
| Ok builds ->
Db.find Builder_db.Build.get_with_main_binary_by_hash hash >|= fun b ->
b :: builds)
(Ok []) sha >|= List.rev
let builds_grouped_by_output_with_failed job_id platform ((module Db : CONN) as db) =
builds_grouped_by_output job_id platform db >>= fun builds ->
Db.collect_list Builder_db.Build.get_failed_builds (job_id, platform) >|= fun failed ->
let failed = List.map (fun b -> b, None) failed in
let cmp (a, _) (b, _) = Ptime.compare b.Builder_db.Build.start a.Builder_db.Build.start in
List.merge cmp builds failed
match acc with
| Error _ as e -> Lwt.return e
| Ok (fail, builds) ->
Db.find Builder_db.Build.get_with_main_binary_by_hash hash >|= fun (build, file) ->
match fail with
| 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) ->
(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 ()
@ -204,42 +193,46 @@ let cleanup_staging datadir (module Db : Caqti_lwt.CONNECTION) =
(cleanup_staged staged))
stageds
let save path data =
let save file data =
let open Lwt.Infix in
Lwt.catch
(fun () ->
Lwt_io.open_file ~mode:Lwt_io.Output (Fpath.to_string path) >>= fun oc ->
Lwt_io.open_file ~mode:Lwt_io.Output (Fpath.to_string file) >>= fun oc ->
Lwt_io.write oc data >>= fun () ->
Lwt_io.close oc
|> Lwt_result.ok)
(function
| Unix.Unix_error (e, _, _) ->
Lwt_result.fail (`Msg (Unix.error_message e))
| e -> Lwt.reraise e)
| e -> Lwt.fail e)
let save_artifacts staging artifacts =
List.fold_left
(fun r (file, data) ->
r >>= fun () ->
let sha256 = Ohex.encode file.Builder_db.sha256 in
let destpath = Fpath.(staging / sha256) in
save destpath data)
(Lwt_result.return ())
artifacts
let save_file dir staging (filepath, data) =
let size = String.length data in
let sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
let localpath = Fpath.append dir filepath in
let destpath = Fpath.append staging filepath in
Lwt_result.lift (Bos.OS.Dir.create (Fpath.parent destpath)) >>= fun _ ->
save destpath data >|= fun () ->
{ Builder_db.filepath; localpath; sha256; size }
let commit_files datadir staging_dir job_name uuid artifacts =
(* First we move the artifacts *)
let save_files dir staging files =
List.fold_left
(fun r artifact ->
r >>= fun () ->
let sha256 = Ohex.encode artifact.Builder_db.sha256 in
let src = Fpath.(staging_dir / sha256) in
let dest = Fpath.(datadir // artifact_path artifact) in
Lwt.return (Bos.OS.Dir.create (Fpath.parent dest)) >>= fun _created ->
Lwt.return (Bos.OS.Path.move ~force:true src dest))
(Lwt_result.return ())
artifacts >>= fun () ->
(* Now the staging dir only contains script & console *)
(fun r file ->
r >>= fun acc ->
save_file dir staging file >>= fun file ->
Lwt_result.return (file :: acc))
(Lwt_result.return [])
files
let save_all staging_dir (job : Builder.script_job) uuid artifacts =
let build_dir = Fpath.(v job.Builder.name / Uuidm.to_string uuid) in
let output_dir = Fpath.(build_dir / "output")
and staging_output_dir = Fpath.(staging_dir / "output") in
Lwt.return (Bos.OS.Dir.create staging_output_dir) >>= fun _ ->
save_files output_dir staging_output_dir artifacts >>= fun artifacts ->
Lwt_result.return artifacts
let commit_files datadir staging_dir job_name uuid =
let job_dir = Fpath.(datadir / job_name) in
let dest = Fpath.(job_dir / Uuidm.to_string uuid) in
Lwt.return (Bos.OS.Dir.create job_dir) >>= fun _ ->
@ -252,32 +245,12 @@ let infer_section_and_synopsis artifacts =
| Some opam -> OpamFile.OPAM.synopsis opam, OpamFile.OPAM.descr_body opam
in
let infer_section switch root =
let root_pkg = root.OpamPackage.name in
let is_unikernel =
(* since mirage 4.2.0, the x-mirage-opam-lock-location is emitted *)
Option.value ~default:false
(Option.map (fun opam ->
Option.is_some (OpamFile.OPAM.extended opam "x-mirage-opam-lock-location" Fun.id))
(OpamPackage.Name.Map.find_opt root_pkg switch.OpamFile.SwitchExport.overlays))
in
let root_pkg_name = OpamPackage.Name.to_string root_pkg in
if is_unikernel || Astring.String.is_prefix ~affix:"mirage-unikernel-" root_pkg_name then
let metrics_influx =
let influx = OpamPackage.Name.of_string "metrics-influx" in
OpamPackage.Set.exists (fun p -> OpamPackage.Name.equal p.OpamPackage.name influx)
switch.OpamFile.SwitchExport.selections.OpamTypes.sel_installed
in
let mirage_monitoring =
let monitoring = OpamPackage.Name.of_string "mirage-monitoring" in
match OpamPackage.Name.Map.find_opt root_pkg switch.OpamFile.SwitchExport.overlays with
| None -> false
| Some opam ->
let depends = OpamFile.OPAM.depends opam in
OpamFormula.fold_left (fun acc (n', _) ->
acc || OpamPackage.Name.equal n' monitoring)
false depends
in
if metrics_influx || mirage_monitoring then
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
"Unikernels"
@ -306,8 +279,7 @@ let compute_input_id artifacts =
get_hash (Fpath.v "build-environment"),
get_hash (Fpath.v "system-packages")
with
| Some a, Some b, Some c ->
Some Digestif.SHA256.(to_raw_string (digestv_string [a;b;c]))
| Some a, Some b, Some c -> Some (Mirage_crypto.Hash.SHA256.digest (Cstruct.concat [a;b;c]))
| _ -> None
let save_console_and_script staging_dir job_name uuid console script =
@ -329,29 +301,8 @@ let prepare_staging staging_dir =
then Lwt_result.fail (`Msg "build directory already exists")
else Lwt_result.return ()
(* saving:
- for each artifact compute its sha256 checksum -- calling Lwt.pause in
between
- lookup artifact sha256 in the database and filter them out of the list: not_in_db
- mkdir -p _staging/uuid/
- save console & script to _staging/uuid/
- save each artifact in not_in_db as _staging/uuid/sha256
committing:
- for each artifact mv _staging/uuid/sha256 _artifacts/sha256
(or _artifacts/prefix(sha256)/sha256 where prefix(sha256) is the first two hex digits in sha256)
- now _staging/uuid only contains console & script so we mv _staging/uuid _staging/job/uuid
potential issues:
- race condition in uploading same artifact:
* if the artifact already exists in the database and thus filesystem then nothing is done
* if the artifact is added to the database and/or filesystem we atomically overwrite it
- input_id depends on a sort order?
*)
let add_build
~datadir
~cachedir
~configdir
datadir
user_id
((job : Builder.script_job), uuid, console, start, finish, result, raw_artifacts)
(module Db : CONN) =
@ -359,7 +310,7 @@ let add_build
let job_name = job.Builder.name in
let staging_dir = Fpath.(staging datadir / Uuidm.to_string uuid) in
let or_cleanup x =
Lwt_result.map_error (fun e ->
Lwt_result.map_err (fun e ->
Bos.OS.Dir.delete ~recurse:true staging_dir
|> Result.iter_error (fun e ->
Log.err (fun m -> m "Failed to remove staging dir %a: %a"
@ -368,35 +319,16 @@ let add_build
e)
x
in
let not_interesting p =
String.equal (Fpath.basename p) "README.md" || String.equal (Fpath.get_ext p) ".build-hashes"
let artifacts_to_preserve =
let not_interesting p =
String.equal (Fpath.basename p) "README.md" || String.equal (Fpath.get_ext p) ".build-hashes"
in
List.filter (fun (p, _) -> not (not_interesting p)) raw_artifacts
in
begin
List.fold_left
(fun r (filepath, data) ->
r >>= fun acc ->
if not_interesting filepath then
Lwt_result.return acc
else
let sha256 = Digestif.SHA256.(to_raw_string (digest_string data))
and size = String.length data in
Lwt_result.ok (Lwt.pause ()) >|= fun () ->
({ filepath; sha256; size }, data) :: acc)
(Lwt_result.return [])
raw_artifacts
end >>= fun artifacts ->
or_cleanup (prepare_staging staging_dir) >>= fun () ->
or_cleanup (save_console_and_script staging_dir job_name uuid console job.Builder.script)
>>= fun (console, script) ->
List.fold_left
(fun r ((f, _) as artifact) ->
r >>= fun acc ->
Db.find Builder_db.Build_artifact.exists f.sha256 >|= fun exists ->
if exists then acc else artifact :: acc)
(Lwt_result.return [])
artifacts >>= fun artifacts_to_save ->
or_cleanup (save_artifacts staging_dir artifacts_to_save) >>= fun () ->
let artifacts = List.map fst artifacts in
or_cleanup (save_all staging_dir job uuid artifacts_to_preserve) >>= fun artifacts ->
let r =
Db.start () >>= fun () ->
Db.exec Job.try_add job_name >>= fun () ->
@ -444,82 +376,31 @@ let add_build
match readme, readme_anywhere with
| None, None -> Lwt_result.return ()
| Some (_, data), _ | None, Some (_, data) -> add_or_update readme_id data) >>= fun () ->
(match List.partition (fun p -> Fpath.(is_prefix (v "bin/") p.filepath)) artifacts with
| [ main_binary ], other_artifacts ->
Db.exec Build_artifact.add (main_binary, id) >>= fun () ->
Db.find Builder_db.last_insert_rowid () >>= fun main_binary_id ->
Db.exec Build.set_main_binary (id, main_binary_id) >|= fun () ->
Some main_binary, other_artifacts
| [], _ ->
Log.debug (fun m -> m "Zero binaries for build %a" Uuidm.pp uuid);
Lwt_result.return (None, artifacts)
| xs, _ ->
Log.warn (fun m -> m "Multiple binaries for build %a: %a" Uuidm.pp uuid
Fmt.(list ~sep:(any ",") Fpath.pp)
(List.map (fun a -> a.filepath) xs));
Lwt_result.return (None, artifacts)) >>= fun (main_binary, remaining_artifacts_to_add) ->
List.fold_left
(fun r file ->
r >>= fun () ->
Db.exec Build_artifact.add (file, id))
(Lwt_result.return ())
remaining_artifacts_to_add >>= fun () ->
commit_files datadir staging_dir job_name uuid (List.map fst artifacts_to_save) >>= fun () ->
Db.commit () >|= fun () ->
main_binary
artifacts >>= fun () ->
Db.collect_list Build_artifact.get_all_by_build id >>= fun artifacts ->
(match List.filter (fun (_, p) -> Fpath.(is_prefix (v "bin/") p.filepath)) artifacts with
| [ (build_artifact_id, _) ] -> Db.exec Build.set_main_binary (id, build_artifact_id)
| [] ->
Log.debug (fun m -> m "Zero binaries for build %a" Uuidm.pp uuid);
Lwt_result.return ()
| xs ->
Log.warn (fun m -> m "Multiple binaries for build %a: %a" Uuidm.pp uuid
Fmt.(list ~sep:(any ",") Fpath.pp)
(List.map (fun (_, a) -> a.filepath) xs));
Lwt_result.return ()) >>= fun () ->
Db.commit () >>= fun () ->
commit_files datadir staging_dir job_name uuid
in
Lwt_result.bind_lwt_error (or_cleanup r)
Lwt_result.bind_lwt_err (or_cleanup r)
(fun e ->
Db.rollback ()
|> Lwt.map (fun r ->
Result.iter_error
(fun e' -> Log.err (fun m -> m "Failed rollback: %a" Caqti_error.pp e'))
r;
e)) >>= function
| None -> Lwt.return (Ok ())
| Some main_binary ->
let time =
let (y, m, d), ((hh, mm, ss), _) = Ptime.to_date_time start in
Printf.sprintf "%04d%02d%02d%02d%02d%02d" y m d hh mm ss
and uuid = Uuidm.to_string uuid
and job = job.name
and platform = job.platform
and sha256 = Ohex.encode main_binary.sha256
in
let fp_str p = Fpath.(to_string (datadir // p)) in
let args =
String.concat " "
(List.map (fun s -> "\"" ^ String.escaped s ^ "\"")
[ "--build-time=" ^ time ; "--sha256=" ^ sha256 ; "--job=" ^ job ;
"--uuid=" ^ uuid ; "--platform=" ^ platform ;
"--cache-dir=" ^ Fpath.to_string cachedir ;
"--data-dir=" ^ Fpath.to_string datadir ;
"--main-binary-filepath=" ^ Fpath.to_string main_binary.filepath ;
fp_str Fpath.(datadir // artifact_path main_binary) ])
in
Log.debug (fun m -> m "executing hooks with %s" args);
let dir = Fpath.(configdir / "upload-hooks") in
(try
Lwt.return (Ok (Some (Unix.opendir (Fpath.to_string dir))))
with
Unix.Unix_error _ -> Lwt.return (Ok None)) >>= function
| None -> Lwt.return (Ok ())
| Some dh ->
try
let is_executable file =
let st = Unix.stat (Fpath.to_string file) in
st.Unix.st_perm land 0o111 = 0o111 &&
st.Unix.st_kind = Unix.S_REG
in
let rec go () =
let next_file = Unix.readdir dh in
let file = Fpath.(dir / next_file) in
if is_executable file && Fpath.has_ext ".sh" file then
ignore (Sys.command (Fpath.to_string file ^ " " ^ args ^ " &"));
go ()
in
go ()
with
| End_of_file ->
Unix.closedir dh;
Lwt.return (Ok ())
e))

View file

@ -2,10 +2,9 @@ type error = [ Caqti_error.call_or_retrieve | `Not_found | `File_error of Fpath.
val pp_error : Format.formatter -> error -> unit
val not_found : 'a option -> ('a, [> `Not_found ]) result Lwt.t
val not_found : 'a option -> ('a, [> error ]) result Lwt.t
val staging : Fpath.t -> Fpath.t
val artifact_path : Builder_db.file -> Fpath.t
val cleanup_staging : Fpath.t -> Caqti_lwt.connection ->
(unit, [> `Msg of string ]) result Lwt.t
@ -22,8 +21,6 @@ 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 solo5_manifest : Fpath.t -> Builder_db.file -> Solo5_elftool.mft option
val platforms_of_job : [`job] Builder_db.id -> Caqti_lwt.connection ->
(string list, [> Caqti_error.call_or_retrieve ]) result Lwt.t
@ -31,9 +28,9 @@ 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 -> string -> Caqti_lwt.connection ->
((Builder_db.Build.t * Builder_db.file) option, [> Caqti_error.call_or_retrieve ]) result Lwt.t
((Builder_db.Build.t * Builder_db.file option) option, [> Caqti_error.call_or_retrieve ]) result Lwt.t
val build_hash : string -> Caqti_lwt.connection ->
val build_hash : Cstruct.t -> Caqti_lwt.connection ->
((string * Builder_db.Build.t) option, [> Caqti_error.call_or_retrieve ]) result Lwt.t
val build_exists : Uuidm.t -> Caqti_lwt.connection ->
@ -42,17 +39,11 @@ val build_exists : Uuidm.t -> 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 latest_successful_build : [`job] Builder_db.id -> string option -> 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 previous_successful_build_different_output : [`build] Builder_db.id -> Caqti_lwt.connection ->
(Builder_db.Build.t option, [> Caqti_error.call_or_retrieve ]) result Lwt.t
val next_successful_build_different_output : [`build] Builder_db.id -> Caqti_lwt.connection ->
(Builder_db.Build.t option, [> Caqti_error.call_or_retrieve ]) result Lwt.t
val failed_builds : start:int -> count:int -> string option -> Caqti_lwt.connection ->
((string * Builder_db.Build.t) list, [> 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
@ -78,9 +69,6 @@ val job_and_readme : string -> Caqti_lwt.connection ->
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 builds_grouped_by_output_with_failed : [`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
@ -96,9 +84,7 @@ val user : string -> Caqti_lwt.connection ->
val authorized : [`user] Builder_db.id -> string -> Caqti_lwt.connection -> (unit, [> Caqti_error.call_or_retrieve | `Msg of string ]) result Lwt.t
val add_build :
datadir:Fpath.t ->
cachedir:Fpath.t ->
configdir:Fpath.t ->
Fpath.t ->
[`user] Builder_db.id ->
(Builder.script_job * Uuidm.t * (int * string) list * Ptime.t * Ptime.t *
Builder.execution_result * (Fpath.t * string) list) ->

View file

@ -10,7 +10,7 @@ let diff_map a b =
let diff a b =
String_map.fold (fun k v acc ->
if not (String_map.mem k b) then (k, v) :: acc else acc)
a [] |> List.rev
a []
in
let added = diff b a
and removed = diff a b
@ -19,7 +19,7 @@ let diff_map a b =
match String_map.find_opt k b with
| None -> acc
| Some v' -> if String.equal v v' then acc else (k, v, v') :: acc)
a [] |> List.rev
a []
in
(added, removed, changed)
@ -44,64 +44,3 @@ let compare_pkgs p1 p2 =
String_map.empty (Astring.String.cuts ~sep:"\n" p)
in
diff_map (parse_pkgs p1) (parse_pkgs p2)
let md_to_html ?adjust_heading ?(safe = true) data =
let open Cmarkit in
let doc = Doc.of_string ~strict:false ~heading_auto_ids:true data in
let doc =
Option.fold ~none:doc
~some:(fun lvl ->
let block _m = function
| Block.Heading (h, meta) ->
let open Block.Heading in
let level = level h
and id = id h
and layout = layout h
and inline = inline h
in
let h' = make ?id ~layout ~level:(level + lvl) inline in
Mapper.ret (Block.Heading (h', meta))
| Block.Blocks _ -> Mapper.default
| x -> Mapper.ret x
in
let mapper = Mapper.make ~block () in
Mapper.map_doc mapper doc)
adjust_heading
in
Cmarkit_html.of_doc ~safe doc
module Path = struct
let to_url ~path ~queries =
let path = match path with
| "" :: [] -> "/"
| path -> "/" ^ String.concat "/" path
in
let query = queries |> List.map (fun (k, v) -> k, [v]) in
Uri.make ~path ~query () |> Uri.to_string
(* Like Dream.path in 1.0.0~alpha2 but on Dream.target *)
let of_url uri_str =
let path_str = uri_str |> Uri.of_string |> Uri.path in
match String.split_on_char '/' path_str with
| "" :: (_ :: _ as tail) -> tail
| path -> path
let matches_dreamroute ~path dreamroute =
let is_match path_elem dpath_elem =
(dpath_elem |> String.starts_with ~prefix:":")
|| path_elem = dpath_elem
in
let rec aux path dreampath =
match path, dreampath with
| [] , _ :: _ -> false (*length path < length dreampath*)
| _ , [] -> true (*length path >= length dreampath *)
| _ :: _ , "" :: [] -> true (*dreampath ends in '/'*)
| p_elem :: path, dp_elem :: dreampath ->
is_match p_elem dp_elem
&& aux path dreampath
in
let dreampath = dreamroute |> of_url in
aux path dreampath
end

File diff suppressed because it is too large Load diff

View file

@ -1,156 +1,13 @@
module Set = OpamPackage.Set
type package = OpamPackage.t
let packages (switch : OpamFile.SwitchExport.t) =
assert (Set.cardinal switch.selections.sel_pinned = 0);
assert (Set.cardinal switch.selections.sel_compiler = 0);
assert (Set.subset switch.selections.sel_roots switch.selections.sel_installed);
switch.selections.sel_installed
let duniverse_dir = "x-opam-monorepo-duniverse-dirs"
module M = Map.Make(String)
let duniverse_dirs_data =
(* the representation in the file is [ URL DIR [ HASH* ] ] *)
let open OpamParserTypes.FullPos in
let ( let* ) = Result.bind in
let string ~ctx = function
| { pelem = String s ; _ } -> Ok s
| _ -> Error (`Msg ("couldn't find a string " ^ ctx))
in
let extract_data = function
| { pelem = List { pelem = [ url ; dir ; hashes ] ; _ } ; _ } ->
let* url = string ~ctx:"url" url in
let* hashes =
match hashes with
| { pelem = List { pelem = hashes ; _ } ; _ } ->
List.fold_left (fun acc hash ->
let* acc = acc in
let* hash = string ~ctx:"hash" hash in
let* h = match OpamHash.of_string_opt hash with
| Some h -> Ok OpamHash.(kind h, contents h)
| None -> Error (`Msg ("couldn't decode opam hash in " ^ hash))
in
Ok (h :: acc))
(Ok []) hashes
| _ -> Error (`Msg "couldn't decode hashes")
in
let* dir = string ~ctx:"directory" dir in
Ok (url, dir, List.rev hashes)
| { pelem = List { pelem = [ url ; dir ] ; _ } ; _ } ->
let* url = string ~ctx:"url" url in
let* dir = string ~ctx:"directory" dir in
Ok (url, dir, [])
| _ -> Error (`Msg "expected a list of URL, DIR, [HASHES]")
in
function
| { pelem = List { pelem = lbody ; _ } ; _ } ->
List.fold_left (fun acc v ->
let* acc = acc in
let* (url, dir, hashes) = extract_data v in
Ok (M.add dir (url, hashes) acc))
(Ok M.empty) lbody
| _ -> Error (`Msg "expected a list or a nested list")
let duniverse (switch : OpamFile.SwitchExport.t) =
let root = switch.OpamFile.SwitchExport.selections.OpamTypes.sel_roots in
if OpamPackage.Set.cardinal root = 1 then
let root = OpamPackage.Set.choose root in
match OpamPackage.(Name.Map.find_opt root.name switch.OpamFile.SwitchExport.overlays) with
| None -> Error (`Msg "opam switch export doesn't contain the main package")
| Some opam ->
match OpamFile.OPAM.extended opam duniverse_dir duniverse_dirs_data with
| None -> Ok None
| Some Error e -> Error e
| Some Ok v -> Ok (Some v)
else
Error (`Msg "not a single root package found in opam switch export")
type duniverse_diff = {
name : string ;
urls : string * string option ;
hash : (OpamHash.kind * string option * string option) list ;
}
let pp_duniverse_diff ppf { name ; urls ; hash } =
let opt_hash = Option.value ~default:"NONE" in
Format.fprintf ppf "%s (%s%s) %s"
name
(fst urls)
(Option.fold ~none:"" ~some:(fun url -> "->" ^ url) (snd urls))
(String.concat ", " (List.map (fun (h, l, r) ->
OpamHash.string_of_kind h ^ " " ^ opt_hash l ^ "->" ^ opt_hash r) hash))
let pp_duniverse_dir ppf (dir, url) =
Format.fprintf ppf "%s (%s)" dir url
let duniverse_diff l r =
let l = Option.value l ~default:M.empty
and r = Option.value r ~default:M.empty
in
let keys_l_only = ref [] and keys_r_only = ref [] and diff = ref [] in
let equal_hashes l r =
(* l and r are lists of pairs, with the hash kind and its value *)
(* for a git remote, the hashes are empty lists *)
(match l with [] -> false | _ -> true) &&
(match r with [] -> false | _ -> true) &&
List.for_all (fun (h, v) ->
match List.assoc_opt h r with
| None -> false
| Some v' -> String.equal v v')
l &&
List.for_all (fun (h, v) ->
match List.assoc_opt h l with
| None -> false
| Some v' -> String.equal v v')
r
in
let _ =
M.merge (fun key l r ->
match l, r with
| None, Some _ -> keys_r_only := key :: !keys_r_only; None
| Some _, None -> keys_l_only := key :: !keys_l_only; None
| None, None -> None
| Some (_, l), Some (_, r) when equal_hashes l r -> None
| Some (url1, []), Some (url2, []) when String.equal url1 url2 -> None
| Some l, Some r -> diff := (key, l, r) :: !diff; None)
l r
in
let dir_only keys map =
let only =
M.filter (fun k _ -> List.mem k keys) map |> M.bindings
in
List.map (fun (key, (url, _)) -> key, url) only
in
let l_only = dir_only !keys_l_only l
and r_only = dir_only !keys_r_only r
and diff =
List.map (fun (name, (url_l, hashes_l), (url_r, hashes_r)) ->
let urls =
if String.equal url_l url_r then url_l, None else url_l, Some url_r
in
let hash =
List.fold_left (fun acc (h, v) ->
match List.assoc_opt h hashes_r with
| None -> (h, Some v, None) :: acc
| Some v' ->
if String.equal v v' then
acc
else
(h, Some v, Some v') :: acc)
[] hashes_l
in
let hash = List.fold_left (fun acc (h', v') ->
match List.assoc_opt h' hashes_l with
| None -> (h', None, Some v') :: acc
| Some _ -> acc)
hash hashes_r
in
{ name ; urls ; hash })
!diff
in
l_only, r_only, diff
type version_diff = {
name : OpamPackage.Name.t;
version_left : OpamPackage.Version.t;
@ -245,12 +102,12 @@ let compare left right =
l
in
let same_version = Set.inter packages_left packages_right in
let opam_diff =
Set.filter
let (same, opam_diff) =
Set.partition
(fun p ->
let find = OpamPackage.Name.Map.find p.name in
let opam_left = find left.overlays and opam_right = find right.overlays in
not (OpamFile.OPAM.effectively_equal opam_left opam_right))
OpamFile.OPAM.effectively_equal opam_left opam_right)
same_version
and version_diff =
List.filter_map (fun p1 ->
@ -269,9 +126,4 @@ let compare left right =
and right_pkgs = diff packages_right packages_left
in
let opam_diff = detailed_opam_diffs left right opam_diff in
let duniverse_ret =
match duniverse left, duniverse right with
| Ok l, Ok r -> Ok (duniverse_diff l r)
| Error _ as e, _ | _, (Error _ as e) -> e
in
(opam_diff, version_diff, left_pkgs, right_pkgs, duniverse_ret)
(same, opam_diff, version_diff, left_pkgs, right_pkgs)

View file

@ -1,39 +0,0 @@
type opam_diff = {
pkg : OpamPackage.t ;
build : (OpamTypes.command list * OpamTypes.command list) option ;
install : (OpamTypes.command list * OpamTypes.command list) option ;
url : (OpamFile.URL.t option * OpamFile.URL.t option) option ;
otherwise_equal : bool ;
}
type version_diff = {
name : OpamPackage.Name.t;
version_left : OpamPackage.Version.t;
version_right : OpamPackage.Version.t;
}
type duniverse_diff = {
name : string ;
urls : string * string option ;
hash : (OpamHash.kind * string option * string option) list ;
}
val pp_opampackage : Format.formatter -> OpamPackage.t -> unit
val pp_version_diff : Format.formatter -> version_diff -> unit
val pp_duniverse_diff : Format.formatter -> duniverse_diff -> unit
val pp_duniverse_dir : Format.formatter -> string * string -> unit
val pp_opam_diff : Format.formatter -> opam_diff -> unit
val commands_to_strings : OpamTypes.command list * OpamTypes.command list -> string list * string list
val opt_url_to_string : OpamFile.URL.t option * OpamFile.URL.t option -> string * string
val compare: OpamFile.SwitchExport.t ->
OpamFile.SwitchExport.t ->
opam_diff list * version_diff list * OpamPackage.Set.t * OpamPackage.Set.t * ((string * string) list * (string * string) list * duniverse_diff list, [> `Msg of string ]) result

View file

@ -1,137 +0,0 @@
#!/bin/sh
set -ex
prog_NAME=$(basename "${0}")
warn()
{
echo "${prog_NAME}: WARN: $*"
}
err()
{
echo "${prog_NAME}: ERROR: $*" 1>&2
}
die()
{
echo "${prog_NAME}: ERROR: $*" 1>&2
exit 1
}
usage()
{
cat <<EOM 1>&2
usage: ${prog_NAME} [ OPTIONS ] FILE
Updates a FreeBSD package repository
Options:
--build-time=STRING
Build timestamp (used for the version of the package).
--sha256=STRING
Hex encoded SHA256 digest of the main binary.
--job=STRING
Job name that was built.
--main-binary-filepath=STRING
The file path of the main binary.
EOM
exit 1
}
BUILD_TIME=
SHA=
JOB=
FILEPATH=
while [ $# -gt 1 ]; do
OPT="$1"
case "${OPT}" in
--build-time=*)
BUILD_TIME="${OPT##*=}"
;;
--sha256=*)
SHA="${OPT##*=}"
;;
--job=*)
JOB="${OPT##*=}"
;;
--main-binary-filepath=*)
FILEPATH="${OPT##*=}"
;;
--*)
warn "Ignoring unknown option: '${OPT}'"
;;
*)
err "Unknown option: '${OPT}'"
usage
;;
esac
shift
done
[ -z "${BUILD_TIME}" ] && die "The --build-time option must be specified"
[ -z "${SHA}" ] && die "The --sha256 option must be specified"
[ -z "${JOB}" ] && die "The --job option must be specified"
[ -z "${FILEPATH}" ] && die "The --main-binary-filepath option must be specified"
FILENAME="${1}"
: "${REPO:="/usr/local/www/pkg"}"
: "${REPO_KEY:="/usr/local/etc/builder-web/repo.key"}"
if [ "$(basename "${FILEPATH}" .pkg)" = "$(basename "${FILEPATH}")" ]; then
echo "Not a FreeBSD package"
exit 0
fi
if ls "${REPO}"/*/All/"${JOB}"-*."${SHA}".pkg > /dev/null; then
echo "Same hash already present, nothing to do"
exit 0
fi
TMP=$(mktemp -d -t repak)
MANIFEST="${TMP}/+MANIFEST"
TMPMANIFEST="${MANIFEST}.tmp"
cleanup () {
rm -rf "${TMP}"
}
trap cleanup EXIT
PKG_ROOT="${TMP}/pkg"
tar x -C "${TMP}" -f "${FILENAME}"
mkdir "${PKG_ROOT}"
mv "${TMP}/usr" "${PKG_ROOT}"
VERSION=$(jq -r '.version' "${MANIFEST}")
# if we've a tagged version (1.5.0), append the number of commits and a dummy hash
VERSION_GOOD=$(echo $VERSION | grep -c '^[0-9]\+\.[0-9]\+\.[0-9]\+$') || true
VERSION_WITH_COMMIT=$(echo $VERSION | grep -c '^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\.g[0-9a-fA-f]\+$') || true
if [ $VERSION_GOOD -eq 0 -a $VERSION_WITH_COMMIT -eq 0 ]; then
die "version does not conform to (MAJOR.MINOR.PATCH[.#NUM_COMMITS.g<HASH>])"
fi
if [ $VERSION_WITH_COMMIT -eq 0 ]; then
VERSION="${VERSION}.0.g0000000"
fi
NAME=$(jq -r '.name' "${MANIFEST}")
FULL_VERSION="${VERSION}.${BUILD_TIME}.${SHA}"
jq -ca ".version=\"$FULL_VERSION\"" "${MANIFEST}" > "${TMPMANIFEST}"
mv "${TMPMANIFEST}" "${MANIFEST}"
ABI=$(jq -r '.abi' "${MANIFEST}")
REPO_DIR="${REPO}/${ABI}"
PKG_DIR="${REPO_DIR}/All"
# to avoid races, first create the package in temporary directory
# and then move it before recreating the index
pkg create -r "${PKG_ROOT}" -m "${MANIFEST}" -o "${TMP}"
mkdir -p "${PKG_DIR}"
rm -f "${PKG_DIR}"/"${NAME}"-*.pkg
mv "${TMP}/${NAME}-${FULL_VERSION}.pkg" "${PKG_DIR}"
pkg repo "${REPO_DIR}" "${REPO_KEY}"

View file

@ -1,8 +1,8 @@
name: builder-web
name: builder_web
version: %%VERSION_NUM%%
origin: local/builder-web
origin: local/builder_web
comment: Builder web service
www: https://git.robur.coop/robur/builder-web
www: https://git.robur.io/robur/builder-web
maintainer: Robur <team@robur.coop>
prefix: /usr/local
licenselogic: single
@ -18,56 +18,9 @@ deps {
},
sqlite3 {
origin = "databases/sqlite3";
},
opam-graph {
origin = "local/opam-graph";
},
modulectomy {
origin = "local/modulectomy";
}
}
scripts : {
pre-install = <<EOD
if [ -n "${PKG_ROOTDIR}" ] && [ "${PKG_ROOTDIR}" != "/" ]; then
PW="/usr/sbin/pw -R ${PKG_ROOTDIR}"
else
PW=/usr/sbin/pw
fi
echo "===> Creating groups."
if ! ${PW} groupshow builder >/dev/null 2>&1; then
echo "Creating group 'builder' with gid '497'."
${PW} groupadd builder -g 497
else
echo "Using existing group 'builder'."
fi
echo "===> Creating users"
if ! ${PW} usershow builder >/dev/null 2>&1; then
echo "Creating user 'builder' with uid '497'."
${PW} useradd builder -u 497 -g 497 -c "builder daemon" -d /nonexistent -s /usr/sbin/nologin
else
echo "Using existing user 'builder'."
fi
EOD;
post-install = <<EOD
mkdir -p -m 700 /var/db/builder-web
chown builder:builder /var/db/builder-web
EOD;
post-deinstall = <<EOD
if [ -n "${PKG_ROOTDIR}" ] && [ "${PKG_ROOTDIR}" != "/" ]; then
PW="/usr/sbin/pw -R ${PKG_ROOTDIR}"
else
PW=/usr/sbin/pw
fi
if ${PW} usershow builder >/dev/null 2>&1; then
echo "==> You should manually remove the \"builder\" user. "
fi
if ${PW} groupshow builder >/dev/null 2>&1; then
echo "==> You should manually remove the \"builder\" group "
fi
EOD;
}
desc = <<EOD
A web server serving and accepting reproducible builds

View file

@ -18,35 +18,25 @@ rootdir=$tmpd/rootdir
sbindir=$rootdir/usr/local/sbin
rcdir=$rootdir/usr/local/etc/rc.d
libexecdir=$rootdir/usr/local/libexec
sharedir=$rootdir/usr/local/share/builder-web
confdir=$rootdir/usr/local/etc/builder-web
trap 'rm -rf $tmpd' 0 INT EXIT
mkdir -p "$sbindir" "$libexecdir" "$rcdir" "$sharedir" "$confdir/upload-hooks"
mkdir -p "$sbindir" "$libexecdir" "$rcdir"
# stage service scripts
install -U "$pdir/rc.d/builder_web" "$rcdir/builder_web"
install -U $pdir/rc.d/builder_web $rcdir/builder_web
# stage app binaries
install -U "$bdir/builder-web" "$libexecdir/builder-web"
install -U $bdir/builder-web $libexecdir/builder-web
install -U "$bdir/builder-migrations" "$sbindir/builder-migrations"
install -U "$bdir/builder-db" "$sbindir/builder-db"
# stage visualization scripts
install -U "$basedir/packaging/batch-viz.sh" "$confdir/batch-viz.sh.sample"
install -U "$basedir/packaging/visualizations.sh" "$confdir/upload-hooks/visualizations.sh.sample"
# example repo scripts
install -U "$basedir/packaging/dpkg-repo.sh" "$sharedir/dpkg-repo.sh"
install -U "$basedir/packaging/FreeBSD-repo.sh" "$sharedir/FreeBSD-repo.sh"
install -U $bdir/builder-migrations $sbindir/builder-migrations
install -U $bdir/builder-db $sbindir/builder-db
# create +MANIFEST
flatsize=$(find "$rootdir" -type f -exec stat -f %z {} + |
awk 'BEGIN {s=0} {s+=$1} END {print s}')
sed -e "s:%%FLATSIZE%%:${flatsize}:" -e "/^[Vv]ersion:/s/-/./g" "$pdir/MANIFEST" > "$manifest"
sed -e "s:%%FLATSIZE%%:${flatsize}:" "$pdir/MANIFEST" > "$manifest"
{
printf '\nfiles {\n'
@ -58,7 +48,7 @@ sed -e "s:%%FLATSIZE%%:${flatsize}:" -e "/^[Vv]ersion:/s/-/./g" "$pdir/MANIFEST"
} | sed -e "s:${rootdir}::" >> "$manifest"
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=format:%ct)
pkg create -r "$rootdir" -M "$manifest" -o "$basedir/"
mv "$basedir"/builder-web-*.pkg "$basedir/builder-web.pkg"
echo 'bin: [ "builder-web.pkg" ]' > "$basedir/builder-web.install"
echo 'doc: [ "README.md" ]' >> "$basedir/builder-web.install"
pkg create -r "$rootdir" -M "$manifest" -o $basedir/
mv $basedir/builder_web-*.pkg $basedir/builder_web.pkg
echo 'bin: [ "builder_web.pkg" ]' > $basedir/builder-web.install
echo 'doc: [ "README.md" ]' >> $basedir/builder-web.install

View file

@ -33,7 +33,7 @@ procname="/usr/local/libexec/builder-web"
builder_web_start () {
echo "Starting ${name}."
/usr/sbin/daemon -S -r -P "${pidfile}" -u "${builder_web_user}" \
/usr/sbin/daemon -S -p "${pidfile}" -u "${builder_web_user}" \
"${procname}" ${builder_web_flags}
}

View file

@ -1,78 +0,0 @@
# Package repository creation and update
Builder-web calls hooks when an upload of a successful build finished. These
shell scripts automatically push builds to deb repositories (using aptly) and
FreeBSD package repositories (using pkg).
Thus, as a client of the infrastructure, system packages can be easily
installed using the package repositories (and updates are straightforward).
The tricky part is verioning: different input may result in the same output
(i.e. if the build system is updated, it is unlikely this will result in change
of output, and clients do not need to update their packages), and also due to
the nature of opam, if a dependency (opam package) is released, the output may
differ (although the final package version is not increased). We solve the
latter by adapting the version number of packages: package version 1.5.2 becomes
1.5.2-TIMESTAMP-SHA256 (on FreeBSD using '.' instead of '-'). The timestamp is
of the form YYYYMMDDhhmmss. The SHA256 is the hex-encoded SHA256 checksum of
the original binary package and can be used for lookup in the database.
## DPKG package repository
The dependencies are aptly and dpkg.
For the initial setup, a GPG private key is needed:
```
$ gpg --full-generate-key
$ gpg --export --armor > gpg.pub
```
Set `REPO_KEYID` in the shell script to the key identifier generated
(`gpg --list-keys`), and make the gpg.pub available to clients
(`cp gpg.pub ~/.aptly/public/`).
On clients, when the `~/.aptly/public` is served via http(s), add it to your
/etc/apt/source.list and import the gpg public key (`apt-key add <gpg.pub>`):
```
deb https://apt.robur.coop/ debian-10 main
```
The `debian-10` can be exchanged with any platform you're building debian
packages for.
Currently, the `dpkg-repo.sh` sets the HOME to `/home/builder` (where aptly
expects its configuration), and uses the platform (from builder) as
distribution.
## FreeBSD package repository
The dependency is FreeBSD's pkg utility.
For the initial setup, a RSA private key is needed:
```
$ openssl genrsa -out repo.key 4096
$ chmod 0400 repo.key
$ openssl rsa -in repo.key -out repo.pub -pubout
```
And a directory that acts as package repository (`mkdir /usr/local/www/pkg`).
Copy the public key to the package repository
(`cp repo.pub /usr/local/www/pkg`) to make it available for clients.
Both can be configured in the shell script itself (`REPO` and `REPO_KEY`). The
public key needs to be distributed to clients - e.g. put it at the root of the
repository.
On clients, when that directory is served via http(s), it can be added to
/usr/local/etc/pkg/repos/robur.conf:
```
robur: {
url: "https://pkg.robur.coop/${ABI}",
mirror_type: "srv",
signature_type: "pubkey",
pubkey: "/path/to/repo.pub",
enabled: yes
}
```

View file

@ -1,189 +0,0 @@
#!/bin/sh
prog_NAME=$(basename "${0}")
warn()
{
echo "${prog_NAME}: WARN: $*"
}
info()
{
echo "${prog_NAME}: INFO: $*"
}
err()
{
echo "${prog_NAME}: ERROR: $*" 1>&2
}
die()
{
echo "${prog_NAME}: ERROR: $*" 1>&2
exit 1
}
usage()
{
cat <<EOM 1>&2
usage: ${prog_NAME} [ OPTIONS ]
Generates visualizations of all things
--data-dir=STRING
Path to the data directory.
--cache-dir=STRING
Optional path to the cache directory. Defaults to DATA_DIR/_cache
--viz-script=STRING
Optional path to the visualizations.sh script. Defaults to ./visualizations.sh
--ignore-done
Optional flag to force script to ignore '.done' files
EOM
exit 1
}
CACHE_DIR=
DATA_DIR=
VISUALIZATIONS_CMD="./visualizations.sh"
IGNORE_DONE="false"
while [ $# -gt 0 ]; do
OPT="$1"
case "${OPT}" in
--cache-dir=*)
CACHE_DIR="${OPT##*=}"
;;
--data-dir=*)
DATA_DIR="${OPT##*=}"
;;
--viz-script=*)
VISUALIZATIONS_CMD="${OPT##*=}"
;;
--ignore-done)
IGNORE_DONE="true"
;;
--*)
warn "Ignoring unknown option: '${OPT}'"
;;
*)
err "Unknown option: '${OPT}'"
usage
;;
esac
shift
done
[ -z "$DATA_DIR" ] && die "The --data-dir option must be specified"
DB="${DATA_DIR}/builder.sqlite3"
[ ! -e "$DB" ] && die "The database doesn't exist: '$DB'"
# Let's be somewhat lenient with the database version.
# In visualizations.sh we can be more strict.
DB_VERSION="$(sqlite3 "$DB" "PRAGMA user_version;")"
[ -z "$DB_VERSION" ] && die "Couldn't read database version from '$DB'"
[ "$DB_VERSION" -lt 16 ] && die "The database version should be >= 16. It is '$DB_VERSION'"
APP_ID="$(sqlite3 "$DB" "PRAGMA application_id;")"
[ -z "$APP_ID" ] && die "Couldn't read application-id from '$DB'"
[ "$APP_ID" -ne 1234839235 ] && die "The application-id should be = 1234839235. It is '$APP_ID'"
echo
echo "-----------------------------------------------------------------------------"
info "Starting batch creation of visualizations: $(date)"
if [ -z "$CACHE_DIR" ]; then
CACHE_DIR="${DATA_DIR}/_cache"
info "Defaulting --cache-dir to '$CACHE_DIR'"
fi
if [ ! -d "${CACHE_DIR}" ]; then
info "Cache directory '$CACHE_DIR' doesn't exist, so it will be made"
if ! mkdir "${CACHE_DIR}"; then
die "Couldn't make cache directory: '$CACHE_DIR'"
fi
fi
[ ! -e "${VISUALIZATIONS_CMD}" ] && die "'$VISUALIZATIONS_CMD' doesn't exist"
if [ -f "${VISUALIZATIONS_CMD}" ] && [ -x "${VISUALIZATIONS_CMD}" ]; then :; else
die "'$VISUALIZATIONS_CMD' is not an executable"
fi
OPAM_GRAPH="opam-graph"
MODULECTOMY="modulectomy"
LATEST_TREEMAPVIZ_VERSION="$($MODULECTOMY --version)"
[ $? -ne 0 ] && die "Couldn't get modulectomy version"
LATEST_DEPENDENCIESVIZ_VERSION="$($OPAM_GRAPH --version)"
[ $? -ne 0 ] && die "Couldn't get opam-graph version"
TREEMAP_CACHE_DIR="${CACHE_DIR}/treemap_${LATEST_TREEMAPVIZ_VERSION}"
DEPENDENCIES_CACHE_DIR="${CACHE_DIR}/dependencies_${LATEST_DEPENDENCIESVIZ_VERSION}"
if
[ "${IGNORE_DONE}" = "false" ] && \
[ -f "${TREEMAP_CACHE_DIR}/.done" ] && \
[ -f "${DEPENDENCIES_CACHE_DIR}/.done" ]; then
info "Nothing to do"
exit 0
fi
ATTEMPTED_VIZS=0
FAILED_VIZS=0
distinct-input () {
{
sqlite3 "${DATA_DIR}/builder.sqlite3" "SELECT b.uuid
FROM build b
JOIN build_artifact opam ON opam.build = b.id
WHERE opam.filepath = 'opam-switch' AND b.main_binary NOT NULL
GROUP BY opam.sha256;"
sqlite3 "${DATA_DIR}/builder.sqlite3" "SELECT b.uuid
FROM build b
JOIN build_artifact debug ON debug.build = b.id
WHERE debug.filepath LIKE '%.debug' AND b.main_binary NOT NULL
GROUP BY debug.sha256;"
} | sort -u
}
for UUID in $(distinct-input); do
if ! "$VISUALIZATIONS_CMD" \
--data-dir="${DATA_DIR}" \
--cache-dir="${CACHE_DIR}" \
--uuid="${UUID}"
then
FAILED_VIZS=$((FAILED_VIZS + 1))
fi
ATTEMPTED_VIZS=$((ATTEMPTED_VIZS + 1))
done
if [ -n "$(ls -A "${TREEMAP_CACHE_DIR}")" ]; then
touch "${TREEMAP_CACHE_DIR}/.done"
V=1
while [ "$V" -lt "$LATEST_TREEMAPVIZ_VERSION" ]; do
DIR_REMOVE="${CACHE_DIR}/treemap_${V}"
if test -d "$DIR_REMOVE" && rm -r "$DIR_REMOVE"; then
info "Removed old cache-directory: '$DIR_REMOVE'"
fi
V=$((V+1))
done
else
warn "Treemap-viz cache-directory is still empty - problem?"
fi
if [ -n "$(ls -A "${DEPENDENCIES_CACHE_DIR}")" ]; then
touch "${DEPENDENCIES_CACHE_DIR}/.done"
V=1
while [ "$V" -lt "$LATEST_DEPENDENCIESVIZ_VERSION" ]; do
DIR_REMOVE="${CACHE_DIR}/dependencies_${V}"
if test -d "$DIR_REMOVE" && rm -r "$DIR_REMOVE"; then
info "Removed old cache-directory: '$DIR_REMOVE'"
fi
V=$((V+1))
done
else
warn "Dependencies-viz cache-directory is still empty - problem?"
fi
info "Batch creation of visualizations for $ATTEMPTED_VIZS binaries, finished with $FAILED_VIZS failures: $(date)"

View file

@ -1,60 +0,0 @@
#!/bin/sh
# input in versions.txt is <v1> <v2> with v1 < v2.
# v1 and v2 are of the form <version>-<date>-<hash>, where <version> includes:
# - 2.0.0
# - 2.0.0-10-gabcdef
freebsd_sanitize_version () {
post=$(echo $1 | rev | cut -d '-' -f 1-2 | rev | sed -e 's/-/./g')
v=$(echo $1 | rev | cut -d '-' -f 3- | rev | sed -e 's/-/./g')
version_good=$(echo $v | grep -c '^[0-9]\+\.[0-9]\+\.[0-9]\+$')
version_with_commit=$(echo $v | grep -c '^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\.g[0-9a-fA-f]\+$')
if [ $version_good -eq 0 -a $version_with_commit -eq 0 ]; then
echo "invalid version $v";
exit 1;
fi
if [ $version_with_commit -eq 0 ]; then
v="${v}.0.g0000000.${post}"
else
v="${v}.${post}"
fi
echo $v
}
echo "using FreeBSD pkg to compare versions now:"
while read version_a version_b; do
version_a=$(freebsd_sanitize_version $version_a)
version_b=$(freebsd_sanitize_version $version_b)
result=$(pkg version -t "$version_a" "$version_b")
printf "%s %s %s\n" "$version_a" "$result" "$version_b"
done < versions.txt
debian_sanitize_version () {
post=$(echo $1 | rev | cut -d '-' -f 1-2 | rev)
v=$(echo $1 | rev | cut -d '-' -f 3- | rev)
version_good=$(echo $v | grep -c '^[0-9]\+\.[0-9]\+\.[0-9]\+$')
version_with_commit=$(echo $v | grep -c '^[0-9]\+\.[0-9]\+\.[0-9]\+\-[0-9]\+\-g[0-9a-fA-f]\+$')
if [ $version_good -eq 0 -a $version_with_commit -eq 0 ]; then
echo "invalid version $v";
exit 1;
fi
if [ $version_with_commit -eq 0 ]; then
v="${v}-0-g0000000-${post}"
else
v="${v}-${post}"
fi
echo $v
}
echo ""
echo "using Debian dpkg to compare versions now:"
while read version_a version_b; do
version_a=$(debian_sanitize_version $version_a)
version_b=$(debian_sanitize_version $version_b)
if dpkg --compare-versions "$version_a" lt "$version_b"; then
echo "$version_a < $version_b"
else
echo "$version_a >= $version_b"
fi
done < versions.txt

View file

@ -6,7 +6,7 @@ After=syslog.target network.target
Type=simple
User=builder
Group=builder
ExecStart=/usr/libexec/builder-web
ExecStart=/usr/bin/builder-web --datadir /var/lib/builder-web/
[Install]
WantedBy=multi-user.target

View file

@ -1 +0,0 @@
/etc/builder-web/upload-hooks/visualizations.sh

View file

@ -1,13 +1,13 @@
Package: builder-web
Version: %%VERSION_NUM%%
Version: 0.0.1-%%VERSION_NUM%%
Section: unknown
Priority: optional
Maintainer: Robur Team <team@robur.coop>
Standards-Version: 4.4.1
Homepage: https://git.robur.coop/robur/builder-web
Vcs-Browser: https://git.robur.coop/robur/builder-web
Vcs-Git: https://git.robur.coop/robur/builder-web.git
Homepage: https://git.robur.io/robur/builder-web
Vcs-Browser: https://git.robur.io/robur/builder-web
Vcs-Git: https://git.robur.io/robur/builder-web.git
Architecture: all
Depends: libgmp10, libsqlite3-0, libev4, opam-graph, modulectomy
Depends: libgmp10, libsqlite3-0, libev4
Description: Web service for storing and presenting builds.
Builder-web stores builds in a sqlite database and serves them via HTTP.

View file

@ -1,8 +1,8 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: builder-web
Upstream-Contact: Robur Team <team@robur.coop>
Source: https://git.robur.coop/robur/builder-web
Source: https://git.robur.io/robur/builder-web
Files: *
Copyright: "Reynir Björnsson <reynir@reynir.dk>" "Hannes Mehnert <hannes@mehnert.org>"
Copyright: "Reynir Björnsson <reynir@reynir.dk" "Hannes Mehnert <hannes@mehnert.org>"
License: ISC

View file

@ -11,44 +11,27 @@ basedir=$(realpath "$(dirname "$0")"/../..)
bdir=$basedir/_build/install/default/bin
tmpd=$basedir/_build/stage
rootdir=$tmpd/rootdir
sbindir=$rootdir/usr/sbin
bindir=$rootdir/usr/bin
systemddir=$rootdir/usr/lib/systemd/system
debiandir=$rootdir/DEBIAN
libexecdir=$rootdir/usr/libexec
sharedir=$rootdir/usr/share/builder-web
confdir=$rootdir/etc/builder-web
trap 'rm -rf $tmpd' 0 INT EXIT
mkdir -p "$sbindir" "$debiandir" "$systemddir" "$libexecdir" "$sharedir" \
"$confdir" "$confdir/upload-hooks"
mkdir -p "$bindir" "$debiandir" "$systemddir"
# stage app binaries
install "$bdir/builder-web" "$libexecdir/builder-web"
install "$bdir/builder-migrations" "$sbindir/builder-migrations"
install "$bdir/builder-db" "$sbindir/builder-db"
install $bdir/builder-web $bindir/builder-web
install $bdir/builder-migrations $bindir/builder-migrations
install $bdir/builder-db $bindir/builder-db
# service script
install -m 0644 "$basedir/packaging/debian/builder-web.service" "$systemddir/builder-web.service"
# visualizations scripts
install "$basedir/packaging/batch-viz.sh" "$confdir/batch-viz.sh"
install "$basedir/packaging/visualizations.sh" "$confdir/upload-hooks/visualizations.sh"
# example repo scripts
install "$basedir/packaging/dpkg-repo.sh" "$sharedir/dpkg-repo.sh"
install "$basedir/packaging/FreeBSD-repo.sh" "$sharedir/FreeBSD-repo.sh"
install -m 0644 $basedir/packaging/debian/builder-web.service $systemddir/builder-web.service
# install debian metadata
install -m 0644 "$basedir/packaging/debian/control" "$debiandir/control"
install -m 0644 "$basedir/packaging/debian/changelog" "$debiandir/changelog"
install -m 0644 "$basedir/packaging/debian/copyright" "$debiandir/copyright"
install -m 0644 "$basedir/packaging/debian/conffiles" "$debiandir/conffiles"
install "$basedir/packaging/debian/postinst" "$debiandir/postinst"
install -m 0644 $basedir/packaging/debian/control $debiandir/control
install -m 0644 $basedir/packaging/debian/changelog $debiandir/changelog
install -m 0644 $basedir/packaging/debian/copyright $debiandir/copyright
ARCH=$(dpkg-architecture -q DEB_TARGET_ARCH)
sed -i -e "s/^Architecture:.*/Architecture: ${ARCH}/" "$debiandir/control"
dpkg-deb --build "$rootdir" "$basedir/builder-web.deb"
echo 'bin: [ "builder-web.deb" ]' > "$basedir/builder-web.install"
echo 'doc: [ "README.md" ]' >> "$basedir/builder-web.install"
dpkg-deb --build $rootdir $basedir/builder-web.deb
echo 'bin: [ "builder-web.deb" ]' > $basedir/builder-web.install
echo 'doc: [ "README.md" ]' >> $basedir/builder-web.install

View file

@ -1,17 +0,0 @@
#!/bin/sh
set -e
BUILDER_WEB_USER=builder
if ! getent group "$BUILDER_WEB_USER" >/dev/null; then
groupadd -g 497 $BUILDER_WEB_USER
fi
if ! getent passwd "$BUILDER_WEB_USER" >/dev/null; then
useradd -g 497 -u 497 -d /nonexistent -s /usr/sbin/nologin $BUILDER_WEB_USER
fi
mkdir -p /var/lib/builder-web
chown "$BUILDER_WEB_USER:$BUILDER_WEB_USER" /var/lib/builder-web
if [ -d /run/systemd/system ]; then
systemctl --system daemon-reload >/dev/null || true
fi

View file

@ -1,144 +0,0 @@
#!/bin/sh
set -ex
export HOME="/home/builder"
prog_NAME=$(basename "${0}")
warn()
{
echo "${prog_NAME}: WARN: $*"
}
err()
{
echo "${prog_NAME}: ERROR: $*" 1>&2
}
die()
{
echo "${prog_NAME}: ERROR: $*" 1>&2
exit 1
}
usage()
{
cat <<EOM 1>&2
usage: ${prog_NAME} [ OPTIONS ] FILE
Updates an aptly package repository
Options:
--build-time=STRING
Build timestamp (used for the version of the package).
--sha256=STRING
Hex encoded SHA256 digest of the main binary.
--job=STRING
Job name that was built.
--platform=STRING
Platform name on which the build was performed.
--main-binary-filepath=STRING
The file path of the main binary.
EOM
exit 1
}
BUILD_TIME=
SHA=
JOB=
PLATFORM=
FILEPATH=
while [ $# -gt 1 ]; do
OPT="$1"
case "${OPT}" in
--build-time=*)
BUILD_TIME="${OPT##*=}"
;;
--sha256=*)
SHA="${OPT##*=}"
;;
--job=*)
JOB="${OPT##*=}"
;;
--platform=*)
PLATFORM="${OPT##*=}"
;;
--main-binary-filepath=*)
FILEPATH="${OPT##*=}"
;;
--*)
warn "Ignoring unknown option: '${OPT}'"
;;
*)
err "Unknown option: '${OPT}'"
usage
;;
esac
shift
done
[ -z "${BUILD_TIME}" ] && die "The --build-time option must be specified"
[ -z "${SHA}" ] && die "The --sha256 option must be specified"
[ -z "${JOB}" ] && die "The --job option must be specified"
[ -z "${PLATFORM}" ] && die "The --platform option must be specified"
[ -z "${FILEPATH}" ] && die "The --main-binary-filepath option must be specified"
FILENAME="${1}"
if [ $(basename "${FILEPATH}" .deb) = $(basename "${FILEPATH}") ]; then
echo "Not a Debian package"
exit 0
fi
if aptly repo show -with-packages "${PLATFORM}" | grep "${SHA}" > /dev/null; then
echo "Package with same SHA256 already in repository"
exit 0
fi
TMP=$(mktemp -d -t debrep)
cleanup () {
rm -rf "${TMP}"
}
trap cleanup EXIT
PKG_ROOT="${TMP}/pkg"
mkdir "${PKG_ROOT}"
dpkg-deb -R "${FILENAME}" "${PKG_ROOT}"
VERSION=$(dpkg-deb -f "${FILENAME}" Version)
# if we've a tagged version (1.5.0), append the number of commits and a dummy hash
VERSION_GOOD=$(echo $VERSION | grep -c '^[0-9]\+\.[0-9]\+\.[0-9]\+$') || true
VERSION_WITH_COMMIT=$(echo $VERSION | grep -c '^[0-9]\+\.[0-9]\+\.[0-9]\+\-[0-9]\+\-g[0-9a-fA-f]\+$') || true
if [ $VERSION_GOOD -eq 0 -a $VERSION_WITH_COMMIT -eq 0 ]; then
die "version does not conform to (MAJOR.MINOR.PATCH[-#NUM_COMMITS-g<HASH>])"
fi
if [ $VERSION_WITH_COMMIT -eq 0 ]; then
VERSION="${VERSION}-0-g0000000"
fi
NEW_VERSION="${VERSION}"-"${BUILD_TIME}"-"${SHA}"
sed -i "" -e "s/Version:.*/Version: ${NEW_VERSION}/g" "${PKG_ROOT}/DEBIAN/control"
dpkg-deb --build "${PKG_ROOT}" "${TMP}"
if ! aptly repo show "${PLATFORM}" > /dev/null 2>&1; then
aptly repo create --distribution="${PLATFORM}" "${PLATFORM}"
fi
PACKAGE=$(dpkg-deb -f "${FILENAME}" Package)
aptly repo remove "${PLATFORM}" "${PACKAGE}"
aptly repo add "${PLATFORM}" "${TMP}"
: "${REPO_KEYID:="D5E2DC92617877EDF7D4FD4345EA05FB7E26053D"}"
if ! aptly publish show "${PLATFORM}" > /dev/null 2>&1; then
aptly publish repo -gpg-key="${REPO_KEYID}" "${PLATFORM}"
else
aptly publish update -gpg-key="${REPO_KEYID}" "${PLATFORM}"
fi

View file

@ -1,9 +0,0 @@
2.0.0-20220202-abcdef 2.0.1-20220203-123456
2.0.0-10-gabcdef-20220202-hahh 2.0.0-10-gabcdef-20220203-hahh
2.0.0-10-gabcdef-20220202-hahh 2.0.0-11-g123456-20220201-abcd
2.0.0-10-gabcdef-20220202-hahh 2.0.0-110-g123456-20220201-abcd
2.0.0-11-g123456-20220201-abcd 2.0.1-20220120-abcd
3.0.0-20230101-abcd 3.0.1-20230204-bdbd
1.5.0-20220516-a0d5a2 1.5.0-3-g26b5a59-20220527-0bc180
1.5.0-3-g26b5a59-20220527-0bc180 1.5.1-20220527-0bc180
0.1.0-20221120104301-f9e456637274844d45d9758ec661a136d0cda7966b075e4426b69fe6da00427b 0.1.0-237-g62965d4-20230527202149-6118c39221f318154e234098b5cffd4dc1d80f19cf2200cc6b1eb768dbf6decb

View file

@ -1,226 +0,0 @@
#!/bin/sh
set -e
#set -x
prog_NAME=$(basename "${0}")
warn()
{
echo "${prog_NAME}: WARN: $*"
}
info()
{
echo "${prog_NAME}: INFO: $*"
}
err()
{
echo "${prog_NAME}: ERROR: $*" 1>&2
}
die()
{
echo "${prog_NAME}: ERROR: $*" 1>&2
exit 1
}
usage()
{
cat <<EOM 1>&2
usage: ${prog_NAME} [ OPTIONS ]
Generates visualizations
Options:
--uuid=STRING
UUID of build.
--data-dir=STRING
Path to the data directory.
--cache-dir=STRING
Path to the cache directory.
EOM
exit 1
}
UUID=
CACHE_DIR=
DATA_DIR=
while [ $# -gt 0 ]; do
OPT="$1"
case "${OPT}" in
--uuid=*)
UUID="${OPT##*=}"
;;
--cache-dir=*)
CACHE_DIR="${OPT##*=}"
;;
--data-dir=*)
DATA_DIR="${OPT##*=}"
;;
*)
warn "Ignoring unknown option: '${OPT}' (Note that this script reads DB)"
;;
esac
shift
done
[ -z "${UUID}" ] && die "The --uuid option must be specified"
[ -z "${CACHE_DIR}" ] && die "The --cache-dir option must be specified"
[ -z "${DATA_DIR}" ] && die "The --data-dir option must be specified"
info "processing UUID '${UUID}'"
DB="${DATA_DIR}/builder.sqlite3"
# A new visualizations.sh script may be installed during an upgrade while the
# old builder-web binary is running. In that case things can get out of sync.
DB_VERSION="$(sqlite3 "$DB" "PRAGMA user_version;")"
[ -z "$DB_VERSION" ] && die "Couldn't read database version from '$DB'"
[ "$DB_VERSION" -ne 18 ] && die "The database version should be 18. It is '$DB_VERSION'"
APP_ID="$(sqlite3 "$DB" "PRAGMA application_id;")"
[ -z "$APP_ID" ] && die "Couldn't read application-id from '$DB'"
[ "$APP_ID" -ne 1234839235 ] && die "The application-id should be 1234839235. It is '$APP_ID'"
get_main_binary () {
sqlite3 "${DB}" "SELECT '_artifacts/' || substr(lower(hex(ba.sha256)), 1, 2) || '/' || lower(hex(ba.sha256))
FROM build AS b
JOIN build_artifact AS ba ON ba.build = b.id AND b.main_binary = ba.id
WHERE uuid = '${UUID}';"
}
BIN="${DATA_DIR}/$(get_main_binary)" || die "Failed to get main binary from database"
[ -z "${BIN}" ] && die "No main-binary found in db '${DB}' for build '${UUID}'"
get_debug_binary () {
sqlite3 "${DB}" "SELECT '_artifacts/' || substr(lower(hex(ba.sha256)), 1, 2) || '/' || lower(hex(ba.sha256))
FROM build AS b
JOIN build_artifact AS ba ON ba.build = b.id
WHERE
uuid = '${UUID}'
AND ba.filepath LIKE '%.debug';"
}
DEBUG_BIN_RELATIVE="$(get_debug_binary)" || die "Failed to get debug binary from database"
get_opam_switch () {
sqlite3 "${DB}" "SELECT '_artifacts/' || substr(lower(hex(ba.sha256)), 1, 2) || '/' || lower(hex(ba.sha256))
FROM build AS b
JOIN build_artifact AS ba ON ba.build = b.id
WHERE
uuid = '${UUID}'
AND ba.filepath = 'opam-switch';"
}
OPAM_SWITCH="$(get_opam_switch)" || die "Failed to get opam switch from database"
[ -z "${OPAM_SWITCH}" ] && die "No 'opam-switch' found in db '${DB}' for build '${UUID}'"
OPAM_SWITCH="${DATA_DIR}/${OPAM_SWITCH}"
OPAM_GRAPH="opam-graph"
MODULECTOMY="modulectomy"
LATEST_TREEMAPVIZ_VERSION="$(${MODULECTOMY} --version)" || die "Failed to get modulectomy version"
LATEST_DEPENDENCIESVIZ_VERSION="$(${OPAM_GRAPH} --version)" || die "Failed to get opam-graph version"
TREEMAP_CACHE_DIR="${CACHE_DIR}/treemap_${LATEST_TREEMAPVIZ_VERSION}"
DEPENDENCIES_CACHE_DIR="${CACHE_DIR}/dependencies_${LATEST_DEPENDENCIESVIZ_VERSION}"
mktemp_aux () {
if [ "$(uname)" = "Linux" ]; then
mktemp -t "$1.XXX"
elif [ "$(uname)" = "FreeBSD" ]; then
mktemp -t "$1"
else
die 'Unsupported platform'
fi
}
TMPTREE=$(mktemp_aux viz_treemap)
TMPDEPENDENCIES=$(mktemp_aux viz_dependencies)
cleanup () {
rm -rf "${TMPTREE}" "${TMPDEPENDENCIES}"
}
trap cleanup EXIT
# /// Dependencies viz
if [ ! -d "${DEPENDENCIES_CACHE_DIR}" ]; then
mkdir "${DEPENDENCIES_CACHE_DIR}" || die "Failed to create directory '${DEPENDENCIES_CACHE_DIR}'"
fi
OPAM_SWITCH_FILEPATH='opam-switch'
get_opam_switch_hash () {
sqlite3 "${DB}" "SELECT lower(hex(ba.sha256)) FROM build AS b
JOIN build_artifact AS ba ON ba.build = b.id
WHERE uuid = '${UUID}'
AND ba.filepath = '${OPAM_SWITCH_FILEPATH}';"
}
DEPENDENCIES_INPUT_HASH="$(get_opam_switch_hash)" || die "Failed to get opam-switch hash from database"
DEPENDENCIES_VIZ_FILENAME="${DEPENDENCIES_CACHE_DIR}/${DEPENDENCIES_INPUT_HASH}.html"
if [ -e "${DEPENDENCIES_VIZ_FILENAME}" ]; then
info "Dependency visualization already exists: '${DEPENDENCIES_VIZ_FILENAME}'"
else
if ${OPAM_GRAPH} --output-format=html "${OPAM_SWITCH}" > "${TMPDEPENDENCIES}"; then
cp "${TMPDEPENDENCIES}" "${DEPENDENCIES_VIZ_FILENAME}"
rm "${TMPDEPENDENCIES}"
else
die "opam-graph failed to generate visualization"
fi
fi
# /// Treemap viz
stat_aux () {
if [ "$(uname)" = "Linux" ]; then
stat -c "%s" "$1"
elif [ "$(uname)" = "FreeBSD" ]; then
stat -f "%z" "$1"
else
die 'Unsupported platform'
fi
}
SIZE="$(stat_aux "${BIN}")"
if [ ! -d "${TREEMAP_CACHE_DIR}" ]; then
mkdir "${TREEMAP_CACHE_DIR}" || die "Failed to create directory '${TREEMAP_CACHE_DIR}'"
fi
get_debug_bin_hash () {
sqlite3 "${DB}" "SELECT lower(hex(ba.sha256)) FROM build AS b
JOIN build_artifact AS ba ON ba.build = b.id
WHERE uuid = '${UUID}'
AND ba.filepath LIKE '%.debug';"
}
TREEMAP_INPUT_HASH="$(get_debug_bin_hash)" || die "Failed to get treemap input hash from database"
TREEMAP_VIZ_FILENAME="${TREEMAP_CACHE_DIR}/${TREEMAP_INPUT_HASH}.html"
if [ -n "${DEBUG_BIN_RELATIVE}" ]; then
DEBUG_BIN="${DATA_DIR}/$(get_debug_binary)"
if [ -e "${TREEMAP_VIZ_FILENAME}" ]; then
info "Treemap visualization already exists: '${TREEMAP_VIZ_FILENAME}'"
else
if
${MODULECTOMY} \
--robur-defaults \
--with-scale="${SIZE}" \
"${DEBUG_BIN}" \
> "${TMPTREE}"
then
cp "${TMPTREE}" "${TREEMAP_VIZ_FILENAME}"
rm "${TMPTREE}"
else
die "modulectomy failed to generate visualization"
fi
fi
else
info "No --debug-binary provided, not producing any treemap"
fi

View file

@ -3,7 +3,7 @@ let ( >>| ) x f = Result.map f x
module type CONN = Caqti_blocking.CONNECTION
let () = Mirage_crypto_rng_unix.initialize (module Mirage_crypto_rng.Fortuna)
let () = Mirage_crypto_rng_unix.initialize ()
let iter f xs = List.fold_left (fun r x -> r >>= fun () -> f x) (Ok ()) xs
let get_opt message = function
@ -25,8 +25,8 @@ module Testable = struct
x.restricted = y.restricted &&
match x.password_hash, y.password_hash with
| `Scrypt (hash, salt, params), `Scrypt (hash', salt', params') ->
String.equal hash hash' &&
String.equal salt salt' &&
Cstruct.equal hash hash' &&
Cstruct.equal salt salt' &&
params = params'
in
let pp ppf { Builder_web_auth.username; password_hash; restricted } =
@ -34,7 +34,7 @@ module Testable = struct
| `Scrypt (hash, salt, { Builder_web_auth.scrypt_n; scrypt_r; scrypt_p }) ->
Format.fprintf ppf "user:%s;(%d,%d,%d);%B;%a;%a" username
scrypt_n scrypt_r scrypt_p restricted
Ohex.pp hash Ohex.pp salt
Cstruct.hexdump_pp hash Cstruct.hexdump_pp salt
in
Alcotest.testable
pp
@ -43,15 +43,18 @@ module Testable = struct
let file =
let equal (x : Builder_db.Rep.file) (y : Builder_db.Rep.file) =
Fpath.equal x.filepath y.filepath &&
String.equal x.sha256 y.sha256 &&
Fpath.equal x.localpath y.localpath &&
Cstruct.equal x.sha256 y.sha256 &&
x.size = y.size
in
let pp ppf { Builder_db.Rep.filepath; sha256; size } =
let pp ppf { Builder_db.Rep.filepath; localpath; sha256; size } =
Format.fprintf ppf "{@[<v 1>@;<1 0>Builder_db.Rep.filepath = %a;@;<1 0>\
localpath = %a;@;<1 0>\
sha256 = %a;@;<1 0>\
size = %d;@;<1 0>\
@]@,}"
Fpath.pp filepath Ohex.pp sha256 size
Fpath.pp filepath Fpath.pp localpath
Cstruct.hexdump_pp sha256 size
in
Alcotest.testable pp equal
@ -123,39 +126,39 @@ let test_user_unauth (module Db : CONN) =
let job_name = "test-job"
let script = Fpath.v "/dev/null"
let uuid = Uuidm.v `V4
let uuid = Uuidm.create `V4
let console = Fpath.v "/dev/null"
let start = Option.get (Ptime.of_float_s 0.)
let finish = Option.get (Ptime.of_float_s 1.)
let result = Builder.Exited 0
let main_binary =
let filepath = Result.get_ok (Fpath.of_string "bin/hello.sh") in
let localpath = Result.get_ok (Fpath.of_string "/dev/null") in
let data = "#!/bin/sh\necho Hello, World\n" in
let sha256 = Digestif.SHA256.(to_raw_string (digest_string data)) in
let sha256 = Mirage_crypto.Hash.SHA256.digest (Cstruct.of_string data) in
let size = String.length data in
{ Builder_db.Rep.filepath; sha256; size }
let main_binary2 =
let data = "#!/bin/sh\necho Hello, World 2\n" in
let sha256 = Digestif.SHA256.(to_raw_string (digest_string data)) in
let size = String.length data in
{ main_binary with sha256 ; size }
{ Builder_db.Rep.filepath; localpath; sha256; size }
let platform = "exotic-os"
let fail_if_none a =
Option.to_result ~none:(`Msg "Failed to retrieve") a
let add_test_build user_id (module Db : CONN) =
let open Builder_db in
Db.start () >>= fun () ->
Db.exec Job.try_add job_name >>= fun () ->
Db.find_opt Job.get_id_by_name job_name >>= fail_if_none >>= fun job_id ->
Db.exec Build.add { Build.uuid; start; finish; result; console; script; platform;
main_binary = None; input_id = None; user_id; job_id } >>= fun () ->
Db.find last_insert_rowid () >>= fun id ->
Db.exec Build_artifact.add (main_binary, id) >>= fun () ->
Db.find last_insert_rowid () >>= fun main_binary_id ->
Db.exec Build.set_main_binary (id, main_binary_id) >>= fun () ->
Db.commit ()
let r =
let open Builder_db in
Db.start () >>= fun () ->
Db.exec Job.try_add job_name >>= fun () ->
Db.find_opt Job.get_id_by_name job_name >>= fail_if_none >>= fun job_id ->
Db.exec Build.add { Build.uuid; start; finish; result; console; script; platform;
main_binary = None; input_id = None; user_id; job_id } >>= fun () ->
Db.find last_insert_rowid () >>= fun id ->
Db.exec Build_artifact.add (main_binary, id) >>= fun () ->
Db.find last_insert_rowid () >>= fun main_binary_id ->
Db.exec Build.set_main_binary (id, main_binary_id) >>= fun () ->
Db.commit ()
in
Result.fold ~ok:Result.ok ~error:(fun _ -> Db.rollback ())
r
let with_build_db f () =
or_fail
@ -195,7 +198,7 @@ let test_build_get_all (module Db : CONN) =
Db.collect_list Builder_db.Build.get_all job_id >>| fun builds ->
Alcotest.(check int) "one build" (List.length builds) 1
let uuid' = Uuidm.v `V4
let uuid' = Uuidm.create `V4
let start' = Option.get (Ptime.of_float_s 3600.)
let finish' = Option.get (Ptime.of_float_s 3601.)
@ -208,7 +211,7 @@ let add_second_build (module Db : CONN) =
Db.exec Build.add { Build.uuid; start; finish; result; console; script; platform;
main_binary = None; input_id = None; user_id; job_id; } >>= fun () ->
Db.find last_insert_rowid () >>= fun id ->
Db.exec Build_artifact.add (main_binary2, id) >>= fun () ->
Db.exec Build_artifact.add (main_binary, id) >>= fun () ->
Db.find last_insert_rowid () >>= fun main_binary_id ->
Db.exec Build.set_main_binary (id, main_binary_id) >>= fun () ->
Db.commit ()
@ -217,34 +220,30 @@ 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_successful_with_binary (job_id, platform)
Db.find_opt Builder_db.Build.get_latest (job_id, platform)
>>| get_opt "no latest build" >>| fun (_id, meta, main_binary') ->
Alcotest.(check Testable.file) "same main binary" main_binary2 main_binary';
Alcotest.(check (option Testable.file)) "same main binary" main_binary' (Some main_binary);
Alcotest.(check Testable.uuid) "same uuid" meta.uuid uuid'
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_different_output id
>>| get_opt "no previous build" >>| fun build ->
Alcotest.(check Testable.uuid) "same uuid" build.Builder_db.Build.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_different_output id >>| function
Db.find_opt Builder_db.Build.get_previous_successful_uuid id >>| function
| None -> ()
| Some build ->
Alcotest.failf "Got unexpected result %a" Uuidm.pp build.Builder_db.Build.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 () ->
Db.find_opt Builder_db.Build.get_with_jobname_by_hash main_binary.sha256
>>| get_opt "no build" >>= fun (job_name', build) ->
Alcotest.(check string) "same job" job_name' job_name;
Alcotest.(check Testable.uuid) "same uuid" build.uuid uuid;
Db.find_opt Builder_db.Build.get_with_jobname_by_hash main_binary2.sha256
>>| get_opt "no build" >>| fun (job_name', build) ->
Alcotest.(check string) "same job" job_name' job_name;
Alcotest.(check Testable.uuid) "same uuid" build.uuid uuid'
@ -261,14 +260,6 @@ let test_artifact_get_by_build_uuid (module Db : CONN) =
get_opt "no build" >>| fun (_id, file) ->
Alcotest.(check Testable.file) "same file" file main_binary
let test_artifact_exists_true (module Db : CONN) =
Db.find Builder_db.Build_artifact.exists main_binary.sha256 >>| fun exists ->
Alcotest.(check bool) "main binary exists" true exists
let test_artifact_exists_false (module Db : CONN) =
Db.find Builder_db.Build_artifact.exists main_binary2.sha256 >>| fun exists ->
Alcotest.(check bool) "main binary2 doesn't exists" false exists
(* XXX: This test should fail because main_binary on the corresponding build
* references its main_binary. This is not the case now due to foreign key. *)
let test_artifact_remove_by_build (module Db : CONN) =
@ -276,39 +267,6 @@ let test_artifact_remove_by_build (module Db : CONN) =
get_opt "no build" >>= fun (id, _build) ->
Db.exec Builder_db.Build_artifact.remove_by_build id
let test_get_builds_older_than (module Db : CONN) =
add_second_build (module Db) >>= fun () ->
let date = Option.get (Ptime.of_float_s (3600. /. 2.)) in
Db.find_opt Builder_db.Job.get_id_by_name job_name >>= fail_if_none >>= fun job_id ->
Db.collect_list Builder_db.Build.get_builds_older_than (job_id, None, date) >>= fun builds ->
let builds = List.map (fun (_, { Builder_db.Build.uuid; _ }) -> uuid) builds in
Alcotest.(check (list Testable.uuid)) "last build" builds [ uuid ];
Db.collect_list Builder_db.Build.get_builds_older_than (job_id, None, Ptime_clock.now ()) >>= fun builds ->
let builds = List.map (fun (_, { Builder_db.Build.uuid; _ }) -> uuid) builds in
(* NOTE(dinosaure): from the most recent to the older. *)
Alcotest.(check (list Testable.uuid)) "last builds" builds [ uuid'; uuid ];
Ok ()
let test_builds_excluding_latest_n (module Db : CONN) =
add_second_build (module Db) >>= fun () ->
Db.find_opt Builder_db.Job.get_id_by_name job_name >>= fail_if_none >>= fun job_id ->
Db.collect_list Builder_db.Build.get_builds_excluding_latest_n (job_id, None, 1) >>= fun builds ->
let builds = List.map (fun (_, { Builder_db.Build.uuid; _ }) -> uuid) builds in
Alcotest.(check (list Testable.uuid)) "keep recent build" builds [ uuid ];
Db.collect_list Builder_db.Build.get_builds_excluding_latest_n (job_id, None, 2) >>= fun builds ->
let builds = List.map (fun (_, { Builder_db.Build.uuid; _ }) -> uuid) builds in
Alcotest.(check (list Testable.uuid)) "keep 2 builds" builds [];
Db.collect_list Builder_db.Build.get_builds_excluding_latest_n (job_id, None, 3) >>= fun builds ->
let builds = List.map (fun (_, { Builder_db.Build.uuid; _ }) -> uuid) builds in
Alcotest.(check (list Testable.uuid)) "last more builds than we have" builds [];
Db.collect_list Builder_db.Build.get_builds_excluding_latest_n (job_id, None, 0) >>= fun builds ->
let builds = List.map (fun (_, { Builder_db.Build.uuid; _ }) -> uuid) builds in
Alcotest.(check (list Testable.uuid)) "delete all builds" builds [ uuid'; uuid ];
Db.collect_list Builder_db.Build.get_builds_excluding_latest_n (job_id, None, -1) >>= fun builds ->
let builds = List.map (fun (_, { Builder_db.Build.uuid; _ }) -> uuid) builds in
Alcotest.(check (list Testable.uuid)) "test an incomprehensible argument (-1)" builds [ uuid'; uuid ];
Ok ()
let () =
let open Alcotest in
Alcotest.run "Builder_db" [
@ -339,12 +297,6 @@ let () =
"build-artifact", [
test_case "Get all by build" `Quick (with_build_db test_artifact_get_all_by_build);
test_case "Get by build uuid" `Quick (with_build_db test_artifact_get_by_build_uuid);
test_case "Artifact exists" `Quick (with_build_db test_artifact_exists_true);
test_case "Other artifact doesn't exists" `Quick (with_build_db test_artifact_exists_false);
test_case "Remove by build" `Quick (with_build_db test_artifact_remove_by_build);
];
"vacuum", [
test_case "Get builds older than now" `Quick (with_build_db test_get_builds_older_than);
test_case "Get older builds and keep a fixed number of then" `Quick (with_build_db test_builds_excluding_latest_n);
]
]

View file

@ -1,16 +1,3 @@
(test
(name test_builder_db)
(modules test_builder_db)
(libraries ptime.clock.os builder_db caqti.blocking alcotest mirage-crypto-rng.unix ohex))
(test
(name markdown_to_html)
(modules markdown_to_html)
(libraries builder_web cmarkit alcotest))
(test
(name router)
(modules router)
(libraries builder_web fmt dream yojson alcotest)
(preprocess
(pps ppx_deriving.std ppx_deriving_yojson)))
(name builder_db)
(libraries builder_db caqti.blocking alcotest mirage-crypto-rng.unix))

View file

@ -1,201 +0,0 @@
let markdown_to_html = Builder_web__Utils.md_to_html
let test_simple () =
let markdown = {|# Hello world|} in
let html = markdown_to_html markdown in
Alcotest.(check string "simple html" "<h1 id=\"hello-world\"><a class=\"anchor\" aria-hidden=\"true\" href=\"#hello-world\"></a>Hello world</h1>\n" html)
let test_html_script () =
let markdown = {|# <script>Hello world</script>|} in
let html = markdown_to_html markdown in
Alcotest.(check string "html script header" "<h1 id=\"hello-world\"><a class=\"anchor\" aria-hidden=\"true\" href=\"#hello-world\"></a><!-- CommonMark raw HTML omitted -->Hello world<!-- CommonMark raw HTML omitted --></h1>\n" html)
let test_preserve_span_content () =
let markdown = {|* <span id="myref">My ref</span>
* [See my ref](#myref) for more information|} in
let html = markdown_to_html markdown in
Alcotest.(check string "html span content preserved"
{|<ul>
<li><!-- CommonMark raw HTML omitted -->My ref<!-- CommonMark raw HTML omitted --></li>
<li><a href="#myref">See my ref</a> for more information</li>
</ul>
|}
html)
let test_remove_script () =
let markdown = {|<script>alert(1);</script>|} in
let html = markdown_to_html markdown in
Alcotest.(check string "html script removed" "<!-- CommonMark HTML block omitted -->\n" html)
let test_list_with_html_block_and_markdown () =
let markdown = "* <div> Hello, World!</div> *this is not html*" in
let html = markdown_to_html markdown in
Alcotest.(check string "list with html block and markdown"
(*"<ul>\n<li><em>this is not html</em>\n</li>\n</ul>\n"*)
"<ul>\n<li>\n<!-- CommonMark HTML block omitted -->\n</li>\n</ul>\n"
html)
let test_list_with_inline_html_and_markdown () =
let markdown = "* <span> Hello, World!</span> *this is not html*" in
let html = markdown_to_html markdown in
Alcotest.(check string "list with html block and markdown"
"<ul>\n<li><!-- CommonMark raw HTML omitted --> Hello, World!<!-- CommonMark raw HTML omitted --> <em>this is not html</em></li>\n</ul>\n"
html)
let test_absolute_link () =
let markdown = "[foo](https://foo.com)" in
let html = markdown_to_html markdown in
Alcotest.(check string "absolute link" "<p><a href=\"https://foo.com\">foo</a></p>\n" html)
let test_relative_link () =
let markdown = "[foo](../foo.jpg)" in
let html = markdown_to_html markdown in
Alcotest.(check string "relative link" "<p><a href=\"../foo.jpg\">foo</a></p>\n" html)
let test_absolute_image () =
let markdown = "![alttext](https://foo.com/bar.jpg)" in
let html = markdown_to_html markdown in
Alcotest.(check string "absolute image"
"<p><img src=\"https://foo.com/bar.jpg\" alt=\"alttext\" ></p>\n" html)
let test_absolute_image_no_alt () =
let markdown = "![](https://foo.com/bar.jpg)" in
let html = markdown_to_html markdown in
Alcotest.(check string "absolute image"
"<p><img src=\"https://foo.com/bar.jpg\" alt=\"\" ></p>\n" html)
let test_relative_image () =
let markdown = "![](/bar.jpg)" in
let html = markdown_to_html markdown in
Alcotest.(check string "relative image" "<p><img src=\"/bar.jpg\" alt=\"\" ></p>\n" html)
let test_absolute_image_script_alt () =
let markdown = "![<script src=\"bla.js\"></script>](https://foo.com/bar.jpg)" in
let html = markdown_to_html markdown in
Alcotest.(check string "absolute image with script alt text"
"<p><img src=\"https://foo.com/bar.jpg\" alt=\"\" ></p>\n" html)
let test_fragment_link () =
let markdown = "[fragment](#fragment)" in
let html = markdown_to_html markdown in
Alcotest.(check string "fragment link" "<p><a href=\"#fragment\">fragment</a></p>\n" html)
let test_heading_adjustment () =
let markdown = {|# foo
## bar
# baz
## bazbar
### bazbarbar
#### bazbarbarbar
##### bazbarbarbarbar
###### bazbarbarbarbarbar
|}
in
let html = markdown_to_html ~adjust_heading:2 markdown in
(* NB: the maximum heading is 6 in cmarkit, thus we reduce the structure *)
let exp = {|<h3 id="foo"><a class="anchor" aria-hidden="true" href="#foo"></a>foo</h3>
<h4 id="bar"><a class="anchor" aria-hidden="true" href="#bar"></a>bar</h4>
<h3 id="baz"><a class="anchor" aria-hidden="true" href="#baz"></a>baz</h3>
<h4 id="bazbar"><a class="anchor" aria-hidden="true" href="#bazbar"></a>bazbar</h4>
<h5 id="bazbarbar"><a class="anchor" aria-hidden="true" href="#bazbarbar"></a>bazbarbar</h5>
<h6 id="bazbarbarbar"><a class="anchor" aria-hidden="true" href="#bazbarbarbar"></a>bazbarbarbar</h6>
<h6 id="bazbarbarbarbar"><a class="anchor" aria-hidden="true" href="#bazbarbarbarbar"></a>bazbarbarbarbar</h6>
<h6 id="bazbarbarbarbarbar"><a class="anchor" aria-hidden="true" href="#bazbarbarbarbarbar"></a>bazbarbarbarbarbar</h6>
|} in
Alcotest.(check string "header adjustment works fine" exp html)
let test_table () =
let markdown = {__|| a | | b | c | d | e |
| --------------------- |-| -------------- | -------------- | --------------- | ------ |
| entry | | **bla.file** | **other.file** | | |
| _another entry_ | | **another.file** | **another.other** | | |
|__}
in
let html = markdown_to_html ~adjust_heading:2 markdown in
let exp = {|<div role="region"><table>
<tr>
<th>a</th>
<th></th>
<th>b</th>
<th>c</th>
<th>d</th>
<th>e</th>
</tr>
<tr>
<td>entry</td>
<td></td>
<td><strong>bla.file</strong></td>
<td><strong>other.file</strong></td>
<td></td>
<td></td>
</tr>
<tr>
<td><em>another entry</em></td>
<td></td>
<td><strong>another.file</strong></td>
<td><strong>another.other</strong></td>
<td></td>
<td></td>
</tr>
</table></div>|} in
Alcotest.(check string "table is rendered as html" exp html)
let test_table2 () =
let markdown = {__|| a | | b | c | d | e |
| --------------------- |-| -------------- | -------------- | --------------- | ------ |
| entry | | | | **bla.file** | **other.file** |
| _another entry_ | | | **another.file** | **another.other** | |
|__}
in
let html = markdown_to_html ~adjust_heading:2 markdown in
let exp = {|<div role="region"><table>
<tr>
<th>a</th>
<th></th>
<th>b</th>
<th>c</th>
<th>d</th>
<th>e</th>
</tr>
<tr>
<td>entry</td>
<td></td>
<td></td>
<td></td>
<td><strong>bla.file</strong></td>
<td><strong>other.file</strong></td>
</tr>
<tr>
<td><em>another entry</em></td>
<td></td>
<td></td>
<td><strong>another.file</strong></td>
<td><strong>another.other</strong></td>
<td></td>
</tr>
</table></div>|} in
Alcotest.(check string "table is rendered as html" exp html)
let markdown_tests = [
Alcotest.test_case "Simple" `Quick test_simple;
Alcotest.test_case "script header" `Quick test_html_script;
Alcotest.test_case "preserve span content" `Quick test_preserve_span_content;
Alcotest.test_case "Remove script" `Quick test_remove_script;
Alcotest.test_case "List with html block and markdown" `Quick test_list_with_html_block_and_markdown;
Alcotest.test_case "List with inline html and markdown" `Quick test_list_with_inline_html_and_markdown;
Alcotest.test_case "absolute link" `Quick test_absolute_link;
Alcotest.test_case "relative link" `Quick test_relative_link;
Alcotest.test_case "absolute image" `Quick test_absolute_image;
Alcotest.test_case "absolute image no alt" `Quick test_absolute_image_no_alt;
Alcotest.test_case "relative image" `Quick test_relative_image;
Alcotest.test_case "absolute image with script alt" `Quick test_absolute_image_script_alt;
Alcotest.test_case "fragment link" `Quick test_fragment_link;
Alcotest.test_case "heading adjustment" `Quick test_heading_adjustment;
Alcotest.test_case "table" `Quick test_table;
Alcotest.test_case "table2" `Quick test_table2;
]
let () =
Alcotest.run "Markdown to HTML" [
"markdown", markdown_tests
]

View file

@ -1,191 +0,0 @@
module Param_verification = struct
(*> None is 'verified'*)
type t = wrong_type option
[@@deriving yojson,show,eq]
and wrong_type = {
param : string;
expected : string;
}
let alcotyp = Alcotest.testable pp equal
module P = struct
let is_string : (string * string) -> _ option =
Fun.const None
let is_uuid (param, value) =
match Uuidm.of_string value with
| Some _ when String.length value = 36 -> None
| _ -> Some {
param;
expected = "Uuidm.t"
}
end
let verify parameters req =
let verified_params =
List.fold_left (fun acc p ->
match acc with
| None ->
if String.starts_with ~prefix:"build" p then
P.is_uuid (p, Dream.param req p)
else
P.is_string (p, Dream.param req p)
| Some _ as x -> x)
None parameters
in
let response_json =
verified_params |> to_yojson |> Yojson.Safe.to_string
in
Dream.respond response_json
end
let find_parameters path =
List.filter_map (fun s ->
if String.length s > 0 && String.get s 0 = ':' then
Some (String.sub s 1 (String.length s - 1))
else
None)
(String.split_on_char '/' path)
let router =
(* XXX: this relies on [Builder_web.routes] only using {data,cache,config}dir
* in the handlers which are never called here. The path /nonexistant is
* assumed to not exist. *)
let nodir = Fpath.v "/nonexistant" in
Builder_web.routes ~datadir:nodir ~cachedir:nodir ~configdir:nodir ~expired_jobs:0
|> List.map (fun (meth, route, _handler) ->
meth, route, Param_verification.verify (find_parameters route))
|> Builder_web.to_dream_routes
|> Dream.router
(* XXX: we test without remove_trailing_url_slash to ensure we don't produce
* urls with trailing slashes: *)
(* |> Builder_web.Middleware.remove_trailing_url_slash *)
|> Dream.test
let test_link method_ target () =
let req = Dream.request ~method_ ~target "" in
let resp = router req in
let status_code = Dream.(status resp |> status_to_int) in
Alcotest.(check' int ~msg:"status-code" ~actual:status_code ~expected:200);
let body =
Dream.body resp
|> Lwt_main.run
|> Yojson.Safe.from_string
|> Param_verification.of_yojson
in
Alcotest.(check' (result Param_verification.alcotyp string) ~msg:"param-verification"
~actual:body ~expected:(Ok None))
let test_link_artifact artifact =
let job_name = "test" in
let build = Uuidm.v `V4 in
test_link `GET @@
Builder_web.Link.Job_build_artifact.make ~job_name ~build ~artifact ()
let () =
Alcotest.run "Router" [
"Link module synced", Alcotest.[
test_case "Link.Root.make" `Quick begin
test_link `GET @@ Builder_web.Link.Root.make ()
end;
test_case "Link.Job.make" `Quick begin
let queries = [ `Platform "test" ] in
let job_name = "test" in
test_link `GET @@ Builder_web.Link.Job.make ~queries ~job_name ()
end;
test_case "Link.Job.make_failed" `Quick begin
let queries = [ `Platform "test" ] in
let job_name = "test" in
test_link `GET @@
Builder_web.Link.Job.make_failed ~queries ~job_name ()
end;
test_case "Link.Job_build.make" `Quick begin
let job_name = "test" in
let build = Uuidm.v `V4 in
test_link `GET @@ Builder_web.Link.Job_build.make ~job_name ~build ()
end;
test_case "Link.Job_build_artifact.make_from_string" `Quick begin
let job_name = "test" in
let build = Uuidm.v `V4 in
let artifact = "" in
test_link `GET @@
Builder_web.Link.Job_build_artifact.make_from_string
~job_name ~build ~artifact ()
end;
] @ Alcotest.(
[
`Main_binary;
`Viz_treemap;
`Viz_dependencies;
`Script;
`Console;
`All_targz;
`File Fpath.(v "some" / "path");
]
|> List.map (fun artifact ->
let descr =
Fmt.str "Job_build_artifact.make: %s" @@
Builder_web.Link.Job_build_artifact.encode_artifact artifact
in
test_case descr `Quick begin
test_link_artifact artifact
end
)
) @ Alcotest.[
test_case "Link.Compare_builds.make" `Quick begin
let left = Uuidm.v `V4 in
let right = Uuidm.v `V4 in
test_link `GET @@
Builder_web.Link.Compare_builds.make ~left ~right ()
end;
test_case "Link.Failed_builds.make" `Quick begin
test_link `GET @@
Builder_web.Link.Failed_builds.make ~count:2 ~start:1 ()
end;
];
(* this doesn't actually test the redirects, unfortunately *)
"Latest", List.map (fun p -> Alcotest.(test_case (""^p) `Quick begin
test_link `GET @@ "/job/test/build/latest" ^ p end))
[
"/f/bin/unikernel.hvt";
"/";
"";
];
"Albatross hardcoded links",
[
(*> Note: these links can be found in
albatross/command-line/albatross_client_update.ml
.. to find them I follewed the trails of 'Albatross_cli.http_host'
*)
begin
let sha_str =
Digestif.SHA256.(to_raw_string (digest_string "foo"))
|> Ohex.encode
in
Fmt.str "/hash?sha256=%s" sha_str
end;
begin
let jobname = "foo" in
"/job/" ^ jobname ^ "/build/latest"
end;
begin
let job = "foo" in
let build = Uuidm.(v `V4 |> to_string) in
"/job/" ^ job ^ "/build/" ^ build ^ "/main-binary"
end;
begin
let old_uuid = Uuidm.(v `V4 |> to_string) in
let new_uuid = Uuidm.(v `V4 |> to_string) in
Fmt.str "/compare/%s/%s" old_uuid new_uuid
end;
]
|> List.map Alcotest.(fun p ->
test_case ("" ^ p) `Quick (test_link `GET p))
]