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

images/figures/container_ship

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.

Listing 1: The final deploy script from “The Power of Scripting”. ~/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

images/figures/ship

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.

Listing 2: Deploying with 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

images/figures/steve_jobs_macbook

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.

1. Shipping container picture used under the terms of the Creative Commons Attribution-ShareAlike 2.0 Generic license.
2. Hint: Just use if git_repo? inside of ship to skip the Git pull & push commands for projects that aren’t being versioned with Git.
3. Image cropped and used under the terms of the Creative Commons Attribution 3.0 Unported license. Other images used in this post are in the public domain.
MORE ARTICLES LIKE THIS:
learnenough-news , tutorials , git , ruby , scripting