ci.scm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2018-2023 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2020 Mathieu Othacehe <othacehe@gnu.org>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (guix ci)
  20. #:use-module (guix http-client)
  21. #:use-module (guix utils)
  22. #:use-module ((guix build download)
  23. #:select (resolve-uri-reference))
  24. #:use-module (json)
  25. #:use-module (srfi srfi-1)
  26. #:use-module (srfi srfi-19)
  27. #:use-module (ice-9 match)
  28. #:use-module (web uri)
  29. #:use-module (guix i18n)
  30. #:use-module (guix diagnostics)
  31. #:autoload (guix channels) (channel)
  32. #:export (build-product?
  33. build-product-id
  34. build-product-type
  35. build-product-file-size
  36. build-product-path
  37. build?
  38. build-id
  39. build-derivation
  40. build-evaluation
  41. build-system
  42. build-status
  43. build-timestamp
  44. build-start-time
  45. build-stop-time
  46. build-duration
  47. build-products
  48. checkout?
  49. checkout-commit
  50. checkout-channel
  51. evaluation?
  52. evaluation-id
  53. evaluation-spec
  54. evaluation-complete?
  55. evaluation-checkouts
  56. evaluation-start-time
  57. evaluation-checkout-time
  58. evaluation-completion-time
  59. job?
  60. job-build-id
  61. job-status
  62. job-name
  63. history?
  64. history-evaluation
  65. history-checkouts
  66. history-jobs
  67. %query-limit
  68. queued-builds
  69. latest-builds
  70. evaluation
  71. evaluation-jobs
  72. build
  73. job-build
  74. jobs-history
  75. latest-evaluations
  76. evaluations-for-commit
  77. channel-with-substitutes-available))
  78. ;;; Commentary:
  79. ;;;
  80. ;;; This module provides a client to the HTTP interface of the Hydra and
  81. ;;; Cuirass continuous integration (CI) tools.
  82. ;;;
  83. ;;; Code:
  84. (define (seconds->date seconds)
  85. "Given SECONDS, a number of seconds since 1970-01-01, return the
  86. corresponding date object."
  87. (time-utc->date (make-time time-utc 0 seconds)))
  88. (define-json-mapping <build-product> make-build-product
  89. build-product?
  90. json->build-product
  91. (id build-product-id) ;integer
  92. (type build-product-type) ;string
  93. (file-size build-product-file-size) ;integer
  94. (path build-product-path)) ;string
  95. (define-syntax-rule (define-enumeration-mapping proc
  96. (names integers) ...)
  97. (define (proc value)
  98. (match value
  99. (integers 'names) ...)))
  100. (define-enumeration-mapping integer->build-status
  101. ;; Copied from 'build-status' in Cuirass.
  102. (submitted -3)
  103. (scheduled -2)
  104. (started -1)
  105. (succeeded 0)
  106. (failed 1)
  107. (failed-dependency 2)
  108. (failed-other 3)
  109. (canceled 4))
  110. (define-json-mapping <build> make-build build?
  111. json->build
  112. (id build-id "id") ;integer
  113. (derivation build-derivation) ;string | #f
  114. (evaluation build-evaluation) ;integer
  115. (system build-system) ;string
  116. (status build-status "buildstatus" ;symbol
  117. integer->build-status)
  118. (timestamp build-timestamp) ;integer
  119. (start-time build-start-time "starttime" ;date
  120. seconds->date)
  121. (stop-time build-stop-time "stoptime" ;date
  122. seconds->date)
  123. (products build-products "buildproducts" ;<build-product>*
  124. (lambda (products)
  125. (map json->build-product
  126. ;; Before Cuirass 3db603c1, #f is always returned.
  127. (if (vector? products)
  128. (vector->list products)
  129. '())))))
  130. (define-json-mapping <job> make-job job?
  131. json->job
  132. (build-id job-build-id "build") ;integer
  133. (status job-status "status" ;symbol
  134. integer->build-status)
  135. (name job-name)) ;string
  136. (define-json-mapping <history> make-history history?
  137. json->history
  138. (evaluation history-evaluation) ;integer
  139. (checkouts history-checkouts "checkouts" ;<checkout>*
  140. (lambda (checkouts)
  141. (map json->checkout
  142. (vector->list checkouts))))
  143. (jobs history-jobs "jobs"
  144. (lambda (jobs)
  145. (map json->job
  146. (vector->list jobs)))))
  147. (define-json-mapping <checkout> make-checkout checkout?
  148. json->checkout
  149. (commit checkout-commit) ;string (SHA1)
  150. (channel checkout-channel)) ;string (name)
  151. (define-json-mapping <evaluation> make-evaluation evaluation?
  152. json->evaluation
  153. (id evaluation-id) ;integer
  154. (spec evaluation-spec "specification") ;string
  155. (complete? evaluation-complete? "status"
  156. (match-lambda
  157. (0 #t)
  158. (_ #f))) ;Boolean
  159. (checkouts evaluation-checkouts "checkouts" ;<checkout>*
  160. (lambda (checkouts)
  161. (map json->checkout
  162. (vector->list checkouts))))
  163. (start-time evaluation-start-time "timestamp" ;date
  164. seconds->date)
  165. (checkout-time evaluation-checkout-time "checkouttime" ;date
  166. seconds->date)
  167. (completion-time evaluation-completion-time "evaltime" ;date
  168. seconds->date))
  169. (define %query-limit
  170. ;; Max number of builds requested in queries.
  171. 1000)
  172. (define* (api-url base-url path #:rest query)
  173. "Build a proper API url, taking into account BASE-URL's trailing slashes.
  174. QUERY takes any number of '(\"name\" value) 2-element lists, with VALUE being
  175. either a string or a number (which will be converted to a string). If VALUE
  176. is #f, the respective element will not be added to the query parameters.
  177. Other types of VALUE will raise an error since this low-level function is
  178. api-agnostic."
  179. (define (build-query-string query)
  180. (let lp ((query (or (reverse query) '())) (acc '()))
  181. (match query
  182. (() (string-concatenate acc))
  183. (((_ #f) . rest) (lp rest acc))
  184. (((name val) . rest)
  185. (lp rest (cons*
  186. name "="
  187. (if (string? val) (uri-encode val) (number->string val))
  188. (if (null? acc) "" "&")
  189. acc))))))
  190. (let* ((query-string (build-query-string query))
  191. (base (string->uri base-url))
  192. (ref (build-relative-ref #:path path #:query query-string)))
  193. (resolve-uri-reference ref base)))
  194. (define (json-fetch url)
  195. (let* ((port (http-fetch url))
  196. (json (json->scm port)))
  197. (close-port port)
  198. json))
  199. (define* (json-api-fetch base-url path #:rest query)
  200. (json-fetch (apply api-url base-url path query)))
  201. (define (build-duration build)
  202. "Return the duration in seconds of BUILD."
  203. (define duration
  204. (time-difference (date->time-utc (build-stop-time build))
  205. (date->time-utc (build-start-time build))))
  206. (time-second duration))
  207. (define* (queued-builds url #:optional (limit %query-limit))
  208. "Return the list of queued derivations on URL."
  209. (let ((queue
  210. (json-api-fetch url "/api/queue" `("nr" ,limit))))
  211. (map json->build (vector->list queue))))
  212. (define* (latest-builds url #:optional (limit %query-limit)
  213. #:key evaluation system job jobset status)
  214. "Return the latest builds performed by the CI server at URL. If EVALUATION
  215. is an integer, restrict to builds of EVALUATION. If SYSTEM is true (a system
  216. string such as \"x86_64-linux\"), restrict to builds for SYSTEM."
  217. (let ((latest (json-api-fetch
  218. url "/api/latestbuilds"
  219. `("nr" ,limit)
  220. `("evaluation" ,evaluation)
  221. `("system" ,system)
  222. `("job" ,job)
  223. `("jobset" ,jobset)
  224. `("status" ,status))))
  225. ;; Note: Hydra does not provide a "derivation" field for entries in
  226. ;; 'latestbuilds', but Cuirass does.
  227. (map json->build (vector->list latest))))
  228. (define (evaluation url evaluation)
  229. "Return the given EVALUATION performed by the CI server at URL."
  230. (let ((evaluation
  231. (json-api-fetch url "/api/evaluation" `("id" ,evaluation))))
  232. (json->evaluation evaluation)))
  233. (define* (latest-evaluations url
  234. #:optional (limit %query-limit)
  235. #:key spec)
  236. "Return the latest evaluations performed by the CI server at URL. If SPEC
  237. is passed, only consider the evaluations for the given SPEC specification."
  238. (map json->evaluation
  239. (vector->list
  240. (json-api-fetch
  241. url "/api/evaluations" `("nr" ,limit) `("spec" ,spec)))))
  242. (define* (evaluations-for-commit url commit #:optional (limit %query-limit))
  243. "Return the evaluations among the latest LIMIT evaluations that have COMMIT
  244. as one of their inputs."
  245. (filter (lambda (evaluation)
  246. (find (lambda (checkout)
  247. (string=? (checkout-commit checkout) commit))
  248. (evaluation-checkouts evaluation)))
  249. (latest-evaluations url limit)))
  250. (define (evaluation-jobs url evaluation-id)
  251. "Return the list of jobs of evaluation EVALUATION-ID."
  252. (map json->job
  253. (vector->list
  254. (json-api-fetch url "/api/jobs" `("evaluation" ,evaluation-id)))))
  255. (define (build url id)
  256. "Look up build ID at URL and return it. Raise &http-get-error if it is not
  257. found (404)."
  258. (json->build
  259. (http-fetch (api-url url (string-append "/build/" ;note: no "/api" here
  260. (number->string id))))))
  261. (define (job-build url job)
  262. "Return the build associated with JOB."
  263. (build url (job-build-id job)))
  264. (define* (jobs-history url jobs
  265. #:key
  266. (specification "master")
  267. (limit 20))
  268. "Return the job history for the SPECIFICATION jobs which names are part of
  269. the JOBS list, from the CI server at URL. Limit the history to the latest
  270. LIMIT evaluations. "
  271. (let ((names (string-join jobs ",")))
  272. (map json->history
  273. (vector->list
  274. (json->scm
  275. (http-fetch
  276. (format #f "~a/api/jobs/history?spec=~a&names=~a&nr=~a"
  277. url specification names (number->string limit))))))))
  278. (define (find-latest-commit-with-substitutes url)
  279. "Return the latest commit with available substitutes for the Guix package
  280. definitions at URL. Return false if no commit were found."
  281. (let* ((job-name (string-append "guix." (%current-system)))
  282. (build (match (latest-builds url 1
  283. #:job job-name
  284. #:jobset "guix"
  285. #:status 0) ;success
  286. ((build) build)
  287. (_ #f)))
  288. (evaluation (and build
  289. (evaluation url (build-evaluation build))))
  290. (commit (and evaluation
  291. (match (evaluation-checkouts evaluation)
  292. ((checkout)
  293. (checkout-commit checkout))))))
  294. commit))
  295. (define (channel-with-substitutes-available chan url)
  296. "Return a channel inheriting from CHAN but which commit field is set to the
  297. latest commit with available substitutes for the Guix package definitions at
  298. URL. The current system is taken into account.
  299. If no commit with available substitutes were found, the commit field is set to
  300. false and a warning message is printed."
  301. (let ((commit (find-latest-commit-with-substitutes url)))
  302. (unless commit
  303. (warning (G_ "could not find available substitutes at ~a~%")
  304. url))
  305. (channel
  306. (inherit chan)
  307. (commit commit))))