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}