After moving my personal blog from WordPress to Jekyll, I decided to create a small deployment script. I’m using a private repository on GitHub to host and version control my blog. So every time I pushed a new post to the repository, I had to ssh into the server to pull the latest changes. That’s an extra friction to write blog posts for me. There were number of advantages I got since moving to Jekyll:

  1. The performance got a boost due to static HTML pages.
  2. The backup problem sorted itself out since the whole site is now on version control.
  3. Moving server to server is not a problem anymore, because all I need is bundler exec jekyll build to recreate the static pages.

The only disadvantage of Jekyll to WordPress now is that it requires manual publishing while when you press Publish on WordPress, your post would be published. So creating a little deployment script right on the repository (so the deployment script can also be on the version control) only made sense. I’m using nginx on Ubuntu 22. Since I was going to install PHP for my Laravel projects, I didn’t want to complicate things and also used PHP for my deployment script along with bash files.

I installed PHP and PHP-FPM first:

apt-get install php8.1
apt-get install php8.1-fpm

And then added index.php to the list of indexes and these location blocks in the site’s nginx server block:

# Deny access to files with git, md, and sh extensions
location ~\.(git|md|sh)$ {
	deny all;
	return 404;
}

# Pass PHP scripts to FastCGI
location ~ \.php$ {
	include snippets/fastcgi-php.conf;
	fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}

# Deny access to Apache .htaccess on Nginx with PHP
location ~ /\.ht {
	deny all;
}

As with all nginx file changes, I tested the configuration and restarted the service:

nginx -t
systemctl restart nginx

I then created three files on Jekyll’s root directory. The first one is the deploy.php which will capture GitHub’s hook shot on every time I push to the repository and create a file to be picked up by the deployment script:

<?php

function denyRequest($msg) {
	http_response_code(403);
	die("Forbidden\n{$msg}\n");
}

$deploySecretEnvName = "BLOG_DEPLOY_SECRET";
$deploySecret = $_SERVER[$deploySecretEnvName];

$postData = file_get_contents('php://input');
$signature = 'sha1=' . hash_hmac('sha1', $postData, $deploySecret);

$requiredHeaders = [
	'REQUEST_METHOD' => 'POST',
	'HTTP_X_GITHUB_EVENT' => 'push',
	'HTTP_X_HUB_SIGNATURE' => $signature,
];

if ($_SERVER['REQUEST_METHOD'] != $requiredHeaders['REQUEST_METHOD']) {
	denyRequest("Wrong request method");
}

if ($_SERVER['HTTP_X_GITHUB_EVENT'] != $requiredHeaders['HTTP_X_GITHUB_EVENT']) {
	denyRequest("Wrong event");
}

if ($_SERVER['HTTP_X_HUB_SIGNATURE'] != $requiredHeaders['HTTP_X_HUB_SIGNATURE']) {
	denyRequest("Wrong signature");
}

file_put_contents("/var/www/ekinocalan.com/html/blog/deploy_flag", "deploy");

http_response_code(200);
die("Successful\n");

?>

We’re going to get back to how the GitHub webhook works and how this php script secures it. But first, the second file which is a small bash script called check.sh that picks up the deployment flag:

#!/bin/sh
FILE=/var/www/ekinocalan.com/html/blog/deploy_flag
if test -f "$FILE"; then
    rm "$FILE"
    bash /var/www/ekinocalan.com/html/blog/deploy.sh
fi

I run this script on crontab, so it always checks whether there is a deployment request or not:

crontab -e
* * * * * bash /var/www/ekinocalan.com/html/blog/check.sh
Esc + :wq

And the actual deploy.sh file that is triggered if there is a request to deploy:

#!/bin/sh
cd /var/www/ekinocalan.com/html/blog && git pull origin main

If you haven’t done before, it’s also necessary to make www-data user the owner of the repository files. Otherwise the php script won’t be able to create deploy_flag file which triggers the whole deployment process.

chown -R www-data:www-data /var/www

In theory, the deployment would work with these adjustments. The only thing left to do is to utilize GitHub webhooks now. This is the configuration I made for the webhook on the repository:

image

And then I followed Securing your webhooks. I used this command to create a random secret:

ruby -rsecurerandom -e 'puts SecureRandom.hex(20)'

The main trouble I had was to add this secret somewhere on the server so that it doesn’t live on the version control. If you see the deploy.php file, you’ll see that I used an environment variable called BLOG_DEPLOY_SECRET. So somehow, I needed to assign the secret value to this environment variable. For that:

vim /etc/php/8.1/fpm/pool.d/www.conf

And then I added this line in the appropriate section (just look for commented-out env[] assignments to find the section):

env[BLOG_DEPLOY_SECRET] = secret_value

That’s all!