Tackling Deployment Failures with Git Hooks
Tags: git, javascript, productivity, github
Published: 2023-12-28 18:49:51

It's 3 am and you worked hard to fix a bug/implement a feature and push your commits to GitHub and went to sleep peacefully only to see a failing deployment build email in your inbox when you wake up.  


If you are someone like me who uses CI/CD to deploy code instead of just pushing the files directly to the production server via SFTP without any VCS, You would have gone through this at least once.


It can be overcome by using stricter lint configs but will end up with more red squiggly lines in your IDE while doing quick edits, and If you are working on a team not everyone enjoys stricter lint configs for quick edits and ends up not using it anyways.  A better solution to this would be using **Git Hooks**.

---

## What are Git Hooks?

> *Hooks are a way to fire off custom scripts when certain important actions occur.* - Git Documentation

As the [documentation](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) says we can write our custom scripts that can be triggered when a specific set of actions occur in our repository. These hooks are divided into two types Client-Side Hooks and Server-side Hooks. In this blog, we are going to implement Client-Side hooks.

---

## How to implement these hooks?

To implement hooks in a git repository we need to go to `.git/hooks/`.

When we do `ls` in this directory we get to see several sample scripts that are present already as follows:
```bash
applypatch-msg.sample*      pre-push.sample*
commit-msg.sample*          pre-rebase.sample*
fsmonitor-watchman.sample*  pre-receive.sample*
post-update.sample*         prepare-commit-msg.sample*
pre-applypatch.sample*      push-to-checkout.sample*
pre-commit.sample*          sendemail-validate.sample*
pre-merge-commit.sample*    update.sample*
```

Each of these scripts is executed when the specified action occurs in the repository. The one we are going to see now is `pre-push`. This script executes before the commits are pushed to the remote repository.

Now create a new file called `pre-push` and open it in a text editor of your choice. I'm using `nvim` here :
```bash
touch pre-push && chmod +x pre-push && nvim pre-push 
```

In that script add the following:
```bash
if npm run build; then
    echo "Build successful. Proceeding with push."
    exit 0
else
    echo "Build failed. Please fix the issues before pushing"
    exit 1
fi
```

We are running `npm run build` (you can change the command if needed) and if it builds successfully we are exiting with `exit 0` else exiting with `exit 1`. Exiting with a non-zero value in `sh` world denotes that the script is executed with errors. Which means the push will not happen.

So is that it? Not really. If you are the person who just pushes everything to the `main` branch then this is done. Otherwise, we need to specify the deployment branch so that this hook runs only when the commits are pushed to that branch. 

To do that we need to find the branch we are currently on. It can be done by using the `rev-prase` command in git. However just calling the `rev-parse` will return the branch id instead of the name so we need to also add the `--abbrev-ref` argument to get the branch name. Now our script will look like this.

```bash
##!/bin/bash

branch=$(git rev-parse --abbrev-ref HEAD)
dep_branch="main"

if [ "$branch" == "$dep_branch" ]; then
    if npm run build; then
        echo "Build successful. Proceeding with push."
        exit 0
    else
        echo "Build failed. Please fix the issues before pushing to the main branch."
        exit 1
    fi
fi

exit 0
```

You can change the `dep_branch` variable to the actual name of your deployment branch.

So is that it? Yes, but there's more. Optionally we can track changes on specific folders and only do the build if there are changes in specific folders. For example, we don't need to check if the build passes if we just modified your `README`.

To do that, We need to use the diff command to see if there are changes in the directory we need. In my case, it's the `src/` directory.

```bash
function are_changes_in_dir {
    local changed_files=$(git diff --name-only origin/main HEAD)
    echo "$changed_files" | grep -q "^src/"
}
```

Alternatively, we can write the function like this so that we can track multiple directories.

```bash
function are_changes_in_folders {
    local folders=("src/" "lib/" "utils/")  # Add your desired folders to this array
    local changed_files=$(git diff --name-only origin/main HEAD)

    for folder in "${folders[@]}"; do
        echo "$changed_files" | grep -q "^$folder" && return 0
    done

    return 1
}
```

This above function checks if there are any changes in the specified directory. Our final script will look something like this:

```bash
##!/bin/bash

branch=$(git rev-parse --abbrev-ref HEAD)

function are_changes_in_dir {
    local folders=("src/" "lib/" "utils/")  # Add your desired folders to this array
    local changed_files=$(git diff --name-only origin/main HEAD)

    for folder in "${folders[@]}"; do
        echo "$changed_files" | grep -q "^$folder" && return 0
    done

    return 1
}


if [ "$branch" == "main" ]; then
    if are_changes_in_dir then
        echo "Changes detected in the specified directories. Running npm run build..."

        if npm run build; then
            echo "Build successful. Proceeding with push."
            exit 0
        else
            echo "Build failed. Please fix the issues before pushing to the main branch."
            exit 1
        fi
    else
        echo "No necessary changes detected. Skipping build."
        exit 0
    fi
fi

exit 0
```

Save this and goodbye to failed deployment builds*


*Your Team members should also use this hook or else they can push failing commits.

---