Developer Task Automation – It’s Easier Than You Think

You have a lot of things to think about when building the next great app. Remembering the command line arguments for your build system shouldn’t be one of them.

Large IDEs make this easy. You can stuff all of your build automation behind one big ▶️ play button. That’s great for traditional iOS or Android apps, but wouldn’t it be nice if we could easily bring the same kind of push-button automation to every project?

This is how I bring automation everywhere.

🤖 Make Automating Easy

The biggest barrier to having great project automation everywhere is usually the mental burden of building it. If you do the same 10-step thing 30 times a day, it’s probably worth spending a little bit of time automating that thing. But when automating is itself a cumbersome process, you’re only going to do it when the original process is really onerous.

So how do you reduce the friction of a cumbersome process? Automate it!

On every new project, I set up at least two automation helpers, direnv (which we’ll get to in a bit) and new-script. Here’s the whole thing:

#!/usr/bin/env bash

script_name="$1"

if [[ -z "${script_name}" ]]; then
  echo "Usage: "
  echo "new-script <your script name>"
  exit 1
fi

script_path="${PROJECT_ROOT}/scripts/shared/${script_name}"

touch "${script_path}"
chmod +x "${script_path}"
printf "#!/usr/bin/env bash\n\n" >"${script_path}"
code "${script_path}"

There are a million basically infinite ways to build this little script. This one works for my environment (a Mac using mostly Terminal.app and VS Code), and it can be trivially modified to work in any environment where you can run commands. Here are the important bits:

Remind Yourself How to Use the Script

script_name="$1"

if [[ -z "${script_name}" ]]; then
  echo "Usage: "
  echo "new-script <your script name>"
  exit 1
fi

☝️ This part reminds me that when I run new-script in my Terminal window, I should give the new script a name. It also makes this automation a little more discoverable when my teammates start exploring project automation on their own. $1 just means the first positional argument that I typed:

> new-script this-argument-right-here
> 

Prepare the way

script_path="${PROJECT_ROOT}/scripts/shared/${script_path}"

touch "${script_path}"
chmod +x "${script_path}"
printf "#!/usr/bin/env bash\n\n" >"${script_path}"

☝️ This part makes a new file for my automation in my project’s shared scripts directory, marks it as executable, and fills out the first few lines that I put in every single script. Most importantly, it adds a shebang that tells my Terminal how to interpret the rest of the script. I use Bash because I like old, boring, portable tools that compose well, but you can just as easily write this in JavaScript, Python, or even C.

$PROJECT_ROOT is a variable that gets set by direnv (we’re almost there). It just points to my project’s root directory.

Open it Up!

code "${script_path}"

☝️ This part opens the new script up in my current favorite text editor, Visual Studio Code. My editor has changed over the years, and so has this line in my automation setup. You can use whichever editor you like. If your system is set up like a proper old-school Unix, you could even do something like $EDITOR "${script_path}" and have your automation adapt as your editor preferences change.

And that’s it. A tiny script that takes care of all the steps for making a new script that I would normally have to remember with my meat 🧠. I drop it into every project that I work on.

👩‍💻👨‍💻👾 The $PATH to team automation

Let’s be real, you’re the automation enthusiast on your team, so you probably like the process enough to spend a lot of time fiddling with scripts. But what about the rest of your team? Are your automations clear and well organized? Are they instantly runnable by anyone on the team? If someone discovers one, could they find all the others?

I keep all of my team-ready automations together in a scripts/shared directory inside each project. I use the indomitable direnv to add that to the $PATH whenever someone opens the project directory, so that anyone can run our automations with no prelude, no task runner, no fuss. When you launch a new Terminal, you just run:

> ~: j proj
> ~/Code/my-project: new-script
> 

To jump into the project directory (~/Code/my-project) and start building a new automation.

Here’s how I set up direnv to make things simple:

# .envrc
export PROJECT_ROOT="$(pwd)"
PATH_add scripts/shared
PATH_add scripts/shared/aliases
git config --local core.hooksPath .shared-git-hooks

First, I set up the $PROJECT_ROOT variable so all of my scripts can use absolute paths. Second, I add the shared scripts directory and the aliases directory to the $PATH, so we can run them without ./.

I like to keep script names long and descriptive (like migrate-test-databases), so they’re easy to find if I’m looking through their directory. But for scripts that I run all the time, I usually add a very short alias (like mtd). I keep those aliases as symlinks in an aliases folder, so they’re easy to see and manipulate with standard file system tools like ls and Finder. As an added bonus, actual shell aliases usually take precedence over symlinks, so these aliases won’t override my teammates’ existing settings.

The last line sets up our shared project git hooks. This is team automation taken to the next level. Say your team wants to run a (quick) command before pushing code to the server. Because you’re using direnv, you have a dead-simple way to share git hooks with the rest of your team in a transparent way that doesn’t involve managing your version control system with a random npm package.

🦾🦿 Automate ALL The Things!

Whenever I’m about to run a command for the third time, I pause and run new-script some-descriptive-name. I type the command into my editor instead of the Terminal and hit save. From then on, my entire team can type some-descriptive-name rather than remembering how that command is implemented.

This serves a few purposes:

  1. It reduces cognitive overhead during every-day development, freeing up brain cycles to solve more interesting problems.
  2. It adds a little documentation about how we run that command (the script) and why (the script’s name).
  3. It increases the level of abstraction in our formal documentation. In the Readme, we can now write “Run some-descriptive-name instead of “Run git stash && git fetch && git rebase origin/develop && git stash apply” without burying the details of that command too deeply. When one of us wants to know what the command does, the can run cat scripts/shared/some-descriptive-name to find out.

🛍🎁 Bonus Automations

These are a few of my favorite recent automations. Feel free to use them if you like.

Quickly Build Shell Pipelines with quick 🚰

#!/usr/bin/env bash

script_name="$(mktemp)"

touch "${script_name}"
chmod +x "${script_name}"
printf "#!/usr/bin/env bash\n\necho hello\n" >"${script_name}"
code "${script_name}"

echo "$script_name" | entr -c "$script_name"

☝️ This one is for sketching out a shell pipeline (like git branch | rg -v '\*') in a text editor while your Terminal runs it whenever you save.

I used to build pipelines in the Terminal directly, but this is nicer. I can use my full editor, with all of my favorite tools like shellcheck and code formatting, to mock up pipelines and see the results change live.

quick makes a new executable temporary file, opens it in my editor, and tells entr to run the file itself whenever it changes (clearing out the last run’s results before doing so).

See How that Script Is Built with catwich 🐱🥪

#!/usr/bin/env bash

script_name=$1
cat $(which "$script_name")

☝️ This one shows you how any automation is built using cat and which. For example:

> catwich catwich 
#!/usr/bin/env bash

script_name=$1
cat $(which "$script_name")
> 

Quicker Terraform Builds with terraformer 🌍

terraformer
#!/usr/bin/env bash

fd '\.(tf|tfvars)' | entr -c -d terraform-check
terraform-check
#!/usr/bin/env bash

cd scripts/terraform
tflint && terraform validate && terraform plan

☝️ This one uses fd to find all the Terraform files in your project, watches them for changes with entr, and on every change runs tflint (super fast), then terraform validate (pretty fast), and then terraform plan (🐢) to catch any goofs in your Terraform scripts as quickly as possible.

What commands do you run all the time?