ci.scm 10 KB

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