Last active
November 28, 2020 12:45
-
-
Save philpennock/275099baeb529f6f1f2d0b6f1f5669f1 to your computer and use it in GitHub Desktop.
git post-receive hook for gitolite to publish updates to NATS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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]" |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
orcapture
in jq(1) invocation.