Skip to content

Instantly share code, notes, and snippets.

@philpennock
Last active November 28, 2020 12:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save philpennock/275099baeb529f6f1f2d0b6f1f5669f1 to your computer and use it in GitHub Desktop.
Save philpennock/275099baeb529f6f1f2d0b6f1f5669f1 to your computer and use it in GitHub Desktop.
git post-receive hook for gitolite to publish updates to NATS
#!/usr/bin/env bash
set -euo pipefail
progname="$(basename "$0" .sh)"
stderr() { printf >&2 '%s: %s\n' "$progname" "$*"; }
die() { stderr "$@"; exit 1; }
# Tunable
: "${NATS_SERVER:=nats.example.org}"
# Contractually has to have been provided, but this helps with command-line
# testing, and maybe very old git servers?
: "${GIT_PUSH_OPTION_COUNT:=0}"
# See <https://gitolite.com/gitolite/dev-notes.html>:
: "${HOSTNAME:?}"
: "${GL_REPO_BASE:?}"
: "${GL_USER:?}"
: "${GL_REPO:?}"
jq --version >/dev/null
: "${BASE_NATS_TOPIC:=git-update.${HOSTNAME%%.*}}"
NATS_ARGS=(-s "$NATS_SERVER")
if [[ $GIT_PUSH_OPTION_COUNT -gt 0 ]]; then
for (( i=0; i < GIT_PUSH_OPTION_COUNT; i++ )); do
declare -n po="GIT_PUSH_OPTION_$i"
case "${po^^}" in
NONATS|NO-NATS|NO_NATS)
stderr 'skipping per request'
exit 0
;;
esac
done
fi
dotted_repo="${GL_REPO//\//.}"
base_msg="$(
jq -ncer --arg R "$GL_REPO" --arg U "$GL_USER" --arg H "$HOSTNAME" \
'.host=$H|.user=$U|.repo=$R'
)"
main_msg="$(
jq <<<"$base_msg" -cer '.updates=[]'
)"
stderr publishing NATS notifications to "${BASE_NATS_TOPIC}.(PERREF,UPDATE).${dotted_repo}"
start_time=$EPOCHREALTIME
main_old=''
main_new=''
main_ref=''
while read -r oldval newval refname; do
jargs=(--arg R "$refname" --arg O "$oldval" --arg N "$newval")
per_msg="$(jq <<<"$base_msg" -cer "${jargs[@]}" '.update = {ref: $R, old: $O, new: $N} | .ref = $R')"
main_msg="$(jq <<<"$main_msg" -cer "${jargs[@]}" '.updates += [{ref: $R, old: $O, new: $N}]')"
# nats CLI at v0.0.19-24-g781e96f will not read stdin if stdout is not a tty.
# So we pass JSON in an argv element. This will only be a problem if a _lot_ of refs are updated,
# for the .UPDATE. post below.
nats "${NATS_ARGS[@]}" pub "${BASE_NATS_TOPIC}.PERREF.${dotted_repo}" "$per_msg" 2>/dev/null
case "$refname" in
refs/heads/master)
if [[ "$main_ref" == "" ]]; then
main_old="$oldval" main_new="$newval" main_ref="$refname"
fi
;;
refs/heads/main)
main_old="$oldval" main_new="$newval" main_ref="$refname"
;;
esac
done
nats "${NATS_ARGS[@]}" pub "${BASE_NATS_TOPIC}.UPDATE.${dotted_repo}" "$main_msg" 2>/dev/null
also_text=''
if [[ "$main_ref" != "" ]]; then
commits="$(git log --date=short --reverse --pretty=tformat:'%h %ad %s' "${main_old}..${main_new}")"
jargs=(--arg R "$main_ref" --arg O "$main_old" --arg N "$main_new" --arg C "$commits")
log_payload="$(
jq <<<"$base_msg" -cer "${jargs[@]}" '.ref=$R|.old=$O|.new=$N|
.logs=[$C|split("\n")|foreach .[] as $l ([];true; $l|capture("(?<abbrev>\\S+)\\s+(?<date>\\S+)\\s+(?<msg>.*)"))]'
)"
nats "${NATS_ARGS[@]}" pub "${BASE_NATS_TOPIC}.MAINLOG.${dotted_repo}" "$log_payload" 2>/dev/null
also_text=' (also published to MAINLOG)'
fi
# 2020-11-10: we have the stupid second line of output mostly so I can see if
# delays are being caused by publishing to NATS or elsewhere later.
#
# zsh spoils me, bash lacks floating-point
stderr "done${also_text} [$(printf '%s - %s\n' "$EPOCHREALTIME" "$start_time" | bc)s]"
@philpennock
Copy link
Author

The second version adds the publishing to MAINLOG variant, with the JSON object also holding an array containing objects, one per commit, ordered first to last (latest). My first time using either foreach or capture in jq(1) invocation.

@philpennock
Copy link
Author

Beware: the latencies on this proved higher than I am happy with, so I rewrote into Go and rigged up a shell wrapper for gitolite to auto-compile the Go hooks as needed, so that normally the hook is a shell check that the binary is up-to-date and then invoking the binary.

Latencies in shell with jq and nats(1): around 0.8s normally, sometimes 5.7s.

Latencies in Go version: around 10 or 11 milliseconds, total.

@philpennock
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment