Set up a simple deploy script
Updating servers manually is a boring, repetitive task, and a thing of the past. Learn how to use Github webhooks to deploy code changes automatically.
The process looks as follows:
- Set up a deploy key on your server and add it to Github
- Create a webhook and point it to a url you will listen to
- Create a shell script that will be executed whenever you want to deploy
- Set up a small Express server to listen in on the url
1. Set up a deploy key
I can’t explain it better than the Github docs, so follow their guidelines for managing deploy keys here:
https://developer.github.com/guides/managing-deploy-keys/#deploy-keys
The rest of the article assumes you have a valid deploy key installed on your server and added to the repository on Github.
2. Add a webhook
Whenever an event happens on Github, Github will make a POST request to a URL you provide. The body of the request will contain all sorts of metadata, including info about the user who performed the action, the branch involved, etc. It’s up to you to decide what to do with that data.
You get to decide what events trigger the webhook. By default, Github will make a request to your endpoint for every push event, which is anytime the remote repository is modified.
Get Marius Craciunoiu’s stories in your inbox
Join Medium for free to get updates from this writer.
To add a webhook, go into your repository settings and switch to the Webhooks & Services tab. Click on Add Webhook and fill out the form.
Payload URL
This is the URL Github will make the POST requests to.
http://deploy.example.com/bananas?mode=masterContent Type
The default, JSON, is perfect.
Secret
The secret will be used to generate a sha1 hex digest of the request body. It will be included in the headers as X-Hub-Signature. We’ll use this to verify the request is coming from Github and not from anyone else. In this example, we’ll make our secret:
githubsecretAs soon as you save your hook, GitHub will make a test call to the URL you specified. It will fail, but don’t worry. We’ll ask Github to resend it later. This is a good opportunity to see the format and data that is sent over.
3. Create your shell script
You could run your deployment steps within node, but writing a shell script is probably easier and less of a headache. In our example, a file called bananas.sh will be executed whenever anyone pushes to the master branch.
vim deployer/scripts/bananas.shWe’ll do a simple example where we turn off an Express server, update master, and restart the server. Feel free to adjust the code to your needs/server.
cd /var/www/mysite
git fetch
forever stop dist/index.js
git merge origin/master
NODE_ENV=production forever start dist/index.js4. Set up an Express deploy server
Let’s begin setting up our deploy server by declaring the dependencies. Create a file called package.json:
{
"name": "deployer",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "~4.6.1",
"body-parser": "~1.4.3"
}
}Next, install the dependencies:
npm installThe structure of our app will look like this:
/deployer
—/scripts
—-bananas.sh
-index.js
-deploy.js
-validate.js
-hmacChecker.jsindex.js
This is the base file which contains our error handler and route. Note that we’ll allow support to deploy multiple scripts based on the url. If you host multiple projects on the same VPS, you can use the same deploy server for all of them.
var express = require('express'),
bodyParser = require('body-parser'),
deploy = require('./deploy'),
validate = require('./validate'),
app = express();app.use(bodyParser.json());// routes
app.post('/deploy/:site', validate, deploy, respond);// error handler
app.use(function(err, req, res, next) {
res.status(err.status ? err.status : 500);
res.send(err.message);
});function respond(req, res, next) {
res.status = 200;
res.send(req.deployResponse);
};var server = app.listen(9301, function() {
console.log('Listening on port %d', server.address().port);
});
hmacChecker.js
This file generates a sha1 hex digest using the request body and the secret key (githubsecret in our example) and compares the result to the hash provided in the request header X-Hub-Signature. The function returns true if the strings match.
var crypto = require('crypto');module.exports = function(requestHmac, body, secret) {
var generatedHmac = crypto.createHmac('sha1', secret).update(JSON.stringify(body)).digest('hex');
return generatedHmac === requestHmac;
};
validate.js
Here we are checking for a few things:
- the request includes X-Hub-Signature in the headers
- the deployer was provided a secret as an env/shell variable for the requested site
- the github signature matches the sha1 hex digest generated from the request body
- there is a deploy script for the requested site
var checkHmac = require('./hmacChecker'),
fs = require('fs');module.exports = function (req, res, next) {
if (!req.headers['x-hub-signature'] || !req.headers['x-hub-signature'].length >= 5) {
var err = new Error('You must include x-hub-signature header. See github docs.');
err.status = 401;
return next(err);
}; if (!process.env[req.params.site.toUpperCase() + '_KEY']) {
var err = new Error('I do not have a secret for this site.');
err.status = 404;
return next(err);
}; var requestHmac = req.headers['x-hub-signature'].substring(5),
secret = process.env[req.params.site.toUpperCase() + '_KEY']; if (!checkHmac(requestHmac, req.body, secret)) {
var err = new Error('HMAC Failed!');
err.status = 403;
return next(err);
} if (!fs.existsSync(__dirname + '/scripts/' + req.params.site + '.sh')) {
var err = new Error('I do not have a deploy script for this site.');
err.status = 404;
return next(err);
}; next();
};
If everything is in order, we’ll proceed to the next function.
deploy.js
This is where the magic happens. If the query parameter ?mode=master is provided, we’ll execute the script for every push to master, otherwise, we’ll only deploy when a tag is pushed.
The function will look for a file that matches the url parameter defined in index.js and execute that file.
If there are any errors when the script is executed, we’ll return a 500 and the error message. Otherwise, we’ll return a 200 and the shell output.
var exec = require('child_process').exec;module.exports = function (req, res, next) {
var shellCommand = 'sh '+ __dirname + '/scripts/' + req.params.site + '.sh'; // Determine mode
var condition;
if (req.query.mode === 'master') {
condition = req.body.ref == 'refs/heads/master';
} else {
condition = req.body.ref.indexOf('refs/tags') >= 0;
}; // Start deployment process
if (condition) {
console.log('Beginning deploy script: ', shellCommand);
// Execute our shell script
exec(shellCommand, function (error, stdout, stderr) {
console.log(stdout); // feedback
if (stderr) console.log('stderr: ' + stderr); // oh noes
if (error) {
error.status = 500;
return next(error);
} else {
req.deployResponse = stdout;
};
console.log('Deploy script complete.')
return next();
});
} else {
req.deployResponse = 'Nothing to do here.';
return next();
}
};
That’s it. Now start your server:
BANANAS_KEY=githubsecret node index.jsContribute
The code for this project is available on github. To get started:
- clone the repository to your server
- add your scripts to the scripts folder
- start the server like above
Suggestions and pull requests are always welcome.





