My Scripts
Ending the week, I figured I should have a showcase of the scripts that brought this site to life, no health checks, no logs, just lots of scaffolds
Before that I should talk about my “system”
Thecist’s git “philosophy” - heavy on the quotes
A bare repository is very limited in what it can do, it doesn’t even have a copy of the code, just the data to create one. So any real work you end up doing will more often than not be on a clone
Luckily git was made to be distributed, so a clone is as good as any other, and as much as I delegate my private git server to be the single source of truth, I can very easily create a new bare repository from my various clones or my synced repos on GitHub
So what does this mean? With the flexibility of git and root access to my git server, I was suddenly given the oppurtunity to make some decisions, so I banned PRs on the server. Every PR must happen off server with me being the final reviewer before it is merged with the main branch and then pushed
The same goes for conflicts or any other type of aggregation that updates the
main
branch
This led to some opinionated design decisions, and some interesting problems to solve - in the future. I am looking forward to it, but for now it should provide enough context for the current scripts holding my site together
post-receive hook
This is less of a script and more of an orchestrator, it calls other scripts passing whatever inputs it gets to respective scripts
Apart from being a really good orchestrator, an extra superpower is the ability of any of the scripts it runs to be able to take on the role of the post-receive hook since it accepts the same inputs
#!/usr/bin/env bash
set -e
HOOKS_DIR="/usr/local/lib/git-hooks"
INPUT=$(cat)
echo "$INPUT" | "$HOOKS_DIR/deploy-main" || echo "Deploying main branch failed"
echo "$INPUT" | "$HOOKS_DIR/mirror-to-github" || echo "Mirroring code to GitHub failed"
echo "$INPUT" | "$HOOKS_DIR/manage-archive" || echo "Archiving branch failed"
Jumpscare!
I’ll make GitHub gist for the longer scripts no worries
Background scripts
Unlike our frontliners from the above code, there are some soldiers in the backline providing support, they aren’t like the flashy scripts that are literally drop in replacement for hooks - show offs -, so to compensate I gave them little underscores to start their names
_deploy-core.sh
It’s the primary script for deploying - gasp, who would have thought
Currently, it’s inputs are REPO_DIR
, TARGET_DIR
, PROJECT_NAME
and BRANCH
but I plan on updating BRANCH
to HASH
- one of the really cool problems to
solve
Let’s go through them, REPO_DIR
is the directory of the REPO
- bear with me,
TARGET_DIR
is the folder you’d like your production code to be in,
PROJECT_NAME
is a unique identifier used to create the Docker image and
BRANCH
- you will not believe this - is the branch you plan on deploying
For the most part, it goes through the motions
- Ensures all variables are present
- Exposes its inputs as environment variables - really useful for any script it calls, as they automatically have access to the inputs
- Searches for and runs a
pre-build
script located in the repo’sscripts/production
folder - Deploys code using
docker compose
- Switches
HEAD
ref back tomain
- incase we deployed a branch
And here’s the gist as promised!
_undeploy-core.sh
Brace yourself for the impending déjà vu
Currently it uno reverses whatever _deploy-core
does, apart from messing with
the HEAD
ref - all must point back to main
It takes in the same input paramters: REPO_DIR
, TARGET_DIR
, PROJECT_NAME
and BRANCH
, although I really want to switch BRANCH
to HASH
but it’s a
good segway so I’ll talk about it later
It also goes through the motions
- Deletes the container assigned to
PROJECT_NAME
- Deletes the image created for the container
- Deletes the directory that stores the production code
And that’s it!
Oh yeah it also spares the main
branch. Don’t tell the other branches, but
“undeploy” is a nice way of saying rm
… -rf
And here’s the gist!
Archiving
Now you might be thinking, “other branches?”, did you… lie to me? And to that I say… yeo?
I didn’t! Anytime I push a branch I’m immediately stopped by this
And for me that’s more than enough to remind me we don’t push new branches around here, so instead of outright blocking other branches from being pushed, I entertained the idea, and came up with archiving!
That’s right, taking advantage of the post-receive
hook I decided to enable
deployments
and undeployments
for branches - but only when they’re created
And if I do end up with more contributors one day, I could easily create users
for them and update the script to prevent pushes that aren’t to main
from them
Or just straight up use GitHub
So technically there are branches but they’re all in this form: v[0-9]+
. I
mean come on, is that really a branch or something I’m going to have to change
the moment I need sub versions of an archive?
Now that I’ve masterfully won your trust back, lets talk about my two cool scripts for archiving and unarchiving repos
They both exist locally, cos there’s no way I’m ssh’ing to archive a repo
archive_repo.ps1
One day I’m going to have original names… today isn’t the day
So this script is mostly used to call my python script archive_repo.py
in the
same directory, but I’ve given bash too much attention already, all things must
be equal
python "$PSScriptRoot\archive_repo.py" $args
archive_repo.py
This script
- Incrementally searches for a branch in the format
v[0-9]+
that hasn’t been taken on the bare repo - repo on my git server - Creates said branch locally
- Pushes the branch to my private git server - this triggers the
post-receive
hook - And then deletes the branch locally - this prevents me from mistakenly updating and pushing the branch
I know it’s a glorified branch creation script, but it prevents me from having a branch that doesn’t show that error from earlier - cos it’s the only thing stopping me from breaking my rule
Oh also, you remember those environment variables _deploy-core
and
_undeploy-core
make available through export
? I end up using it to generate
dynamic .env
files like HOST=v1.blog.thecist.dev
- which may or may not
exist ;)
Here’s the gist!
unarchive_repo.ps1
It’s the law!
python "$PSScriptRoot\unarchive_repo.py" $args
unarchive_repo.py
I’ll be honest with you, I was way too lost in the prompts and chats to
realize this was a single command, but it kind of worked out, and my Scripts
directory only has two global
scripts
So what does it do?
It deletes the version branch on the git server…
And yes, it has a gist - it’s that many lines
Hook replacements
And now for the cool guys, the drop in hook replacements, the mini orchestrators
deploy_main.sh
This guy checks all inputs to see if the main branch was updated, if it was, it
calls _deploy-core
on the main branch
#!/usr/bin/env bash
set -e
REPO_DIR=$(pwd) # e.g., /opt/repos/lib.git
REPO_NAME=$(basename "$REPO_DIR" .git) # e.g., lib
TARGET_DIR="/home/git/deploy/${REPO_NAME}"
HOOKS_DIR="/usr/local/lib/git-hooks"
# Read input and look for main branch
DEPLOY=false
while read oldrev newrev refname; do
branch=$(basename "$refname")
if [[ "$branch" == "main" ]]; then
DEPLOY=true
break
fi
done
if [[ "$DEPLOY" != "true" ]]; then
echo "No changes to main branch; skipping deployment."
exit 0
fi
TARGET_DIR="/home/git/deploy/${REPO_NAME}"
"$HOOKS_DIR/_deploy-core" "$REPO_DIR" "$TARGET_DIR" "$REPO_NAME" "main"
mirror-to-github.sh
This superstar keeps GitHub in sync with my repo - free backup!
#!/usr/bin/env bash
set -e
REPO_DIR=$(pwd)
CHANGES=$(cat)
echo "Changes being pushed:"
echo "$CHANGES"
cd "$REPO_DIR"
unset GIT_WORK_TREE
git push --mirror github
manage-archive.sh
And the script responsible for my recent sleep schedule
It searches for the creation and/or deletion of branches, and based on that runs
either _undeploy-core
or _deploy-core
#!/usr/bin/env bash
set -e
REPO_DIR=$(pwd)
REPO_NAME=$(basename "$REPO_DIR" .git)
HOOKS_DIR="/usr/local/lib/git-hooks"
# Read from stdin
while read oldrev newrev refname; do
branch=$(basename "$refname")
if [[ "$branch" =~ ^v[0-9]+$ ]]; then
TARGET_DIR="/home/git/deploy/${branch}.${REPO_NAME}"
PROJECT_NAME="${branch}-${REPO_NAME}"
if [[ "$oldrev" == "0000000000000000000000000000000000000000" ]]; then
echo "Archiving $REPO_NAME at branch $branch..."
"$HOOKS_DIR/_deploy-core" "$REPO_DIR" "$TARGET_DIR" "$PROJECT_NAME" "$branch"
elif [[ "$newrev" == "0000000000000000000000000000000000000000" ]]; then
echo "Unarchiving $REPO_NAME from branch $branch..."
"$HOOKS_DIR/_undeploy-core" "$REPO_DIR" "$TARGET_DIR" "$PROJECT_NAME" "$branch"
fi
fi
done
But I’ll be honest, I had to pull myself out to get the blog out today. After successfully creating the archive logic with dynamic secret - thanks to redis!
I started thinking about rollbacks
- What if I had a system that deploys hashes, not branches?
- What if deploy-main gets the most recent hash on the main branch to deploy, but
uses the same
TARGET_DIR
andPROJECT_NAME
so there aren’t any orphan folders and images when a new deployment comes in - What if the same is done for branches?
- What if moving HEAD from the top of
main
to a hash inmain
deploys main from thathash
What if we had a new pointer just for that? A pointer that determines what part
of main
is deployed with archiving still being supported.
If I nail that, and maybe create new inputs for _deploy-core
and
_undeploy-core
, rolling back could be as simple as printing a statement
Plans for the future
Luckily I caught myself - I’m working on a time management system, and being TheCist is not helping haha
I’ll stock that up for next week, along with
- A framework for testing - regression, unit, integration
- A script to update discord invite links weekly