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.