Instantly share code, notes, and snippets.

@oxguy3 /deploy.php
Last active Nov 13, 2018

Embed
What would you like to do?
Script used to automatically deploy from GitHub to a cPanel shared hosting server

deploy.php

This is the script I wrote to deploy all commits to the master branch on GitHub onto my shared hosting cPanel server for coebot.tv. Because my cPanel server doesn't allow exec() or shell_exec() from PHP, I had to get creative. I worked around this by SSHing into the local server and running the command that way.

I also took care to validate the secret that GitHub passes along to prevent anyone else from activating the webhook. This is something I notice a lot of the other variations of this script I've found via googling neglect to do, but it seems like a good idea so I went for it.

Installation

First, you need to put deploy.php on your server, somewhere where it's accessible to the public (I've only tested putting it right at /public_html/deploy.php, but I can't think of any reason you couldn't rename it or put it in a subdirectory). At the top of the script is a series of define() statements -- you will need to change most of these to suit your setup (they all have comments describing what they do).

Now, odds are, your web server is still configured to use your hosting provider's native version of PHP, and odds are that version won't include the ssh2 module that deploy.php requires. So, open cPanel, and go to "Select PHP Version" (it's under "Software and Services"). There will be a dropdown that will probably have "native (X.Y)" as the selected option (where "X.Y" is your PHP version number). Switch the dropdown to just "X.Y". It should give you a big list of modules with checkboxes. For this script, make sure you have the "ssh2" and "json" modules checked. Also be sure to include any modules that your project needs (This varies hugely depending on what your project is, but mcrypt, mysql, mysqli, http, mbstring, tidy, pdo are all pretty common ones. It's safer to accidentally enable ones you don't need than to leave out ones you do need.)

Now you'll need to generate a keypair so that your deploy.php script can SSH into the local server (it's silly, I know). First, SSH into the cPanel server (i.e. using PuTTY or whatever). Then, you'll need to generate a keypair named "deploy" with no passphrase, and then append the public half of the keypair to your authorized_keys file. Here's what your SSH session should basically look like (please use a better passphrase than "abc123"):

$ cd .ssh
$ ssh-keygen -t rsa -b 4096 -C "deploy"
Generating public/private rsa key pair.
Enter file in which to save the key (/home/johndoe/.ssh/id_rsa): deploy
Enter passphrase (empty for no passphrase): abc123
Enter same passphrase again: abc123
Your identification has been saved in deploy.
Your public key has been saved in deploy.pub.
The key fingerprint is:
cd:48:4e:6d:69:9f:de:67:af:44:2d:d8:07:e4:e1:70 deploy
The key's randomart image is:
+--[ RSA 4096]----+
|            . E  |
|         . . * . |
|        o =   +  |
|       + * . + o |
|        S o + + o|
|           . o o |
|            . o o|
|             . o.|
|              ...|
+-----------------+
$ cat deploy.pub >> authorized_keys

You'll need to create a webhook in the settings for your repo on GitHub. Set the Payload URL to the URL of this script (i.e. "http://example.com/deploy.php"), set the Content type to "application/json", and set the Secret to the same string of random characters that you set GITHUB_SECRET to in deploy.php. Check the radio box for "Just the push event" and then save the webhook.

You also need to have already cloned your repository into ~/public_html on the cPanel server. You can do this by manually SSHing in to the server from your local machine and running the git clone command.

I think that covers it all? Tweet me @oxguy3 if you have any issues.

<?php
/**
* deploy.php by Hayden Schiff (oxguy3)
* Available at https://gist.github.com/oxguy3/70ea582d951d4b0f78edec282a2bebf9
*
* No rights reserved. Dedicated to public domain via CC0 1.0 Universal.
* See https://creativecommons.org/publicdomain/zero/1.0/ for terms.
*/
// random string of characters; must match the "Secret" defined in your GitHub webhook
define('GITHUB_SECRET', 'qwertyuiop1234567890');
// name of the git branch that you're deploying
define('GITHUB_BRANCH', 'master');
// your email address, where you'll receive notices of deploy successes/failures
define('EMAIL_RECIPIENT', 'johndoe@example.com');
// domain of your website
define('SITE_DOMAIN', 'example.com');
// port number of SSH for your server (may vary by hosting provider -- for me, it was 21098)
define('SSH_PORT', 22);
// username for SSH (should be the same as your cPanel login username)
define('SSH_USERNAME', 'johndoe');
// filename for the keypair to use -- no need to change this if you follow the readme instructions
define('KEYPAIR_NAME', 'deploy');
// the passphrase for your keypair
define('KEYPAIR_PASSPHRASE', 'abc123');
// END OF CONFIGURATION OPTIONS
/**
* Convenience function for sending emails
*
* If you want to disable email sending, just replace the content of this
* function with "return true;".
*/
function sendEmail($success, $message)
{
$headers = 'Content-type: text/plain' . "\r\n" .
'From: admin@'.SITE_DOMAIN;
$subject = '['.SITE_DOMAIN.'] ';
if ($success) {
$subject .= 'Deploy success';
} else {
$subject .= 'Deploy failure';
$headers .= "\r\n" .
'X-Priority: 1 (Highest)' . "\r\n" .
'X-MSMail-Priority: High' . "\r\n" .
'Importance: High';
}
return mail(
EMAIL_RECIPIENT,
$subject,
$message,
$headers
);
}
try {
$signature = $_SERVER['HTTP_X_GITHUB_EVENT'];
if (is_null($signature) || $signature != 'push') {
header('HTTP/1.0 400 Bad Request');
die('go away');
}
$payload = file_get_contents('php://input');
// get the signature out of the headers and split it into parts
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE'];
$sigParts = explode('=', $signature);
if (sizeof($sigParts) != 2) {
throw new Exception('Bad signature: wrong number of \'=\' chars');
}
$sigAlgo = $sigParts[0];
$sigHash = $sigParts[1];
// verify that the signature is correct
$hash = hash_hmac($sigAlgo, $payload, GITHUB_SECRET);
if ($hash === false) {
throw new Exception("Unknown signature algo: $sigAlgo");
}
if ($hash != $sigHash) {
throw new Exception("Signatures didn't match. Ours: '$hash', theirs: '$sigHash'.");
}
// read the payload
$data = json_decode($payload);
if (is_null($data)) {
throw new Exception('Failed to decode JSON payload');
}
// make sure it's the right branch
$branchRef = $data->ref;
if ($branchRef != 'refs/heads/'.GITHUB_BRANCH) {
die("Ignoring push to '$branchRef'");
}
// ssh into the local server
$sshSession = ssh2_connect('localhost', SSH_PORT);
$authSuccess = ssh2_auth_pubkey_file(
$sshSession,
SSH_USERNAME,
'/home/'.SSH_USERNAME.'/.ssh/'.KEYPAIR_NAME.'.pub',
'/home/'.SSH_USERNAME.'/.ssh/'.KEYPAIR_NAME,
KEYPAIR_PASSPHRASE
);
if (!$authSuccess) {
throw new Exception('SSH authentication failure');
}
// start a shell session
$shell = ssh2_shell($sshSession, 'xterm');
if ($shell === false) {
throw new Exception('Failed to open shell');
}
stream_set_blocking($shell, true);
stream_set_timeout($shell, 15);
// run the commands
$output = '';
$endSentinel = "!~@#_DONE_#@~!";
fwrite($shell, 'cd ~/public_html' . "\n");
fwrite($shell, 'git pull' . "\n");
fwrite($shell, 'echo ' . escapeshellarg($endSentinel) . "\n");
while (true) {
$o = stream_get_contents($shell);
if ($o === false) {
throw new Exception('Failed while reading output from shell');
}
$output .= $o;
if (strpos($output, $endSentinel) !== false) {
break;
}
}
fclose($shell);
fclose($sshSession);
$mailBody = "GitHub payload:\r\n"
. print_r($data, true)
. "\r\n\r\n"
. "Output of `git pull`:\r\n"
. $output
. "\r\n"
. 'That\'s all, toodles!';
$mailSuccess = sendEmail(true, $mailBody);
} catch (Exception $e) {
$mailSuccess = sendEmail(false, strval($e));
}
if(!$mailSuccess) {
header('HTTP/1.0 500 Internal Server Error');
die('Failed to send email to admin!');
}
die("All good here!");
?>
@rhali786

This comment has been minimized.

rhali786 commented Jan 9, 2018

Hi it appears that this requires a clone to already have been done.

@oxguy3

This comment has been minimized.

Owner

oxguy3 commented Feb 16, 2018

@rhali786 Sorry, just saw this. Yes, that's correct -- you should just manually SSH in and clone your repo into ~/public_html (or wherever you have your site's root -- just be sure to update the cd command in deploy.php as well).

@shadowsyntax

This comment has been minimized.

shadowsyntax commented Feb 22, 2018

Hello, I'm getting an authentication failure. Tried changing my passphrase and creating new pub_keys but still same result.
Exception: SSH authentication failure in public_html/deploy.php:109 Stack trace: #0 {main}
What could be the problem? Thanks.

@azazqadir

This comment has been minimized.

azazqadir commented Mar 19, 2018

Instead of going through all this trouble for deploying a php website to cpanel via github, why not just move to a better hosting platform? For example, there is Cloudways PHP web hosting PaaS that has a built-in git integration. You only have to connect the your server on the platform to your github repo and then start deploying your PHP code to the server in a click of a button.

@iman-charleselena

This comment has been minimized.

iman-charleselena commented Mar 28, 2018

Hi, does it require Git to be installed on the shared hosting?

@davidshare

This comment has been minimized.

davidshare commented May 13, 2018

Is it possible to setup autodeploy on viewen.com?

@kingaslan30

This comment has been minimized.

kingaslan30 commented Aug 28, 2018

@oxguy3 I am trying to reference my deploy.php for payload like this: http://www.example.com/deploy.php, but keep running into 301 and 404 errors. I get 301 errors when I do it without www and 404 errors when I add www. It seems like it can't find the file but deploy.php is sitting on my root folder (public_html). Any help on this would be much appreciated!

ps. deploy.php has 644 permissions
and example.com is replaced with my domain name

UPDATE: After some investigation, I moved deploy.php to public folder on my server and the webhook is actually able to find the file, but when it tries to deliver I get a service timeout message. Any thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment