GitHub Actions

Table of contents

Loading...

Introduction

GitHub Actions is very practical for Continuous Integration and Deployment. It automates tasks such as building the application, running tests and deploying the code.

Automating these tasks not only makes life easier, but also reduces the risk for potentially fatal mistakes when forgetting to run a task.

GitHub Actions workflow

A GitHub workflow is a YAML file that defines a set of jobs and steps that GitHub Actions will automatically run on specified events. This file is typically stored in the .github/workflows directory of the repository.

The workflow file contains the following main sections:

GitHub env configuration

The GitHub Actions environment has its own configuration file config/env/env.github.php.

For settings.php to load the values of this file, the APP_ENV environment variable has to be set in the workflow file.

# ...

jobs:
  run:
    steps:
      - name: Run test suite
        run: composer test
        env:
          APP_ENV: github
          PHP_CS_FIXER_IGNORE_ENV: 1

In addition to the GitHub Actions configuration values, the test values also need to be loaded since tests are run on the GitHub server.

File: config/env/env.github.php

<?php

// `require` has to be used instead of `require_once`.
// The values have to be loaded for each test case not only once for the whole test suite.
require __DIR__ . '/env.test.php';

// Database
$settings['db']['host'] = '127.0.0.1';
$settings['db']['database'] = 'slim_example_project_test';
$settings['db']['username'] = 'root';
// The password in the workflow file is 'root' and not empty
$settings['db']['password'] = 'root';

Secret configuration

Sensitive values that should be hidden in the workflow file, such as API tokens, are stored as GitHub secrets accessible in the repository Settings > Security > Secrets and variables > Actions.

They can be accessed in the workflow file with the syntax ${{ secrets.SECRET_NAME }}.

Build testing

With the following workflow file build.yml, the application is built, and the tests are run on every push to the master or develop branch as well as on every pull request to these branches.

The matrix section defines the different variables that are used in the workflow.
They can be accessed with ${{ matrix.variable-name }}.

File: .github/workflows/build.yml

name: ๐Ÿงช Build test
on:
  push:
    branches:
      - master
      - develop
  pull_request:
    types: [ opened, synchronize, reopened ]

env:
  # Set APP_ENV to 'github' so that settings.php loads the correct configuration for database migrations and testing
  APP_ENV: github

jobs:
  run:
    runs-on: ${{ matrix.operating-system }}
    strategy:
      matrix:
        operating-system: [ ubuntu-latest ]
        php-versions: [ '8.2' ]
        test-database: [ 'slim_example_project_test' ]
    name: PHP ${{ matrix.php-versions }} Test

    services:
      mysql:
        image: mysql:8.0.23
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_ALLOW_EMPTY_PASSWORD: true
          MYSQL_DATABASE: test
        ports:
          - 33306:3306

    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0 #sonarcloud shallow clone warning https://stackoverflow.com/a/62500400/9013718

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-versions }}
          extensions: mbstring, pdo, pdo_mysql, intl, zip
          coverage: xdebug

      - name: Check PHP version
        run: php -v

      - name: Check Composer version
        run: composer -V

      - name: Check PHP extensions
        run: php -m

      - name: Check MySQL version
        run: mysql -V

      - name: Start MySQL
        run: sudo systemctl start mysql

      - name: Check MySQL variables
        run: mysql -uroot -proot -e "SHOW VARIABLES LIKE 'version%';"

      - name: Set MySQL timezone to swiss time # Change to your timezone
        run: mysql -uroot -proot -e "SET GLOBAL time_zone = '+01:00';"

      - name: Create database
        run: mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS ${{ matrix.test-database }} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'

      - name: Validate composer.json and composer.lock
        run: composer validate

      - name: Install dependencies
        run: composer update --no-ansi --no-interaction --no-progress

      - name: Execute database migrations
        run: composer migrate:prod

      - name: Show test db tables
        run: mysql -uroot -proot -D ${{ matrix.test-database }} -e "SHOW TABLES;"

      - name: Run test suite
        run: composer test:coverage
        env:
          PHP_CS_FIXER_IGNORE_ENV: 1

Continuous integration with Scrutinizer

For a more extended quality and security analysis, I recommend using a cloud-based code review tool that detects bugs, vulnerabilities, code smells and makes coverage reports. There are many great services, and most are free for open source projects (e.g. Code Climate, Codacy, SonarCloud etc.).

This guide shows how to set up Scrutinizer for open source projects.

Deploying to server

The deployment process involves multiple steps that can be automated with GitHub Actions.

The code should only be deployed to the server if the tests are passing and the code was pushed to the master branch.

With FTP-Deploy-Action the files are uploaded to the server via FTP. The paths .git*/**, tests/** and docs/** are excluded.

The database migrations are executed on the server by making an ssh connection to the terminal via ssh-action and then running the migration command.

File: .github/workflows/deployment.yml

name: ๐Ÿš€ Deployment

# Only trigger, when the build workflow is done
on:
  workflow_run:
    workflows: [ "๐Ÿงช Build test" ] # Replace with the name of the build workflow
    types:
      - completed
jobs:
  run:
    # Run job only on success of the build test workflow, and if the event was a push to the master branch
    if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'master' }}
    runs-on: ubuntu-latest
    name: Deploy PHP application
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.2
          # extensions: mbstring, pdo, pdo_mysql, intl, zip
          coverage: none

      - name: Cache Composer packages
        id: composer-cache
        uses: actions/cache@v2
        with:
          path: vendor
          key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-php-

      - name: Validate composer.json and composer.lock
        run: composer validate

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress

      - name: ๐Ÿ“‚ Sync files
        uses: SamKirkland/FTP-Deploy-Action@4.3.3
        with:
          server: ${{ secrets.FTP_HOST }}
          username: ${{ secrets.FTP_USERNAME }}
          password: ${{ secrets.FTP_PASSWORD }}
          server-dir: /
          exclude: |
            .git*/**
            docs/**
            tests/**

      - name: Executing database migrations
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_KEY }}
          passphrase: ${{ secrets.SSH_KEY_PASSPHRASE }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            cd ${{ secrets.DEMO_PROJECT_ROOT }}
            chmod +x vendor/bin/phinx 
            composer migrate:prod
^