Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@oxguy3
Last active August 5, 2023 20:21
Show Gist options
  • Star 80 You must be signed in to star a gist
  • Fork 24 You must be signed in to fork a gist
  • Save oxguy3/70ea582d951d4b0f78edec282a2bebf9 to your computer and use it in GitHub Desktop.
Save oxguy3/70ea582d951d4b0f78edec282a2bebf9 to your computer and use it in GitHub Desktop.
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 @haydenschiff 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!");
?>
@kingaslan30
Copy link

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?

@gficher
Copy link

gficher commented Jan 29, 2019

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

Yes, it does require.

This script receives updates from github and automatically pulls new commits from the remote repository.

@gficher
Copy link

gficher commented Jan 29, 2019

I've tried to create a script like that some time ago but Github seems to have a short response timeout and it logs as an error sometimes but the script continues running on the server normally. Another way to do that is to use the PHP exec() to run git pull locally.

@timzprof
Copy link

I keep getting the Signatures don't match exception and i cross-checked that my GITHUB_SECRET is the same as the secret for my webhook

@umanga907
Copy link

I keep getting the Signatures don't match exception and i cross-checked that my GITHUB_SECRET is the same as the secret for my webhook

I am stuck with the same problem.

@bereczandor
Copy link

It works like a charm. For a newby like me it took about 2 hours to get it working :D
Thanks!

@boypanjaitan16
Copy link

Is it anything I need to configure to make mail-sending process work? because seems like my mail sender not working

@rkoebrugge
Copy link

Hi @oxguy3 ,
I run into an issue that the git pull request wants the SSH passphrase. I tried echo-ing that passphrase after row 123 of the echo git pull, but that doesn't work. I do see the passphrase in the mail, but the script doesn't pick the passphrase. I also searched for saving the passphase into the webserver settings, but that is all in the SSH session; for my putty session that works, but the session dit deploy scripts start is from Github off course. Hope you can help!

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