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.
1 | <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
1 | var header = require('gulp-header'); |
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
1 | var lastCommitId = require('ggit').lastCommitId; |
A typical build file will contain an id like this
1 | { |
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
1 | var header = require('gulp-header'); |
I make sure to sequence the build tasks to build the full bundles first, then grab the commit id, then to minify each budle
1 | gulp.task('min-app-scripts', function () { |
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.
1 | public/bundles/*.min.* |
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 | # build and test the bundles |
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 | ... |
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
1 | "scripts": { |
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 | function loadLastCommitId(serverApp) { |
If the entire SHA commit id is too long (it is in my opinion), just shorten it to the first 7 chars
1 | var shortCommitId = build.id.substr(0, 7); |
One can then embed the commit id in any view, for example
1 | function middleware(req, res) { |
where the viewFileName.jade
includes the meta tag
1 | doctype html |
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.
1 | var nconf = require('nconf'); |
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
1 | function showIndex(req, res) { |
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
1 | "scripts": { |
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 | var getLastCommitId = require('last-commit'); |
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 | var app = express(); |
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 | { |
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 | deployment: |
Substitute the project (repo) name in the Heroku URL in the last line to push.