#!/bin/sh
# Tell the forge's admins that something on this host went wrong, through
# the notify companion quarry already uses: the quarry-ops workflow
# (inbox + push, internal/notify/workflows.go), addressed to the admins
# topic. Installed at /srv/quarry/quarry-alert.sh (docs/running.md).
#
# quarry-alert.sh UNIT a unit failed: OnFailure=quarry-alert@%N.service
# quarry-alert.sh --key KEY --message TEXT anything else (quarry-deploy-watch.sh)
#
# QUARRY_NOTIFY_KEY is notify's API key, the one quarry.service reads: the
# units run this under `doppler run --project quarry --config prd`, with
# the service token from /srv/quarry/env. Every alert is also a journal
# line (logger -t quarry-alert), so one that could not be sent is still
# on the host.
set -eu
NOTIFY=${QUARRY_NOTIFY_URL:-http://127.0.0.1:3000}
TOPIC=${QUARRY_ALERT_TOPIC:-admins}
ORIGIN=${QUARRY_ORIGIN:-https://zeitforge.app}
# A unit in a crash loop fails every few seconds (quarry.service has
# Restart=on-failure, and OnFailure= fires on each one): one alert per
# unit per QUIET seconds is news, the rest is noise.
QUIET=${QUARRY_ALERT_QUIET:-600}
RUN=${QUARRY_ALERT_RUN:-/run/quarry-alert}
key=; text=; unit=
case "${1:-}" in
--key)
[ $# -eq 4 ] && [ "$3" = --message ] || { echo "usage: quarry-alert.sh UNIT | --key KEY --message TEXT" >&2; exit 2; }
key=$2; text=$4; summary=$(printf '%s' "$text" | head -1 | cut -c1-120) ;;
"" | -*) echo "usage: quarry-alert.sh UNIT | --key KEY --message TEXT" >&2; exit 2 ;;
*)
unit=$1
case "$unit" in *.*) ;; *) unit=$unit.service ;; esac
mkdir -p "$RUN"
mark="$RUN/$unit"
now=$(date +%s)
if [ -f "$mark" ] && [ $((now - $(cat "$mark"))) -lt "$QUIET" ]; then
logger -t quarry-alert "quiet: $unit failed again within ${QUIET}s; not sent"
exit 0
fi
echo "$now" >"$mark"
result=$(systemctl show -p Result --value "$unit" 2>/dev/null || echo unknown)
inv=$(systemctl show -p InvocationID --value "$unit" 2>/dev/null || true)
# The failed run's own lines: a deploy's FAILED line when it wrote
# one, else the last few lines the unit logged.
if [ -n "$inv" ]; then
log=$(journalctl -o cat _SYSTEMD_INVOCATION_ID="$inv" 2>/dev/null || true)
else
log=$(journalctl -o cat -n 20 -u "$unit" 2>/dev/null || true)
fi
text=$(printf '%s\n' "$log" | grep 'FAILED stage=' | tail -1 || true)
[ -n "$text" ] || text=$(printf '%s\n' "$log" | tail -5)
summary="$unit failed on $(hostname) ($result)"
key="unit:$unit:${inv:-$now}" ;;
esac
logger -t quarry-alert "$summary — $(printf '%s' "$text" | tr '\n' ' ')"
if [ -z "${QUARRY_NOTIFY_KEY:-}" ]; then
logger -t quarry-alert "not sent: no QUARRY_NOTIFY_KEY (run under doppler run --project quarry --config prd)"
exit 1
fi
body=$(jq -n --arg topic "$TOPIC" --arg summary "$summary" --arg text "$text" \
--arg link "$ORIGIN/" --arg key "$key" \
'{name: "quarry-ops", to: {topicKey: $topic},
payload: {summary: $summary, text: $text, link: $link},
transactionId: $key}')
if ! curl -sf -m 10 -o /dev/null -X POST "$NOTIFY/v1/events/trigger" \
-H 'Content-Type: application/json' -H "Authorization: ApiKey $QUARRY_NOTIFY_KEY" \
--data-binary "$body"; then
logger -t quarry-alert "not sent: notify at $NOTIFY refused or did not answer"
exit 1
fi