As many organizations with many microservices and employee churn, you can and should expect to often step into a new codebase that needs some smaller updates. It can be really helpful to be able to very quickly go from cloning a repo to being able to run tests and check things in a developer environment. The goal of a number of different helper scripts is to help reduce the total maintenance cost of repositories over their lifetime and accelerate anyone onboarding into the repository.
The goal of bin/setup
is to go from git clone to being able to run the test suite and boot the app in development mode. The workflow should allow someone to be running any repository very quickly.
> git clone someproject.git
> cd someproject
> bin/setup
> Bootstrapping someproject
> ...
> # checks various depenencies and installs what is missing
> # ... redis, postgres, gems, etc...
> # checks if DBs exist if not creates them and sets them up
> # ... db create, db migrate, db seed, etc...
> ...
> All complete, now you can run `bin/start` for a webserver or `bin/test` to run the tests
The script might have a few assumptions depending on where you work. We have some that if on a Mac will require Homebrew, we have some that might bail if you don’t have Docker. The required dependencies are very small and the script can point you at the documentation to get the minimum requirements up and running.
While I think bin/setup
is the most valuable. You can see I already referenced a few other scripts. Some of the scripts are just one-line aliases, but it still keeps the concept language agnostic. I can get up and running on a Go, Ruby, and Node app on the same day to upgrade a feature touching on the frontend and a number of internal services. In Ruby, for example, the scripts can be very simple.
example bin/test
for a Ruby repository:
#!/bin/sh
bundle exec rspec
example bin/start
for a Ruby repository:
#!/bin/sh
PORT=${PORT:-3000} RACK_ENV=development RAILS_ENV=development foreman start -f Procfile.dev --color
The bin/setup
script is by far the most complicated, but normally looks something like below:
#!/usr/bin/env ruby
require "fileutils"
APP_ROOT = File.expand_path("..", __dir__)
def system!(*args)
system(*args) || abort("Command failed: #{args}")
end
def executable?(command)
ENV["PATH"].split(File::PATH_SEPARATOR).map do |path|
(ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]).map do |extension|
File.executable?(File.join(path, "#{command}#{extension}"))
end
end.flatten.any?
end
# WARNING: not portable (macOS and Linux only)
def running?(process_name)
system("{ ps -ef; docker ps 2>/dev/null; } | grep #{process_name} | grep -v grep > /dev/null 2>&1")
end
def database_exists?(database_name)
puts %(running database_exists? check for #{database_name})
system(%(psql -lqt | cut -d \| -f 1 | grep -qw #{database_name}))
end
def create_database!
system!("bundle exec rails db:create")
end
def check_dependencies
# Check all dependencies first, and guide the user to the right place
# to learn more if any are missing.
#
# you can add anything you might need here like redis, rabbit, kafka, etc
# you can either warn or try to detect and install
unless executable?("psql")
abort <<~WARNING
############################ WARNING ############################
PostgreSQL is required for using this setup, for tests, the
server, or the console! For more information, see
https://internal-wiki.com/psql
WARNING
end
unless running?("postgres")
abort <<~WARNING
############################ WARNING ############################
PostgreSQL must be running for using this setup, for tests, the
server, or the console! For more information, see
https://internal-wiki.com/postgres
WARNING
end
unless require "bundler"
abort <<~WARNING
############################ WARNING ############################
Bundler is required for using this setup, for tests, the server,
or the console! For more information, see
See https://internal-wiki.com/bundler
WARNING
end
end
def run_setup
# This section will go through the basic steps to setup the app to the point specs or server start would work
puts "Checking dependencies using Bundler"
system("bundle check") || system!("bundle install")
puts "Copying example files into place if needed"
FileUtils.cp "config/database.yml.example", "config/database.yml" unless File.exist?("config/database.yml")
puts "Creating tmp folder if needed"
FileUtils.mkdir "tmp" unless File.exist?("tmp")
create_database!("app_name_development") if !database_exists?("app_name_development")
if database_exists?("app_name_development")
puts "Migrating development database"
system!({"RAILS_ENV" => "development"}, "bin/rails db:migrate")
puts "seeding development database"
system!({"RAILS_ENV" => "development"}, "bin/rails db:seed")
end
create_database!("app_name_test") if !database_exists?("app_name_test")
if database_exists?("app_name_test")
puts "Migrating test database"
system!({"RAILS_ENV" => "test"}, "RAILS_ENV=test bin/rails db:migrate")
end
puts "Installing foreman"
system! "gem install foreman"
end
FileUtils.chdir APP_ROOT do
check_dependencies
run_setup
puts <<~SUCCESS
The app is setup
run tests try:
bin/test
To run as a server, try:
bin/start
SUCCESS
end
If these scripts aren’t run often and kept up to date, they are bound to break and go un-noticed. It is helpful to verify the scripts on CI. It also generally had the dual purpose of helping keep the script working on both Docker and Macs. Let’s take a look at the things we can run on CI to help verify our app can be setup and run for folks immediately.
While the examples I am giving are for CircleCI, they should easily integrate into any continuous integration toolchain.
bin/setup
& bin/start
on CI check_bin_setup:
<<: *base-job
steps:
- checkout
- restore_cache:
key: *gem_cache_key
- run:
name: Run setup
command: bin/setup
- run:
name: Run setup again (idempotency check)
command: bin/setup
- run:
name: Boot dev server in the background
command: bin/start
background: true
- run:
name: Start the rails server, wait for it to be available, then make a request and verify the response.
command: |
dockerize -wait tcp://localhost:3000/ -timeout 1m
STATUS_CODE=`curl -s -o /dev/null -w "%{http_code}" -H "Accept: application/json;" -H "Content-Type: application/json"' http://localhost:3000/app_name/healthcheck`
if [ $STATUS_CODE != "200" ]; then
echo "Server failed to return a 200"
exit 2
fi
environment:
RAILS_ENV: development
RACK_ENV: development
Long as we are verifying things work as expected on CI, another thing that can help avoid an incident is verifying the app works correctly with eager loading. We add a Rake task like below and have our CI testing script call it.
desc "Loads the Rails Application with 'eager_loading=true' as our production deployment eagerloads and we don't want to find out about a missing depedency that late."
task :eager_load_check do
require File.expand_path('./config/environment', File.dirname(__FILE__))
Rails.application.eager_load!
end
Suppose your teams make it easy to jump into any repository. In that case, you will help reduce siloing and encourage understanding not only the systems that teams own but working in and helping contribute to the systems in the ecosystem they participate in. It will promote collaboration and exploration and reduce the friction when folks move between teams and projects. There are other steps teams can take to reduce the friction of getting started, but maintaining these few scripts is a low-cost and high-value return for teams managing many repositories.