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.
Dakota is a Software Engineer with over ten years of coding experience, with expertise in a broad range of development technologies. Historically he’s served in web development roles, but lately he’s been optimizing everything he can in a DevOps position, where he can flex his analytical skills.