Real Artists Ship
Mar 1, 2021 • posted by Michael Hartl
Michael Hartl here from the Rails Tutorial and Learn Enough.
As legendary Apple cofounder Steve Jobs once said: Real artists ship. What he meant was that, as tempting as it is to privately polish in perpetuity, makers must ship their work—that is, actually finish it and get it out into the world. This can be scary, because shipping means exposing your work not only to fans but also to critics. “What if people don’t like what I’ve made?” Real artists ship.
It’s important to understand that shipping is a separate skill from making. Many makers get good at making things but never learn to ship. To prevent this sad situation, the Learn Enough tutorials emphasize shipping along with making.
Shipping with a deploy
script
Shipping is the real motivation for the deploy
script mentioned in my previous post, “The Power of Scripting: A Deploy Script”. In particular, deploy
is designed to lower as much as possible the friction associated with shipping.1
In this spirit, this post will add a couple of the extensions proposed in “The Power of Scripting”, namely, pulling and pushing any Git changes associated with the project being deployed. The method I use in my own private version of the deploy
script has always been called ship
in honor of the philosophy discussed above.
Listing 1 shows where we left off (with the vertical ellipsis indicating omitted boolean method definitions). The structure of the main script is simple: we have a bunch of if
-else
statements that test for the project type and then issue the corresponding system command (using the Ruby system
method). Our strategy in this post is to replace system
with ship
, and then have ship
push and pull the Git repo before running the given command.
~/bin/deploy
#!/usr/bin/env ruby
require "fileutils"
# Deploys a project.
.
.
.
# Returns true for a Git repository.
def git_repo?
File.directory?(".git")
end
# The main script.
if softcover_book?
system "softcover deploy"
elsif ruby_gem?
system "rake release"
elsif ruby_web_app?
system "git push heroku"
elsif jekyll? || git_repo?
system "git push"
end
As it happens, all the project types supported by the deploy
script in Listing 1 are typically Git repos as well. For the purposes of this post, then, we’ll assume that we can run git pull
and git push
with no ill effect. Handling exceptions to this case is left as an exercise to the reader.2
So, what should ship
do? Since we don’t want to deploy a project without pulling in remote changes, we should run git pull
first:
$ git pull
If that command succeeds, then we can run git push
as well. The way to do this at the command line is using “and”, written with two ampersands:
$ git pull && git push # Runs git push only if git pull succeeds
Finally, we can run the specific command needed by the project type in question:
$ git pull && git push && <specific command>
For example, for a Rails app, we would run this:
$ git pull && git push && git push heroku
The preceding discussion suggests defining a ship
command as follows:
def ship(command)
system "git pull && git push && #{command}"
end
This accomplishes exactly the task required: first we pull in any remote changes, then we push up any local changes, and finally we run the project-specific deployment command.
Replacing system
with ship
in Listing 1 then gives us the “shipping” version of our deploy
script, as shown in Listing 2.
ship
. ~/bin/deploy
#!/usr/bin/env ruby
require "fileutils"
# Deploys a project.
.
.
.
# Returns true for a Git repository.
def git_repo?
File.directory?(".git")
end
# Ships a product.
def ship(command)
system "git pull && git push && #{command}"
end
# The main script.
if softcover_book?
ship "softcover deploy"
elsif ruby_gem?
ship "rake release"
elsif ruby_web_app?
ship "git push heroku"
elsif jekyll? || git_repo?
ship "git push"
end
At this point, running
$ deploy
will automatically sync up all relevant Git repos and then deploy the project to production.
By the way, you may have noticed that Listing 2 contains a minor redundancy: Jekyll sites and Git repos already have a git push
as their deployment command. In that case, ship
will run git push
twice:
$ git pull && git push && git push
Luckily, this does no harm—running git push
on an already-pushed repo just outputs “Everything up-to-date”:
$ git push && git push
Enumerating objects: 18, done.
Counting objects: 100% (18/18), done.
Delta compression using up to 4 threads
Compressing objects: 100% (11/11), done.
Writing objects: 100% (11/11), 263.30 KiB | 26.33 MiB/s, done.
Total 11 (delta 7), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (7/7), completed with 7 local objects.
To https://github.com/softcover/learnenough-news.git
3e7a7ff..53705ec master -> master
Everything up-to-date
Here the first git push
pushes up some changes (to GitHub, in this case), while the second git push
gives the highlighted output shown.
Time to ship
That’s it! Time to make Steve Jobs proud and ship.3
To learn more about how to ship a wide variety of products, including Git repos, static Jekyll sites, dynamic JavaScript sites, and Sinatra and Rails web apps, check out the Learn Enough All Access subscription. 7-day free trial, 60-day money-back guarantee.
if git_repo?
inside of ship
to skip the Git pull & push commands for projects that aren’t being versioned with Git.