Setting up a solid Github CI/CD

Hello, nice to meet you πŸ‘‹

Session outline

  • Introduction to GitHub actions
  • Have a quick glance at the code
  • Setting up a repository
  • Configuring a CI workflow
  • Configuring a build & deploy workflow
  • Hungry for more? πŸ”

Demo repository

A fully working example is available at

CI/CD, what's up with that?

Acronyms for

  • Continuous integration
  • Continuous delivery / deployment

Continuous integration

DevOps best practice where developers frequently merge code changes into a central repository where automated builds and tests run.

Continuous delivery

An automated release process where you can deploy your application any time by clicking a button.

GitHub actions

GitHub actions

GitHub actions

A CI/CD platform that allows you to

  • Automate your tests
  • Automate your builds
  • Automate your deployment pipeline


  • Configurable automated process (YAML)
  • Triggered by an event, manually, or at a defined schedule
  • Will run one or more jobs


A runner is a server that runs your workflows when they're triggered

  • Ubuntu Linux
  • Microsoft Windows
  • macOS
  • Self-hosted


An event is a specific activity in a repository that triggers a workflow run

  • PR is created
  • Issue was openend
  • Commit was pushed


Contains a set of steps that execute on the same runner. Each step is either a

  • shell script
  • a pre-defined action

Steps are executed in order and are dependent on each other


                                name: learn-github-actions
                                on: [push] # Event
                                    name: install-dependencies
                                    runs-on: ubuntu-latest # Runner
                                        steps: # Actions
                                          - name: Setup PHP 8.1 with Xdebug 3.x # Pre-defined action
                                            uses: shivammathur/setup-php@v2
                                            php-version: '8.1'
                                            coverage: xdebug

                                          - name: Install dependencies # Shell script
                                            run: composer install --prefer-dist                    

The code

The code

Pizza anyone? πŸ•

Small and easy testable app to order pizza

  • Decorator pattern
  • Builder Pattern
  • Factory Pattern

Decorator pattern

                                $pizza = new ExtraCheese(new Pepperoni(new BasicPizza(Size::MEDIUM, Crust::THIN)));

                                // [
                                //   "amount": "1575",
                                //   "currency": "EUR"  
                                // ]
                                // Medium pizza, Thin crust, Tomato sauce, Cheese, Extra cheese, Pepperoni
Using decorators you can wrap objects countless number of times since both target objects and decorators follow the same interface. The resulting object will get a stacking behavior of all wrappers.

Builder pattern

                                $pizza = PizzaBuilder::fromSizeAndCrust(Size::MEDIUM, Crust::THIN)

                                  // [
                                  //   "amount": "1575",
                                  //   "currency": "EUR"  
                                  // ]
                                  // Medium pizza, Thin crust, Tomato sauce, Cheese, Extra cheese, Pepperoni                                 
Builder is a creational design pattern, which allows constructing complex objects step by step.

Factory pattern

                                  $pizza = PizzaFactory::pepperoni(Size::MEDIUM, Crust::THIN)

                                  // [
                                  //   "amount": "1575",
                                  //   "currency": "EUR"  
                                  // ]
                                  // Medium pizza, Thin crust, Tomato sauce, Cheese, Extra cheese, Pepperoni                                 
(Abstract) Factory is a creational design pattern, which solves the problem of creating entire product families without specifying their concrete classes.

Repository settings


Configure your repository

  • Set up a default branch
  • Add branch protection rules
  • Configure issue & PR templates (optional)

The default branch

You can set the default branch to whatever you want, but usually "main" or "master" are used.

Default branch

Branch protection rules

Important to ensure code quality and have a solid CI

  • Disable force pushing
  • Prevent branches from being deleted
  • Rrequire status checks before merging

Branch protection rules

Branch protection rules

All other options should stay unchecked... for now 😎

Issue & PR templates

Issue templates

Standardize the information for issues and pull requests in your repository

Issue & PR templates

New issue

CI Workflow


CI Workflow

Contains two jobs that should ensure code quality

  • Test suite
  • PHPStan & PHPcs
                                name: CI

1st job: Test suite

Install PHP and Xdebug

                                name: Setup PHP 8.1 with Xdebug 3.x
                                uses: shivammathur/setup-php@v2
                                  php-version: '8.1'
                                  coverage: xdebug                

Test suite

Pull in the code and install dependencies

                                - name: Checkout code
                                  uses: actions/checkout@v2
                                - name: Install dependencies
                                  run: composer install --prefer-dist             

Run test suite

                              - name: Run test suite
                                run: vendor/bin/phpunit --testsuite unit --fail-on-incomplete  --log-junit junit.xml --coverage-clover clover.xml         
  • --fail-on-incompleteForces PHPUnit to fail on incomplete tests
  • --log-junit junit.xmlGenerates XML file to publish the test results
  • --coverage-clover clover.xmlGenerates an XML file to check the test coverage

Visualise test results

                                - name: Publish test results
                                  uses: EnricoMi/publish-unit-test-result-action@v1.31
                                  if: always()
                                    files: "junit.xml"
                                    check_name: "Unit test results"       
Uni test results

Test coverage insights

with and generated clover.xml report

                                - name: Send test coverage to
                                  uses: codecov/codecov-action@v2.1.0
                                    files: clover.xml
                                    fail_ci_if_error: true # optional (default = false)
                                    verbose: true # optional (default = false)       

Test coverage insights

Codecov results

Minimum test coverage

Ensure minimum test coverage across the project.

                                - name: Check minimum required test coverage
                                  run: |
                                    CODE_COVERAGE=$(vendor/bin/coverage-checker clover.xml 90 --processor=clover-coverage)
                                    echo ${CODE_COVERAGE}
                                    if [[ ${CODE_COVERAGE} == *"test coverage, got"* ]] ; then
                                      exit 1;

πŸ”₯PRO tipπŸ”₯

Run your test suite against multiple PHP versions and/or operating systems

                                name: Test suite PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }}
                                runs-on: ${{ matrix.operating-system }}
                                    operating-system: ['ubuntu-latest', 'ubuntu-18.04']
                                    php-versions: [ '7.4', '8.0', '8.1' ]
                                  - name: Setup PHP ${{ matrix.php-versions }} with Xdebug 3.x
                                    uses: shivammathur/setup-php@v2
                                      php-version: ${{ matrix.php-versions }}
                                      coverage: xdebug

πŸ”₯PRO tipπŸ”₯

This should result in a workflow run for all possible combinations in the matrix

CI matrix

2nd job: Static code analysis & coding standards

  • PHPStan PHPStan focuses on finding errors in your code without actually running it
  • PHP Coding Standards Fixer The PHP Coding Standards Fixer (PHP CS Fixer) tool fixes your code to follow standards

Install PHP & dependencies

                                - name: Setup PHP 8.1
                                  uses: shivammathur/setup-php@v2
                                    php-version: '8.1'
                                - name: Checkout code
                                  uses: actions/checkout@v2
                                - name: Install dependencies
                                  run: composer install --prefer-dist

Run static code analysis & coding standards

Run static code analyser

                                - name: Run PHPStan
                                  run: vendor/bin/phpstan analyse

Check coding standards

                                - name: Run PHPcs fixer dry-run
                                  run: vendor/bin/php-cs-fixer fix --dry-run --stop-on-violation --config=.php-cs-fixer.dist.php

Update repository settings

Tighten branch protection rules by adding extra required status checks
Both jobs in CI workflow need to succeed before the PR can be merged.

Protected branch settings

Putting it all together

Example pull requests

Build & deploy Workflow


Build & deploy Workflow

Contains two jobs

  • Create a build
  • Deploying build to a remote server
                                name: Build & deploy

1s job: Create build

Only run when initialized with proper branches

                                if: github.ref_name == 'master' || github.ref_name == 'development'
                                name: Create build ${{ github.run_number }} for ${{ github.ref_name }}
                                runs-on: ubuntu-latest                      

Install PHP & dependencies

                                - name: Checkout code
                                  uses: actions/checkout@v2
                                - name: Setup PHP 8.1
                                  uses: shivammathur/setup-php@v2
                                    php-version: '8.1'
                                - name: Install dependencies
                                  run: composer install --prefer-dist --no-dev                                           

Create atrifact

                                - name: Create artifact
                                  uses: actions/upload-artifact@v3
                                    name: release-${{ github.run_number }}
                                    path: |

Verify created artifacts

Can be downloaded and verified from the workflow summary page.


2nd job: Deploy to remote server

Deploy the build we created in the previous step, but before we can do this, we first need to configure environments.

Configure environments

  • Master
  • Development

Configure environments

Environment settings
  • Enforce that the correct branch is deployed
  • Secrets can be used during deploy in workflows

Configure the Job

                                needs: build
                                  name: ${{ github.ref_name }}
                                  url: https://${{ github.ref_name }}.env                           

${{ github.ref_name }} contains branch or tag.

  • Reference build step
  • Reference environment to deploy
    • Use the secrets configured environment
    • Validate that the correct branch is deployed
    • Indicate if a PR has been deployed (or not)

Setting concurrency

Make sure only one deploy (per environment) at a time can be run.

                                concurrency: ${{ github.ref_name }}                        

Download atrifact

                                - name: Download artifact
                                  uses: actions/download-artifact@v3
                                    name: release-${{ github.run_number }}                    

Rsync build to server

                                - name: Rsync build to server
                                  uses: burnett01/rsync-deployments@5.2
                                    switches: -avzr --delete
                                    path: .
                                    remote_path: /var/www/release-${{ github.run_number }}/
                                    remote_host: ${{ secrets.SSH_HOST }}
                                    remote_user: ${{ secrets.SSH_USERNAME }}
                                    remote_key: ${{ secrets.SSH_KEY }}                 

Run deploy script

                                - name: Run remote SSH commands
                                  uses: appleboy/ssh-action@master
                                    host: ${{ secrets.HOST }}
                                    username: ${{ secrets.USERNAME }}
                                    key: ${{ secrets.KEY }}
                                    port: ${{ secrets.PORT }}
                                    script: |
                                      RELEASE_DIRECTORY=/var/www/release-${{ github.run_number }}
                                      # Manage symlinks.
                                      rm -r "${CURRENT_DIRECTORY}"
                                      ls -s "${RELEASE_DIRECTORY}" "${CURRENT_DIRECTORY}"
                                      # Run database migrations
                                      drush updb -y
                                      drush cim -y
                                      # Install updated crontab
                                      crontab ${RELEASE_DIRECTORY}/crontab
                                      # Rebuild cache
                                      drush cr -y         

We've shipped new features

At this point new features and/or bug fixes are deployed to your remote server. You should be good to go to repeat this cycle over and over and over again 😌

Hungry for more?


Integration tests

Integration testing is the phase in software testing in which individual software modules arecombined and tested as a group.

End-to-end tests

End-to-end testing is a technique that tests the entire software product from beginning to end to ensure the application flow behaves as expected.

Visual regression tests

A visual regression test checks what the user will see after any code changes have been executed by comparing screenshots taken before and after deploys.

Continuous deploy

                                      - master
                                      - develop              
Note: Continuous deploy != Continuous releasing (feature flags)

Speed up your test suite

  • Use Paratest to run test in parallel
  • Cache your vendor dependencies
  • Use an in-memory SQLite database for tests that hit your database
  • Disable Xdebug, if you don't need test coverage

Composite actions

Composite actions can be used to split workflows into smaller, reusable components.

This blogpost does a perfect job at explaining how to define and use them. Big up to the author James Wallis πŸ‘Œ

Fancy badges

You can add several badges on the readme page of your repository to indicate the status of your project

  • CI status CI/CD
  • Test coverage
  • License License
  • PHPStan PHPStan Enabled
  • PHP version support PHP