James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 1 | :title: Project Gating |
| 2 | |
| 3 | Project Gating |
| 4 | ============== |
| 5 | |
| 6 | Traditionally, many software development projects merge changes from |
| 7 | developers into the repository, and then identify regressions |
| 8 | resulting from those changes (perhaps by running a test suite with a |
| 9 | continuous integration system such as Jenkins), followed by more |
| 10 | patches to fix those bugs. When the mainline of development is |
| 11 | broken, it can be very frustrating for developers and can cause lost |
| 12 | productivity, particularly so when the number of contributors or |
| 13 | contributions is large. |
| 14 | |
| 15 | The process of gating attempts to prevent changes that introduce |
| 16 | regressions from being merged. This keeps the mainline of development |
| 17 | open and working for all developers, and only when a change is |
| 18 | confirmed to work without disruption is it merged. |
| 19 | |
| 20 | Many projects practice an informal method of gating where developers |
| 21 | with mainline commit access ensure that a test suite runs before |
| 22 | merging a change. With more developers, more changes, and more |
| 23 | comprehensive test suites, that process does not scale very well, and |
| 24 | is not the best use of a developer's time. Zuul can help automate |
| 25 | this process, with a particular emphasis on ensuring large numbers of |
| 26 | changes are tested correctly. |
| 27 | |
| 28 | Zuul was designed to handle the workflow of the OpenStack project, but |
| 29 | can be used with any project. |
| 30 | |
Antoine Musso | 5586753 | 2014-01-10 18:24:35 +0100 | [diff] [blame] | 31 | Testing in parallel |
| 32 | ------------------- |
| 33 | |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 34 | A particular focus of Zuul is ensuring correctly ordered testing of |
| 35 | changes in parallel. A gating system should always test each change |
| 36 | applied to the tip of the branch exactly as it is going to be merged. |
| 37 | A simple way to do that would be to test one change at a time, and |
| 38 | merge it only if it passes tests. That works very well, but if |
| 39 | changes take a long time to test, developers may have to wait a long |
| 40 | time for their changes to make it into the repository. With some |
| 41 | projects, it may take hours to test changes, and it is easy for |
| 42 | developers to create changes at a rate faster than they can be tested |
| 43 | and merged. |
| 44 | |
Clark Boylan | 00635dc | 2012-09-19 14:03:08 -0700 | [diff] [blame] | 45 | Zuul's DependentPipelineManager allows for parallel execution of test |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 46 | jobs for gating while ensuring changes are tested correctly, exactly |
| 47 | as if they had been tested one at a time. It does this by performing |
| 48 | speculative execution of test jobs; it assumes that all jobs will |
| 49 | succeed and tests them in parallel accordingly. If they do succeed, |
| 50 | they can all be merged. However, if one fails, then changes that were |
| 51 | expecting it to succeed are re-tested without the failed change. In |
| 52 | the best case, as many changes as execution contexts are available may |
| 53 | be tested in parallel and merged at once. In the worst case, changes |
| 54 | are tested one at a time (as each subsequent change fails, changes |
| 55 | behind it start again). In practice, the OpenStack project observes |
| 56 | something closer to the best case. |
| 57 | |
| 58 | For example, if a core developer approves five changes in rapid |
| 59 | succession:: |
| 60 | |
| 61 | A, B, C, D, E |
| 62 | |
| 63 | Zuul queues those changes in the order they were approved, and notes |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 64 | that each subsequent change depends on the one ahead of it merging: |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 65 | |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 66 | .. blockdiag:: |
| 67 | |
| 68 | blockdiag foo { |
| 69 | node_width = 40; |
| 70 | span_width = 40; |
| 71 | A <- B <- C <- D <- E; |
| 72 | } |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 73 | |
| 74 | Zuul then starts immediately testing all of the changes in parallel. |
| 75 | But in the case of changes that depend on others, it instructs the |
| 76 | test system to include the changes ahead of it, with the assumption |
| 77 | they pass. That means jobs testing change *B* include change *A* as |
| 78 | well:: |
| 79 | |
| 80 | Jobs for A: merge change A, then test |
| 81 | Jobs for B: merge changes A and B, then test |
| 82 | Jobs for C: merge changes A, B and C, then test |
| 83 | Jobs for D: merge changes A, B, C and D, then test |
| 84 | Jobs for E: merge changes A, B, C, D and E, then test |
| 85 | |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 86 | Hence jobs triggered to tests A will only test A and ignore B, C, D: |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 87 | |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 88 | .. blockdiag:: |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 89 | |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 90 | blockdiag foo { |
| 91 | node_width = 40; |
| 92 | span_width = 40; |
| 93 | master -> A -> B -> C -> D -> E; |
| 94 | group jobs_for_A { |
| 95 | label = "Merged changes for A"; |
| 96 | master -> A; |
| 97 | } |
| 98 | group ignored_to_test_A { |
| 99 | label = "Ignored changes"; |
| 100 | color = "lightgray"; |
| 101 | B -> C -> D -> E; |
| 102 | } |
| 103 | } |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 104 | |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 105 | The jobs for E would include the whole dependency chain: A, B, C, D, and E. |
| 106 | E will be tested assuming A, B, C, and D passed: |
| 107 | |
| 108 | .. blockdiag:: |
| 109 | |
| 110 | blockdiag foo { |
| 111 | node_width = 40; |
| 112 | span_width = 40; |
| 113 | group jobs_for_E { |
| 114 | label = "Merged changes for E"; |
| 115 | master -> A -> B -> C -> D -> E; |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | If changes *A* and *B* pass tests (green), and *C*, *D*, and *E* fail (red): |
| 120 | |
| 121 | .. blockdiag:: |
| 122 | |
| 123 | blockdiag foo { |
| 124 | node_width = 40; |
| 125 | span_width = 40; |
| 126 | |
| 127 | A [color = lightgreen]; |
| 128 | B [color = lightgreen]; |
| 129 | C [color = pink]; |
| 130 | D [color = pink]; |
| 131 | E [color = pink]; |
| 132 | |
| 133 | master <- A <- B <- C <- D <- E; |
| 134 | } |
| 135 | |
| 136 | Zuul will merge change *A* followed by change *B*, leaving this queue: |
| 137 | |
| 138 | .. blockdiag:: |
| 139 | |
| 140 | blockdiag foo { |
| 141 | node_width = 40; |
| 142 | span_width = 40; |
| 143 | |
| 144 | C [color = pink]; |
| 145 | D [color = pink]; |
| 146 | E [color = pink]; |
| 147 | |
| 148 | C <- D <- E; |
| 149 | } |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 150 | |
| 151 | Since *D* was dependent on *C*, it is not clear whether *D*'s failure is the |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 152 | result of a defect in *D* or *C*: |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 153 | |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 154 | .. blockdiag:: |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 155 | |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 156 | blockdiag foo { |
| 157 | node_width = 40; |
| 158 | span_width = 40; |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 159 | |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 160 | C [color = pink]; |
| 161 | D [label = "D\n?"]; |
| 162 | E [label = "E\n?"]; |
| 163 | |
| 164 | C <- D <- E; |
| 165 | } |
| 166 | |
| 167 | Since *C* failed, Zuul will report its failure and drop *C* from the queue, |
| 168 | keeping D and E: |
| 169 | |
| 170 | .. blockdiag:: |
| 171 | |
| 172 | blockdiag foo { |
| 173 | node_width = 40; |
| 174 | span_width = 40; |
| 175 | |
| 176 | D [label = "D\n?"]; |
| 177 | E [label = "E\n?"]; |
| 178 | |
| 179 | D <- E; |
| 180 | } |
James E. Blair | cdd0007 | 2012-06-08 19:17:28 -0700 | [diff] [blame] | 181 | |
| 182 | This queue is the same as if two new changes had just arrived, so Zuul |
| 183 | starts the process again testing *D* against the tip of the branch, and |
Antoine Musso | 3a43e14 | 2013-10-30 23:51:58 +0100 | [diff] [blame] | 184 | *E* against *D*: |
| 185 | |
| 186 | .. blockdiag:: |
| 187 | |
| 188 | blockdiag foo { |
| 189 | node_width = 40; |
| 190 | span_width = 40; |
| 191 | master -> D -> E; |
| 192 | group jobs_for_D { |
| 193 | label = "Merged changes for D"; |
| 194 | master -> D; |
| 195 | } |
| 196 | group ignored_to_test_D { |
| 197 | label = "Skip"; |
| 198 | color = "lightgray"; |
| 199 | E; |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | .. blockdiag:: |
| 204 | |
| 205 | blockdiag foo { |
| 206 | node_width = 40; |
| 207 | span_width = 40; |
| 208 | group jobs_for_E { |
| 209 | label = "Merged changes for E"; |
| 210 | master -> D -> E; |
| 211 | } |
| 212 | } |
| 213 | |
Antoine Musso | 5586753 | 2014-01-10 18:24:35 +0100 | [diff] [blame] | 214 | |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 215 | Cross Project Testing |
| 216 | --------------------- |
Antoine Musso | 5586753 | 2014-01-10 18:24:35 +0100 | [diff] [blame] | 217 | |
| 218 | When your projects are closely coupled together, you want to make sure |
| 219 | changes entering the gate are going to be tested with the version of |
| 220 | other projects currently enqueued in the gate (since they will |
| 221 | eventually be merged and might introduce breaking features). |
| 222 | |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 223 | Such relationships can be defined in Zuul configuration by registering |
| 224 | a job in a DependentPipeline of several projects. Whenever a change |
| 225 | enters such a pipeline, it will create references for the other |
| 226 | projects as well. As an example, given a main project ``acme`` and a |
| 227 | plugin ``plugin`` you can define a job ``acme-tests`` which should be |
| 228 | run for both projects: |
Antoine Musso | 5586753 | 2014-01-10 18:24:35 +0100 | [diff] [blame] | 229 | |
| 230 | .. code-block:: yaml |
| 231 | |
| 232 | pipelines: |
| 233 | - name: gate |
| 234 | manager: DependentPipelineManager |
| 235 | |
| 236 | projects:: |
| 237 | - name: acme |
| 238 | gate: |
| 239 | - acme-tests |
| 240 | - name: plugin |
| 241 | gate: |
| 242 | - acme-tests # Register job again |
| 243 | |
| 244 | Whenever a change enters the ``gate`` pipeline queue, Zuul creates a reference |
| 245 | for it. For each subsequent change, an additional reference is created for the |
| 246 | changes ahead in the queue. As a result, you will always be able to fetch the |
| 247 | future state of your project dependencies for each change in the queue. |
| 248 | |
| 249 | Based on the pipeline and project definitions above, three changes are |
| 250 | inserted in the ``gate`` pipeline with the associated references: |
| 251 | |
| 252 | ======== ======= ====== ========= |
| 253 | Change Project Branch Zuul Ref. |
| 254 | ======== ======= ====== ========= |
| 255 | Change 1 acme master master/Z1 |
| 256 | Change 2 plugin stable stable/Z2 |
| 257 | Change 3 plugin master master/Z3 |
| 258 | ======== ======= ====== ========= |
| 259 | |
| 260 | Since the changes enter a DependentPipelineManager pipeline, Zuul creates |
| 261 | additional references: |
| 262 | |
| 263 | ====== ======= ========= ============================= |
| 264 | Change Project Zuul Ref. Description |
| 265 | ====== ======= ========= ============================= |
| 266 | 1 acme master/Z1 acme master + change 1 |
| 267 | ------ ------- --------- ----------------------------- |
| 268 | 2 acme master/Z2 acme master + change 1 |
| 269 | 2 plugin stable/Z2 plugin stable + change 2 |
| 270 | ------ ------- --------- ----------------------------- |
| 271 | 3 acme master/Z3 acme master + change 1 |
| 272 | 3 plugin stable/Z3 plugin stable + change 2 |
| 273 | 3 plugin master/Z3 plugin master + change 3 |
| 274 | ====== ======= ========= ============================= |
| 275 | |
| 276 | In order to test change 3, you would clone both repositories and simply |
| 277 | fetch the Z3 reference for each combination of project/branch you are |
| 278 | interested in testing. For example, you could fetch ``acme`` with |
| 279 | master/Z3 and ``plugin`` with master/Z3 and thus have ``acme`` with |
| 280 | change 1 applied as the expected state for when Change 3 would merge. |
| 281 | When your job fetches several repositories without changes ahead in the |
| 282 | queue, they may not have a Z reference in which case you can just check |
| 283 | out the branch. |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 284 | |
| 285 | |
| 286 | Cross Repository Dependencies |
| 287 | ----------------------------- |
| 288 | |
| 289 | Zuul permits users to specify dependencies across repositories. Using |
| 290 | a special header in Git commit messages, Users may specify that a |
| 291 | change depends on another change in any repository known to Zuul. |
| 292 | |
| 293 | Zuul's cross-repository dependencies (CRD) behave like a directed |
| 294 | acyclic graph (DAG), like git itself, to indicate a one-way dependency |
| 295 | relationship between changes in different git repositories. Change A |
| 296 | may depend on B, but B may not depend on A. |
| 297 | |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 298 | To use them, include ``Depends-On: <gerrit-change-id>`` in the footer of |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 299 | a commit message. Use the full Change-ID ('I' + 40 characters). |
| 300 | |
| 301 | |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 302 | Dependent Pipeline |
| 303 | ~~~~~~~~~~~~~~~~~~ |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 304 | |
| 305 | When Zuul sees CRD changes, it serializes them in the usual manner when |
| 306 | enqueuing them into a pipeline. This means that if change A depends on |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 307 | B, then when they are added to a dependent pipeline, B will appear first |
| 308 | and A will follow: |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 309 | |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 310 | .. blockdiag:: |
| 311 | :align: center |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 312 | |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 313 | blockdiag crd { |
| 314 | orientation = portrait |
| 315 | span_width = 30 |
| 316 | class greendot [ |
| 317 | label = "", |
| 318 | shape = circle, |
| 319 | color = green, |
| 320 | width = 20, height = 20 |
| 321 | ] |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 322 | |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 323 | A_status [ class = greendot ] |
| 324 | B_status [ class = greendot ] |
| 325 | B_status -- A_status |
| 326 | |
| 327 | 'Change B\nChange-Id: Iabc' <- 'Change A\nDepends-On: Iabc' |
| 328 | } |
| 329 | |
| 330 | If tests for B fail, both B and A will be removed from the pipeline, and |
| 331 | it will not be possible for A to merge until B does. |
| 332 | |
| 333 | |
| 334 | .. note:: |
| 335 | |
| 336 | If changes with CRD do not share a change queue then Zuul is unable |
| 337 | to enqueue them together, and the first will be required to merge |
| 338 | before the second is enqueued. |
| 339 | |
| 340 | Independent Pipeline |
| 341 | ~~~~~~~~~~~~~~~~~~~~ |
| 342 | |
| 343 | When changes are enqueued into an independent pipeline, all of the |
| 344 | related dependencies (both normal git-dependencies that come from parent |
| 345 | commits as well as CRD changes) appear in a dependency graph, as in a |
| 346 | dependent pipeline. This means that even in an independent pipeline, |
| 347 | your change will be tested with its dependencies. So changes that were |
| 348 | previously unable to be fully tested until a related change landed in a |
| 349 | different repository may now be tested together from the start. |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 350 | |
| 351 | All of the changes are still independent (so you will note that the |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 352 | whole pipeline does not share a graph as in a dependent pipeline), but |
| 353 | for each change tested, all of its dependencies are visually connected |
| 354 | to it, and they are used to construct the git references that Zuul uses |
| 355 | when testing. |
| 356 | |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 357 | When looking at this graph on the status page, you will note that the |
| 358 | dependencies show up as grey dots, while the actual change tested shows |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 359 | up as red or green (depending on the jobs results): |
| 360 | |
| 361 | .. blockdiag:: |
| 362 | :align: center |
| 363 | |
| 364 | blockdiag crdgrey { |
| 365 | orientation = portrait |
| 366 | span_width = 30 |
| 367 | class dot [ |
| 368 | label = "", |
| 369 | shape = circle, |
| 370 | width = 20, height = 20 |
| 371 | ] |
| 372 | |
| 373 | A_status [class = "dot", color = green] |
| 374 | B_status [class = "dot", color = grey] |
| 375 | B_status -- A_status |
| 376 | |
| 377 | "Change B" <- "Change A\nDepends-On: B" |
| 378 | } |
| 379 | |
| 380 | This is to indicate that the grey changes are only there to establish |
| 381 | dependencies. Even if one of the dependencies is also being tested, it |
| 382 | will show up as a grey dot when used as a dependency, but separately and |
| 383 | additionally will appear as its own red or green dot for its test. |
| 384 | |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 385 | |
| 386 | Multiple Changes |
| 387 | ~~~~~~~~~~~~~~~~ |
| 388 | |
| 389 | A Gerrit change ID may refer to multiple changes (on multiple branches |
| 390 | of the same project, or even multiple projects). In these cases, Zuul |
| 391 | will treat all of the changes with that change ID as dependencies. So |
| 392 | if you say that change in project A Depends-On a change ID that has |
| 393 | changes in two branches of project B, then when testing the change to |
| 394 | project A, both project B changes will be applied, and when deciding |
| 395 | whether the project A change can merge, both changes must merge ahead |
| 396 | of it. |
| 397 | |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 398 | .. blockdiag:: |
| 399 | :align: center |
| 400 | |
| 401 | blockdiag crdmultirepos { |
| 402 | orientation = portrait |
| 403 | span_width = 30 |
| 404 | class greendot [ |
| 405 | label = "", |
| 406 | shape = circle, |
| 407 | color = green, |
| 408 | width = 20, height = 20 |
| 409 | ] |
| 410 | |
| 411 | B_stable_status [ class = "greendot" ] |
| 412 | B_master_status [ class = "greendot" ] |
| 413 | A_status [ class = "greendot" ] |
| 414 | B_stable_status -- B_master_status -- A_status |
| 415 | |
| 416 | A [ label = "Repo A\nDepends-On: I123" ] |
| 417 | group { |
| 418 | orientation = portrait |
| 419 | label = "Dependencies" |
| 420 | color = "lightgray" |
| 421 | |
| 422 | B_stable [ label = "Repo B\nChange-Id: I123\nBranch: stable" ] |
| 423 | B_master [ label = "Repo B\nChange-Id: I123\nBranch: master" ] |
| 424 | } |
| 425 | B_master <- A |
| 426 | B_stable <- A |
| 427 | |
| 428 | } |
| 429 | |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 430 | A change may depend on more than one Gerrit change ID as well. So it |
| 431 | is possible for a change in project A to depend on a change in project |
Antoine Musso | 281a89e | 2015-09-30 15:05:49 +0200 | [diff] [blame^] | 432 | B and a change in project C. Simply add more ``Depends-On:`` lines to |
| 433 | the commit message footer. |
| 434 | |
| 435 | .. blockdiag:: |
| 436 | :align: center |
| 437 | |
| 438 | blockdiag crdmultichanges { |
| 439 | orientation = portrait |
| 440 | span_width = 30 |
| 441 | class greendot [ |
| 442 | label = "", |
| 443 | shape = circle, |
| 444 | color = green, |
| 445 | width = 20, height = 20 |
| 446 | ] |
| 447 | |
| 448 | C_status [ class = "greendot" ] |
| 449 | B_status [ class = "greendot" ] |
| 450 | A_status [ class = "greendot" ] |
| 451 | C_status -- B_status -- A_status |
| 452 | |
| 453 | A [ label = "Repo A\nDepends-On: I123\nDepends-On: Iabc" ] |
| 454 | group { |
| 455 | orientation = portrait |
| 456 | label = "Dependencies" |
| 457 | color = "lightgray" |
| 458 | |
| 459 | B [ label = "Repo B\nChange-Id: I123" ] |
| 460 | C [ label = "Repo C\nChange-Id: Iabc" ] |
| 461 | } |
| 462 | B, C <- A |
| 463 | } |
James E. Blair | 096a80a | 2015-09-28 14:19:31 -0700 | [diff] [blame] | 464 | |
| 465 | Cycles |
| 466 | ~~~~~~ |
| 467 | |
| 468 | If a cycle is created by use of CRD, Zuul will abort its work very |
| 469 | early. There will be no message in Gerrit and no changes that are part |
| 470 | of the cycle will be enqueued into any pipeline. This is to protect |
| 471 | Zuul from infinite loops. |