Detailed Description of LUCI Swarmbucket CQ Flow

Gerrit -> CQ -> Buildbucket

What to do with a CL is defined by a cq.cfg (example) file. In particular, it says what builders to trigger in what buckets. Each build is parameterized with a JSON blob. buildset is a string of the form patch/gerrit/chromium-review.googlesource.com/<CL id>/<Patch id>. We can use this form to search for builds from a CL using a query like this.

Here are buildbucket builds that correspond to the CL in the results from the query above. Buildbucket field parameters_json contains a string that looks like this:

{
  "builder_name": "Infra Win Tester",
  "changes": [
    {
      "author": {
        "email": "martiniss@chromium.org"
      },
      "repo_url": "https:\/\/chromium.googlesource.com\/infra\/infra"
    }
  ],
  "properties": {
    "attempt_start_ts": 1.472083514e+15,
    "category": "cq",
    "event.change.id": "infra%2Finfra~master~I2582d437489ec9688788cbaea026d8eba7e790c3",
    "event.change.number": 375219,
    "event.change.url": "https:\/\/chromium-review.googlesource.com\/#\/c\/375219",
    "event.patchSet.ref": "refs\/changes\/19\/375219\/1",
    "gerrit": "https:\/\/chromium-review.googlesource.com",
    "master": "luci.infra.try",
    "patch_project": "infra\/infra",
    "patch_storage": "gerrit",
    "reason": "CQ",
    "revision": "HEAD"
  }
}

properties are generated in get_try_job_properties here and they all end up available from recipes, as you'll see below.

Buildbucket -> Swarming

At this point we have a Buildbucket build posted to the buildbucket that has a JSON blob of particular form as its parameters_json field.

Configuration for buckets are defined in cr-buildbucket.cfg. cq.cfg and cr-buildbucket.cfg are in different locations because bucket config is same for entire repo/project, while there could be several CQs for individual branches in the repo. As an example:

buckets {
  name: "luci.infra.try"
  
  swarming {
    hostname: "chromium-swarm.appspot.com"
    url_format: "https://luci-milo.appspot.com/swarming/task/{task_id}"
    builder_defaults {
      swarming_tags: "allow_milo:1"
      dimensions: "cpu:x86-64"
      dimensions: "pool:Chrome"
      recipe {
        repository: "https://chromium.googlesource.com/infra/infra"
      }
      execution_timeout_secs: 1800  # 30 min
    }

    ...

    builders {
      category: "Infra"
      name: "Infra Win Tester"
      dimensions: "os:Windows-7-SP1"
      recipe {
        name: "infra_repo_trybot"
      }
    }

    ...
  }
}

Here's the corresponding *.proto file.

Let's follow the build parameters. Looks like parameters_json is immediately deserialized into parameters field of the Build object. Then the service looks at the bucket config and builder_name parameter in parameters and figures out whether it needs to start a Swarming job (by searching for Swarming.Build messages in the bucket proto in cr-buildbucket.cfg).

And we end up in create_task_async that looks scary. It fetches Swarming and Builder proto from corresponding cr-buildbucket.cfg config and calls create_task_def_async (that looks even scarier) to convert build parameters and all configs into Swarming task definition.

The function merges builders proto with builder_defaults proto with build.parameters['swarming'] (that is missing in our example) into something that looks like this:

builder {
  swarming_tags: "allow_milo:1"
  
  dimensions: "cpu:x86-64"
  dimensions: "pool:Chrome"
  dimensions: "os:Windows-7-SP1"

  recipe {
    name: "infra_repo_trybot"
    repository: "https://chromium.googlesource.com/infra/infra"
  }
  
  execution_timeout_secs: 1800
}

Then it sees recipe section and does some strange transformations to come up with something called properties_json. In particular, all build.parameters['properties'] (generated by CQ at the very start) end up in properties_json as is. E.g. properties_json looks like:

{
  <stuff>
  "attempt_start_ts": 1.472083514e+15,
  "category": "cq",
  "event.change.id": "infra%2Finfra~master~I2582d437489ec9688788cbaea026d8eba7e790c3",
  "event.change.number": 375219,
  "event.change.url": "https:\/\/chromium-review.googlesource.com\/#\/c\/375219",
  "event.patchSet.ref": "refs\/changes\/19\/375219\/1",
  "gerrit": "https:\/\/chromium-review.googlesource.com",
  "master": "luci.infra.try",
  "patch_project": "infra\/infra",
  "patch_storage": "gerrit",
  "reason": "CQ",
  "revision": "HEAD"
}

Next Buildbucket fetches something called “task template” from global Buildbucket config. Here it is. It then renders this template using the data generated above. Relevant chunk of the template:

"command": [
  "kitchen${EXECUTABLE_SUFFIX}",
  "cook",
  "-python-path", "third_party",
  "-timestamps",
  "-repository", "$repository",
  "-revision", "$revision",
  "-recipe", "$recipe",
  "-properties", "$properties_json",
  "-logdog-host", "luci-logdog.appspot.com",
  "-logdog-project", "$project",
  "-logdog-tee",
  "-logdog-enable-annotee"
],

Here $repository comes from cr-buildbucket.cfg (“https://chromium.googlesource.com/infra/infra” in our case) $revision comes from build.properties['swarming']['recipe']['revision'] (empty in our case) $recipe comes from cr-buildbucket.cfg (“infra_repo_trybot” in our case). $properties_json is JSON blob I‘ve described above (the one that contains data from CQ). $project is indirectly derived from cr-buildbucket.cfg (it’s the name of the project that defined the bucket, “infra” in our case). So we end up with a Swarming task that invokes “kitchen” tool, passing it a JSON blob with properties. Here is the example: https://chromium-swarm.appspot.com/user/task/30d7d5a2288f9710

Swarming -> Recipe engine (through kitchen)

Kitchen is a tool that bootstraps recipe engine and its environment. It‘s source code is here. Let’s follow the build properties.

Kitchen reads them from the command line and appends one more property “path_config”: “swarmbucket”.

It then checks out $repository at $revision. In our case “https://chromium.googlesource.com/infra/infra” at FETCH_HEAD.

Next it prepares a command to invoke recipe engine. Reads “infra/config/recipes.cfg” from the checked out repo (in our case this file), there it finds “recipes_path” field (“recipes” in our case). It uses it to construct a path to recipes.py file. This one.

The final command is python ...../recipes.py run --properties-file ...

(In our case “” is “infra_repo_trybot”).

--properties-file points to a JSON file with serialized build properties, in our case it looks like this:

{
  "attempt_start_ts": 1.472083514e+15,
  "blamelist": [
    "martiniss@chromium.org"
  ],
  "buildername": "Infra Win Tester",
  "category": "cq",
  "event.change.id": "infra%2Finfra~master~I2582d437489ec9688788cbaea026d8eba7e790c3",
  "event.change.number": 375219,
  "event.change.url": "https:\/\/chromium-review.googlesource.com\/#\/c\/375219",
  "event.patchSet.ref": "refs\/changes\/19\/375219\/1",
  "gerrit": "https:\/\/chromium-review.googlesource.com",
  "master": "luci.infra.try",
  "patch_project": "infra\/infra",
  "patch_storage": "gerrit",
  "path_config": "swarmbucket",
  "reason": "CQ",
  "repository": "https:\/\/chromium.googlesource.com\/infra\/infra",
  "revision": "HEAD"
}

As a reminder, it came directly from CQ, with few fields like buildername, blamelist and category added by Swarmbucket code.

Recipe engine -> recipes & recipe modules

recipes.py checks out recipe_engine code and all dependent modules (based on recipes.cfg) and then it passes control to the actual recipe.

The properties are available via api.properties dictionary. (or self.m.properties if used from inside some recipe module). Documentation on properties in recipes is here.

For example event.patchSet.ref property (which is refs/changes/19/375219/1 in our case, as supplied by CQ) is transformed into gerrit_ref parameter of bot_update recipe module. So bot_update recipe module ends up “knowing” about CL ref as self._gerrit_ref (and it then passes it to actual bot_update.py script).