ci.scm 11 KB

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