Four Kitchens
Insights

Running CircleCI builds based on many repositories

4 Min. ReadDevelopment

Recently I was working in a Drupal 8 project and we were using the improved Features module to create configuration container modules with some special purposes. Due to client architectural needs, we had to move the /features folder into a separate repository. We basically needed to make it available to many sites in a way we could keep doing active development over it, and we did so by making the new repo a composer dependency of all our projects.

 
The Features folder of each project is pulled from an additional repository using composer.

One of the downsides of this new direction was the effects in CircleCI builds for individual projects, since installing and reverting features was an important part of it. For example, to make a new feature module available, we’d push it to this ‘shared’ repo, but to actually enable it we’d need to push the bit change in the core.extension.yml config file to our project repo. Yes, we were using a mixed approach: both features and conventional configuration management.

So a new pull request would be created in both repositories. The problem for Circle builds—given the approach previously outlined—is that builds generated for the pull request in the project repository would require the master branch of the ‘shared’ one. So, for the pull request in the project repo, we’d try to build a site by importing configuration that says a particular feature module should be enabled, and that module wouldn’t exist (likely not present in shared master at that time, still a pull request), so it would totally crash.

 
If you always pull the shared master and you have pushed changes to both repos that depend on each other, builds will be useless.

There is probably no straightforward way to solve this problem, but we came with a solution that is half code, half strategy. Beyond technical details, there is no practical way to determine what branch of the shared repo should be required for a pull request in the project repo, unless we assume conventions. In our case, we assumed that the correct branch to pair with a project branch was one named the same way. So if a build was a result of a pull request from branch X, we could try to find a PR from branch X in the shared repo and if it existed, that’d be our guy. Otherwise we’d keep pulling master.

 
Composer will always use the shared master, unless we can find a branch in the shared repo named like the one the build is running over.

So we created a script to do that:

<?php

$branch = $argv[1];
$github_token = $argv[2];
$github_user = $argv[3];
$project_user = $argv[4];
$shared_repos = array(
  'organization/shared'
);

foreach ($shared_repos as $repo) {
  print_r("Checking repo $repo for a pull request in a '$branch' branch...\n");
  $pr = getPRObjectFromBranch($branch, $github_token, $github_user, $project_user, $repo);

if (!empty($pr)) {
    print_r("Found. Requiring...\n");
    exec("composer require $repo:dev-$branch");
    print_r("$repo:dev-$branch pulled.\n");
  }
  else {
    print_r("Nothing found.\n");
  }
}

function getPRObjectFromBranch($branch_name, $github_token, $github_user, $project_user, $repo) {
  $ch = curl_init();  
  curl_setopt($ch,CURLOPT_URL,"https://api.github.com/repos/$repo/pulls?head=$project_user:$branch_name");
  curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
  curl_setopt($ch, CURLOPT_USERPWD, "$github_user:$github_token");
  curl_setopt($ch, CURLOPT_USERAGENT, "$github_user");

$output=json_decode(curl_exec($ch), TRUE);
  curl_close($ch);
  return $output;
}

As you probably know, Circle builds are connected to the internet, so you can make remote requests. What we’re doing here is using the Github API in the middle of a build in the project repo to connect to our shared repo with cURL and try to find a pull request whose branch name matches the one we’re building over. If the request returned something then we can safely say there is a branch named the same way than the current one and with an open pull request in the shared repo, and we can require it.

What’s left for this to work is actually calling the script:

- php scripts/require_feature_branch.php "$CIRCLE_BRANCH" "$GITHUB_TOKEN" "$CIRCLE_USERNAME" "$CIRCLE_PROJECT_USERNAME"

We can do this at any point in circle.yml, since composer require will actually update the composer.json file, so any other composer interaction after executing the script should take your requirement in consideration. Notice that the shared repo will be required twice if you have the requirement in your composer.json file. You could safely remove it from there if you instruct to require the master branch when no matching branch has been found in the script, although this could have unintended effects in other types of environments, like for local development.

Note: A quick reference about the parameters passed to the script:

$GITHUB_TOKEN: #Generate from 
https://github.com/settings/tokens
$CIRCLE_*: #CircleCI vars, automatically available

[Editor’s Note: The post “Running CircleCI Builds Based on Many Repositories” was originally published on Joel Travieso’s Medium blog.]