Skip to content

Instantly share code, notes, and snippets.

@phase
Created February 6, 2016 09:13
Show Gist options
  • Save phase/f501fd03471d4081eb34 to your computer and use it in GitHub Desktop.
Save phase/f501fd03471d4081eb34 to your computer and use it in GitHub Desktop.
A walkthrough for creating a new Ruby project

New Ruby software project procedure

First, ensure the following tools are available on the local system and reasonably up to date:

  • Git
  • Git-flow
  • RVM
  • Bash
  • SSH

What follows will assume that your local command prompt is provided by a Bash shell.

Now, come up with a name for your project. Whether or not the project is to be distributed as a gem, at least bear in mind this advice:

Next, cd to the directory above the one in which you intend to place your project code. E.g. if you want to end up with projects/myproject then go to the projects directory.

Intended to be distributed as a RubyGem?

If yes

Enter the following at the command prompt, replacing "myproject" with the name of your new project, to create a stub gem:

bundle gem myproject
cd myproject

Else

Enter the following at the command prompt, replacing "myproject" with the name of your new project:

mkdir myproject
cd myproject

Open-source or closed-source?

We'll assume that Git is to be used for source control; so as a precaution against accidentally checking in temporary files used by Vim or OS X, enter the following at the command prompt:

echo '*.swp'      >> .gitignore
echo '.DS_Store'  >> .gitignore

If open-source with repository on GitHub

Once logged in to the GitHub website, click the button to create a new repository, and then note that new repository's "remote" URL (e.g. https://github.com/sampablokuper/myproject.git).

Enter the following at the command prompt, replacing GIT_REMOTE_URL with the URL just noted:

if [ ! -d .git ]; then git init; fi         # Initialises a new Git repository, if doesn't already exist.
if [ ! -f README.md ]; touch README.md; fi  # Creates an empty README.md file,  if doesn't already exist.
git add -A                                  # Stages any files/directories present, in preparation to commit them to local Git repo.
git commit -m 'first commit'                # Commits the staged files/dirs to the local Git repo.
git remote add origin GIT_REMOTE_URL        # Adds the GitHub repo created above as a "Git remote" with the alias "origin".

Else if closed-source with repository on DreamHost

This assumes a working Gitolite 3.x installation on DreamHost, with a relevant SSH Host called "gitolitespk-git" set in the local SSH config file, and all necessary keys set up.

At the command prompt on the local (i.e. development) computer:

git clone gitolitespk-git:gitolite-admin
cd gitolite-admin

Within the gitolite-admin directory just entered, edit conf/gitolite.conf to add a new repository, as per the instructions at http://sitaramc.github.com/gitolite/repos.html, and then, replacing "NEWREPOSITORYNAME" with the name of your new repository:

git add conf/gitolite.conf
git commit -m "Create repository NEWREPOSITORYNAME"
git push
cd ..
rm -rf gitolite-admin
if [ ! -d .git ]; then git init; fi                     # Initialises a new Git repository, if doesn't already exist.
if [ ! -f README.md ]; touch README.md; fi              # Creates an empty README.md file,  if doesn't already exist.
git add -A                                              # Stages any files/directories present, in preparation to commit them to local Git repo.
git commit -m 'first commit'                            # Commits the staged files/dirs to the local Git repo.
git remote add origin gitolitespk-git:NEWREPOSITORYNAME # Adds the GitHub repo created above as a "Git remote" with the alias "origin".

Dependent upon gems?

Whether dependent upon external gems or not, you should first "Git push" to the remote repo, and then initialise git-flow (which you should install right now if it isn't already installed!), by entering the following at the command prompt:

git push -u origin master  # Pushes (i.e. copies, essentially) the local commits to the remote repo.
git flow init -d           # Initialises git-flow to manage branching, using the default options.
git push origin develop    # Git-flow will have created & switched to this branch. Ensure the remote knows about it.

If project won't use external gems in development/deployment

If project doesn't depend upon any particular version of Ruby

Don't even bother using RVM. Just code the project, and copy any external libraries (e.g. micro-optparse) into the project directory or even into your source code files (with comments present to indicate any necessary attribution).

Else - i.e. if project is intended to use a particular Ruby interpreter

From within the project's root directory, create a project .rvmrc file by entering e.g. (if 1.8.6 is the Ruby interpreter desired):

rvm --rvmrc --create 1.8.6

If this works, great. If it fails with an error like "WARN: ruby ruby-1.8.6-p420 is not installed. To install do: 'rvm install ruby-1.8.6-p420'", then enter:

rvm install 1.8.6

Once rvm has installed the relevant Ruby interpreter try again to create the .rvmrc:

rvm --rvmrc --create 1.8.6

Now enter the following:

cd .

in order to "change" into the directory you are currently in. This will cause the .rvmrc just created to kick in (it may give a warning the first time around) and switch the current interpreter to the one requested.

Else - i.e. project will depend upon gems

Don't use rvm's "gemsets" feature. Use Bundler instead, as per the instructions below. See this link for reasons why.

First of all, unless you expect to use installed gems' offline documentation or have some other compelling reason not to follow this step, enter the following into the file ~/.gemrc, to avoid gem installations from utilising unnecessary time (several hours per gem, in some cases!) and disk space:

# As per http://stackoverflow.com/a/7662245/82216
install: --no-rdoc --no-ri 
update:  --no-rdoc --no-ri

If you used bundle gem myproject earlier, then list your gem project's dependencies in the gemspec file, via the add_development_dependency and add_runtime_dependency attributes; Bundler will be able to install these appropriately.

Alternatively, if you did not use bundle gem myproject earlier, then you presumably have neither a "gemspec" file nor a "Gemfile" file present in your project so far. Create a file called "Gemfile" (e.g. using vim Gemfile) and give it a source of gems and a list of gems to install, e.g.:

source :rubygems

gem "trollop"

Either way, once finished specifying the dependencies for your project, save that file and exit the editor.

Depending upon particular gems doesn't necessarily require a project to require/expect to be used with a particular Ruby interpreter. However, in order to simplify development, it's probably best to settle on one. So, make an informed guess about the most appropriate, using, e.g. the following points of information:

Then create a .rvmrc file by cd'ing to the project's root directory and entering e.g. (if 1.8.6 is the Ruby interpreter desired):

rvm --rvmrc --create 1.8.6

If this works, great. If it fails with an error like "WARN: ruby ruby-1.8.6-p420 is not installed. To install do: 'rvm install ruby-1.8.6-p420'", then enter:

rvm install 1.8.6

Once rvm has installed the relevant Ruby interpreter try again to create the .rvmrc:

rvm --rvmrc --create 1.8.6

Now edit the .rvmrc just created, and uncomment the lines that get Bundler to run "bundle" (i.e. "bundle install" - see http://www.sampablokuper.com/2011/09/26/the-bundle-commands-default-behaviour/) each time the project's directory is changed into.

Next, edit the line on which "bundle" is called, which will probably look a bit like this:

bundle | grep -v '^Using ' | grep -v ' is complete' | sed '/^$/d'

or this:

bundle install

so that it instead reads more as follows:

bundle install --path vendor | grep -v '^Using ' | grep -v ' is complete' | sed '/^$/d'

or:

bundle install --path vendor

respectively.

Then enter the following command at the command prompt:

echo 'vendor/ruby' >> .gitignore

Now enter the following:

cd .

in order to "change" into the directory you are currently in. This will cause the .rvmrc just created to kick in (it may give a warning the first time around) and switch the current interpreter to the one requested.

Next, create a file called "main.rb" (e.g. using vim main.rb), which you'll use as though you were starting to get your application code started, to check that the above has worked. It should simply load the bundled environment and require the gems specified in the Gemfile, along the following lines:

# Load the bundled environment
require "rubygems"
require "bundler/setup"

# Require gems specified in the Gemfile
require "trollop"

Save this file and exit the editor, then run:

ruby main.rb

All being well, there will be no error messages. That being so, you can now delete the "main.rb" file:

rm main.rb

Package all the gems on which the project depends into the project's vendor/cache directory, as per, by running:

bundle package

Now run the following commands to add the config files and directories to version control:

git add Gemfile Gemfile.lock .rvmrc .gitignore .bundle vendor
git commit -m 'Add remaining config files and packaged bundled gems to version control'
git push origin develop

Testing: TDD/BDD

If your project so far does not have a testing framework activated that you are happy with, then decide on a syntax to use (i.e. "assertion syntax" or "specification syntax") and a suitable test framework (e.g. MiniTest or RSpec). E.g. if you decide to use "specification syntax" tests, via MiniTest, then do something like this:

git flow feature start basictestframework
mkdir spec
cat >> Rakefile << EOF
require 'rake/testtask'

Rake::TestTask.new do |t|
  t.pattern = 'spec/*_spec.rb'
end
EOF
cat >> spec/quality_spec.rb << EOF
require 'minitest/autorun'

describe "The project" do
  it "must provide the ability to run tests" do
    true.must_equal true
  end
end
EOF

and then run this sample test using the Rake task just created, by entering the following at the command prompt:

rake test

Assuming that has worked, finish that new feature branch thus:

git add -A
git commit -m 'Create a basic test framework'
git push origin develop
git flow feature end basictestframework

Release early

If you used bundle gem myproject earlier, and your project is intended to be published as a gem to rubygems.org, then fill in the missing details in the generated "gemspec" file (e.g. set the version number to 0.0.0) and any other necessary files. Consider using the files gemspec attribute to ignore the Gemfile.lock file, as per Yehuda Katz's advice.

Now enter the following set of commands at the command prompt, assuming each of them completes successfully, replacing "myproject" with the name of your gem:

gem build myproject.gemspec
gem push myproject-0.0.0.gem

A few minutes later, your gem should be available via rubygems.org .

What's next?

Now get coding! And remember:

  • use TDD or BDD principles, especially the advice here, here and here;
  • "git add" and commit any changes that should be versioned;
  • stick to Git commit message conventions;
  • use git-flow and semantic versioning conventions (among others) to keep the code manageable;
  • if project is a gem, structure the project according to the RubyGems Guides.
  • check the .rvmrc into version control as per.

If project created according to recipe above is a Rails project to be deployed to DreamHost

The Gemfile should already include a line saying:

gem "rails"

Copy the .gitignore file and the Gemfile:

cp .gitignore .gitignore-old
cp Gemfile Gemfile-old

In order to create a Rails application in the current folder, enter this at the command prompt (don't forget to include the full stop at the end!):

bundle exec rails new .

Allow this to overwrite .gitignore and Gemfile. This command may end with the following error: Could not find gem 'jquery-rails (>= 0) ruby' in the gems available on this machine. (Bundler::GemNotFound) but if so, don't worry; we'll sort it out shortly.

Next, unless you want to store the database.yml file in the Git repository, do:

cp config/database.yml config/database.yml.example
echo 'config/database.yml' >> .gitignore

and then edit config/database.yml as follows, replacing DATABASENAME, MYSQLUSERNAME, MYSQLPASSWORD, and MYSQLDOMAIN with the appropriate values.

# SQLite version 3.x
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem 'sqlite3'
development:
  adapter: sqlite3
  database: db/development.sqlite3
  pool: 5
  timeout: 5000

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  adapter: sqlite3
  database: db/test.sqlite3
  pool: 5
  timeout: 5000

production:
  adapter: mysql2
  encoding: utf8
  database: DATABASENAME
  username: MYSQLUSERNAME
  password: MYSQLPASSWORD
  host: MYSQLDOMAIN

We'll get this file onto the server later on.

Now do:

vimdiff .gitignore .gitignore-old

and restore any needed lines from .gitignore-old into .gitinore. Then do:

vimdiff Gemfile Gemfile-old

and similarly, restore any needed lines from Gemfile-old to Gemfile.

Next:

rm .gitignore-old Gemfile-old

Finally, cd.. out of and then cd back into your project directory. This will call bundler install --path vendor which should fix the Bundler::GemNotFound error mentioned above.

(Note: when deploying Rails apps to DreamHost, if the version of Rails used requires a version of Rack newer than the DreamHost system Rack, then you will have to use FastCGI instead of Passenger, because Passenger will always use the system Rack: http://wiki.dreamhost.com/index.php?title=Rails_3&oldid=31201#Rails_3.1.x and http://www.jacoulter.com/2011/12/14/rails-3-1-rack-1-3-5-passenger-and-dreamhost-shared-servers/ and email from DreamHost 13/5/2012.)

Now get coding! And remember:

  • "git add" and commit any changes that should be versioned;
  • stick to Git commit message conventions;
  • use git-flow and semantic versioning conventions (among others) to keep the code manageable;
  • check the .rvmrc into version control as per.

Taking a fresh Rails 3.2.3 app from the above state and making it deployable to DreamHost shared with FastCGI

At the command line, enter:

bundle exec rails server

and visit http://localhost:3000 in your browser. You should see the Rails "Welcome aboard" page.

Back at the command line, use Ctrl+C to quit the rails server.

If you wish to use plain JavaScript instead of CoffeeScript, and/or plain CSS instead of SASS, comment out one or both of the following line as appropriate, from Gemfile:

gem 'sass-rails',   '~> 3.2.3'
gem 'coffee-rails', '~> 3.2.1'

Also in Gemfile:

  • move the line gem 'sqlite3' into the group :assets do code block, to avoid sqlite3 being required in the production environment.

  • Uncomment the line, # gem 'capistrano'.

  • Add the following two lines below the initial source line:

      ## Line below added as per https://gist.github.com/1391900
      gem 'fcgi'
    

Save and close Gemfile.

cd out of and back into your project directory again, to trigger Bundler to run.

Now, at the command prompt, enter:

bundle exec rails generate controller home index
mv public/index.html public/index-old.html
vim config/routes.rb

Change the line # root :to => 'welcome#index' to root :to => 'home#index', then save the file and exit Vim.

Again, enter:

bundle exec rails server

and refresh the browser tab which is showing http://localhost:3000 . You should see a page which reads something like:

Home#index

Find me in app/views/home/index.html.erb

Back at the command line, use Ctrl+C to quit the rails server again. Then enter:

bundle package
git add -A
git commit -m "Make Rails controller and view operational."
bundle exec capify .

Now SSH into the server you wish to deploy to, and install RVM (if it isn't already installed), and the desired version of Ruby via RVM (if it isn't already installed). Then, being sure that the output from which ruby is the desired one to deploy to, enter the following at the remote server's command prompt:

gem install bundler
gem env

Make a note of the output, and end the SSH session.

Now, back on the local computer, edit config/deploy.rb as follows, tweaking as appropriate including paying particular attention to the PATH, GEM_HOME and GEM_PATH variables, which should make sense in the light of the above, and being sure to uncomment either the block for git-less deploys or the one for using a git remote.

## Based on https://gist.github.com/1391900

require "bundler/capistrano"

## If not using a git remote to deploy from, uncomment these lines:  
# set :scm, :none
# set :repository, "."
# set :deploy_via, :copy

## If instead using a git remote to deploy from, uncomment these lines and amend the repository access strings as appropriate:
# set :scm, "git"
# set :repository, "[email protected]:ruby193p194test"
# set :local_repository, "gitolitespk-git:ruby193p194test"
# set :deploy_via, :remote_cache

set :domain,  "spike.sampablokuper.com"
set :user,    "spkspike"
server "#{domain}", :app, :web, :db, :primary => true
set :application, "spike.sampablokuper.com"
set :deploy_to, "/home/#{user}/www/#{application}"
set :use_sudo, false

set :default_environment, {
  'PATH' => "/home/spkspike/.rvm/gems/ruby-1.9.2-p290/bin:/home/spkspike/.rvm/bin:$PATH",
  'GEM_HOME'     => '/home/spkspike/.rvm/gems/ruby-1.9.2-p290',
  'GEM_PATH'     => '/home/spkspike/.rvm/gems/ruby-1.9.2-p290',
}

# Define tasks

namespace :rvm do
  desc "Avoids the need to have to manually 'trust' the .rvmrc file if it has been updated"
  task :trust_rvmrc do
    run "rvm rvmrc trust #{release_path}"
  end
end

## The next few lines based on:
## http://affy.blogspot.co.uk/2008/07/using-scp-with-capistrano.html
## http://webonrails.com/2008/10/25/capistrano-uploading-downloading-directory-to-from-remote-server-via-scp-or-sftp/
## http://www.ruby-forum.com/topic/174627
namespace :deploy do
  desc "Uploads the database.yml to shared/config/database.yml"
  task :upload_database_yml do
    # Need to call `top.upload()` instead of just `upload()` is due to this issue:
    # http://www.mail-archive.com/[email protected]/msg04699.html
    top.upload("config/database.yml", "#{release_path}/config/database.yml", :via=> :scp)
  end
end

# Run tasks defined above

after "deploy:update_code", "deploy:upload_database_yml"
after "deploy", "rvm:trust_rvmrc"


## Lines from running "bundle exec capify ."

# if you want to clean up old releases on each deploy uncomment this:
# after "deploy:restart", "deploy:cleanup"

# if you're still using the script/reaper helper you will need
# these http://github.com/rails/irs_process_scripts

# If you are using Passenger mod_rails uncomment this:
# namespace :deploy do
#   task :start do ; end
#   task :stop do ; end
#   task :restart, :roles => :app, :except => { :no_release => true } do
#     run "#{try_sudo} touch #{File.join(current_path,'tmp','restart.txt')}"
#   end
# end

If you would prefer to deploy using Git than by simply copying (Git is faster), then consider pushing relevant git commits to a remote repository that is accessible (e.g. via appropriate SSH certificates and/or Gitolite, etc, if needed) to the user account on the deployment server and further editing the config/deploy.rb file to use set :deploy_via, :remote_cache instead of set :deploy_via, :copy, as described at http://help.github.com/deploy-with-capistrano . If doing this, it may also be necessary to add a line beginning set :local_repository to config/deploy.rb, if your deployment server and your development computer do not use identical credentials to access the remote repository.

Create and save the file public/dispatch.fcgi with the following content, tweaking as necessary and especially remembering to replace RailsInTopDirectory with the name of your Rails application (which you can find in config/environment.rb if you're unsure of what it is):

#!/usr/bin/env /home/spkspike/.rvm/bin/ruby-1.9.2-p290

require 'rubygems'
require 'bundler/setup'
require 'fcgi'

ENV['RAILS_ENV'] ||= 'production'

ENV['PATH'] = "/home/spkspike/.rvm/gems/ruby-1.9.2-p290/bin:/home/spkspike/.rvm/bin:$PATH"
ENV['GEM_HOME']     = '/home/spkspike/.rvm/gems/ruby-1.9.2-p290'
ENV['GEM_PATH']     = '/home/spkspike/.rvm/gems/ruby-1.9.2-p290'

require File.join(File.dirname(__FILE__), '../config/environment')

class Rack::PathInfoRewriter
 def initialize(app)
   @app = app
 end

 def call(env)
   env.delete('SCRIPT_NAME')
   parts = env['REQUEST_URI'].split('?')
   env['PATH_INFO'] = parts[0]
   env['QUERY_STRING'] = parts[1].to_s
   @app.call(env)
 end
end

Rack::Handler::FastCGI.run  Rack::PathInfoRewriter.new(RailsInTopDirectory::Application)

Now enter the following at the command prompt:

chmod +x public/dispatch.fcgi

Create and save the file public/.htaccess with the following content, which shouldn't need to be tweaked:

<IfModule mod_fastcgi.c>
AddHandler fastcgi-script .fcgi
</IfModule>
<IfModule mod_fcgid.c>
AddHandler fcgid-script .fcgi
</IfModule>

Options +FollowSymLinks +ExecCGI

RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.fcgi/$1 [QSA,L]

At the command prompt, enter:

bundle exec rake assets:precompile
bundle exec cap deploy:setup

And if this goes OK, then:

bundle exec cap deploy:check

And if this goes OK, then:

bundle exec cap deploy

Now get coding! And remember:

  • "git add" and commit any changes that should be versioned;
  • stick to Git commit message conventions;
  • use git-flow and semantic versioning conventions (among others) to keep the code manageable;
  • check the .rvmrc into version control as per.
  • update the config/database.yml.example file from the config/database.yml file as necessary to illustrate any desired settings, but of course omitting passwords and other sensitive information.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment