Skip to main content

Do you want set up a CI/CD process using GitHub Actions?

This is a walk-through basic setup to automate your integrations and deployments for your projects.

 

GitHub Actions

GitHub Actions allows for your code can be built, tested and deployed from GitHub.  Let's explore an approach in managing CI/CD processes using GitHub Actions.

 

Set-up - having a plan

Creating a basic CI/CD setup for a Laravel application.  The CI/CD setup will monitor push and pull requests made to our repository.  Therefore being able to:

  • Run tests on pushes
  • Run tests on pull requestDeploy
  • Deploy to staging server when push is on staging branch
  • Deploy to production server when push is on master

 

Workflow?

A workflow provides a sequence of steps required to complete a CI/CD process.  It will defined within the repository and subsequently you might have guessed that it will be committed as part of the repository.  So, when commiting / pushing to GitHub, GitHub Actions will detect the workflow and kick into action.  Immediately parse the workflow and begin to action your CI/CD process based on the defined instructions.  Workflows are written with YAML and stored inside .github/workflows directory of your project root.

 

Configuring workflows

Pull request workflow: tests

If you are part of a team working on the same project, then this workflow is when its leverage shows itself.  Benefits include being able to run checks whenever a pull request is created.  Much better than waiting post merge to test it.  This is achieved by defining a workflow that will run when a pull request is made.  

The following example should be saved as a YAML file in the .github/workflows directory as pr_workflow.yml.  An example of this type of workflow:

name: PR WorkFlow

on:
  pull_request:
    branches:
      - master
      - staging

jobs:
  app-checks:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:5.7
        env:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          MYSQL_DATABASE: test_db
        ports:
          - 3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
    steps:
      - uses: actions/checkout@v1
      - name: Copy .env
        run: php -r "file_exists('.env') || copy('.env.example', '.env');"
      - name: Install Composer Dependencies
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
      - name: Install NPM Dependencies
        run: npm install
      - name: Generate key
        run: php artisan key:generate
      - name: Execute tests (Unit and Feature tests) using PHPUnit
        env:
          DB_PORT: ${{ job.services.mysql.ports[3306] }}
        run: ./vendor/bin/phpunit
      - name: Execute tests (Unit and Feature tests) using JEST
        run: node_modules/.bin/jest

 

name: PR WorkFlow

on:
  pull_request:
    branches:
      - master
      - staging

As you might have picked up, going past the name of the workflow - PR is short for pull request, the GitHub Actions are defined to run when a pull request is made to the master and staging branch.

jobs:
  app-checks:
    runs-on: ubuntu-latest
    services:
      mysql:
        / ... /
    steps: 
        / ... /

A job named app-checks works through it's purpose, which is to run both tests using Jest and PHPUnit. Through instructing Github Actions to include MySQL as a service when setting up the action.  Whereas, the steps set out the order to be performed.

 

Push workflow: tests

Whenever a change is made, best practice is to ensure that the project is good to go.  And is cross-checked using tests that confirm that the project broken due to change.  The following example needs to be saved in the .github/workflows directory as push_workflow.yml:

name: Push Workflow

on:
  push:
    branches:
      - master
      - staging

jobs:
  app-checks:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:5.7
        env:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          MYSQL_DATABASE: test_db
        ports:
          - 3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
    steps:
      - uses: actions/checkout@v1
      - name: Copy .env
        run: php -r "file_exists('.env') || copy('.env.example', '.env');"
      - name: Install Composer Dependencies
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
      - name: Install NPM Dependencies
        run: npm install
      - name: Generate Key
        run: php artisan key:generate
      - name: Execute tests (Unit and Feature tests) via PHPUnit
        env:
          DB_PORT: ${{ job.services.mysql.ports[3306] }}
        run: vendor/bin/phpunit
      - name: Execute tests (Unit and Feature tests) via JEST
        run: node_modules/.bin/jest
  build-js-production:
    name: Build JavaScript/CSS for PRODUCTION Server
    runs-on: ubuntu-latest
    needs: app-checks
    if: github.ref == 'refs/heads/master'
    steps:
      - uses: actions/checkout@v1
      - name: NPM Build
        run: |
          npm install
          npm run prod
      - name: Put built assets in Artifacts
        uses: actions/upload-artifact@v1
        with:
          name: assets
          path: public
  build-js-staging:
    name: Build JavaScript/CSS for STAGING Server
    runs-on: ubuntu-latest
    needs: app-checks
    if: github.ref == 'refs/heads/staging'
    steps:
      - uses: actions/checkout@v1
      - name: NPM Build
        run: |
          npm install
          npm run dev
      - name: Put built assets in Artifacts
        uses: actions/upload-artifact@v1
        with:
          name: assets
          path: public
  deploy-production:
    name: Deploy Project to production server
    runs-on: ubuntu-latest
    needs: [build-js-production, app-checks]
    if: github.ref == 'refs/heads/master'
    steps:
      - uses: actions/checkout@v1
      - name: Fetch built assets from Artifacts
        uses: actions/download-artifact@v1
        with:
          name: assets
          path: public
      - name: Setup PHP
        uses: shivammathur/setup-php@master
        with:
          php-version: 7.4
          extension-csv: mbstring, bcmath
      - name: Composer install
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
      - name: Setup Deployer
        uses: atymic/deployer-php-action@master
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
          ssh-known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}
      - name: Deploy to PRODUCTION Server
        env:
          DOT_ENV: ${{ secrets.DOT_ENV_PRODUCTION }}
        run: dep deploy production --tag=${{ env.GITHUB_REF }} -vvv
  deploy-staging:
    name: Deploy Project to staging server
    runs-on: ubuntu-latest
    needs: [build-js-staging, app-checks]
    if: github.ref == 'refs/heads/staging'
    steps:
      - uses: actions/checkout@v1
      - name: Fetch built assets from Artifacts
        uses: actions/download-artifact@v1
        with:
          name: assets
          path: public
      - name: Setup PHP
        uses: shivammathur/setup-php@master
        with:
          php-version: 7.4
          extension-csv: mbstring, bcmath
      - name: Composer install
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
      - name: Setup Deployer
        uses: atymic/deployer-php-action@master
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
          ssh-known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}
      - name: Deploy to Prod
        env:
          DOT_ENV: ${{ secrets.DOT_ENV_STAGING }}
        run: dep deploy staging --tag=${{ env.GITHUB_REF }} -vvv

 

When there is a push on the repository, the Push Workflow will run frontend tests with Jest and backend tests with PHPUnit.  However, you will have picked up on some significant changes between this push workflow and the previous pull request workflow.  In a nut shell, there are more jobs – four in fact.  These four jobs are build-js-production, build-js-staging, deploy-staging, and deploy-production.  Yes they are paired to production and staging jobs.

Four extra jobs – what are they doing?

Grouping together build-js-production and build-js-staging, there purpose is to build the JavaScript assets, upload them to the GitHub Actions artifacts and be available when deployed.

To determine which job GitHub Actions is going to be deployed, this is set by an if statement.  Positioned in the deploy-production and deploy-staging jobs.  The grouping becomes apparent here where the pairing of JS and workflow are managed.  The production job needs

[build-js-production, app-checks]

... which informs GitHub Actions that deploy-production requires build-js-production and app-checks jobs to run successfully. Therefore, the deploy-staging job will run after both build-js-staging and app-checks runs and exited without any error.  This ensures if the test or asset build fails, then the deployment doesn’t proceed. The same goes for deploy-staging. See below highlighted text:

deploy-production:
    name: Deploy Project to production server
    runs-on: ubuntu-latest
    needs: [build-js-production, app-tests]
    if: github.ref == 'refs/heads/master'

For staging the line is simply – refs/heads/staging:

deploy-staging:
    name: Deploy Project to staging server
    runs-on: ubuntu-latest
    needs: [build-js-staging, app-tests]
    if: github.ref == 'refs/heads/staging'

 

name: Fetch built assets from Artifacts
        uses: actions/download-artifact@v1
        with:
          name: assets
          path: public

These instructions download the uploaded assets from the artifacts into the public folder of the project.  Whereas, to install composer dependencies, a step that is required to run Deployer later.

- name: Composer install
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
Setup PHP action

To setup the PHP Version to use and the extensions we need:

- name: Setup PHP
        uses: shivammathur/setup-php@master
        with:
          php-version: 7.4
          extension-csv: mbstring, bcmath
Deployer GitHub action

Set up Deployer with atymic/deployer-php-action@master and is used with the SSH_PRIVATE_KEY and SSH_KNOWN_HOSTS.

- name: Setup Deployer
        uses: atymic/deployer-php-action@master
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
          ssh-known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}

- name: Deploy to Prod
        env:
          DOT_ENV: ${{ secrets.DOT_ENV_STAGING }}
        run: dep deploy staging --tag=${{ env.GITHUB_REF }} -vvv

- name: Deploy to Prod
        env:
          DOT_ENV: ${{ secrets.DOT_ENV_PRODUCTION }}
        run: dep deploy staging --tag=${{ env.GITHUB_REF }} -vvv

Now let's initiate the deploy process.  Deployer will upload the project to the server.  The difference between the two parts:

  • secrets.DOT_ENV_PRODUCTION;
  • secrets.DOT_ENV_STAGING

So different environment variables for different deployments can be injected.  In most situations,  different environment variables for production and staging.

 

Server credentials

If you haven't already you will need to generate a SSH key.  How come?  GitHub actions needs access to the server to deploy the changes.  Do this using the private key for your server.  To do this, you will need to generate an SSH key if you haven’t done that already, log in to your server and run:

ssh-keygen

Work through a few prompts and you can accept the default values.  Make sure that there is no passphrase given to your SSH key during the prompt setup.

 

id_rsa

Two files named id_rsa.pub and id_rsa will be generated for you in the ~/.ssh directory. Next, copy the content of id_rsa, not id_rsa.pub. id_rsa.pub contains your public key while id_rsa contains your private key:

cat ~/.ssh/id_rsa

You need to add the public key generated by SSH keygen to the authorized_keys so that any connection attempted using the private key will be allowed. This is done by running this command:

cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

The command above will produce no output so do not worry if nothing happens after running the command.

Adding SSH to GItHub

Steps to add:

  1. In GitHub, go to the corresponding project, click the Settings tab of the project;
  2. Click on Secrets, then click Actions;
  3. Create a new secret by clicking New repository secret;
  4. Enter a name that corresponds to what has been defined in the Workflow.  Such as SSH_PRIVATE_KEY and then put the content you copied as the value;

Obtain the SSH Known Hosts of the server and also add it to the Secrets section.  From your local machine you have to run to get the ssh known hosts:

ssh-keyscan rsa -t {server_ip_address}

Copy the output of this command – {server_ip_address} sh-rsa, add it to the secrets with the name SSH_KNOWN_HOSTS.

Repeat this process for the staging and the production servers. There are other ways to resolve this, but I'll keep it to this step for now.

 

Application environment files

Add the environment variables in the secret as per what was covered previously in 'Adding SSH to GItHub'.  This step will update the environment variables when the action is running.  How?  By compiling the environment variables needed by the app on the server into a .env file.

Then we will copy the content of this file the add to secrets with the name:

`DOT_ENV_STAGING` and `DOT_ENV_PRODUCTION`

 

Preparing Deployer

To initiate the deployment by using Deployer.  To use this, set it up locally in our project by running:

composer require deployer/deployer deployer/recipes

The package deployer/deployer is the main Deployer project while deployer/recipes includes components to configure Deployer for specific project(s) and tools like Laravel, Symfony, RSync, and etc...

Configure Deployer by creating a file named deploy.php at the root of the project:

<?php

namespace Deployer;

require 'recipe/laravel.php';
require 'recipe/rsync.php';

set('application', 'My App');
set('ssh_multiplexing', true);

set('rsync_src', function () {
    return __DIR__;
});

add('rsync', [
    'exclude' => [
        '.git',
        '/.env',
        '/storage/',
        '/vendor/',
        '/node_modules/',
        '.github',
        'deploy.php',
    ],
]);

task('deploy:secrets', function () {
    file_put_contents(__DIR__ . '/.env', getenv('DOT_ENV'));
    upload('.env', get('deploy_path') . '/shared');
});

host('myapp.io')
  ->hostname('{ip_address}')
  ->stage('production')
  ->user('root')
  ->set('deploy_path', '/var/www/my-app');

host('staging.myapp.io')
  ->hostname('{ip_address}')
  ->stage('staging')
  ->user('root')
  ->set('deploy_path', '/var/www/my-app-staging');

after('deploy:failed', 'deploy:unlock');

desc('Deploy the application');

task('deploy', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'rsync',
    'deploy:secrets',
    'deploy:shared',
    'deploy:vendors',
    'deploy:writable',
    'artisan:storage:link',
    'artisan:view:cache',
    'artisan:config:cache',
    'artisan:migrate',
    'artisan:queue:restart',
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
]);

Take note to replace your IP Address in the braces {ip_address}.

Creating the deploy.php file and commit / push the changes to the master branch on GitHub.  Go to your project in GitHub, click on the Actions tab to monitor the running actions.  Once you do click on it, you'll see that your push has triggered the workflow and the process has started already.  This process will run in stages as defined earlier in the workflowYAML file.

 

Related articles

Andrew Fletcher12 Apr 2022
Mapping out your Laravel project
Every project has to kick off somewhere. &nbsp;Yep well that's a no brainer. &nbsp;However, the number of projects that I've seen where the planning step has been omitted is too high. &nbsp;A throughly planned out project with as many features scoped before you start coding is critical in completing...
Andrew Fletcher17 Mar 2022
lando PHP version issue 7.4.x instead wanting >= 8.0.2
I installed Lando 3.6.2 and Laravel 9. &nbsp;When I visit the web page, I getting Fatal error: Composer detected issues in your platform: Your Composer dependencies require a PHP version "&gt;= 8.0.2". You are running 7.4.28. in /app/vendor/composer/platform_check.php on line...