Githooks is an event automation layer for GitHub, GitLab, and Bitbucket. It receives webhooks, evaluates configurable rules, and publishes matching events to your message broker via Watermill. The Worker SDK then consumes those events with provider-aware clients, so your business logic stays focused on outcomes, not plumbing.
Warning: This project is for research and development only and is not production-ready. Do not deploy it in production environments.
- Unify SCM events without writing three webhook stacks. 🔗
- Route events by rules (JSONPath + boolean logic) instead of hardcoding. 🧠
- Use any broker supported by Watermill, with optional fan-out per rule. 📬
- Act with real clients (GitHub App, GitLab, Bitbucket) inside workers. 🔐
- Multi-Provider Webhooks: GitHub, GitLab, and Bitbucket. 🌍
- Rule Engine: JSONPath + boolean rules with multi-match support. 🧩
- Protobuf Event Envelope: Broker payloads use
cloud.v1.EventPayload, with raw JSON preserved. 📦 - Flexible Publishing: AMQP, NATS Streaming, Kafka, HTTP, SQL, GoChannel, RiverQueue. 🚚
- Multi-Driver Fan-Out: Publish to all drivers by default or target per rule. 🧯
- Worker SDK: Concurrency, middleware, topics, and graceful shutdown. 🧰
- SCM Auth Resolution: GitHub App (JWT → installation token), GitLab/Bitbucket OAuth tokens stored on install. 🔑
- Observability: Request IDs and structured logs. 🔎
- Ship-Ready Assets: Docker Compose, examples, boilerplate, Helm charts. 📚
- Release orchestration: Trigger CI/CD or internal workflows from PR merges.
- Preview automation: Post preview links on PR/MR events across providers.
- Compliance hooks: Enforce policy when branch protection or approvals change.
-
Start dependencies:
docker compose up -d
-
Run the server:
Set the secret for validating GitHub webhooks and run the server with the local Docker config.
export GITHUB_WEBHOOK_SECRET=devsecret go run ./main.go serve --config app.docker.yaml -
Run a worker:
In another terminal, run an example worker that listens for events.
go run ./example/github/worker/main.go --config app.docker.yaml
-
Send a test webhook:
Use the provided script to simulate a GitHub
pull_requestevent../scripts/send_webhook.sh github pull_request example/github/pull_request.json
You should see the server log the event and the worker log its "pr.opened.ready" message.
Single‑binary (in‑process)
Use GoChannel to run the server and multiple workers in one process:
go run ./example/inprocess/main.go --config app.docker.yamlDocs:
- Driver configuration
- Event compatibility
- Getting started (GitHub)
- Getting started (GitLab)
- Getting started (Bitbucket)
- Rules engine
- Observability
- SCM authentication
- Installation storage
- CLI usage
- OAuth callbacks
- Webhook setup
- SDK client injection
- SDK DSL (portable worker spec)
- API authentication (OAuth2/OIDC)
- Secure API quickstart
Githooks is configured using a single YAML file. Environment variables like ${VAR} are automatically expanded.
Requests use or generate X-Request-Id, which is echoed back in responses and included in logs.
The providers section configures webhook endpoints and SCM auth for each Git provider.
providers.*.key sets the provider instance key (default: default). Use different keys to run multiple app installs per provider.
If webhook.path is omitted, defaults are used: /webhooks/github, /webhooks/gitlab, /webhooks/bitbucket.
Set server.public_base_url when running behind ngrok or a reverse proxy so OAuth callbacks resolve to your public domain.
providers.*.oauth is reserved for OAuth2 expansion in future releases.
providers:
github:
enabled: true
key: default
webhook:
path: /webhooks/github
secret: ${GITHUB_WEBHOOK_SECRET}
app:
app_id: ${GITHUB_APP_ID}
private_key_path: ${GITHUB_PRIVATE_KEY_PATH}
app_slug: ${GITHUB_APP_SLUG}
api:
base_url: https://api.github.com
web_base_url: https://github.com
oauth:
client_id: ${GITHUB_OAUTH_CLIENT_ID}
client_secret: ${GITHUB_OAUTH_CLIENT_SECRET}
scopes: ["read:user"]
gitlab:
enabled: false
key: default
webhook:
path: /webhooks/gitlab
secret: ${GITLAB_WEBHOOK_SECRET} # Optional
api:
base_url: https://gitlab.com/api/v4
web_base_url: https://gitlab.com
oauth:
client_id: ${GITLAB_OAUTH_CLIENT_ID}
client_secret: ${GITLAB_OAUTH_CLIENT_SECRET}
scopes: ["read_api"]
bitbucket:
enabled: false
key: default
webhook:
path: /webhooks/bitbucket
secret: ${BITBUCKET_WEBHOOK_SECRET} # Optional, for X-Hook-UUID
api:
base_url: https://api.bitbucket.org/2.0
web_base_url: https://bitbucket.org
oauth:
client_id: ${BITBUCKET_OAUTH_CLIENT_ID}
client_secret: ${BITBUCKET_OAUTH_CLIENT_SECRET}
scopes: ["repository"]providers:
github:
app:
app_id: 123
private_key_path: /secrets/github.pem
app_slug: githooks
api:
base_url: https://api.github.com
web_base_url: https://github.com
gitlab:
api:
base_url: https://gitlab.com/api/v4
web_base_url: https://gitlab.com
bitbucket:
api:
base_url: https://api.bitbucket.org/2.0
web_base_url: https://bitbucket.orgGitHub Enterprise: set providers.github.api.base_url to your API base (for example,
https://ghe.example.com/api/v3). The SDK derives the upload URL automatically.
server:
port: 8080
public_base_url: https://app.example.com
read_timeout_ms: 5000
write_timeout_ms: 10000
idle_timeout_ms: 60000
read_header_timeout_ms: 5000
max_body_bytes: 1048576
debug_events: falseauth:
oauth2:
enabled: true
issuer: https://<your-okta-domain>/oauth2/default
audience: api://githooksWhen enabled, all Connect RPC endpoints require a bearer token. Webhooks and /auth/* remain public.
See docs/auth.md for client_credentials and human login flows.
storage:
driver: postgres
dsn: postgres://githooks:githooks@localhost:5432/githooks?sslmode=disable
dialect: postgres
auto_migrate: trueoauth:
redirect_base_url: https://app.example.com/oauth/completeCallback endpoints:
/auth/github/callback/auth/gitlab/callback/auth/bitbucket/callback
GitHub App installs are initiated from the GitHub App installation page. The GitHub callback is only used when "Request user authorization" is enabled in the app settings.
REST endpoints are replaced by Connect/GRPC handlers. Use the generated client from
pkg/gen/cloud/v1/cloudv1connect or call the procedures directly.
/cloud.v1.InstallationsService/ListInstallations
/cloud.v1.InstallationsService/GetInstallationByID
/cloud.v1.NamespacesService/ListNamespaces
/cloud.v1.NamespacesService/SyncNamespaces
/cloud.v1.NamespacesService/GetNamespaceWebhook
/cloud.v1.NamespacesService/SetNamespaceWebhook
/cloud.v1.RulesService/MatchRules
/cloud.v1.DriversService/ListDrivers
/cloud.v1.DriversService/GetDriver
/cloud.v1.DriversService/UpsertDriver
/cloud.v1.DriversService/DeleteDriver
/cloud.v1.ProvidersService/ListProviders
/cloud.v1.ProvidersService/GetProvider
/cloud.v1.ProvidersService/UpsertProvider
/cloud.v1.ProvidersService/DeleteProvider
Use the Connect RPC to get the provider URL and state.
Notes:
- GitHub webhooks are always enabled by the GitHub App and cannot be toggled.
- GitLab/Bitbucket create/delete provider webhooks when toggled.
CLI shortcuts (via Connect RPC):
githooks --endpoint http://localhost:8080 installations list --state-id <state-id>
githooks --endpoint http://localhost:8080 installations get --provider github --installation-id <id>
githooks --endpoint http://localhost:8080 namespaces list --state-id <state-id>
githooks --endpoint http://localhost:8080 namespaces sync --state-id <state-id> --provider gitlab
githooks --endpoint http://localhost:8080 namespaces webhook get --state-id <state-id> --provider gitlab --repo-id <repo-id>
githooks --endpoint http://localhost:8080 namespaces webhook set --state-id <state-id> --provider gitlab --repo-id <repo-id> --enabled
githooks --endpoint http://localhost:8080 rules match --payload-file payload.json --rules-file rules.yaml
githooks --endpoint http://localhost:8080 providers list --provider github
githooks --endpoint http://localhost:8080 providers get --provider github --key default
githooks --endpoint http://localhost:8080 providers set --provider github --key acme-prod --config-file github.json
githooks --endpoint http://localhost:8080 providers delete --provider github --key default
githooks --endpoint http://localhost:8080 drivers list
githooks --endpoint http://localhost:8080 drivers get --name amqp
githooks --endpoint http://localhost:8080 drivers set --name amqp --config-file amqp.json
githooks --endpoint http://localhost:8080 drivers delete --name amqpStart an install/authorize flow by redirecting users to:
http://localhost:8080/?provider=github
http://localhost:8080/?provider=gitlab
http://localhost:8080/?provider=bitbucket
To target a specific provider instance, pass instance=<key>:
http://localhost:8080/?provider=github&instance=acme-prod
GitHub uses the App installation URL. GitLab/Bitbucket use OAuth authorize URLs built from providers.* config.
The watermill section configures the message broker(s) to publish events to.
driver: (string) Default publisher driver.drivers: (array) Fan-out to all listed drivers by default.
Single Driver (AMQP)
watermill:
driver: amqp
amqp:
url: amqp://guest:guest@localhost:5672/
mode: durable_queue # Or: nondurable_queue, durable_pubsub, nondurable_pubsubMultiple Drivers (Fan-Out)
watermill:
drivers: [amqp, http]
amqp:
url: amqp://guest:guest@localhost:5672/
http:
mode: base_url
base_url: http://localhost:9000/hooksRiverQueue (Postgres Job Queue)
watermill:
driver: riverqueue
riverqueue:
driver: postgres
dsn: postgres://user:pass@localhost:5432/dbname?sslmode=disable
table: river_job # Optional, default is river_job
queue: default # Optional, default is default
kind: githooks.event # The job type to insertSee the Watermill documentation for details on each driver's configuration.
The rules section defines which events to publish and where. Each rule has a when condition and an emit topic.
rules_strict: false # Optional: if true, rules with missing fields won't match
rules:
# If a PR is opened and not a draft, emit to 'pr.opened.ready'
- when: action == "opened" && pull_request.draft == false
emit: pr.opened.ready
# If a PR is merged, emit to 'pr.merged' on specific drivers
- when: action == "closed" && pull_request.merged == true
emit: pr.merged
drivers: [amqp, http]
# Fan-out to multiple topics
- when: action == "closed" && pull_request.merged == true
emit: [pr.merged, audit.pr.merged]when: A boolean expression evaluated against the webhook payload.- Bare identifiers (e.g.,
action) are treated as JSONPath$.action. - You can use full JSONPath syntax (e.g.,
$.pull_request.head.ref). - Helper functions:
contains(value, needle)andlike(value, pattern)(%wildcard).
- Bare identifiers (e.g.,
emit: The topic name to publish the event to if thewhencondition is true.emitcan also be a list to publish to multiple topics.drivers: (Optional) A list of specific drivers to publish this event to. If omitted, the defaultdriverordriversfrom the Watermill config are used.
The worker SDK provides a simple way to consume events from the message broker.
Minimal Example
package main
import (
"context"
"log"
"githooks/sdk/go/worker"
)
func main() {
// Load subscriber settings from the same config file the server uses.
subCfg, err := worker.LoadSubscriberConfig("config.yaml")
if err != nil {
log.Fatalf("Failed to build subscriber: %v", err)
}
sub, err := worker.BuildSubscriber(subCfg)
if err != nil {
log.Fatalf("Failed to build subscriber: %v", err)
}
wk := worker.New(
worker.WithSubscriber(sub),
worker.WithTopics("pr.opened.ready"), // List of topics to subscribe to
worker.WithConcurrency(10),
)
// Register a handler for a specific topic
wk.HandleTopic("pr.opened.ready", func(ctx context.Context, evt *worker.Event) error {
log.Printf("Received event: %s/%s", evt.Provider, evt.Type)
// Do something with evt.Payload or evt.Normalized
return nil
})
// Run the worker (blocking call)
if err := wk.Run(context.Background()); err != nil {
log.Fatal(err)
}
}If you need provider‑aware clients inside handlers, set server.public_base_url in your worker config
or export GITHOOKS_API_BASE_URL so the worker can resolve installation tokens.
Watermill Middleware
You can use any Watermill middleware with the provided adapter.
import wmmw "github.com/ThreeDotsLabs/watermill/message/router/middleware"
retryMiddleware := worker.MiddlewareFromWatermill(
wmmw.Retry{MaxRetries: 3}.Middleware,
)
wk := worker.New(
// ... other options
worker.WithMiddleware(retryMiddleware),
)If you like this model of Git provider webhook management, you can build your own Go app by reusing the same pattern: validate provider signatures, normalize payloads, evaluate rules, then publish to a broker and consume with workers. Use the SDK to wire provider clients into handlers and keep business logic isolated from transport.
The example/ directory contains several working examples:
example/github: A simple server and worker for handling GitHub webhooks.example/realworld: A more complex setup with multiple workers consuming events from a single topic.example/riverqueue: Demonstrates publishing events to a River job queue.example/vercel: Production-style preview/production deploy hooks for Vercel.example/gitlab: Sample setup for GitLab webhooks.example/bitbucket: Sample setup for Bitbucket webhooks.example/inprocess: Single-binary server + multiple workers using GoChannel.
Helm charts for deploying the server and a generic worker are available in the charts/ directory.
Install from GitHub Pages
helm repo add githooks https://yindia.github.io/githooks
helm repo update
helm install my-githooks githooks/githooks
helm install my-worker githooks/githooks-worker- Code Releases: Tagging a commit with
vX.Y.Ztriggers a workflow that publishes a new Go module version and a container image toghcr.io/yindia/githooks. - Chart Releases: Tagging a commit with
chart-X.Y.Zpublishes the Helm charts to thegh-pagesbranch. Ensure you updateversionandappVersionincharts/*/Chart.yamlfirst.
Run Tests
go test ./...Notes
- When using the SQL publisher, you must blank-import a database driver (e.g.,
_ "github.com/lib/pq"). - The default webhook secret for local testing is
devsecret. - Rules are evaluated in the order they appear in the config file. Multiple rules can match a single event, causing multiple messages to be published.