Set up Circle CI to run phpcs on only changed files in a PR

Configure CircleCI so you can run phpcs on only changed files for a pull request.

Set up Circle CI to run phpcs on only changed files in a PR

Code reviews are awesome. Having code style standards are also awesome. Having to manually reject a PR because it violates agreed upon code styling sucks, because the implied question is “seriously, you haven’t run the linter before you committed / pushed to the branch?”, which can be difficult to hear from a human.

tl;dr

https://github.com/javorszky/circleci-calibration has the code. Look in the .circleci folder for the config.yml and phpcs.sh. Details below. Link at the end of the article again.

Computer says “no”

A far better approach is to have Github tell you that build failed. That build could be for whatever reason, but suddenly you, the human, isn’t blamed for the outcome.

We want to avoid seeing this:

Screenshot of a pull request on Github where the CircliCI build failed.

And we do want to see this:

Screenshot of a pull request on Github where the CircleCI build passed.

I worked around some of the weird things in Circle CI so I can use it to do a phpcs check on only the changed files between the feature branch and the base branch of a pull request.

PHPCS locally

You, or a pre-commit hook, would run phpcs on the list of changed files in the commit. To run it manually on a bunch of files, you’d do somthing like this:

$ phpcs --standard=WordPress-Extra file1.php file2.php dir1/file3.php

To run it on a pull request, you want to get the list of changed files between your branch (feature branch), and the target branch. If your feature branch is called issue-1/fix-something and you PR it to master, you can get the list of changed php files like so:

git diff --name-only master..issue-1/fix-something -- '*.php'

Then you can put the two together, and run phpcs as such:

phpcs --standard=WordPress-Extra $(git diff --name-only master..issue-1/fix-something -- '*.php')

That assumes that you have phpcs and the WordPress-Extra ruleset installed, and they are available on your PATH.

PHPCS on Circle CI

In order to get the above to work, you need to install everything, and you also need to work around some weird git issues Circle CI gives you.

Install all the things

In your .circleci/config.yml file, you should have these at minimum:

version: 2.1
jobs:
  build:
    docker:
      - image: circleci/php:7.1-node-browsers

    steps:
      - checkout

      - run:
          name: Install Composer
          command: |
            php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
            php -r "if (hash_file('SHA384', 'composer-setup.php') === trim(file_get_contents('https://composer.github.io/installer.sig'))) { echo 'Installer verified'; } else { echo 'Installer invalid'; unlink('composer-setup.php'); } echo PHP_EOL;"
            php composer-setup.php
            php -r "unlink('composer-setup.php');"
      - run:
          name: Install project dependencies
          command: php composer.phar install
      - run:
          name: Running PHPCS
          command: bash ./.circleci/phpcs.sh

This will install composer, which you need to install phpcs, and then run the phpcs.sh file.

In the phpcs.sh file, you should have something like this:

Let’s break this down

First off, Circle CI gives us a number of awesome environment variables, like the PR url ($CIRCLE_PULL_REQUEST), the branch name ($CIRCLE_BRANCH), the Github user name and repo name ( $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME), and a bunch of others that we aren’t using.

Supposedly it also gives you the PR number (should be $CIRCLE_PR_NUMBER) according to https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables, but that env var is empty.

It also does not give you the target branch.

To get the list of changed files, we need the target branch. To get the target branch, we need the PR number and a personal access token. To get the PR number, we need to parse the PR url using regex.

This bit gives us the PR number:

regexp="[[:digit:]]\+$"
PR_NUMBER=`echo $CIRCLE_PULL_REQUEST | grep -o $regexp`

The regex means “one or more digits at the end of the string”. We need \+, because we need to escape + in grep’s regex implementation. See https://ss64.com/bash/grep-regex.html:

In basic regular expressions the metacharacters
? , + , { , | , ( , and  ) lose their special meaning;

instead use the backslashed versions:
\? ,\+ ,\{ ,\| ,\( , and \)

Now we have a PR number stored in the $PR_NUMBER local variable.

I’m going to assume you’ve created a personal access token (repo permissions, all 4, full access to private repositories) and have set it as GITHUB_TOKEN here: https://circleci.com/gh/your-gh-username/repo-name/edit#advanced-settings.

This bit gets us the base branch of the PR:

url="https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls/$PR_NUMBER"

target_branch=$(curl -s -X GET -G \
$url \
-d access_token=$GITHUB_TOKEN | jq '.base.ref' | tr -d '"')

It queries the Github API for that pull request, and parses the resulting json with the jq package (should be installed on the image we’re using), then selecting the base.ref node in the parsed json, and returns the value.

Get around git weirdness

We need the list of changed files, so we need the difference between base and feature branch. If the base is master, Circle CI does something odd that we need to undo.

In its checkout step, this is what happens when checking out branch issue/1 (shortened for relevance and brevity):

git clone "$CIRCLE_REPOSITORY_URL" .
git fetch --force origin "issue/1:remotes/origin/issue/1"
git reset --hard "$CIRCLE_SHA1"
git checkout -q -B "$CIRCLE_BRANCH"
git reset --hard "$CIRCLE_SHA1"

It checks out the repo into the empty working directory, force fetches origin via its refspec (see https://git-scm.com/docs/git-fetch), then on the default branch (happens to be master, which is also incidentally the base branch) checks out the commit that’s the tip of the PR, checks out the feature branch, and also resets that branch to the same commit.

Which means that the default branch (same as master, same as target branch in this case), and the feature branch are at the exact same commit, which means there is no difference between them, which means no list of changed files, which means running phpcs is moot.

To fix that, we need to check out target branch, reset the branch to where its remote is, go back to feature branch, and then get the list of changed files. And that’s what this piece of code does:

echo "Resetting $target_branch to where the remote version is..."
git checkout -q $target_branch

git reset --hard -q origin/$target_branch

git checkout -q $CIRCLE_BRANCH

echo "Getting list of changed files..."
changed_files=$(git diff --name-only $target_branch..$CIRCLE_BRANCH -- '*.php')

The rest of the script is grabbing coding standards, doing some sanity checks, and actually running phpcs.

Get the code here: https://github.com/javorszky/circleci-calibration

Photo by Gemma Evans on Unsplash