# Super simple static site deployment bash script, that keeps Git in sync

An uncomplicated deploy script that doesn’t use Git, but still keeps your
upstream Git repo up-to-date with the live server.

I’ve been using this simple script to deploy a few side projects for a couple of
years now. There are brilliant services like [Vercel](https://vercel.com) or
[Netlify](https://www.netlify.com/) that let you deploy static sites super
easily. But this one is a 100% DIY solution, which assumes you have a VPS (like
[DigitalOcean](https://m.do.co/c/5101def33865) etc.) with an
[ssh access](/how-to-setup-and-manage-multiple-ssh-keys).

## Requirements for the "simple" deploy script

Basically a dumb script that deploys to a simple server, in my case it only
assumes that
[`rsync`](sync-large-quatities-of-files-or-directories-between-local-machine-and-server-with-rsync)
works to the target machine. If you deploy to an AWS bucket, you can change the
upload step on the script.

**No Git:** the actual transporting of the files doesn’t happen via Git. This
way Git is not needed to be installed onto the server, nor have any hooks
listening. Which enables to roll with a really simple, perhaps rootless server
setup, etc.

**Build locally:** if the build step happens locally, it also doesn’t expect
_anything_ from the server.

**Keep Git and the server in sync:** the problem with having the build step run
locally, is that the deploy script can be run before the new code has been
pushed upstream (Github, GitLab...), and then your server isn’t up-to-date with
the remote repo. **But this can be solved with a bit of locally run Git
smartness**.

## The script

I’ll divide the explanation into 5 parts
([or jump to the full script](#the-whole-deploy-script)):

- Setup
- Tools
- Handle Git
- Transport the files
- Run the functions

### Setting things up

```bash
#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset
# set -o xtrace

readonly PROJECT_NAME='example'
readonly TARGET_PATH='/var/www/example.com/public_html'
readonly SOURCE_PATH='public/'
readonly SITE_URL='https://example.com'
readonly SERVER="${1:-}"
```

**First**, it sets on the
[unofficial Bash strict mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/),
in this case written in longhand syntax. It could also be written as
`set -euo pipefail`. It tells Bash to: 1) stop executing the script if an error
happens, 2) Error and stop the script if an undefined variable is being
called, 3) prevent errors in a pipeline from being masked.

**Second**: configure the deployment script, paths and names.

## Tools

These totally optional helper functions are only for printing different colored
messages to the terminal as you deploy.
[More on the colors in Bash](https://clubmate.fi/basics-of-bash-programming-aka-shell-scripting#using-colors-and-text-styles).

```bash
green() {
  echo "\\033[32m${1}\\033[m"
}

red() {
  echo "\\033[31m${1}\\033[m"
}

yellow() {
  echo "\\033[33m${1}\\033[m"
}
```

### Handle Git

This is the most interesting bit of the code I think. It uses diffing and other
Git wizardry to figure out if the local Git tree is clean and up-to-date with
the upstream one.

```bash
die_if_wrong_project() {
  local CURRENT_PROJECT=$(basename "$(git rev-parse --show-toplevel)")

  if [ "$CURRENT_PROJECT" != "$PROJECT_NAME" ]; then
    red "\\n😱  wrong project, this script is configured to be used in a project named ${PROJECT_NAME}\\n"
    exit 1
  fi
}

die_if_dirty() {
  local CHANGED_FILES=$(git diff-index --name-only HEAD --)

  if [ "$CHANGED_FILES" != "" ]; then
    red "\\n😱  the repository is not clean:\\n ${CHANGED_FILES}\\n"
    exit 1
  fi
}

die_if_untracked_files() {
  local UNTRACKED=$(git ls-files --exclude-standard --others)

  if [ "$UNTRACKED" != "" ]; then
    red "\\n😱  there are untracked files:\\n ${UNTRACKED}\\n"
    exit 1
  fi
}

die_if_ahead_of_remote() {
  local UPSTREAM="@{u}"
  local LOCAL=$(git rev-parse @)
  local REMOTE=$(git rev-parse "$UPSTREAM")

  if [ "$LOCAL" != "$REMOTE" ]; then
    red "\\n😱  the local branch is not up to date with the remote, please do a git push\\n"
    exit 1
  fi
}
```

Explanations:

`die_if_wrong_project()`

This is just a precaution, since we’re doing some `rm` stuff,
    better check if this is the right Git project so it doesn’t reek havoc if
    run in a wrong directory.

`die_if_dirty()`

This function will exit with the script if the Git tree is dirty, i.e.:
    there are uncommitted changes.

`die_if_untracked_files()`

This function will exit the script if there are untracked files loitering
    around.

`die_if_ahead_of_remote()`

Exits the script if you forgot to push changes to the upstream branch.

### Move the files

The function that physically moves the files over to the server using `rsync`:

```bash
move_files_to_server() {
  yellow "File transfer started, might take a moment."
  rsync -az --info=progress2 --exclude=".*/" --chmod=+rwx --delete "${SOURCE_PATH}" "${SERVER}":"${TARGET_PATH}"
  green "Files moved to ${SERVER}:${TARGET_PATH}."
}
```

Here’s what the `rsync` does:

`-a`, `--archive`

This is equivalent to `-rlptgoD`. It is a quick way of saying you
    want recursion and want to preserve almost everything (with -H being a
    notable omission).

`-z`, `--compress`

With this option, `rsync` compresses the file data as it is sent
    to the destination machine, which reduces the amount of data being
    transmitted.

`--info=progress2`

This option outputs statistics based on the whole transfer, rather than
    individual files. Note that this only applies to rsync versions 3.0 or
    newer.

[See here how to update rsync](/update-rsync-on-osx-with-homebrew)



if you’re running an old version.

`--exclude=".*/"`

This excludes dotfiles, for example `.env`,
    **which is super important!**

`--chmod=+rwx`

Make the transferred files readable.

`--delete`

This tells rsync to delete extraneous files from the receiving side (ones
    that aren’t on the sending side), but only for the directories that are
    being synchronized. **Meaning**: it will keep the directories
    synchronized.

### Run the functions and build

Then lastly, run all the functions and the build step. In this case I have the
build script as an npm script. **First run the sanity check, then build, then
move the files**:

```bash
yellow "Started the deploy process, may take a moment..."
die_if_wrong_project
die_if_dirty
die_if_untracked_files
die_if_ahead_of_remote
npm run build
move_files_to_server
green "Deployd to: ${SITE_URL}. Have a nice day!"
```

## The whole deploy script

Below is the wle script. You need to of course configure it to fit your project.

Run it like so:

```shell
$ sh deploy.sh bob@192.168.0.0
```

Or put it in your projects `package.json` along with the other scripts your
project needs:

```json
{
  "scripts": {
    "deploy": "sh bin/deploy.sh bob@192.168.0.0",
    "develop": "gatsby develop",
    "clean": "gatsby clean",
    "build": "gatsby build",
    "serve": "gatsby serve"
  }
}
```

The whole `deploy.js` script:

```bash
#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset
# set -o xtrace

readonly PROJECT_NAME='example'
readonly TARGET_PATH='/var/www/example.com/public_html'
readonly SOURCE_PATH='public/'
readonly SITE_URL='https://example.com'
readonly SERVER="${1:-}"

green() {
  echo "\\033[32m${1}\\033[m"
}

red() {
  echo "\\033[31m${1}\\033[m"
}

yellow() {
  echo "\\033[33m${1}\\033[m"
}

# Since we're doing some rm stuff, better check if this is the right Git project
# so it don't reek havoc if run in a wrong directory
die_if_wrong_project() {
  local CURRENT_PROJECT=$(basename "$(git rev-parse --show-toplevel)")

  if [ "$CURRENT_PROJECT" != "$PROJECT_NAME" ]; then
    red "\\n😱  wrong project, this script is configured to be used in a project named ${PROJECT_NAME}\\n"
    exit 1
  fi
}

die_if_dirty() {
  local CHANGED_FILES=$(git diff-index --name-only HEAD --)

  if [ "$CHANGED_FILES" != "" ]; then
    red "\\n😱  the repository is not clean:\\n ${CHANGED_FILES}\\n"
    exit 1
  fi
}

die_if_untracked_files() {
  local UNTRACKED=$(git ls-files --exclude-standard --others)

  if [ "$UNTRACKED" != "" ]; then
    red "\\n😱  there are untracked files:\\n ${UNTRACKED}\\n"
    exit 1
  fi
}

die_if_ahead_of_remote() {
  local UPSTREAM="@{u}"
  local LOCAL=$(git rev-parse @)
  local REMOTE=$(git rev-parse "$UPSTREAM")

  if [ "$LOCAL" != "$REMOTE" ]; then
    red "\\n😱  the local branch is not up to date with the remote, please do a git push\\n"
    exit 1
  fi
}

move_files_to_server() {
  yellow "File transfer started, might take a moment."
  rsync -az --info=progress2 --exclude=".*/" --chmod=+rwx --delete "${SOURCE_PATH}" "${SERVER}":"${TARGET_PATH}"
  green "Files moved to ${SERVER}:${TARGET_PATH}."
}

yellow "Started the deploy process, may take a moment..."
die_if_wrong_project
die_if_dirty
die_if_untracked_files
die_if_ahead_of_remote
npm run build
move_files_to_server
green "Deployd to: ${SITE_URL}. Have a nice day!"
```

## Possible improvements

The script could take a version number as the second arg, and then create a tag
based on that, so rollbacks would be possible by checking out the previous tag.

It could have a `--force` option that ignores the Git stuff.

The script could also be configured with args or a config file, it could read in
`package.json` etc.
