No description
  • Go 97.5%
  • Makefile 1.3%
  • Dockerfile 0.7%
  • Shell 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mike 'Fuzzy' Partin c9f9a0ec29
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
fix(logging): change NewFromWriter parameter to io.Writer
*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
2026-07-06 11:37:40 -07:00
.forgejo/workflows fix(ci): install golangci-lint via binary download in pre-commit job 2026-07-05 23:53:03 -07:00
cmd/ttheart feat(cli): add rule testing command with -test-rules 2026-07-06 04:54:22 -07:00
deploy feat(notify): add notification plugin system 2026-07-06 11:10:49 -07:00
internal feat(dashboard): add web dashboard with activity, rules, and live log tail 2026-07-06 11:29:11 -07:00
logging fix(logging): change NewFromWriter parameter to io.Writer 2026-07-06 11:37:40 -07:00
man/man8 feat(build): add Makefile with build, test, cross-compile, and man targets 2026-07-06 02:15:36 -07:00
pkg/models feat(templates): add custom template files and CLI preview 2026-07-06 04:47:26 -07:00
test test: add load tests for burst throughput, worker pool, and goroutine leaks 2026-07-06 03:30:21 -07:00
.golangci-lint.yml chore(ci): add golangci-lint configuration 2026-07-05 23:02:28 -07:00
.pre-commit-config.yaml fix(ci): install goimports and golangci-lint in pre-commit job 2026-07-05 23:50:09 -07:00
config.yaml.example feat(config): implement configuration loader 2026-07-06 00:08:05 -07:00
docker-compose.yml docs(deploy): add Dockerfile, Compose, Kubernetes, and Ansible manifests 2026-07-06 04:31:14 -07:00
Dockerfile docs(deploy): add Dockerfile, Compose, Kubernetes, and Ansible manifests 2026-07-06 04:31:14 -07:00
go.mod feat(config): add configuration hot-reload via fsnotify 2026-07-06 03:47:39 -07:00
go.sum feat(config): add configuration hot-reload via fsnotify 2026-07-06 03:47:39 -07:00
Makefile feat(build): add Makefile with build, test, cross-compile, and man targets 2026-07-06 02:15:36 -07:00
README.md docs: add comprehensive README with quick start, config reference, and examples 2026-07-06 02:08:08 -07:00
ROADMAP.md docs: mark Add web UI tasks as complete 2026-07-06 11:29:25 -07:00
ttheart.service feat(systemd): add ttheart.service unit file 2026-07-06 01:45:56 -07:00

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:

  1. Path specified via -config flag
  2. /etc/ttheart/config.yaml (production)
  3. ./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

  1. Go to your repository SettingsWebhooks
  2. Click Add WebhookForgejo
  3. Set Target URL to http://your-server:8080/webhook
  4. Choose a Webhook Secret (must match secret in config.yaml)
  5. Select events to trigger (e.g., Push, Issues, Pull Request, Release)
  6. 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/