- Go 97.5%
- Makefile 1.3%
- Dockerfile 0.7%
- Shell 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
CI / Test (pull_request) Successful in 2m8s
CI / Lint (golangci-lint) (pull_request) Successful in 2m30s
CI / Pre-commit Hooks (pull_request) Successful in 3m31s
CI / Test (push) Successful in 2m8s
CI / Lint (golangci-lint) (push) Successful in 2m20s
CI / Pre-commit Hooks (push) Successful in 3m29s
*os.File restricted logging to file-only writers. io.Writer accepts all writer types (bytes.Buffer, network connections, etc.). Backward-compatible: *os.File implements io.Writer. Fixes #177 |
||
| .forgejo/workflows | ||
| cmd/ttheart | ||
| deploy | ||
| internal | ||
| logging | ||
| man/man8 | ||
| pkg/models | ||
| test | ||
| .golangci-lint.yml | ||
| .pre-commit-config.yaml | ||
| config.yaml.example | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
| ROADMAP.md | ||
| ttheart.service | ||
ttheart
Webhook server for Forgejo that executes shell commands based on event-triggered rules.
Quick Start
# Build
go build -o ttheart ./cmd/ttheart/
# Create a config
cp config.yaml.example config.yaml
# Run
./ttheart -config config.yaml
Or with systemd:
# Install
sudo cp ttheart /usr/local/bin/
sudo mkdir -p /etc/ttheart
sudo cp config.yaml /etc/ttheart/
# Enable service
sudo cp ttheart.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ttheart
Configuration Reference
ttheart reads YAML configuration from the first available location:
- Path specified via
-configflag /etc/ttheart/config.yaml(production)./config.yaml(development)
Options
| Field | Type | Default | Description |
|---|---|---|---|
listen |
string | :8080 |
Address to listen on |
secret |
string | (empty) | Webhook secret for HMAC-SHA256 signature verification |
max_concurrency |
int | 20 |
Maximum concurrent command executions |
rules |
[]Rule | (required) | List of rules to evaluate |
Rule Fields
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Unique rule identifier |
match.events |
[]string | yes | Event types to match (e.g., push, issues, pull_request) |
match.conditions |
[]Condition | no | Conditions that must all be satisfied |
actions |
[]Action | yes | Commands to execute when rule matches |
Condition Fields
| Field | Type | Required | Description |
|---|---|---|---|
field |
string | yes | Dot-separated path in payload (e.g., repository.full_name) |
operator |
string | yes | eq, ne, contains, matches, exists |
value |
string | depends | Comparison value (required for all operators except exists) |
Condition Operators
| Operator | Description | Example |
|---|---|---|
eq |
String equality | value: "thwap/ttheart" |
ne |
String inequality | value: "deleted" |
contains |
Substring match | value: "main" |
matches |
Regex match | value: "^tth" |
exists |
Field exists in payload | (no value needed) |
Action Fields
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | (required) | Human-readable action name |
command |
string | (required) | Shell command with optional Go templates |
timeout |
string | 30s |
Command timeout duration |
retry |
int | 0 |
Number of retry attempts on failure |
Action Template Functions
{{ .field.path }}— Access payload fields using dot notation{{ json .field }}— JSON-marshal a field{{ timestamp }}— Current UTC time in RFC3339 format{{ quote .field }}— Shell-safe single-quoted string
Example Rules
CI Workflow Failure Notifications
rules:
- name: "ci-failure-alert"
match:
events: ["check_suite"]
conditions:
- field: "check_suite.conclusion"
operator: "eq"
value: "failure"
actions:
- name: "alert"
command: "curl -X POST -d 'CI failed for {{ .repository.full_name }}' https://alerts.example.com/webhook"
Push Events
rules:
- name: "deploy-on-push"
match:
events: ["push"]
conditions:
- field: "ref"
operator: "eq"
value: "refs/heads/main"
- field: "repository.full_name"
operator: "eq"
value: "thwap/ttheart"
actions:
- name: "deploy"
command: "/usr/local/bin/deploy.sh {{ quote .after }}"
timeout: "5m"
Issue Events
rules:
- name: "log-new-issues"
match:
events: ["issues"]
conditions:
- field: "action"
operator: "eq"
value: "opened"
actions:
- name: "log-issue"
command: "echo 'New issue #{{ .issue.number }}: {{ .issue.title }}' >> /var/log/ttheart/issues.log"
Pull Request Events
rules:
- name: "pr-review-notify"
match:
events: ["pull_request"]
conditions:
- field: "action"
operator: "ne"
value: "synchronize"
actions:
- name: "notify"
command: "echo 'PR {{ .pull_request.title }} by {{ .sender.login }}' | mail -s 'PR Update' team@example.com"
Release Events
rules:
- name: "release-announce"
match:
events: ["release"]
conditions:
- field: "action"
operator: "eq"
value: "published"
actions:
- name: "announce"
command: "curl -X POST -d 'Release {{ .release.tag_name }} published' https://chat.example.com/hook"
Forgejo Webhook Setup
- Go to your repository Settings → Webhooks
- Click Add Webhook → Forgejo
- Set Target URL to
http://your-server:8080/webhook - Choose a Webhook Secret (must match
secretin config.yaml) - Select events to trigger (e.g., Push, Issues, Pull Request, Release)
- Click Add Webhook
The server verifies webhook signatures using HMAC-SHA256. If no secret is configured, signature verification is skipped.
Troubleshooting
Server won't start:
Check config syntax with ttheart -config config.yaml. Validation errors are logged on startup. Ensure all rules have a name and at least one event type.
Webhooks return 401:
The webhook secret in config.yaml doesn't match the secret in Forgejo's webhook settings. If you don't need verification, remove or leave the secret field empty.
Commands not executing:
Check that the event type in your Forgejo webhook configuration matches the events list in your rule. Event headers are X-Gitea-Event (or X-Forgejo-Event as fallback).
Template errors:
Verify your template syntax. Use {{ .field.path }} for nested fields. Test templates with echo.
High load / resource exhaustion:
Adjust max_concurrency in the config to limit simultaneous command executions. Default is 20.
Command timed out:
Increase the per-action timeout field. Default is 30 seconds.
Debug mode:
All request details are logged at INFO level including method, path, source IP, event type, status code, and duration. Command outputs are included in success/failure log entries.
Environment Variables
Override config values using environment variables (before defaults are applied):
| Variable | Effect |
|---|---|
TTHEART_LISTEN |
Override listen address |
TTHEART_SECRET |
Override webhook secret |
CLI Flags
| Flag | Description |
|---|---|
-config <path> |
Path to config file |
-version |
Print version and exit |
Building
# Build with version
go build -ldflags "-X main.version=1.0.0" -o ttheart ./cmd/ttheart/
# Cross-compile for ARM64
GOOS=linux GOARCH=arm64 go build -o ttheart-arm64 ./cmd/ttheart/