Deployed commit

How to embed the commit id in the Express application using Codeship and Heroku.

I like Codeship CI - it has a very intuitive setup and a reliable continuous build and deployment service. One can build, test and deploy a Nodejs application in a few clicks. Recently I figured out how to embed the last built commit when building an Express application using Codeship and deploying it to Heroku.

Imagine you have an Express application, and you need two features to better track the run time errors

Feature 1

Pass the last git commit id when configuring Raven server-side or client-side handler using the release option. This will allow to group the real time crash data by the release / commit id.

index.ejs
1
2
3
4
5
6
7
8
9
10
<script>
(function initRaven() {
var url = '<%- config.SENTRY.url %>';
var release = '<%- config.RELEASE_ID %>' || 'dev';
var sentryConfig = {
release: release
};
Raven.config(url, sentryConfig).install();
}());
</script>

Feature 2

When viewing the bundled script files, it would be nice to see a banner text with version and commit id string to quickly determine which commit is running on the client side. I build 4 full bundles to be statically served to the client browser: vendor-scripts.js, vendor-styles.css, application.js and application.css. These are minified for the production release to vendor-scripts.min.js, vendor-styles.min.css, application.min.js and application.min.css. Into each file I would like to add a banner comment using gulp-header

gulpfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var header = require('gulp-header');
var pkg = require('./package.json');
var banner = [
'/*! <%= pkg.name %> - <%= pkg.description %>',
'* @version v<%= pkg.version %> <%= pkg.commitId %>',
'*/',
''].join('\n');
// task to minifiy already created application.js
gulp.task('min-app-scripts', function () {
var inputFilename = join(browserDestination, 'application.js');
return gulp.src(inputFilename)
.pipe(rename({suffix: '.min'}))
.pipe(uglify())
.pipe(header(banner, { pkg: pkg }))
.pipe(gulp.dest(browserDestination));
});

There is one more added difficulty. When building the javascript source from a git commit on Codeship, there is an environment variable $COMMIT_ID with the HEAD commit id. After the code has been built, the code is pushed to Heroku, where the environment does not have the commit reference or even the git repository itself! Thus we need to figure out how to build the bundle, minify it, embed the git commit id and then ship the application to the Heroku dyno.

Solution

I solved this problem by building full bundles on Codeship, then grabbing the commit id (using my own script from ggit package to avoid relying on Codeship-specific assumptions). I save the commit SHA id into a JSON file called build.json

gulpfile.js
1
2
3
4
5
6
7
8
9
10
11
var lastCommitId = require('ggit').lastCommitId;
gulp.task('save-commit', function (done) {
lastCommitId()
.then(function (id) {
var fs = require('fs');
fs.writeFileSync('./build.json', JSON.stringify({ RELEASE_ID: id }, null, 2), 'utf8');
// add to package to make available everywhere after this task
pkg.commitId = id;
done();
});
});

A typical build file will contain an id like this

build.json
1
2
3
{
"RELEASE_ID": "d7ad085b40120b012a6b0404e801fa80e6e644ca"
}

In addition to saving the id in the build.json file I have attached the id to the package object, making available to the banner task

gulpfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
var header = require('gulp-header');
var pkg = require('./package.json');
var banner = [
'/*! <%= pkg.name %> - <%= pkg.description %>',
'* @version v<%= pkg.version %> <%= pkg.commitId %>',
'*/',
''].join('\n');
...
gulp.task('save-commit', function (done) {
...
pkg.commitId = id;
done();
});

I make sure to sequence the build tasks to build the full bundles first, then grab the commit id, then to minify each budle

gulpfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gulp.task('min-app-scripts', function () {
var inputFilename = join(browserDestination, 'application.js');
return gulp.src(inputFilename)
.pipe(rename({suffix: '.min'}))
.pipe(uglify())
.pipe(header(banner, { pkg: pkg }))
.pipe(gulp.dest(browserDestination));
});
gulp.task('min-bundles',
['min-app-scripts', 'min-vendor-scripts', 'min-app-styles', 'min-vendor-styles']);
gulp.task('bundle-and-save-commit', ['bundles'], function () {
gulp.start('save-commit');
});
gulp.task('release', ['bundle-and-save-commit'], function () {
gulp.start('min-bundles');
});

Thus each minified bundle used in the production environment has the commit id embedded inside the banner comment at the top of the file. Looking at the commit id makes it simple to trace the code back to the repo.

Publishing version file

The minified bundles and build.json are built on the Codeship server. I avoid committing the minified bundle files into the git repo. They take up space and never merge without conflicts. The build.json is also transient and should be ignored by the git tool, otherwise it will constantly be modified. I added the minified files and the build.json to the list of ignored filed by default.

.gitignore
1
2
public/bundles/*.min.*
build.json

As part of the Codeship build step I forcibly add these files to the git repo because I will need to copy these files to the Heroku. Thus my Codeship Node test script in addition to the normal NPM install and test commands has the following git commands

1
2
3
4
5
6
7
8
# build and test the bundles
gulp
# commit minified bundle files and build.json
git config user.email "[email protected]"
git config user.name "ci"
git add -f public/dist/*.min.*
git add -f build.json
git commit -m "added min files and build json file"

Codeship pushes code to the Heroku remote using the commit id too. Since we just committed new code, the commit Heroku tries to push using the built-in command is 1 commit too old. Here is the command Codeship uses to push git push heroku_<project name> $COMMIT_ID:refs/heads/master. To push the commit we just created we need to update the $COMMIT_ID variable to the new HEAD commit id

1
2
3
4
5
...
git add -f build.json
git commit -m "added min files and build json file"
COMMIT_ID=`git log --format="%H" -n 1`
# commit_id now points at the new last commit

One last option needs to be set. Because we are building this commit temporarily, it should be overwritten without merging the next time we try to deploy. Thus we need to go to the Codeship project deployment settings and flip the force push switch to make sure the temp commit is force pushed git push heroku_<project name> $COMMIT_ID:refs/heads/master -f.

Now our production bundles have the right commit id embedded in them, and there is build.json file with the last commit id.

Alternative: last commit without gulp or grunt

If you prefer to use npm scripts and avoid gulp or grunt tools, you can still easily grab the last commit id and save it in build.json. Just add dependency on ggit package and use its command by adding command to your package.json

npm install --save ggit
package.json
1
2
3
"scripts": {
"last-commit": "ggit-last -f build.json"
}

The add additional command at the end of the Codeship test script

...
npm run last-commit

This should save build.json after each successful test run.

Using commit id in Express server

If you use express.js as a server, it would be convenient to embed the commit id in the served meta tags. One can read the build.json and pass the commit to the response.render method

1
2
3
4
5
6
7
8
9
10
11
12
function loadLastCommitId(serverApp) {
var exists = require('fs').existsSync;
var join = require('path').join;
// assume the generated build.json is in the same folder
var filename = join(__dirname, './build.json');
if (exists(filename)) {
var build = require(filename);
serverApp.set('commit', build.id);
console.log('set last commit id to', build.id);
}
}
loadLastCommit(app);

If the entire SHA commit id is too long (it is in my opinion), just shorten it to the first 7 chars

1
2
3
var shortCommitId = build.id.substr(0, 7);
serverApp.set('commit', shortCommitId);
console.log('set last commit id to %s from long %s', shortCommitId, build.id);

One can then embed the commit id in any view, for example

1
2
3
4
5
6
function middleware(req, res) {
res.render('viewFileName', {
title: 'My title',
commit: req.app.get('commit') || 'unknown'
});
}

where the viewFileName.jade includes the meta tag

viewFileName.jade
1
2
3
4
5
6
7
doctype html
html
head
meta(charset='utf-8')
meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name='commit', content='#{commit}')
title #{title}

Having commit meta tag in each page is very convenient for debugging any potential bugs.

Passing commit id to Sentry

How do we load the RELEASE_ID from build.json and pass it to both the server-side (I am using raven-express) and client-side Raven config? To manage all configuration settings depending on the environment / command line option / settings file and defaults, I am using nconf. Its best feature is the hierarchical merging of settings it loads from one or json files, with the process environment variables and command line arguments. The server can simply load each config file in addition to the defaults. Non-existent files will be ignored.

config.js
1
2
3
4
5
6
7
var nconf = require('nconf');
nconf.env().argv();
nconf.file('build.json', path.join(rootPath, '/build.json'));
nconf.defaults({
RELEASE_ID: 'dev',
// other settings
});

Thus during runtime, the RELEASE_ID setting from the build.json will overwrite the default dev value. We can use the config value when rendering the index.ejs template shown above

index.js
1
2
3
4
5
6
7
function showIndex(req, res) {
var config = {
RELEASE_ID: conf.get('RELEASE_ID')
// other settings
};
return res.render('index', {config: config});
}

That is it. All Sentry errors will be tagged with the code commit id, making tracing the errors to the original source code very simple.

Update 1 - last-commit and built-version

To simplify saving / loading of the last commit SHA, I created last-commit. Install the package and save it as normal dependency in your server / application.

npm install --save last-commit

After CI builds (probably has Git repo information), run the following NPM command to save the last commit in build.json: npm run last

package.json
1
2
3
"scripts": {
"last": "last-commit"
}

Follow the instructions above to deploy the generated build.json with the rest of the code onto the hosted environment.

At the server startup, use the function exported by last-commit to read either the last commit id from the repo or from build.json

1
2
3
4
5
6
var getLastCommitId = require('last-commit');
getLastCommitId()
.tap(function setCommitId(id) {
// for example, set on the server
app.set('commit', id);
});

I also wrote built-version that saves both latest Git SHA and NPM version (even for semantically released packages).

Update 2 - render-vars

Commit id, version information, error catcher api string - these variables should be included in each rendered page (assuming ExpressJS). To avoid pasting same code into each view to add these variables, use render-vars utility. Just add necessary variables one by one and they will be added to whatever local variables used when calling res.render.

1
2
3
4
5
6
7
8
9
10
11
var app = express();
var renderVar = include('render-vars');
renderVar(app, 'foo', 'bar');
renderVar(app, 'life', 42);
// somewhere in the controller
function index(req, res) {
var locals = {
title: 'Home'
};
res.render('home', locals);
}

The index provides just the page-specific title value, but when rendered, the template home can use foo and life variables too, because they were combined with the locals before calling app.render internally.

{
    title: 'Home',
    foo: 'bar',
    life: 42
}

Update 3 - complete deploy from CircleCI to Heroku

Here is a complete deploy command from CirclECI to Heroku. This uses NPM command save-build which runs git-last command.

1
2
3
4
5
{
"scripts": {
"save-build": "git-last -f build.json"
}
}

We also need to generate a local .npmrc file and include it with Heroku deploy IF we have private NPM modules and need Heroku to authenticate when installing.

We will pass new commit (with build.json and .npmrc files) via COMMIT_ID environment variable that we will write into .circlerc file to pass to the next command. Each CircleCI command runs as its own shell script, so we cannot simply export it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
deployment:
production:
branch: master
commands:
- npm run save-build
- cat build.json
# include the generated build file with deploy, but we have to
# force it because it is normally ignored by Git
- git add -f build.json
# copy .npmrc file with token variable
- echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' >> .npmrc
- git add -f .npmrc
# before we can commit we need to set user info
- git config --global user.email "$CIRCLE_USERNAME@$CIRCLE_PROJECT_USERNAME"
- git config --global user.name "CircleCI Deploy"
- git commit -m "added build.json"
- echo export COMMIT_ID=`git log --format="%H" -n 1` >> $HOME/.circlerc
# commit_id now points at the new last commit
- echo "New commit to deploy is $COMMIT_ID"
# push specific commit and force it because it is not on master
- git push -f [email protected]:<project name>.git $COMMIT_ID:master

Substitute the project (repo) name in the Heroku URL in the last line to push.