Files
github-to-signal/main.go
Tai Groot b73aeaed91 feat: initial github-to-signal webhook server
HTTP server that receives GitHub webhook events and sends formatted
notifications to Signal via signal-cli's JSON-RPC API.

Supported events: push, issues, issue comments, pull requests,
PR reviews, PR review comments, releases, stars, forks, workflow
runs, branch/tag creation, branch/tag deletion.

Uses cbrgm/githubevents for webhook handling and taigrr/signalcli
for Signal delivery. Config via TOML file or GH2SIG_ env vars
(powered by taigrr/jety).
2026-03-10 23:25:54 +00:00

150 lines
4.3 KiB
Go

// github-to-signal is an HTTP server that receives GitHub webhook events
// and forwards formatted notifications to Signal via signal-cli's JSON-RPC API.
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/cbrgm/githubevents/v2/githubevents"
"github.com/google/go-github/v70/github"
"github.com/taigrr/signalcli"
)
func main() {
cfg := loadConfig()
if cfg.SignalAccount == "" {
log.Fatal("signal_account is required (set GH2SIG_SIGNAL_ACCOUNT or config.toml)")
}
if cfg.SignalRecipient == "" {
log.Fatal("signal_recipient is required (set GH2SIG_SIGNAL_RECIPIENT or config.toml)")
}
signal := signalcli.NewClient(cfg.SignalURL, cfg.SignalAccount)
handle := githubevents.New(cfg.WebhookSecret)
notifier := &notifier{
signal: signal,
recipient: cfg.SignalRecipient,
}
// Register event handlers.
handle.OnPushEventAny(notifier.onPush)
handle.OnIssuesEventAny(notifier.onIssue)
handle.OnIssueCommentEventAny(notifier.onIssueComment)
handle.OnPullRequestEventAny(notifier.onPR)
handle.OnPullRequestReviewEventAny(notifier.onPRReview)
handle.OnPullRequestReviewCommentEventAny(notifier.onPRReviewComment)
handle.OnReleaseEventAny(notifier.onRelease)
handle.OnStarEventAny(notifier.onStar)
handle.OnForkEventAny(notifier.onFork)
handle.OnWorkflowRunEventAny(notifier.onWorkflowRun)
handle.OnCreateEventAny(notifier.onCreate)
handle.OnDeleteEventAny(notifier.onDelete)
handle.OnError(func(_ context.Context, _ string, _ string, _ interface{}, err error) error {
log.Printf("webhook error: %v", err)
return nil
})
mux := http.NewServeMux()
mux.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
if err := handle.HandleEventRequest(r); err != nil {
log.Printf("handle event: %v", err)
http.Error(w, "webhook processing failed", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /health", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
log.Printf("listening on %s", cfg.ListenAddr)
if err := http.ListenAndServe(cfg.ListenAddr, mux); err != nil {
log.Fatal(err)
}
}
type notifier struct {
signal *signalcli.Client
recipient string
}
func (n *notifier) send(ctx context.Context, msg string) {
if msg == "" {
return
}
_, err := n.signal.Send(ctx, signalcli.SendParams{
Recipient: n.recipient,
Message: msg,
})
if err != nil {
log.Printf("signal send error: %v", err)
}
}
func (n *notifier) onPush(ctx context.Context, _ string, _ string, event *github.PushEvent) error {
n.send(ctx, formatPush(event))
return nil
}
func (n *notifier) onIssue(ctx context.Context, _ string, _ string, event *github.IssuesEvent) error {
n.send(ctx, formatIssue(event))
return nil
}
func (n *notifier) onIssueComment(ctx context.Context, _ string, _ string, event *github.IssueCommentEvent) error {
n.send(ctx, formatIssueComment(event))
return nil
}
func (n *notifier) onPR(ctx context.Context, _ string, _ string, event *github.PullRequestEvent) error {
n.send(ctx, formatPR(event))
return nil
}
func (n *notifier) onPRReview(ctx context.Context, _ string, _ string, event *github.PullRequestReviewEvent) error {
n.send(ctx, formatPRReview(event))
return nil
}
func (n *notifier) onPRReviewComment(ctx context.Context, _ string, _ string, event *github.PullRequestReviewCommentEvent) error {
n.send(ctx, formatPRReviewComment(event))
return nil
}
func (n *notifier) onRelease(ctx context.Context, _ string, _ string, event *github.ReleaseEvent) error {
n.send(ctx, formatRelease(event))
return nil
}
func (n *notifier) onStar(ctx context.Context, _ string, _ string, event *github.StarEvent) error {
n.send(ctx, formatStar(event))
return nil
}
func (n *notifier) onFork(ctx context.Context, _ string, _ string, event *github.ForkEvent) error {
n.send(ctx, formatFork(event))
return nil
}
func (n *notifier) onWorkflowRun(ctx context.Context, _ string, _ string, event *github.WorkflowRunEvent) error {
n.send(ctx, formatWorkflowRun(event))
return nil
}
func (n *notifier) onCreate(ctx context.Context, _ string, _ string, event *github.CreateEvent) error {
n.send(ctx, formatCreate(event))
return nil
}
func (n *notifier) onDelete(ctx context.Context, _ string, _ string, event *github.DeleteEvent) error {
n.send(ctx, formatDelete(event))
return nil
}