Auto Deploy Site with Github Webhooks

The Scenario

You work on your website locally and you use Github to source control your site. After pushing changes to Github, you login to your server and pull the changes. In this article we will discuss how to automate the pulling of changes from the Github repository on your server.

Prerequisites

This article assumes that you are running a server with PHP where running shell_exec('whoami') returns www-data and that all files and folders in the git respository are owned by www-data.

The Webhook

In your repository settings, create a webhook that points to https://yoursite/github-webhook-handler.php. Set the content type to application/x-www-form-urlencoded and set a secret.

The Server Files

The github-webhook-handler.php script is given by:

<?php
/**
 * GitHub webhook handler template.
 * 
 * @see  https://developer.github.com/webhooks/
 * @orignal author  Miloslav Hůla (https://github.com/milo)
 */
$hookSecret = 's.e.c.r.e.t'; 
set_error_handler(function($severity, $message, $file, $line) {
    throw new \ErrorException($message, 0, $severity, $file, $line);
});
set_exception_handler(function($e) {
    header('HTTP/1.1 500 Internal Server Error');
    echo "Error on line {$e->getLine()}: " . htmlSpecialChars($e->getMessage());
    die();
});
$rawPost = NULL;
if ($hookSecret !== NULL) {
    if (!isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) {
        throw new \Exception("HTTP header 'X-Hub-Signature' is missing.");
    } elseif (!extension_loaded('hash')) {
        throw new \Exception("Missing 'hash' extension to check the secret code validity.");
    }
    list($algo, $hash) = explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'], 2) + array('', '');
    if (!in_array($algo, hash_algos(), TRUE)) {
        throw new \Exception("Hash algorithm '$algo' is not supported.");
    }
    $rawPost = file_get_contents('php://input');
    if ($hash !== hash_hmac($algo, $rawPost, $hookSecret)) {
        throw new \Exception('Hook secret does not match.');
    }
};
if (!isset($_SERVER['CONTENT_TYPE'])) {
    throw new \Exception("Missing HTTP 'Content-Type' header.");
} elseif (!isset($_SERVER['HTTP_X_GITHUB_EVENT'])) {
    throw new \Exception("Missing HTTP 'X-Github-Event' header.");
}
switch ($_SERVER['CONTENT_TYPE']) {
    case 'application/json':
        $json = $rawPost ?: file_get_contents('php://input');
        break;
    case 'application/x-www-form-urlencoded':
        $json = $_POST['payload'];
        break;
    default:
        throw new \Exception("Unsupported content type: $_SERVER[CONTENT_TYPE]");
}
# Payload structure depends on triggered event
# https://developer.github.com/v3/activity/events/types/
$payload = json_decode($json);
switch (strtolower($_SERVER['HTTP_X_GITHUB_EVENT'])) {
    case 'ping':
        echo 'pong';
        break;
    default:
        header('HTTP/1.0 404 Not Found');
        $output = shell_exec('./git-pull.sh 2>&1');
        echo $output; // For debug only. Can be found in GitHub hook log.
        die();
}

The git-pull.sh in the same directory is given by:

#!/bin/bash

sudo -u www-data -H git fetch origin
reslog=$(sudo -u www-data -H git log HEAD..origin/master --oneline)
if [[ "${reslog}" != "" ]] ; then
  sudo -u www-data -H git merge origin/master # completing the pull
  echo "pulled repository"
fi

This script resets the your local git repository to the state of the master branch on your Github repository. Therefore any untracked or unpushed changes on the server will be lost.

Giving www-data Access to git

On the server run

sudo -u www-data ssh-keygen -t rsa

and add /var/www/.ssh/id_rsa.pub to your github account. Do not add a passphrase.

Next run visudo and add the line

www-data ALL = (www-data) NOPASSWD: /usr/bin/git

to the end of the file.

To test whether www-data has permission to run git commands, cd to your repository and run sudo -u www-data -H git pull to try and pull any changes. If this works then you are done and whenever you push to Github the changes should be automatically pulled on the server.