One of the main requirements of my server was that I would be able to deploy my projects with a git push
, the same way that you can with Heroku, Dokku and similar. After a lot of experimenting this was what I ended up with:
/var/repos
./var/www
.In the following code samples $repoPath
is /var/repos
and $sitePath
is /var/www
.
A bare git repository differs from a regular git repository in that it does not contain the actual files of your project.
exec { "initialize_${title}_repo":
command => 'git init --bare',
path => '/usr/bin',
cwd => "${repoPath}/${repoName}",
user => 'helm108',
}
Note the git init --bare
.
I've lost the source that I based this decision on, but the idea was to use a bare repo because it stops the repo from slowly using up harddrive space by tracking the entire git repo's history unnecessarily.
I then add the project's origin to the newly-created repo so that it has somewhere to pull from:
exec { "add_${title}_remote":
command => "git remote add origin ${remoteUrl}",
path => '/usr/bin',
cwd => "${repoPath}/${repoName}",
user => 'helm108',
require => Exec["initialize_${title}_repo"]
}
And finally I copy in my post-receive and post-checkout hooks from .erb templates:
# Copy post-commit hook into repo and make it executable.
file { "${repoPath}/${repoName}/hooks/post-receive":
require => Exec["initialize_${title}_repo"],
owner => 'helm108',
ensure => file,
content => template('githook/post-receive.erb'),
mode => 'ug+x',
}
# Copy post-commit hook into repo and make it executable.
file { "${repoPath}/${repoName}/hooks/post-checkout":
require => Exec["initialize_${title}_repo"],
owner => 'helm108',
ensure => file,
content => template('githook/post-checkout.erb'),
mode => 'ug+x',
}
For each repo that I add to my site, I add this line to a bash file that gets run on deploy (split into two lines here for legibility):
cd $repoPath/$title && git fetch origin &&
git --work-tree=${sitePath}/${title} --git-dir=${repoPath}/${title} checkout -f master
This script cd's to the repo in /var/repos, fetches the origin, and checks out the master branch to the same folder in /var/www.
Speculation: In writing this post I thought to myself 'does giving a bare repo a working path defeat the point of using a bare repo?'. It's something I need to verify. I don't think it does, but if it does actually result in the working file history being tracked like a regular repo than it seems like there's no way around it anyway.
The final checkout at the end of that line triggers the post-checkout githook on the repo, which contains the following:
#!/bin/bash
source /etc/profile.d/helm108envvars.sh
cd /var/www/<%= @title %>
echo "Building <%= @title %>."
npm run dist
pm2 restart <%= @title %>
pm2 start server.js --name "<%= @title %>"
pm2 save
The first thing the script does is sources a file that contains some required environment variables. The githook runs in its own bash instance which means that it has no environment variables available to it.
It then cd's to the project's directory and runs npm run dist
. Each project that goes on Helm108 must have a dist
command in its packages.json, and it must install the project's dependencies and then run a production build.
Next the script starts the project using pm2. The reason that it calls restart and then start is because I didn't know about the pm2 ecosystem file; this is a configuration file that describes the running state of the project, and running pm2 ecosystem
ensures that the project is running according to this file.
This is important because of the post-receive hook. When I push changes to helm108.com it triggers a post-receive hook:
#!/bin/bash
echo "Push received."
source /etc/profile.d/helm108envvars.sh
git --work-tree=/var/www/<%= @title %> --git-dir=/var/repos/<%= @title %> checkout -f
As you can see, this ends with checking out the repo which triggers the above post-checkout hook which means that the post-checkout hook has to handle initializing a new project and restarting that project once changes have been received. Running pm2 start server
on an already-running project causes pm2 to go "it's already running" and not do anything, but calling restart on a project that isn't running yet doesn't do anything bad, so the final messy solution is:
I could have set pm2 to use its watch function and then it would automatically restart after the build command was run, and I should have read the documentation more thoroughly and used the ecosystem file, but I didn't so here I am! Learn from my mistakes. Please.