Jeff Farley
Posted on October 31, 2017
Deploy Atomically with Travis & npm
I think I am a software developer because I am lazy.
The second or third time I have to perform the same exact task, I find myself saying, “Ugh, can’t I tell the computer how to do it?”Â
So imagine my reaction when our team’s deployment process started looking like this:
git pull
-
npm run build
to create the minified packages git commit -am "Create Distribution" && git push
- Navigate to GitHub
- Create a new release
I was thinking steps 1–3 are easy enough to put in a shell script and steps 4–5 are probably scriptable, but is that all? What else needs to be done?
- The version in
package.json
was never getting updated and it would be nice to have that in synch with the GitHub release. - Can this script be run after the CI build without having to task a human to manually run it?
GitHub and Releases
My first step to full automation was digging into the details of GitHub releases. For starters, tags and releases are not the same thing.
Tags
Tags are part of Git, the underlying version control system. They come in two types:
- Lightweight – A pointer to an existing commit
- Annotated – A full object with name, timestamp and checksum hash
However, they do not contain any files or changes to the code in the repository.
Releases
Releases are a GitHub extension of tags. They provide all the functionality of tags while also providing:
- A zipped archive of the codebase at that point in time
- Any associated release binaries (likeÂ
.jar
orÂ.dll
)
Does this fit our use case?
Not 100%. In our case, the minified files had to exist within the repository as part of a Django plugin in a larger architecture. We couldn’t really take advantage of GitHub releases since their files exist outside the code base.
Also, there seems to be a chicken and egg problem with the package.json
version. I don’t know what the version number is until I tag, but I can’t store any code changes with the tag.
npm version to the rescue
The clever folks at node already figured out this problem for me:
npm version [major | minor | patch] -m "message"
When running the command, npm will:
- Read the current git tag
- Bump the version in
package.json
according to the type of bump (major, minor or patch) - Run the
version
script indicated inside ofpackage.json
git add
git commit -m "message"
git tag <new tag number>
Perfect! And if we run npm run build
during the version script, the minified files will be packaged along with the updated package.json
.
That’s great, how do I get Travis to do that?
We use Travis as our continuous integration server, but this step could be easily ported to CircleCI, AppVeyor or a number of other CI services.
The default use case is for Releases
If you start reading the Travis documentation, it is clear that their main use case is to build assets and push them to GitHub Releases. Since we already determined that isn’t for us, it was time to start experimenting and Googling.
Naïve First Steps
In package.json
, I wrote the version script to build the minified assets:
"scripts": {
...
"version": "npm run build && git add ."
"postversion": "git push && git push --tags"
}
In travis.yml
, I had it call npm version
before deploying:
before_deploy:
- npm version patch
deploy:
provider: releases
skip_cleanup: true
on:
branch: master
And then ran it in Travis:
> foo@0.6.2 postversion /home/travis/build/foo/bar
> git push && git push --tags
fatal: You are not currently on a branch.
To push the history leading to the current (detached HEAD)
state now, use
git push origin HEAD:<name-of-remote-branch>
Detached Head Mode
After a bit of Googling, it turns out that Travis always runs in detached HEAD mode. It’s sort of a safety measure, but really puts a dent in how this auto deployment is supposed to work.
I may be lazy, but I like a challenge.
Basically, we have to have Travis perform the version steps at the tip of master
and then push it to our branch. Wait, how is it going to have the credentials to do that?
API Key
Usually, Travis is performing read-only actions on the repository. When it has to write back to the repository, it needs to know which account to use. We could specify the credentials in the travis.yml
file, but then it would be exposed to everyone who views the file online. Doing this properly and safely takes the following (one time) setup:
- Create a new token at https://github.com/settings/tokens
- Copy the string of characters
- Open the settings in Travis
- Create a new environment variable
GITHUB_API_KEY
- Paste the characters in the value and make sure to uncheck “Display value in the build log”
Ok, so the credentials can be exchanged, but we still haven’t fixed the detached head problem.
Writing a Travis Script
A few Google search results (1, 2) showed me that writing a shell script which gets executed by Travis was the solution to the detached head problem.
.travis.yml
.travis/pu.sh
This does everything we set out to do:
- Builds the minified files
- Increments the version in
package.json
- Creates a new tag
- Pushes it back to the repository
- Which triggers another Travis build that…
oh no!
Avoiding the Infinite Loop
Luckily, Travis has a way of saying “don’t run”. Change line 31 to say:
npm version patch -m "chore: release version %s [skip ci]"
Whenever it encounters a commit message that contains [skip ci]
, Travis will not run.
Next Steps
Wouldn’t it be nice if Travis could also update CHANGELOG.MD
too?
Posted on October 31, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.