Laravel Continuous Integration with GitHub Actions

Laravel Continuous Integration with GitHub Actions

Working with frameworks like Laravel brings a plethora of features and efficiencies. These frameworks make major features like Dependency Injection, Real-time Events, Queuing systems, Mailing, Localization, Caching, and more simple to implement for developers. Not only that, but these frameworks can bring quality of life to local development too, with features such as local development containers (Laravel Sail), Testing Suites, starter kits, CLI Toolkits, and readily available packages to use in development and production environments. These robust features give developers the power to build scalable, enterprise-ready web applications with relative ease, but with great power comes great responsibility.

  • How do you monitor all of these different features your web application uses?
  • How do you ensure your squad of developers are writing code in an effective manner?
  • How do you measure the productivity of a team working on these applications?
  • How do you ensure code updates won’t break any part of your application system?
  • How do you ensure those developers run tests and deploy excellent code?

While you or your team may have different answers to those questions, the common connection between these questions is the fact that these processes can be automated. That automation can materialize in different ways, and balancing the automation with your current workflow can seem daunting. Not only that, but if you have critical workloads and applications that are heavily and manually monitored throughout it’s deployment lifecycle, it may seem like automating parts of that would introduce more points of failure. After reading through the Continuous Improvement and Continuous Integration processes that we have implemented at AdAction, we hope that you’ll be more confident in the boons that CI can bring to you and your team.

GitHub Actions for Laravel Applications

There are a few different tools for integrating Continuous Integration with Laravel, but we’re going to focus primarily on GitHub Actions and their use with Laravel. With that said, many of these integrations can be “easily” ported over to whatever tooling you desire, as long as you can figure the configuration part out.

Also, keep in mind, the requirements to integrate these tools and tests we’re running may be different than the requirements to run the application itself.

Requirements

  • PHP (We like to use phpbrew for managing different PHP versions on our local machines)
  • Composer
  • Node (We like to use nvm for managing different Node version on our local machines)
  • npm
  • Laravel
  • GitHub Actions We’ll walk through integrating/installing this together.

Install example application

If you’d like to see how these workflows look like in an actual project, you may locally clone our example repo to follow along. https://github.com/AdAction/articles-laravel-continuous-integration. You may also view the GitHub Actions execution results directly from that repository.

Installing GitHub Actions

If you haven’t already, let’s get GitHub Actions integrated into our project! The only requirement to integrate GitHub Actions with your project is for that project repository to be a GitHub Repository. After that, adding new GitHub Workflows is as simple as adding new files in that projects .github/workflows directory.

Web Application Use Cases

The following examples aren’t necessarily Laravel specific, and are common use-cases amongst all web applications. For the sake of this article, though, we’re going to be writing these in the context of a Laravel Application. We’ve presented these use-cases in order of operations, where the first of theses use-cases are being run earlier in the SDLC, and the latter ones being run later.

Building

We want to ensure, at the very least, our application code can be built. For Laravel applications, that means installing the dependencies and running npm run build.

name: Build  
  
concurrency:  
  group: ${{ github.workflow }}-${{ github.ref }}  
  cancel-in-progress: true  
  
on:  
  workflow_call:  
  pull_request:  
  
env:  
  EXTENSIONS: pcntl, bcmath, zip, gd, sodium, :php-psr  
  PHP_VERSION: "8.2"  
  NODE_VERSION: 20  
  
jobs:  
  build-application:  
    runs-on: ubuntu-latest-m  
  
    steps:  
    - name: Checkout code  
      uses: actions/checkout@v4  
  
    - name: Setup PHP  
      uses: shivammathur/setup-php@v2  
      with:  
        php-version: ${{ env.PHP_VERSION }}  
        extensions: ${{ env.EXTENSIONS }}  
        tools: composer, pecl  
        coverage: xdebug  
  
    - name: Install Deps  
      uses: "ramsey/composer-install@v3"    
  
	- name: Setup node  
	  uses: actions/setup-node@v4  
	  with:  
	    node-version: ${{ env.NODE_VERSION }}  
	    cache: "npm"
  
  

Formatting and Styling

Code styling and formatting are important for a few different reasons. Having a common code style configuration for all developers to use allows developers to focus their attentions on stuff that can’t be automated and easily checked and tested. Instead of taking a non-trivial amount of time discussing and reviewing code styles for updates, a common format and style can be applied to code intermittently during development, as well as automatically in Pull Requests. See the Wikipedia on the Law of Triviality.

name: Linting  
  
on:  
  workflow_call:  
  pull_request:  
  
env:  
  COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}  
  EXTENSIONS: pcntl, bcmath, zip, gd, sodium, :php-psr  
  PHP_VERSION: "8.2"  
  
jobs:  
  php-formatting-and-styling:  
    runs-on: ubuntu-latest  
    needs: build  
    concurrency:  
      group: release  
  
    steps:  
      - name: Checkout  
        uses: actions/checkout@v4  
  
      - name: Setup PHP  
        uses: shivammathur/setup-php@v2  
        with:  
          php-version: ${{ env.PHP_VERSION }}  
          extensions: ${{ env.EXTENSIONS }}  
          tools: composer, pecl  
          coverage: xdebug  
  
      - name: Install Deps  
        uses: "ramsey/composer-install@v3"  
  
      - name: Check Code Styles  
        run: "vendor/bin/pint --test"  
  
  node-formatting-and-styling:  
    runs-on: ubuntu-latest  
    needs: build  
    concurrency:  
      group: release  
  
    steps:  
      - name: Checkout  
        uses: actions/checkout@v4  
  
      - name: Setup node  
        uses: actions/setup-node@v4  
        with:  
          node-version: ${{ env.NODE_VERSION }}  
          cache: "npm"  
  
      - name: Build App  
        run: npm ci  
  
      - name: Check Code Styles  
        run: "npm run prettier"

Static Analysis

Different from Formatting and Styling, Static Analysis tells us that our code is written efficiently. For example, does the code have unused variables, obvious memory leaks, hanging processes, circular references, etc. The configuration used to report these issues can be altered to reflect how strictly you want to analyze your code, and you can even create a baseline file for integrating Static Analysis in an existing project to enable analysis on all future files, while giving your team time to handle the existing issues separately.

name: Linting  
  
on:  
  workflow_call:  
  pull_request:  
  
env:  
  COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}  
  EXTENSIONS: pcntl, bcmath, zip, gd, sodium, :php-psr  
  PHP_VERSION: "8.2"  
  
jobs:  
  php-linting:  
    runs-on: ubuntu-latest  
    needs: build  
    concurrency:  
      group: release  
  
    steps:  
      - name: Checkout  
        uses: actions/checkout@v4  
  
      - name: Setup PHP  
        uses: shivammathur/setup-php@v2  
        with:  
          php-version: ${{ env.PHP_VERSION }}  
          extensions: ${{ env.EXTENSIONS }}  
          tools: composer, pecl  
          coverage: xdebug  
  
      - name: Install Deps  
        uses: "ramsey/composer-install@v3"  
  
      - name: Check Code Styles  
        run: "vendor/bin/pint --test"  
  
      - name: Setup problem matchers for PHP  
        run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"  
  
      - name: Run PHPStan  
        run: vendor/bin/phpstan analyse --no-progress  
  
  node-linting:  
    runs-on: ubuntu-latest  
    needs: build  
    concurrency:  
      group: release  
  
    steps:  
      - name: Checkout  
        uses: actions/checkout@v4  
  
      - name: Setup node  
        uses: actions/setup-node@v4  
        with:  
          node-version: ${{ env.NODE_VERSION }}  
          cache: "npm"  
  
      - name: Build App  
        run: npm ci  
  
      - name: Check Code Styles  
        run: "npm run prettier"  
  
      - name: Run ESLint  
        run: npx eslint . --ext .vue,.js

Testing and Code Coverage

We have a large amount of tests to run, for both our PHP and JS/Vue part of our Application. We want those tests to be run for different occasions. We want to run our tests when PRs are opened so reviewers can more easily know that what they’re reviewing is quality code. We also want to run these tests before creating new Builds and Releases. We have different suites for Unit, Feature, and Integration tests. The Code Coverage test tells us if an update strengthens or weakens our overall tests, and tells us if the update itself is well tested. Pick a code coverage threshold that fits your team’s existing testing structure, don’t try to enforce un-enforceable requirements that will hinder your deployment process.

name: Test  
  
concurrency:  
  group: ${{ github.workflow }}-${{ github.ref }}  
  cancel-in-progress: true  
  
on:  
  workflow_call:  
  pull_request:  
  
env:  
  COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}  
  EXTENSIONS: pcntl, bcmath, zip, gd, sodium, :php-psr  
  PHP_VERSION: "8.2"  
  NODE_VERSION: 20  
  
jobs:  
  php-tests:  
    runs-on: ubuntu-latest-m  
  
    steps:  
    - name: Checkout code  
      uses: actions/checkout@v4  
  
    - name: Setup PHP  
      uses: shivammathur/setup-php@v2  
      with:  
        php-version: ${{ env.PHP_VERSION }}  
        extensions: ${{ env.EXTENSIONS }}  
        tools: composer, pecl  
        coverage: xdebug  
  
    - name: Install Deps  
      uses: "ramsey/composer-install@v3"  
  
    - name: Initialize Project  
      run: |  
        cp .env.ci .env  
        composer app:init  
  
    - name: Run PHP Unit Tests  
      run: php artisan test --ci --coverage-clover=coverage.xml --log-junit=test-report.xml  
  
  js-tests:  
    runs-on: ubuntu-latest  
  
    steps:  
    - name: Checkout code  
      uses: actions/checkout@v4  
  
    - name: Setup node  
      uses: actions/setup-node@v4  
      with:  
        node-version: ${{ env.NODE_VERSION }}  
        cache: "npm"  
  
    - name: Build App  
      run: npm ci  
  
    - name: Run CDK Tests  
      run: npm test

Releasing

On pushes to our main branch, we want to automate releases to our GitHub repo. These releases are used to trigger another workflow that deploys the release to an S3 Bucket (which then triggers a deployment from CodePipeline, but that’s beyond the scope of this Article). These releases follow Semantic Versioning, which help keep our dependency management cleaner and Software Development Lifecycle easier to follow along with on a timeline. Semantic versioning is automated by the use of an npm package called semantic-release.

name: Release  
  
on:  
  push:  
    branches:  
    - main  
  
jobs:  
  semantic-release:  
    runs-on: ubuntu-latest  
  
    steps:  
      - name: Checkout  
        uses: actions/checkout@v4  
        with:  
          fetch-depth: 0  
          token: ${{ secrets.PUBLIC_REPO_BOT_GITHUB_TOKEN }}  
  
      - name: Setup Node.js  
        uses: actions/setup-node@v4  
        with:  
          node-version: "lts/*"  
  
      - name: Install semantic-release  
        run: |  
          npm install -g semantic-release@21  
          npm install -g @semantic-release/changelog  
          npm install -g @semantic-release/git  
  
      - name: Install Deps  
        uses: "ramsey/composer-install@v3"  
  
      - name: Release  
        env:  
          GITHUB_TOKEN: ${{ secrets.PUBLIC_REPO_BOT_GITHUB_TOKEN }}  
          SLACK_TOKEN: ${{ secrets.PUBLIC_REPO_SLACK_TOKEN }}  
          GIT_AUTHOR_NAME: ${{ vars.PUBLIC_BOT_USERNAME }}  
          GIT_AUTHOR_EMAIL: ${{ vars.PUBLIC_BOT_EMAIL }}  
          GIT_COMMITTER_NAME: ${{ vars.PUBLIC_BOT_USERNAME }}  
          GIT_COMMITTER_EMAIL: ${{ vars.PUBLIC_BOT_EMAIL }}  
        run: npx semantic-release

Integrating 3rd Party Tools

We use a few different 3rd party tools in our projects, such as DataDog, MaxMind GeoIP, and DevCycle. Most of these tools require installation outside of the PHP Ecosystem, meaning files and commands need to be installed and external resources configured. We currently have a GitHub Workflow for automatically displaying code snippets for each DevCycle variable usage within our project.

name: DevCycle Usages  
  
on:  
  push:  
    branches: [main]  
  
env:  
  DVC_PROJECT_KEY: "test_project_key"  
  
jobs:  
  dvc-code-usages:  
    runs-on: ubuntu-latest  
    name: DevCycle Feature Flag Code Usages  
    steps:  
      - uses: actions/checkout@v4  
        with:  
          fetch-depth: 0  
      - uses: DevCycleHQ/feature-flag-code-usage-action@v1.1.2  
        with:  
          github-token: ${{ secrets.GITHUB_TOKEN }}  
          client-id: ${{ secrets.DVC_CLIENT_ID }}  
          client-secret: ${{ secrets.DVC_CLIENT_SECRET }}  
          project-key: ${{ env.DVC_PROJECT_KEY }}

Deploying

When a Release has been made in our project, another Workflow gets triggered that deploys our new release to an S3 bucket. Though our method of deployment is straightforward from the GitHub Actions side, a more involved process may be required depending on your deployment needs.

name: Deploy

concurrency: deployment

on:
  release:
    types: [published]

permissions:
  id-token: write   # This is required for requesting the JWT
  contents: read    # This is required for actions/checkout
  pull-requests: write

jobs:
  upload-release:
    runs-on: ubuntu-latest

    steps:

    - name: Set start time
      run: |
        START_TIME=$(date +%s) 
        echo "START_TIME=$START_TIME" >> $GITHUB_ENV

    - name: configure aws credentials
      uses: aws-actions/configure-aws-credentials@v3
      with:
        role-to-assume: arn:aws:iam::172122050326:role/PublicGitHubRepoOIDCRole
        role-session-name: DeployGitHubActions
        aws-region: us-east-2

    - name: Checkout code
      uses: actions/checkout@v4

    - name: Zip files
      run: |
        zip -r laravel-app-latest.zip .

    - name: Copy file to S3 with metadata
      env:
        AWS_REGION: us-east-2
      run: |
        aws s3 cp --metadata github-sha=${{ github.sha }},start-time=${START_TIME} ./laravel-app-latest.zip s3://articles-laravel-continuous-integration/laravel-app-latest.zip

Cleanup and Efficiency

Having the build, lint, and test workflows separate is a waste of resources, lets combine them to save a bit of time and money!

name: Build

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  workflow_call:
  pull_request:

env:
  COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
  EXTENSIONS: pcntl, bcmath, zip, gd, sodium, :php-psr
  PHP_VERSION: "8.2"
  NODE_VERSION: 20

jobs:
  build-lint-and-test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql

        env:
          MYSQL_DATABASE: articles-laravel-ci
          MYSQL_USER: exampleuser
          MYSQL_PASSWORD: examplepassword
          MYSQL_ROOT_PASSWORD: examplepassword

        ports:
          - 33306:3306

        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ env.PHP_VERSION }}
        extensions: ${{ env.EXTENSIONS }}
        tools: composer, pecl
        coverage: xdebug

    - name: Install Deps
      uses: "ramsey/composer-install@v3"

    - name: Setup node
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: "npm"

    - name: Build App
      run: npm ci

    - name: Check Code Styles
      run: "vendor/bin/pint --test"

    - name: Setup problem matchers for PHP
      run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"

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

    - name: Initialize Project
      run: |
        cp .env.example .env
        php artisan key:generate
        php artisan migrate --seed

    - name: Run PHP Unit Tests
      run: php artisan test

    - name: Check Code Styles
      run: "npm run prettier"

    - name: Run ESLint
      run: npm run lint

    - name: Run CDK Tests
      run: npm test

Future Use Cases

Integrating reusable workflows

We have quite a few repeated uses of our dependency installations. Putting that in a separate build workflow, with all the others referencing it is in the plans too for our future, so look out for that article too!

Caching dependencies

We install dependencies for node and dependencies for php through npm and composer. We do it multiple times throughout our different workflows, and it’s not efficient. What we can do instead is use GitHub’s dependency caching integration. That would save us a significant time spent on jobs, but there are caching management concerns to make this an easy task.

Database Migrations

Sometimes our code changes necessitate changes to our DB Schema as well. Right now, we run Database Migrations on every successful deployment, but sometimes, those Schema changes don’t run properly in a non-local environment, and they cause issues in development or production environments. To combat this, we’ve been planning out an update to our CI/CD process where Database Migrations are run before the code deployment actually happens, and would alert us if any bad schema changes happen. This would take careful planning on our teams part; Ensuring code changes are precipitated by their required schema changes is difficult, but a well oiled team with strong communication and planning can handle that.

File manipulation and preparation

During our Elastic Beanstalk Deployment, we make changes to the codebase to ensure that the project can be run on the target environment. This includes changing permissions, setting up log files, adding folders, installing and updating tools, etc. This logic is stored in AWS EB Platform Hooks, but we want to move project changes to GitHub Actions, while keeping environment changes in those Platform Hooks.

Elevating Laravel with GitHub Actions

Implementing CI with Laravel using GitHub Actions presents a transformative opportunity for web developers, from setting up your environment with PHP, Composer, and Node.js, to integrating third-party tools for a seamless development workflow. By automating processes such as testing, building, and deploying, developers can ensure their code is both efficient and robust, minimizing the risk of errors and enhancing the overall quality of their applications. Future prospects, like deploying directly to Elastic Beanstalk and refining database migrations, highlight the evolving landscape of CI/CD processes.

At AdAction, as we continue embracing these technological advancements, the opportunities to streamline our development processes, enhance the quality of our code, and speed up our project timelines are vast. Integrating GitHub Actions into our Laravel projects not only improves our development methodologies but also ensures our team is aligned with the pursuit of more innovative, dependable, and scalable web applications.

Most Popular

Examining the State of Mobile App Fraud
10 minutes
Beyond the Click: AdGem’s Journey to 1 Billion Interactions
2 minutes
Innovating Privileges Governance: AWS CDK Constructs for RBAC
9 minutes
AdAction’s Fiesta Maya: A Staff Retreat in Paradise
2 minutes
Can’t Stop Chronicles: Matt Swim, Strategic Account Manager
3 minutes

Subscribe

Subscribe to the AdAction Connect blog for industry updates, UA tactics and best practices, and more! Get tips that you can apply to your existing strategy, and feel confident that we’ll never serve you ads.

Social Links