main
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.job-notify;
11in
12{
13 options = {
14 services.job-notify = {
15 enable = mkEnableOption ''
16 Generic job notification service that sends ntfy notifications for systemd units
17 '';
18
19 ntfyServer = mkOption {
20 type = types.str;
21 default = "https://ntfy.sbr.pm";
22 description = ''
23 The ntfy server URL to send notifications to
24 '';
25 };
26
27 ntfyTokenFile = mkOption {
28 type = types.str;
29 description = ''
30 Path to file containing the ntfy authentication token
31 '';
32 };
33
34 defaultTopic = mkOption {
35 type = types.str;
36 default = "builds";
37 description = ''
38 Default ntfy topic for notifications that don't match any specific pattern
39 '';
40 };
41 };
42 };
43
44 config = mkIf cfg.enable {
45 systemd.services."job-notify@" = {
46 description = "Generic job notification for %i";
47 serviceConfig = {
48 Type = "oneshot";
49 ExecStart = "${pkgs.writeShellScript "job-notify" ''
50 #!/usr/bin/env bash
51 set -euo pipefail
52
53 UNIT_NAME="$1"
54 RESULT=$(${pkgs.systemd}/bin/systemctl show -p Result --value "$UNIT_NAME")
55 EXIT_CODE=$(${pkgs.systemd}/bin/systemctl show -p ExecMainStatus --value "$UNIT_NAME")
56
57 # Get execution timestamps
58 START_TIME=$(${pkgs.systemd}/bin/systemctl show -p ExecMainStartTimestamp --value "$UNIT_NAME")
59 EXIT_TIME=$(${pkgs.systemd}/bin/systemctl show -p ExecMainExitTimestamp --value "$UNIT_NAME")
60
61 # Calculate duration in seconds
62 START_EPOCH=$(${pkgs.coreutils}/bin/date -d "$START_TIME" +%s 2>/dev/null || echo "0")
63 EXIT_EPOCH=$(${pkgs.coreutils}/bin/date -d "$EXIT_TIME" +%s 2>/dev/null || echo "0")
64 DURATION=$((EXIT_EPOCH - START_EPOCH))
65
66 # Format duration as human-readable
67 if [ "$DURATION" -ge 60 ]; then
68 MINUTES=$((DURATION / 60))
69 SECONDS=$((DURATION % 60))
70 DURATION_STR="''${MINUTES}m ''${SECONDS}s"
71 else
72 DURATION_STR="''${DURATION}s"
73 fi
74
75 # Parse unit name to determine job type and topic
76 # Supports: git-<job>-<repo>-<timestamp>, build-<type>-<name>-<timestamp>, scheduled-<name>-<timestamp>
77 PREFIX=$(echo "$UNIT_NAME" | cut -d'-' -f1)
78
79 case "$PREFIX" in
80 git)
81 JOB_TYPE=$(echo "$UNIT_NAME" | cut -d'-' -f2)
82 NAME=$(echo "$UNIT_NAME" | cut -d'-' -f3)
83 TOPIC="git-builds"
84 EMOJI_SUCCESS="✅"
85 EMOJI_FAIL="❌"
86 ;;
87 build)
88 JOB_TYPE=$(echo "$UNIT_NAME" | cut -d'-' -f2)
89 NAME=$(echo "$UNIT_NAME" | cut -d'-' -f3)
90 TOPIC="remote-builds"
91 EMOJI_SUCCESS="🏗️"
92 EMOJI_FAIL="💥"
93 ;;
94 scheduled)
95 JOB_TYPE="scheduled"
96 NAME=$(echo "$UNIT_NAME" | cut -d'-' -f2)
97 TOPIC="scheduled-tasks"
98 EMOJI_SUCCESS="⏰"
99 EMOJI_FAIL="⚠️"
100 ;;
101 *)
102 JOB_TYPE="job"
103 NAME=$(echo "$UNIT_NAME" | cut -d'-' -f1)
104 TOPIC="${cfg.defaultTopic}"
105 EMOJI_SUCCESS="✅"
106 EMOJI_FAIL="❌"
107 ;;
108 esac
109
110 # Send notification
111 if [ "$RESULT" = "success" ]; then
112 ${pkgs.curl}/bin/curl -s \
113 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.ntfyTokenFile})" \
114 -H "Title: $EMOJI_SUCCESS $JOB_TYPE Success: $NAME ($DURATION_STR)" \
115 -H "Tags: white_check_mark,$JOB_TYPE" \
116 -H "Priority: default" \
117 -d "Job $UNIT_NAME completed successfully in $DURATION_STR (exit code: $EXIT_CODE)" \
118 "${cfg.ntfyServer}/$TOPIC" || true
119 else
120 ${pkgs.curl}/bin/curl -s \
121 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.ntfyTokenFile})" \
122 -H "Title: $EMOJI_FAIL $JOB_TYPE Failed: $NAME (after $DURATION_STR)" \
123 -H "Priority: high" \
124 -H "Tags: x,$JOB_TYPE,warning" \
125 -d "Job $UNIT_NAME failed after $DURATION_STR (exit code: $EXIT_CODE). Check logs: journalctl -u $UNIT_NAME" \
126 "${cfg.ntfyServer}/$TOPIC" || true
127 fi
128 ''} %i";
129 };
130 };
131 };
132}