read

I was working on a Ruby app that depended on a few private gems, the development was not just changing the app but also those gems.

During development the Gemfile would look like:

gem 'cargo_estimates', path: 'local_engines/cargo_estimates'
gem 'shipping_calculator', path: 'local_engines/shipping_calculator'

Running bundle would set the Gemfile.lock to those gem local version.

Committing Gemfile.lock in version control now could have disastrous consequences if the code is deployed.

Once the local gems updates are published you must manually change the Gemfile to:

gem 'cargo_estimates'
gem 'shipping_calculator'

and run bundle to update the Gemfile.lock to use the remote gems.

This works but if your daily workflow mean multiple changes to the local gems it could get tedious to manually changing where to point those gems and to make sure Gemfile.lock is pointing to the right gems.

So I came up with a set of scripts to automate the development bootstrap and facilitate the workflow.

In my project folder I had a dev_bootstrap.sh calling a set of script located in PROJECT_DIRECTORY/dev_bootstrap_scripts:

#!/bin/bash
./dev_bootstrap_scripts/precommit_hooks.sh
./dev_bootstrap_scripts/local_engines.sh
./dev_bootstrap_scripts/set_rvmrc.sh

Conditional Gemfile.lock generation

Bundler is built to have one Gemfile.lock so I came up with is a unix environment variable to switch the local or remote gems. The Gemfile now looks like:

if ENV['LOCAL_ENGINES']
  gem 'cargo_estimates', path: 'local_engines/cargo_estimates'
  gem 'shipping_calculator', path: 'local_engines/shipping_calculator'
else
  gem 'cargo_estimates'
  gem 'shipping_calculator'
end

But you have to run LOCAL_ENGINES=true bundle, in the project root I had a script lebundle.sh to facilitate that:

#!/bin/bash
export LOCAL_ENGINES=true
bundle

An alternative is to set that env variable via rvmrc in dev_bootstrap_scripts/set_rvmrc.sh I had:

echo "rvm use $(cat .ruby-version)" > .rvmrc
echo "export LOCAL_ENGINES=true" >> .rvmrc
echo "!!!! MANUAL STEP REQUIRED !!!!"
echo "==> You need to use the correct rvm environment. Run:"
echo "source .rvmrc'"
echo "~~~~"

I used the .ruby-version to generate the .rvmrc which will be executed automatically when you enter the project directory and setup the LOCAL_ENGINES to true.

The .rvmrc must be in your .gitignore or it could override your setup when deployed.

One problem using the .rvmrc solution is you need to unset LOCAL_ENGINES when you want to bundle your application for production. I like a bit better the lebundle script because it’s less magic and more decoupled.

Setting up local engines

I added a script to facilitate the setup of the local engines by creating a local folder and checking out the repositories from dev_bootstrap_scripts/local_engines.sh:

#!/bin/bash
DIR=local_engines
mkdir $DIR

git clone git@github.com:worldwide_shipping/cargo_estimates.git $DIR/cargo_estimates
git clone git@github.com:worldwide_shipping/shipping_calculator.git $DIR/shipping_calculator

Precommit hook to prevent commits of tainted Gemfile.lock

And here’s the link up of my git precommit script dev_bootstrap_scripts/precommit_hooks.sh:

#!/bin/bash
ln -s ../../pre_commit.sh .git/hooks/pre-commit

Currently the pre_commit.sh was just calling no_local_engines_in_gemfile_lock.sh.

Inside pre_commit_scripts/no_local_engines_in_gemfile_lock.sh I use grep to locate the string PATH in my Gemfile.lock indicating a local gem is being used.

#!/bin/bash

grep "PATH" Gemfile.lock > /dev/null
if [ $? = 0 ]; then
  echo "==> WARNING"
  echo "==  You have a PATH in your Gemfile.lock! That usually mean that some gems are local to the project!"
  echo "***"
  echo "If you are sure this is what you want you can bypass precommits with 'git commit --no-verify'"
  echo "If the local engines are there because you where developing them, you should run 'bundle' to update Gemfile.lock."
  echo "with the correct dependencies location."
  echo "***"
  exit 1
fi

Summary

This worked for me cause I didn’t have any local engines when the project was deployed to production. If you have a mix of local and remote gems in your project you will have to come up with a smart regular expression knowledgeable of which dependencies are allowed as a local gem.

I looked in to git server side hooks but hosted version control (github or bitbucket) allow you to run custom scripts.

So precommit git client side hooks require an initial manual bootstrap step but are a really great tool to facilitate development workflow. If you work with local gems I’d like to hear about your workflow in the comments.

comments powered by Disqus
Image

Enrico Teotti

developer and agile mentor with 15 years experience in the IT industry across Europe, Australia and the US.

Back to Overview