Streamline Development with CI/CD on GitHub

/ github actions, pipelines, workflow, ci/cd

Continuous Integration and Continuous Deployment or CI/CD have become a key tool for software development. CI/CD makes it easy to deliver code frequently, accurately and with the best quality possible since it removes the requirement of manual testing and deployment.

What is GitHub Actions?

GitHub Actions is a powerful feature integrated into GitHub, designed to help us automate our software development pipelines. It allows us to build, test, and deploy our code directly from GitHub. The magic of GitHub Actions relies in its flexibility and ease of use. With just a few YAML configuration files, we can build custom workflows that run whenever specific events occur in our repository, such as a push to a branch or pull request creation.

Imagine not having to worry about manually run tests or deploy applications. GitHub Actions will do it all for us. It supports a wide range of programming languages and platforms, making it suitable for any project. And obviously, it seamlessly integrates with the GitHub ecosystem, so we can use it alongside other tools we already love.

What is a Workflow?

In the world of GitHub Actions, a workflow is a customizable set of automated processes that can handle anything from running tests to deploying your latest masterpiece We may also know them as pipelines. These workflows are defined using YAML files, which we simply check into our repository.

Setting Up a Workflow

Setting up GitHub Actions is simple, we just need to create a YAML file that defines our workflow and place it in the .github/workflows directory of our repository. Let's add a workflow into the project of the previous post Static Analysis of PHP Code with PHPStan.The public repository is hexagonal-architecture-example-in-php.

name: Test  

    branches: [ 'master' ]  
    types: [ 'opened', 'synchronize', 'reopened' ]  

    runs-on: ubuntu-latest  

        php-version: [ '8.2', '8.3' ]  

      - name: Checkout source code  
        uses: actions/checkout@v2  
          fetch-depth: 0

In the example above we have defined a new workflow, let's break down the code and explain what is happening:

  • The workflow name is Test.
  • This work flow will be executed for two events:
    • When push code into the branch master.
    • When pull_request is opened, synchronize and reopened.
  • We have define one job named test.
  • The test job runs on a ubuntu-latest operative system.
  • As our code is PHP we define that the test job needs to be executed for two different PHP versions 8.2 and 8.3.
  • Finally, we use the steps section to define each action we need to execute within our workflow or pipeline.

What to Include in the Workflow

At least, our workflows should include the following steps:

  • Runtime Configuration: Set up the necessary runtime environment for the project.
  • Code Checkout: Retrieve the latest code from the repository.
  • Install Dependencies: Install all required dependencies for the project.
  • Test Execution: Run all necessary tests to ensure code functionality.
  • Code Style Check: Verify that the code adheres to the project's style guidelines.
  • Static Code Analysis: Analyze the code to identify potential issues and enforce coding standards.

Let's add some of these steps to our workflow:

name: Test  

    branches: [ 'master' ]  
    types: [ 'opened', 'synchronize', 'reopened' ]  

    runs-on: ubuntu-latest  

        php-version: [ '8.2', '8.3' ]  

      - name: Checkout Source Code  
        uses: actions/checkout@v4  
          fetch-depth: 0  

      - name: Set up PHP ${{ matrix.php-version }}  
        uses: shivammathur/setup-php@v2  
          php-version: ${{ matrix.php-version }}  
          coverage: xdebug  
          tools: composer:v2  

      - name: Cache Dependencies  
        uses: actions/cache@v4
          path: ~/.composer/cache  
          key: php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}  
          restore-keys: php-${{ matrix.php-version }}-composer-  

      - name: Validate Composer Files  
        run: composer validate  

      - name: Install Dependencies  
        if: steps.composer-cache.outputs.cache-hit != 'true'  
        run: composer install --prefer-dist --no-progress --no-suggest  

      - name: Prepare App Installation  
        run: |  
          mkdir -p data  
          composer db:initialize  

      - name: Execute Static Code Analysis  
        run: composer workflow:analyze  

      - name: Execute Unit, Integration and Acceptance Tests  
        run: composer workflow:test

Again, let's break down the code and explain what we just did:

  1. Code Checkout: Use the Checkout Action.
  2. Set Up PHP Version: Use the Setup PHP Action.
  3. Cache Dependencies: Use the Cache Action. For more information, see Caching dependencies to speed up workflows.
  4. Validate Composer Files: Run the composer validate command.
  5. Install Dependencies: First, check if cached dependencies are available. If not, run the usual composer install command.
  6. Prepare Project Installations: Run commands to prepare and install the application, such as deploying the database schema if required.
  7. Execute Static Code Analysis.
  8. Execute Tests.

Running a Workflow

Now that everything is set up, all we have to do is commit and push our code to a new branch. After that, we'll create a new pull request (PR). GitHub Actions will kick in automatically since we've configured our workflow to run when a pull request is opened. On the PR page, we'll see the status of the workflows we've defined.


In this case the workflow has been successful, let's review an example of a failed workflow. For example, we can remove some PHPDoc to force PHPStan to fail static code analysis.


We can get more intel about the error by clicking in the "Details" link of each workflow check, let's review the 8.2 execution.


Here, we can check out the details about the errors. Pretty cool, huh? But guess what? There's another way to get even more detailed insights into what's gone wrong. We've set up PHPStan with --error-format=github, so this will give us the following output:

> vendor/bin/phpstan analyse --error-format=github --no-progress
Note: Using configuration file hexagonal-architecture-example/phpstan.neon.
::error file=src/UserManagement/Application/Contract/UserRepository.php,line=13,col=0::Method OtherCode\UserManagement\Application\Contract\UserRepository::all() return type has no value type specified in iterable type array.
::error file=src/UserManagement/Infrastructure/Persistence/DoctrineUserRepository.php,line=28,col=0::Method OtherCode\UserManagement\Infrastructure\Persistence\DoctrineUserRepository::all() return type has no value type specified in iterable type array.
::error file=src/UserManagement/Infrastructure/Persistence/JsonFileUserRepository.php,line=65,col=0::Method OtherCode\UserManagement\Infrastructure\Persistence\JsonFileUserRepository::all() return type has no value type specified in iterable type array.
Script vendor/bin/phpstan analyse --error-format=github --no-progress handling the workflow:analyze event returned with error code 1

This is a special output that GitHub knows how to handle, letting us see the errors directly in the "file changes" tab of the PR.


Now, this is pretty amazing! With this setup, we can easily understand what is happening and fix it properly. This integration dramatically improves the developer experience.


In conclusion, integrating CI/CD with GitHub Actions really improve our development workflow. By automating tasks like code analysis and testing, we ensure quicker and more reliable software delivery. GitHub Actions gives us this awesome platform to define our development pipelines in a easy way, which means our code gets better and our team works more efficiently.

Next Post Previous Post