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
- systemd-run - Native systemd tool for creating transient services
- git-notify@.service - Systemd service template that sends ntfy notifications
- generate-gitmal.sh - Helper script for gitmal static site generation
- 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
-
Navigate to your git repository:
cd /home/vincent/git/public/myrepo.git -
Copy the example hook:
cp /etc/git-hooks/post-receive.example hooks/post-receive chmod +x hooks/post-receive -
Configure the theme (optional):
Edit
hooks/post-receiveand set the theme at the top:# Configuration GITMAL_THEME="github-dark" # Options: github-dark, github-light, dark, light, auto -
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 themedark- Dark themelight- Light themeauto- 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:
- nq - Filesystem-based job queue (simple, no daemon)
- task-spooler - Job queue with email notifications (not in nixpkgs)
- GNU Parallel - Parallel job execution (different use case)
- Gitea Actions / Woodpecker CI - Full CI/CD (overkill for simple hooks)
- 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
- systemd-run man page
- systemd.service man page
- Git Hooks Documentation
- ntfy Documentation
- Gitmal GitHub
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)