GitHub Actions CI/CD with Yarn, Composer, phpcs, phpunit and built branches

GitHub recently launched the new Actions CI/CD feature (which is still in beta so you may need to request access) which allows you to run the usual CI/CD checks and builds that you would normally use via a third-party platform. The documentation is still a little light at the time of writing this, but below we’ve documented how to set the usual tasks:

  • Yarn/npm install & build
  • Composer install
  • phpcs (with WP & VIP Coding Standard)
  • phpunit
  • committing the built code to a {branch}-built

To get started you’ll need to create a .github folder in your repository root. Inside, you should create another folder, called workflows. You can use multiple workflows, but for this example we only need one so we will create a file main.yml. The next step is to add the following to the file:

name: CI Build
on: [push]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v1

    - name: Show files
      run: |
        ls -la

name is just what we want to call this workflow, and on is the action which triggers it (we will use push for now). Actions will run on a VM, therefore, you will need to specify the type. runs-on lets you select Linux, Windows, OSX. We will use Ubuntu as it provides the most flexible solution for us.

Under steps you should add each task you will need to carry out. There are two types of steps, one that uses an action and the other is a terminal command. The action type Checkout Repository calls an action to check out the repository. These work by pointing to a GitHub repository:

uses: {org}/{repo}@{version}

The action organisation is managed by GitHub and you can find lots of actions there. The other step Show files runs a command, or it can be a list of commands each on a new line.

Ubuntu VM comes with a lot preinstalled including PHP, Yarn and Composer on there making the set up relatively quick and easy. The only other action step we will use it to control the Node version actions/setup-node@v1

name: CI Build
on: [push]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v1

    - name: Set Node version to 12
      uses: actions/setup-node@v1
      with:
        version: 12

    - name: Run yarn install
      run: |
        yarn install

    - name: Run yarn build
      run: |
        yarn run build --if-present

    - name: Run composer install
      run: |
        composer install --prefer-dist

    - name: Install PHPCS with WordPress Coding Standards
      run: |
        composer global require dealerdirect/phpcodesniffer-composer-installer:0.5.0 wp-coding-standards/wpcs:2.1.1 automattic/vipwpcs:2.0.0
    
    - name: Run PHPCS Coding Standards
      run: |
        /home/runner/.composer/vendor/bin/phpcs $GITHUB_WORKSPACE

    - name: Create a built branch
      run: |
        BRANCH_NAME=$(echo $GITHUB_REF | grep -oP '(?<=refs\/heads\/).*')
        rm .gitignore
        mv .deployignore .gitignore
        git config --global user.email "ci@company.com"
        git config --global user.name "Company CI"
        git remote set-url origin https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY.git
        git checkout -b $BRANCH_NAME-built
        git add -A && git commit -m "built from $GITHUB_SHA"
        git push --force -u origin $BRANCH_NAME-built

The above should seem similar to using any other CI, but a few things to note is the environment variables – you will find a list of them on the outdated docsGITHUB_WORKSPACE points to where your files will be, so it is needed when running certain tasks. The final step is a little bit more complex as we work out the branch name, remove the .gitignore to allow to commit files in the vendor and build folders. However, there are still things we will want to ignore, such as node modules, so we use .deployignoreGITHUB_TOKEN is passed down to us automatically, which allows us access to the GitHub API and allow the ability to write to the repository. If using something like Travis CI, you would need to manually provide access to each repository.

Unfortunately, there are a few issues with the above set up – we have no control over the PHP version and if we wanted to run PHPunit we would need set up MySQL. As it’s just Ubuntu, we can write a command to uninstall and reinstall a different version of PHP, and we could easily set up MySQL if needed. However, GitHub provides a better platform to extend and work with whatever set up you need, you can use Docker. Taking the simple example from earlier and adding Docker:

name: CI Build
on: [push]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    container:
      image: php:7.3-apache
      env:
        NODE_ENV: production
      ports:
      - 80
      volumes:
      - $GITHUB_WORKSPACE:/var/www/html
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v1

    - name: Show files
      run: |
        ls -la

We add the container section and select an image from Docker Hub. We will use the official PHP image php:7.3-apacheenvwhich allows us to set any environment variables we might need, in this case, I set the NODE_ENV to production. Then portslets us map ports from host to the container, which in this case 80 is mapped to 80. Then lastly, we can point to the workspace to apache root.

Now that we are using a Docker image we no longer have Yarn and Composer installed by default. So we must do some setup of the container:

name: CI Build
on: [push]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    container:
      image: php:7.3-apache
      env:
        NODE_ENV: production
      ports:
      - 80
      volumes:
      - $GITHUB_WORKSPACE:/var/www/html
    steps:
    - name: Set up container
      run: |
        echo "Update package lists."
        apt-get update
        echo "Install base packages."
        apt-get install -y build-essential libssl-dev gnupg libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libicu-dev libxml2-dev vim wget unzip git subversion default-mysql-client
        echo "Add yarn package repository."
        curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
        echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
        echo "Update package lists."
        apt-get update
        echo "Install NVM."
        curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
        . ~/.nvm/nvm.sh
        echo "Install node."
        nvm install 12.9.0
        nvm use 12.9.0
        echo "Install yarn."
        apt-get install -y yarn
        echo "Install composer."
        curl -sS https://getcomposer.org/installer | php 
        mv composer.phar /usr/local/bin/composer
        echo "Install PHP extensions."
        docker-php-ext-install -j$(nproc) iconv intl xml soap opcache pdo pdo_mysql mysqli mbstring
        docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/
        docker-php-ext-install -j$(nproc) gd
        pecl install mcrypt-1.0.2
        docker-php-ext-enable mcrypt

    - name: Checkout repository
      uses: actions/checkout@v1

    - name: Run yarn install
      run: |
        yarn install

    - name: Run yarn build
      run: |
        yarn run build --if-present

    - name: Run composer install
      run: |
        composer install --prefer-dist

    - name: Install PHPCS with WordPress Coding Standards
      run: |
        composer global require dealerdirect/phpcodesniffer-composer-installer:0.5.0 wp-coding-standards/wpcs:2.1.1 automattic/vipwpcs:2.0.0
    
    - name: Run PHPCS Coding Standards
      run: |
        ~/.composer/vendor/bin/phpcs $GITHUB_WORKSPACE

    - name: Create a built branch
      run: |
        BRANCH_NAME=$(echo $GITHUB_REF | grep -oP '(?<=refs\/heads\/).*')
        rm .gitignore
        mv .deployignore .gitignore
        git config --global user.email "ci@company.com"
        git config --global user.name "Company CI"
        git remote set-url origin https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY.git
        git checkout -b $BRANCH_NAME-built
        git add -A && git commit -m "built from $GITHUB_SHA"
        git push --force -u origin $BRANCH_NAME-built

So we install some base packages then NVM to allow use to select a Node version, then yarn and composer. The last section add PHP extensions, this can be very slow so only do so if needed:

echo "Install PHP extensions."
docker-php-ext-install -j$(nproc) iconv intl xml soap opcache pdo pdo_mysql mysqli mbstring
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/
docker-php-ext-install -j$(nproc) gd
pecl install mcrypt-1.0.2
docker-php-ext-enable mcrypt

After that, the steps are pretty much the same. Now to add MySQL we could install in this current container (but that’s not how Docker is meant to be used) so instead we add service, which is other Docker containers. Taking the short example from earlier:

name: CI Build
on: [push]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    container:
      image: php:7.3-apache
      env:
        NODE_ENV: production
      ports:
      - 80
      volumes:
      - $GITHUB_WORKSPACE:/var/www/html
    services:
      mysql:
        image: mysql:5.7.27
        env:
          MYSQL_ROOT_PASSWORD: root
        ports:
        - 3306
        volumes:
        - $HOME/mysql:/var/lib/mysql
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v1

    - name: Show files
      run: |
        ls -la

Adding the services section allows us to add a list of named containers. In this case, we will just be using one, so we will name it mysql and specify the image mysql:5.7.27 which is another official image. The rest of the setup is setting MySQL password and setting the correct port and volume. So now we can add PHPUnit:

name: CI Build
on: [push]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    container:
      image: php:7.3-apache
      env:
        NODE_ENV: production
      ports:
      - 80
      volumes:
      - $GITHUB_WORKSPACE:/var/www/html
    services:
      mysql:
        image: mysql:5.7.27
        env:
          MYSQL_ROOT_PASSWORD: root
        ports:
        - 3306
        volumes:
        - $HOME/mysql:/var/lib/mysql
    steps:
    - name: Set up container
      run: |
        echo "Update package lists."
        apt-get update
        echo "Install base packages."
        apt-get install -y build-essential libssl-dev gnupg libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libicu-dev libxml2-dev vim wget unzip git subversion default-mysql-client
        echo "Add yarn package repository."
        curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
        echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
        echo "Update package lists."
        apt-get update
        echo "Install NVM."
        curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
        . ~/.nvm/nvm.sh
        echo "Install node."
        nvm install 12.9.0
        nvm use 12.9.0
        echo "Install yarn."
        apt-get install -y yarn
        echo "Install composer."
        curl -sS https://getcomposer.org/installer | php 
        mv composer.phar /usr/local/bin/composer
        echo "Install PHP extensions."
        docker-php-ext-install -j$(nproc) iconv intl xml soap opcache pdo pdo_mysql mysqli mbstring
        docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/
        docker-php-ext-install -j$(nproc) gd
        pecl install mcrypt-1.0.2
        docker-php-ext-enable mcrypt

    - name: Checkout repository
      uses: actions/checkout@v1

    - name: Run yarn install
      run: |
        yarn install

    - name: Run yarn build
      run: |
        yarn run build --if-present

    - name: Run composer install
      run: |
        composer install --prefer-dist

    - name: Install WordPress Test Suite
      run: |
        bash bin/install-wp-tests.sh wordpress_test root root mysql 5.2.2

    - name: Install PHPUnit
      run: | 
        composer global require "phpunit/phpunit=7.5.15"

    - name: Run PHPUnit
      run: |
        ~/.composer/vendor/bin/phpunit $GITHUB_WORKSPACE

    - name: Install PHPCS with WordPress Coding Standards
      run: |
        composer global require dealerdirect/phpcodesniffer-composer-installer:0.5.0 wp-coding-standards/wpcs:2.1.1 automattic/vipwpcs:2.0.0
    
    - name: Run PHPCS Coding Standards
      run: |
        ~/.composer/vendor/bin/phpcs $GITHUB_WORKSPACE

    - name: Create a built branch
      run: |
        BRANCH_NAME=$(echo $GITHUB_REF | grep -oP '(?<=refs\/heads\/).*')
        rm .gitignore
        mv .deployignore .gitignore
        git config --global user.email "ci@company.com"
        git config --global user.name "Company CI"
        git remote set-url origin https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY.git
        git checkout -b $BRANCH_NAME-built
        git add -A && git commit -m "built from $GITHUB_SHA"
        git push --force -u origin $BRANCH_NAME-built

The last remaining issue we have is the above, and luckily is the easiest to update. If you wanted to change the version of PHP or even if you needed to test against two versions of PHP or WordPress, GitHub provides a solution to this called a matrix. You can add variables which can be one value or a list of values, and if a list is provided the action is run once for each value. For example, if you provided two versions of PHP and two versions of WordPress it would run a total of four times. Taking the short example from earlier:

name: CI Build
on: [push]

jobs:
  build:
    name: Build
    strategy:
      matrix:
        php: [7.3,7.2]
    runs-on: ubuntu-latest
    container:
      image: php:${{ matrix.php }}-apache
      env:
        NODE_ENV: production
      ports:
      - 80
      volumes:
      - $GITHUB_WORKSPACE:/var/www/html
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v1

    - name: Show files
      run: |
        ls -la

Adding a strategy section with a matrix section within. We then add each variable, in this case, I used php and give it two values, which I can later use by calling ${{ matrix.php }}. Adding this to the final example you should get:

name: CI Build
on: [push]

jobs:
  build:
    name: Build
    strategy:
      matrix:
        php: [7.3]
        node: [12.9.0]
        mysql: [5.7.27]
        wordpress: [5.2.2]
        phpunit: [7.5.15]
        wpcs: [2.1.1]
        vipcs: [2.0.0]
    runs-on: ubuntu-latest
    container:
      image: php:${{ matrix.php }}-apache
      env:
        NODE_ENV: production
      ports:
      - 80
      volumes:
      - $GITHUB_WORKSPACE:/var/www/html
    services:
      mysql:
        image: mysql:${{ matrix.mysql }}
        env:
          MYSQL_ROOT_PASSWORD: root
        ports:
        - 3306
        volumes:
        - $HOME/mysql:/var/lib/mysql
    steps:
    - name: Set up container
      run: |
        echo "Update package lists."
        apt-get update
        echo "Install base packages."
        apt-get install -y build-essential libssl-dev gnupg libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libicu-dev libxml2-dev vim wget unzip git subversion default-mysql-client
        echo "Add yarn package repository."
        curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
        echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
        echo "Update package lists."
        apt-get update
        echo "Install NVM."
        curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
        . ~/.nvm/nvm.sh
        echo "Install node."
        nvm install ${{ matrix.node }}
        nvm use ${{ matrix.node }}
        echo "Install yarn."
        apt-get install -y yarn
        echo "Install composer."
        curl -sS https://getcomposer.org/installer | php 
        mv composer.phar /usr/local/bin/composer
        echo "Install PHP extensions."
        docker-php-ext-install -j$(nproc) iconv intl xml soap opcache pdo pdo_mysql mysqli mbstring
        docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/
        docker-php-ext-install -j$(nproc) gd
        pecl install mcrypt-1.0.2
        docker-php-ext-enable mcrypt

    - name: Checkout repository
      uses: actions/checkout@v1

    - name: Run yarn install
      run: |
        yarn install

    - name: Run yarn build
      run: |
        yarn run build --if-present

    - name: Run composer install
      run: |
        composer install --prefer-dist

    - name: Install WordPress Test Suite
      run: |
        bash bin/install-wp-tests.sh wordpress_test root root mysql ${{ matrix.wordpress }}

    - name: Install PHPUnit
      run: | 
        composer global require "phpunit/phpunit=${{ matrix.phpunit }}"

    - name: Run PHPUnit
      run: |
        ~/.composer/vendor/bin/phpunit $GITHUB_WORKSPACE

    - name: Install PHPCS with WordPress Coding Standards
      run: |
        composer global require dealerdirect/phpcodesniffer-composer-installer:0.5.0 wp-coding-standards/wpcs:${{ matrix.wpcs }} automattic/vipwpcs:${{ matrix.vipcs }}
    
    - name: Run PHPCS Coding Standards
      run: |
        ~/.composer/vendor/bin/phpcs $GITHUB_WORKSPACE

    - name: Create a built branch
      run: |
        BRANCH_NAME=$(echo $GITHUB_REF | grep -oP '(?<=refs\/heads\/).*')
        rm .gitignore
        mv .deployignore .gitignore
        git config --global user.email "ci@company.com"
        git config --global user.name "Company CI"
        git remote set-url origin https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY.git
        git checkout -b $BRANCH_NAME-built
        git add -A && git commit -m "built from $GITHUB_SHA"
        git push --force -u origin $BRANCH_NAME-built

Hopefully, this will get you started in the right direction, however, if you need more information, you can find some documentation here. I’m sure the documentation from GitHub will improve and we will start seeing more example in the wild. If you do know ways improving my examples feel free to reach out.