PostReceive Hooks

Git has the concept of hooks

These are basically BASH scripts that are run on certain events. The one we are talking about here is called the post receive hook.

As these hook files contain valuable code, we version them like anything else. These are tracked in a repo called post-receive-hooks and are checked out in the /home/ec folder in the gitBareRepos container

Writing Post-Receive Hooks

The post receive hook is like any other BASH script with the following needing to be born in mind:

The script does not take arguments, only STDIN

The post receive hook does not receive numbered arguments like a normal BASH script. Instead we have to read standard input. The established best practice for this looks like:

1
2
3
4
5
6
7
#!/usr/bin/env bash
# example post receive hook
while read oldrev newrev refname
do
    echo "Processing Branch: $refname"
    # do stuff here
done

The script is run from the repo root

When pushing to a bare repository for example, the workding directory is the main repo folder, i.e the folder that contains the hooks folder that in turn contains your post-receive hook.

Using a lock file

For expensive operations, you might want to use a lock file to ensure that multiple process are not kicked off.

For a good example of how to do this, see this SO answer and this gist

Running Tasks in Parallel and in the Background

In order to make git pushes quick and painless, especially if you have multiple processes being kicked off, then it makes sense to background those processes.

The way to do that is use the nohup {cmd} &> /dev/null & pattern. This means the script will continue executing once we have "hung up" from the process. The process output is all redirected to /dev/null, i.e totally discared and the process is disowned.

This will allow the process to be started, but then detatched from the post receive hook execution itself, so that the terminal for the developer who pushed is released very quickly.

For example, this post receive hook kicks off three separate actions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/env bash
pwd

repoName="$(pwd | grep -P -o '(?<=/repos/)(.+)')"

bitbucketSlug="git@bitbucket.org:edmondscommerce/${repoName////-}.git"

hooksDir=/home/ec/post-receive-hooks

echo "
Mirror to Bitbucket: ${bitbucketSlug}
";
nohup bash  "${hooksDir}/bitbucket-sync.bash" "$bitbucketSlug" &> /dev/null &

isMaster=no
while read oldrev newrev refname
do
    branch=$(git rev-parse --symbolic --abbrev-ref $refname)
    if [ "master" == "$branch" ]; then
        isMaster=yes
    fi
done

if [[ "yes" == "$isMaster" ]]
then
    echo "
Triggering CI Build for $repoName
"
    nohup bash "${hooksDir}/ci-runner.bash" "$repoName" "192.168.236.130" &> /dev/null &
else
    echo "
branch $branch has not triggered a CI build
"
fi

echo "
Updating Satis
"
nohup bash  "${hooksDir}/edmonds/satis-update.bash" &> /dev/null &

The output when git pushing to a repo with this hook looks something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
✔ /var/www/vhosts/dev [master ↑·1|✔]
09:33 $ git push
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (10/10), done.
Writing objects: 100% (11/11), 1.32 KiB | 0 bytes/s, done.
Total 11 (delta 6), reused 0 (delta 0)
remote: /home/ec/repos/edmonds/dev
remote:
remote: Mirror to Bitbucket: git@bitbucket.org:edmondscommerce/edmonds-dev.git
remote:
remote:
remote: Triggering CI Build for edmonds/dev
remote:
remote:
remote: Updating Satis
remote:
To ssh://gitBare/home/ec/repos/edmonds/dev
   004905d..182a532  master -> master

The above message is output almost instantaneously.

When doing this approach, it makes sense to confirm success by sending a Slack message

Example Post Receive Hooks

Bitbucket Sync

This hook script will push the gitBare repo to it's corresponding Bitbucket repo

Here is the usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
✔ /var/www/vhosts/post-receive-hooks [master L|✚ 1]
10:10 $ bash bitbucket-sync.bash

===========================================
bitbucket-sync.bash
===========================================

    BitBucket Sync

    Usage:

    ./bitbucket-sync.bash [bitBucketSlug]

    Example:

    ./bitbucket-sync.bash git@bitbucket.org/orgname/repo-name.git

CI Runner

This is hook will trigger the CI process in your specified container

You must pass in the repo name (path from ~/repos), for example edmonds/dev which is located in /home/ec/repos/edmonds/dev on gitBareRepos

The second parameter is the IP address of the container which you want the CI process to run.

Third (optional) parameter is the path that the CI process should take place. This defaults to /var/www/vhosts/ci

Usage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
ec@gitBareRepos bash ci-runner.bash

===========================================
gitBareRepos ci-runner.bash
===========================================



    Edmonds CI

    Usage:

    ./ci-runner.bash [repoName] [containerIP] (slackChannel - defaults to edmonds_marketing) (ciWorkDir - defaults to /var/www/vhosts/ci)

    - repoName is the path from ~/repos in gitBareRepos
    - containerIP is the internal IP, eg 192.168.236.130

    eg:

    ./ci-runner.bash edmonds/dev 192.168.236.130

ci.bash

The CI Runner works on the assumption that there is a bash script in the root of your project called ci.bash

This should be a generic task runner that should be totally silent (actions should log to file) apart from a final success or failure message

That message will then be echoed out on slack

Here is an example ci.bash script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env bash
mkdir -p var
echo -n "QA Process Started at $(date)\n\n" > var/qa.log
composer install &>> var/qa.log
bin/qa &>> var/qa.log
qaExitCode=$?
if [[ "0" == "$qaExitCode" ]]
then
    echo "CI Success :)"
    exit 0
fi
echo "CI Fail :'("
tail var/qa.log

You can see that this script is:

  • Making the var dir (a git ignored directory for transient stuff)
  • Truncating a qa.log file and then setting the top to be the date
  • Running composer install - redirecting all output to the log file
  • Running the full QA process, again redirecting all output to the log file
  • If the QA process exited with 0, eg success, then echoing a success messsage
  • If the QA process failed, echoing a failure message and also the tail of the log fileG

Note

The message sent to Slack will have the "repoName" prepended, so you don't need to add that

Slack Ouptut

Here is an example slack message:

slack message

Limitations

The CI process currently only runs for pushes that update the master branch

The tests are only run against master

It is possible to extend this to support other branches in the future shoudl we decide that we need it

Warning

Please read the above!

Slack Message Sender

To allow you to send Slack messages, there is another script in the post-receive-hooks repo that handles this.

This script is located in the root of the post-receive-hooks.

Here is the usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
✔ /var/www/vhosts/post-receive-hooks [master L|✔]
09:56 $ bash slack-message-sender.bash

===========================================
slack-message-sender.bash
===========================================



    Slack Message Sender

    Usage:

    ./slack-message-sender.bash [message] [channel]

    eg:

    ./slack-message-sender.bash 'slack, baby' 'general'

    Note - you SHOULD NOT use the # in the channel name

Simply enough, this will post a slack message.

As it is expected that this will often be used for outputting CLI output, the message is wrapped with ``` for you already, so you do not need to do this.