README
¶
Eventic
Eventic was born out of pure frustration. Every GitOps tool out there assumes you're running Kubernetes in the cloud with a fancy service mesh, an ingress controller, and a fleet of managed runners. But what if you're not? What if your builds run on bare-metal servers in a closet? What if your deployment target is a VM behind a firewall that GitHub Actions will never reach?
There is no simple, lightweight solution for triggering on-prem or local builds and arbitrary automation based on GitHub webhooks — so we built one.
Eventic is a minimal, two-component system that bridges GitHub webhooks to any machine that can make an outbound WebSocket connection. No inbound ports required on your local network. No complex infrastructure. Just a relay server and a lightweight client.
Architecture
GitHub ──webhook──▶ [Eventic Server] ◀──websocket──▶ [Eventic Client] ──▶ run hooks
(public / cloud) (on-prem / local)
- Server — A lightweight relay that receives GitHub webhooks, validates signatures, and fans out events to connected clients over WebSocket.
- Client — A small daemon that connects to the server, listens for events matching its subscriptions, checks out the relevant repo/ref, and executes hooks defined in
.eventic.yaml.
Server
The server is a single static binary packaged as a Docker container. It exposes three endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/webhook/github |
POST | Receives GitHub webhooks (HMAC-SHA256 validated) |
/ws |
GET | WebSocket endpoint for client connections |
/healthz |
GET | Health check |
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
EVENTIC_WEBHOOK_SECRET |
Yes | — | HMAC secret configured in your GitHub webhook |
EVENTIC_CLIENT_TOKENS |
Yes | — | Comma-separated list of tokens that clients use to authenticate |
EVENTIC_LISTEN_ADDR |
No | :8080 |
Address and port to listen on |
Running with Docker
docker run -d \
--name eventic-server \
-p 8080:8080 \
-e EVENTIC_WEBHOOK_SECRET="your-github-webhook-secret" \
-e EVENTIC_CLIENT_TOKENS="token1,token2" \
nitecon/eventic:latest
Docker Compose
services:
eventic:
image: nitecon/eventic:latest
restart: unless-stopped
ports:
- "8080:8080"
environment:
EVENTIC_WEBHOOK_SECRET: "your-github-webhook-secret"
EVENTIC_CLIENT_TOKENS: "token1,token2"
Deploying to Google Cloud Run
The repository includes a cloudbuild.yaml that builds the Docker image and deploys it to Cloud Run automatically. To set this up:
-
Create a GCP project (or use an existing one) and enable the following APIs:
- Cloud Build
- Cloud Run
- Container Registry (or Artifact Registry)
-
Create a Cloud Run service named
eventic-git(or update the service name incloudbuild.yaml) and configure the environment variables:Variable Value EVENTIC_WEBHOOK_SECRETYour GitHub webhook HMAC secret EVENTIC_CLIENT_TOKENSComma-separated client auth tokens You can set these via the Cloud Run console under Edit & Deploy New Revision > Variables & Secrets, or with the CLI:
gcloud run services update eventic-git \ --region=us-east4 \ --set-env-vars="EVENTIC_WEBHOOK_SECRET=your-secret,EVENTIC_CLIENT_TOKENS=token1,token2" -
Set up a Cloud Build trigger to build and deploy on push:
- Go to Cloud Build > Triggers > Create Trigger
- Connect your GitHub repository
- Set the trigger to use the existing
cloudbuild.yamlat the repo root - Choose the branch pattern to trigger on (e.g.,
^main$)
-
Enable connectivity — Cloud Run services are publicly accessible by default, which is required so GitHub can deliver webhooks and clients can connect via WebSocket. Ensure:
- The service allows unauthenticated invocations (set under the Cloud Run service's Security tab)
- The Cloud Run service URL is used as your GitHub webhook payload URL (e.g.,
https://eventic-git-xxxxx-ue.a.run.app/webhook/github)
Once the trigger is configured, every push to your selected branch will automatically build and deploy the latest Eventic server to Cloud Run.
GitHub Webhook Setup
- Go to your repository (or organization) Settings > Webhooks > Add webhook
- Set Payload URL to
https://your-server:8080/webhook/github - Set Content type to
application/json - Set Secret to the same value as
EVENTIC_WEBHOOK_SECRET - Select the events you want to receive (or choose "Send me everything")
Bulk Webhook Setup
If you have many repositories, the included setup-eventic-webhooks.sh script can add the Eventic webhook to all of them in one pass. It uses the GitHub CLI (gh) to iterate over your repos and create the webhook wherever it doesn't already exist.
# Preview what would be created (no changes made)
./setup-eventic-webhooks.sh \
--url https://your-server/webhook/github \
--secret your-webhook-secret \
--dry-run
# Apply to all repos for the authenticated user and their orgs
./setup-eventic-webhooks.sh \
--url https://your-server/webhook/github \
--secret your-webhook-secret
# Target specific owners only
./setup-eventic-webhooks.sh \
--url https://your-server/webhook/github \
--secret your-webhook-secret \
--owner myuser \
--owner my-org
| Flag | Required | Description |
|---|---|---|
--url |
Yes | Your Eventic server's webhook endpoint |
--secret |
Yes | HMAC secret (must match EVENTIC_WEBHOOK_SECRET) |
--owner |
No | GitHub user or org to process (repeatable). When omitted, auto-detects the authenticated user and all their organisations |
--dry-run |
No | Show what would be created without making changes |
Requirements: GitHub CLI (
gh) authenticated with admin scope on the target repos, andjq.
Client
The client is a small, self-contained binary that runs as a systemd service on any Linux machine. It maintains a persistent WebSocket connection to the server, automatically reconnects with exponential backoff, and executes hooks when events arrive.
Quick Install
The install script creates the eventic user, downloads the binary, installs the systemd service, and drops a template config:
curl -fsSL https://raw.githubusercontent.com/nitecon/eventic/refs/heads/main/install.sh | sudo bash
Then edit /etc/eventic/config.yaml with your relay URL, token, and subscriptions, and start the service:
sudo systemctl start eventic
Manual Installation
Download the latest release for your platform from the Releases page:
# Download to temp, then install into /opt/eventic/bin
curl -fsSL -o /tmp/eventic-client \
https://github.com/nitecon/eventic/releases/latest/download/eventic-client-linux-amd64
sudo mkdir -p /opt/eventic/bin
sudo mv /tmp/eventic-client /opt/eventic/bin/eventic-client
sudo chmod +x /opt/eventic/bin/eventic-client
sudo chown eventic:eventic /opt/eventic/bin/eventic-client
# Symlink into PATH for convenience
sudo ln -sf /opt/eventic/bin/eventic-client /usr/local/bin/eventic-client
Configuration
Create /etc/eventic/config.yaml:
relay: "wss://your-server:8080/ws"
token: "your-auth-token"
client_id: "build-server-01"
repos_dir: "/opt/eventic/repos"
subscribe:
- "myuser/*"
- "myworkorg/*"
- "kubernetes/specific-repo"
auto-update: true
auto-check: true
max-workers: 4
global-hooks:
pre: "echo preparing ${EVENTIC_REPO}..."
post: "echo done with ${EVENTIC_REPO}"
| Field | Description |
|---|---|
relay |
WebSocket URL of the Eventic server |
token |
Authentication token (must be listed in the server's EVENTIC_CLIENT_TOKENS) |
client_id |
Unique identifier for this client |
repos_dir |
Directory where repos will be cloned and managed |
subscribe |
List of glob patterns to filter which repositories trigger hooks (see Subscription Patterns) |
auto-update |
When true, the client checks for new releases every 5 minutes and updates itself automatically |
auto-check |
Verifies repo health every 5 minutes and re-clones broken repos (one at a time). Defaults to true — set to false to disable |
max-workers |
Maximum concurrent event processors (defaults to the number of CPUs). Events for the same repo are always serialized |
global-hooks.pre |
Fallback pre hook — runs for repos that have no .eventic.yaml or .deploy/deploy.yml |
global-hooks.post |
Fallback post hook — runs for repos that have no .eventic.yaml or .deploy/deploy.yml |
Subscription Patterns
Subscriptions use Go's path.Match glob syntax to filter which repositories trigger hooks on the client. Repositories arrive as org/repo, so patterns must account for the / separator — a bare * will not match across it.
| Pattern | Matches | Use case |
|---|---|---|
myuser/* |
All repos under myuser |
Subscribe to everything in your personal account |
myworkorg/* |
All repos under myworkorg |
Subscribe to everything in your work organization |
kubernetes/kops |
Exactly kubernetes/kops |
Subscribe to a specific repo you contribute to |
myorg/infra-* |
myorg/infra-web, myorg/infra-db, etc. |
Subscribe to repos matching a naming convention |
*/* |
Every org and every repo | Subscribe to absolutely everything (see note below) |
A typical setup combines broad org-level wildcards for accounts you own with pinpoint subscriptions for external repos you contribute to:
subscribe:
- "myuser/*" # all personal repos
- "myworkorg/*" # all work org repos
- "kubernetes/kops" # specific external repo I contribute to
- "prometheus/node_*" # external repos matching a prefix
Note:
*/*subscribes to every event the server receives. This only makes sense if your server's webhook is scoped to repos you care about. The only requirement for any pattern to work is that the corresponding GitHub webhook has been added to the repository or organization — Eventic can only relay events it actually receives.
Systemd Service
Create /etc/systemd/system/eventic.service:
[Unit]
Description=Eventic Client
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/eventic-client -c /etc/eventic/config.yaml
Restart=always
RestartSec=5
User=eventic
Group=eventic
WorkingDirectory=/opt/eventic
[Install]
WantedBy=multi-user.target
Enable and start:
sudo useradd -r -s /bin/false eventic
sudo mkdir -p /opt/eventic/{bin,repos} /etc/eventic
sudo chown -R eventic:eventic /opt/eventic
sudo systemctl daemon-reload
sudo systemctl enable --now eventic
Auto-Update
When auto-update: true is set in the client config, the client will check GitHub releases every 5 minutes for a newer version. If one is found, it downloads the matching binary for the current OS and architecture, resolves any symlinks to find the real binary location (e.g. /opt/eventic/bin/eventic-client), replaces itself, and exits. The systemd service (configured with Restart=always) then restarts automatically with the new binary.
Because the binary lives in /opt/eventic/bin/ (owned by the eventic user), the auto-updater can write updates without requiring root privileges. The symlink in /usr/local/bin/ continues to point to the updated binary automatically.
Source builds (without a release version) will update to the latest published release on the first check.
Hook Configuration
Hooks are defined per-repository in .eventic.yaml at the repo root:
hooks:
pre: "echo preparing..."
post: "echo done"
events:
push:
post: "make deploy"
pull_request:
post: "make lint"
pull_request.opened:
post: "claude -p 'Review this PR' --headless"
pull_request.synchronize:
post: "make test"
release.published:
post: "/opt/scripts/notify-release.sh"
issues.opened:
post: "claude -p 'Triage this issue' --headless"
Execution Order
For each event the client receives:
- Global pre hook (
hooks.pre) — runs before anything else - Event-specific pre hook (
events.<event>.pre) — runs before checkout - Git checkout — switches to the correct ref/branch/PR
- Event-specific post hook (
events.<event>.post) — runs after checkout - Global post hook (
hooks.post) — runs after everything
Event Matching
Hooks are matched with action specificity: pull_request.opened takes precedence over pull_request. If no action-specific hook exists, the event-level hook is used as a fallback.
Environment Variables
Every hook receives these environment variables:
| Variable | Description |
|---|---|
EVENTIC_REPO |
Full repository name (e.g., org/repo) |
EVENTIC_REF |
Git ref (branch name, tag, or PR ref) |
EVENTIC_EVENT |
GitHub event type (e.g., push, pull_request) |
EVENTIC_ACTION |
Event action (e.g., opened, synchronize) |
EVENTIC_SENDER |
GitHub username that triggered the event |
EVENTIC_PR_NUMBER |
Pull request number (0 if not a PR event) |
EVENTIC_DELIVERY_ID |
Unique GitHub delivery ID for tracing |
Hook Resolution Order
The client resolves hooks using the following precedence (first match wins):
.eventic.yamlin the repository root — full per-repo hook configuration.deploy/deploy.ymlin the repository — used as a Bruce manifest for push events- Client-level global hooks (
global-hooks.pre/global-hooks.postin the client config) — fallback for repos with no hook configuration of their own
This means you can set up default automation for all subscribed repos by adding a global-hooks: block to your client config, and individual repos can override it by adding their own .eventic.yaml.
Building from Source
# Server
CGO_ENABLED=0 go build -ldflags="-s -w" -o eventic-server ./server/cmd/
# Client
CGO_ENABLED=0 go build -ldflags="-s -w" -o eventic-client ./client/cmd/
License
Apache License 2.0 — see LICENSE for details.