fedora-csb-system-manager

Git Post-Receive Hooks with Background Task Execution

Overview

This document describes the git post-receive hook system for kerkouane, which enables background task execution with automatic notifications via ntfy.

The system uses systemd-run to execute long-running tasks (like static site generation) in the background, with automatic success/failure notifications sent to ntfy.

Architecture

Components

  1. systemd-run - Native systemd tool for creating transient services
  2. git-notify@.service - Systemd service template that sends ntfy notifications
  3. generate-gitmal.sh - Helper script for gitmal static site generation
  4. post-receive hook - Git hook that triggers background tasks

Flow

Git Push → post-receive hook → systemd-run (background) → OnSuccess/OnFailure → git-notify@.service → ntfy

Key Features

  • Background execution - Git push returns immediately, work happens asynchronously
  • Automatic notifications - ntfy alerts on success/failure
  • Excellent logging - Full logs via journalctl -u <unit-name>
  • No additional dependencies - Uses native systemd (already on NixOS)
  • Robust process management - systemd handles process lifecycle

Configuration

NixOS Configuration (systems/kerkouane/extra.nix)

The system is configured in /home/vincent/src/home/systems/kerkouane/extra.nix:

# Git hook background task execution with notifications
systemd.services."git-notify@" = {
  description = "Git build notification for %i";
  serviceConfig = {
    Type = "oneshot";
    ExecStart = pkgs.writeShellScript "git-notify" ''
      # ... notification script ...
    '' "%i";
  };
};

Notification Service

The git-notify@.service template:

  • Receives the unit name as parameter (%i)
  • Checks the service exit status
  • Parses unit name to extract job type and repository
  • Sends ntfy notification with appropriate emoji and priority

Notification format:

  • Success: ✅ Git Success: (duration)
  • Failure: ❌ Git Failed: (after duration) (with high priority)

Duration display:

  • Less than 60 seconds: “42s”
  • 60 seconds or more: “1m 23s”

Unit name format: git-<job>-<repo>-<timestamp>

  • Example: git-gitmal-myrepo-20260107-143022

Usage

Setting Up a Repository

  1. Navigate to your git repository:

    cd /home/vincent/git/public/myrepo.git
    
  2. Copy the example hook:

    cp /etc/git-hooks/post-receive.example hooks/post-receive
    chmod +x hooks/post-receive
    
  3. Configure the theme (optional):

    Edit hooks/post-receive and set the theme at the top:

    # Configuration
    GITMAL_THEME="github-dark"  # Options: github-dark, github-light, dark, light, auto
    
  4. Customize if needed (the example works for gitmal generation out of the box)

Example Post-Receive Hook

#!/usr/bin/env bash
set -euo pipefail

# Configuration
GITMAL_THEME="github-dark"  # Options: github-dark, github-light, dark, light, auto

REPO_PATH="$(pwd)"
REPO_NAME=$(basename "$REPO_PATH" .git)
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
UNIT_NAME="git-gitmal-${REPO_NAME}-${TIMESTAMP}"

echo "Queuing gitmal generation for $REPO_NAME with theme: $GITMAL_THEME..."

# Run gitmal generation in background with systemd-run
sudo systemd-run \
  --unit="$UNIT_NAME" \
  --description="Gitmal generation for $REPO_NAME" \
  --property="OnSuccess=git-notify@${UNIT_NAME}.service" \
  --property="OnFailure=git-notify@${UNIT_NAME}.service" \
  --property="User=vincent" \
  --property="Group=users" \
  --working-directory="$REPO_PATH" \
  /etc/git-hooks/generate-gitmal.sh "$REPO_PATH" "$GITMAL_THEME"

echo "✓ Gitmal generation queued as: $UNIT_NAME"
echo "  View status: systemctl status $UNIT_NAME"
echo "  View logs:   journalctl -u $UNIT_NAME"

Monitoring Jobs

Check job status:

systemctl status git-gitmal-myrepo-20260107-143022

View job logs:

journalctl -u git-gitmal-myrepo-20260107-143022

List all git build jobs:

systemctl list-units 'git-*'

Follow logs in real-time:

journalctl -f -u git-gitmal-myrepo-20260107-143022

Customizing Gitmal Theme

Gitmal supports different themes that can be configured per-repository:

Available themes:

  • github-dark - GitHub dark theme (default)
  • github-light - GitHub light theme
  • dark - Dark theme
  • light - Light theme
  • auto - Automatically switch based on system preference

To set the theme for a repository:

Edit the repository’s hooks/post-receive file:

# Configuration
GITMAL_THEME="github-light"  # Change to your preferred theme

The theme is passed as the second parameter to generate-gitmal.sh:

/etc/git-hooks/generate-gitmal.sh "$REPO_PATH" "$GITMAL_THEME"

Default theme: If no theme is specified, the system defaults to github-dark.

Extending the System

Creating Custom Build Scripts

You can create additional scripts in /etc/git-hooks/ for different tasks:

Example: Build forwarding script

Add to systems/kerkouane/extra.nix:

environment.etc."git-hooks/forward-build.sh" = {
  text = ''
    #!/usr/bin/env bash
    set -euo pipefail

    REPO_PATH="$1"
    TARGET_HOST="$2"

    echo "Forwarding build to $TARGET_HOST..."
    ssh "$TARGET_HOST" "cd /path/to/repo && git pull && make build"
    echo "Build forwarding complete"
  '';
  mode = "0755";
};

Use in post-receive hook:

UNIT_NAME="git-build-${REPO_NAME}-${TIMESTAMP}"

sudo systemd-run \
  --unit="$UNIT_NAME" \
  --description="Build forwarding for $REPO_NAME" \
  --property="OnSuccess=git-notify@%n.service" \
  --property="OnFailure=git-notify@%n.service" \
  --property="User=vincent" \
  --property="Group=users" \
  /etc/git-hooks/forward-build.sh "$REPO_PATH" "aomi"

Multiple Jobs in Sequence

To run multiple tasks sequentially, create a wrapper script:

environment.etc."git-hooks/multi-task.sh" = {
  text = ''
    #!/usr/bin/env bash
    set -euo pipefail

    REPO_PATH="$1"

    # Task 1: Generate gitmal
    /etc/git-hooks/generate-gitmal.sh "$REPO_PATH"

    # Task 2: Forward build
    /etc/git-hooks/forward-build.sh "$REPO_PATH" "aomi"

    echo "All tasks completed"
  '';
  mode = "0755";
};

Customizing Notifications

Modify the git-notify@.service in kerkouane/extra.nix to customize:

  • Notification content - Add more context from job logs
  • Notification channels - Send to different ntfy topics
  • Priority levels - Adjust based on job type
  • Additional actions - Trigger webhooks, update dashboards, etc.

Example: Different topics per job type

if [ "$JOB_TYPE" = "gitmal" ]; then
  TOPIC="git-builds"
elif [ "$JOB_TYPE" = "deploy" ]; then
  TOPIC="deployments"
else
  TOPIC="git-general"
fi

curl -H "Title: ..." -d "..." "http://localhost:8111/$TOPIC"

Troubleshooting

Hook Doesn’t Execute

Check hook permissions:

ls -l /home/vincent/git/public/myrepo.git/hooks/post-receive

Should be executable: -rwxr-xr-x

Check hook output: When you push, you should see:

remote: Queuing gitmal generation for myrepo...
remote: ✓ Gitmal generation queued as: git-gitmal-myrepo-20260107-143022

Job Fails

Check systemd logs:

journalctl -u git-gitmal-myrepo-20260107-143022

Check service status:

systemctl status git-gitmal-myrepo-20260107-143022

Common issues:

  • Permission errors (check User/Group in systemd-run)
  • Missing directories (check /home/vincent/git/public/)
  • Gitmal errors (check gitmal output in logs)

Notifications Not Received

Check ntfy service:

systemctl status ntfy-sh

Test notification manually:

curl -H "Title: Test" -d "Test message" http://localhost:8111/git-builds

Check notification script:

# View the notification script
systemctl cat git-notify@.service

sudo Password Prompt

If the post-receive hook prompts for sudo password, configure passwordless sudo for systemd-run:

Add to systems/kerkouane/extra.nix:

security.sudo.extraRules = [
  {
    users = [ "vincent" ];
    commands = [
      {
        command = "${pkgs.systemd}/bin/systemd-run";
        options = [ "NOPASSWD" ];
      }
    ];
  }
];

Performance Considerations

Resource Management

Systemd provides excellent resource control. Add limits to prevent resource exhaustion:

sudo systemd-run \
  --unit="$UNIT_NAME" \
  --property="CPUQuota=50%" \
  --property="MemoryMax=512M" \
  --property="TasksMax=100" \
  # ... other properties ...

Concurrent Jobs

By default, multiple pushes can trigger multiple concurrent jobs. To limit concurrency, use systemd slices or queue systems like nq (see research documentation).

Log Retention

Systemd automatically manages transient unit cleanup. Configure retention:

systemd.services.systemd-tmpfiles-clean.serviceConfig = {
  # Keep transient units for 7 days
  RuntimeMaxSec = "7d";
};

Security Considerations

Permissions

  • Git hooks run as the git repository owner (vincent)
  • systemd-run requires sudo for system service creation
  • Build scripts should not have elevated privileges
  • Use systemd sandboxing for untrusted code

Notification Content

Avoid sensitive information in notifications:

  • ❌ Don’t include secrets, tokens, or credentials
  • ❌ Don’t include full file paths with sensitive names
  • ✅ Use generic job names and repository names
  • ✅ Link to logs for details (secured by system permissions)

Network Access

The notification service sends HTTP requests to ntfy:

  • Uses localhost (127.0.0.1:8111) - no external network exposure
  • Can be configured for authentication (ntfy tokens)

Alternative Approaches Considered

See comprehensive research documentation for detailed analysis of alternatives:

  1. nq - Filesystem-based job queue (simple, no daemon)
  2. task-spooler - Job queue with email notifications (not in nixpkgs)
  3. GNU Parallel - Parallel job execution (different use case)
  4. Gitea Actions / Woodpecker CI - Full CI/CD (overkill for simple hooks)
  5. Simple shell patterns - nohup/background jobs (no management)

Why systemd-run was chosen:

  • Native to NixOS (zero dependencies)
  • Robust process management
  • Excellent logging via journalctl
  • OnSuccess/OnFailure for clean notification integration
  • Production-ready and well-documented

References

Examples

Example 1: Simple Gitmal Generation

# In /home/vincent/git/public/myproject.git/hooks/post-receive
#!/usr/bin/env bash
set -euo pipefail

REPO_PATH="$(pwd)"
REPO_NAME=$(basename "$REPO_PATH" .git)
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
UNIT_NAME="git-gitmal-${REPO_NAME}-${TIMESTAMP}"

sudo systemd-run \
  --unit="$UNIT_NAME" \
  --property="OnSuccess=git-notify@%n.service" \
  --property="OnFailure=git-notify@%n.service" \
  --property="User=vincent" \
  --property="Group=users" \
  /etc/git-hooks/generate-gitmal.sh "$REPO_PATH"

echo "Gitmal generation queued"

Example 2: Custom Build Script

# Create custom build script in kerkouane/extra.nix
environment.etc."git-hooks/build-project.sh" = {
  text = ''
    #!/usr/bin/env bash
    set -euo pipefail

    REPO_PATH="$1"
    cd "$REPO_PATH"

    # Run tests
    make test

    # Build project
    make build

    # Deploy to staging
    make deploy-staging
  '';
  mode = "0755";
};

# Use in post-receive hook
sudo systemd-run \
  --unit="git-build-${REPO_NAME}-${TIMESTAMP}" \
  --property="OnSuccess=git-notify@%n.service" \
  --property="OnFailure=git-notify@%n.service" \
  --property="User=vincent" \
  /etc/git-hooks/build-project.sh "$REPO_PATH"

Example 3: Conditional Execution by Branch

#!/usr/bin/env bash
set -euo pipefail

# Read stdin from git (old-rev new-rev refname)
while read oldrev newrev refname; do
  BRANCH=$(echo "$refname" | sed 's|refs/heads/||')

  if [ "$BRANCH" = "main" ]; then
    # Production deployment
    sudo systemd-run \
      --unit="git-deploy-prod-$(date +%s)" \
      --property="OnSuccess=git-notify@%n.service" \
      --property="OnFailure=git-notify@%n.service" \
      --property="User=vincent" \
      /etc/git-hooks/deploy-production.sh "$PWD"
  elif [ "$BRANCH" = "develop" ]; then
    # Staging deployment
    sudo systemd-run \
      --unit="git-deploy-staging-$(date +%s)" \
      --property="OnSuccess=git-notify@%n.service" \
      --property="OnFailure=git-notify@%n.service" \
      --property="User=vincent" \
      /etc/git-hooks/deploy-staging.sh "$PWD"
  else
    echo "Branch $BRANCH - no deployment configured"
  fi
done

Last updated: 2026-01-07 (v2: Added execution time tracking and theme selection) Author: Vincent Demeester (with Claude Code assistance)