mirror of
https://github.com/taigrr/github-to-signal.git
synced 2026-04-02 03:09:09 -07:00
feat: add event type and action filtering
Configure which events to forward via comma-separated 'events' config.
Supports event-level ('push', 'pull_request') or event:action-level
('pull_request:opened', 'issues:closed') filtering.
Empty/omitted = forward everything (backwards compatible).
This commit is contained in:
@@ -15,3 +15,18 @@ signal_recipient = ""
|
|||||||
|
|
||||||
# OR: Signal group ID for group notifications (overrides signal_recipient)
|
# OR: Signal group ID for group notifications (overrides signal_recipient)
|
||||||
# signal_group_id = ""
|
# signal_group_id = ""
|
||||||
|
|
||||||
|
# Event filter — comma-separated list of event types to forward.
|
||||||
|
# Use "event" for all actions, or "event:action" for specific actions.
|
||||||
|
# Leave empty or omit to forward everything.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# events = "pull_request:opened, pull_request:closed, issues:opened"
|
||||||
|
# events = "push, pull_request, workflow_run"
|
||||||
|
# events = "pull_request:opened"
|
||||||
|
#
|
||||||
|
# Available events: push, issues, issue_comment, pull_request,
|
||||||
|
# pull_request_review, pull_request_review_comment, release,
|
||||||
|
# star, fork, workflow_run, create, delete
|
||||||
|
#
|
||||||
|
# events = ""
|
||||||
|
|||||||
21
config.go
21
config.go
@@ -1,6 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/taigrr/jety"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/taigrr/jety"
|
||||||
|
)
|
||||||
|
|
||||||
// Config holds the application configuration.
|
// Config holds the application configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -16,6 +20,8 @@ type Config struct {
|
|||||||
SignalRecipient string
|
SignalRecipient string
|
||||||
// SignalGroupID is the Signal group ID for group notifications (overrides SignalRecipient).
|
// SignalGroupID is the Signal group ID for group notifications (overrides SignalRecipient).
|
||||||
SignalGroupID string
|
SignalGroupID string
|
||||||
|
// Events is the event filter. Empty means all events are forwarded.
|
||||||
|
Events EventFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() Config {
|
func loadConfig() Config {
|
||||||
@@ -27,6 +33,18 @@ func loadConfig() Config {
|
|||||||
jety.SetConfigType("toml")
|
jety.SetConfigType("toml")
|
||||||
_ = jety.ReadInConfig()
|
_ = jety.ReadInConfig()
|
||||||
|
|
||||||
|
// Parse events filter from comma-separated string or TOML array.
|
||||||
|
var filters []string
|
||||||
|
raw := jety.GetString("events")
|
||||||
|
if raw != "" {
|
||||||
|
for _, s := range strings.Split(raw, ",") {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s != "" {
|
||||||
|
filters = append(filters, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
ListenAddr: jety.GetString("listen_addr"),
|
ListenAddr: jety.GetString("listen_addr"),
|
||||||
WebhookSecret: jety.GetString("webhook_secret"),
|
WebhookSecret: jety.GetString("webhook_secret"),
|
||||||
@@ -34,5 +52,6 @@ func loadConfig() Config {
|
|||||||
SignalAccount: jety.GetString("signal_account"),
|
SignalAccount: jety.GetString("signal_account"),
|
||||||
SignalRecipient: jety.GetString("signal_recipient"),
|
SignalRecipient: jety.GetString("signal_recipient"),
|
||||||
SignalGroupID: jety.GetString("signal_group_id"),
|
SignalGroupID: jety.GetString("signal_group_id"),
|
||||||
|
Events: ParseEventFilter(filters),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
filter.go
Normal file
82
filter.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// EventFilter determines which event/action combinations to forward.
|
||||||
|
// An empty filter allows everything (default behavior).
|
||||||
|
type EventFilter struct {
|
||||||
|
rules map[string]map[string]bool // event -> actions (empty map = all actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseEventFilter parses a list of filter strings into an EventFilter.
|
||||||
|
// Format: "event" (all actions) or "event:action" (specific action).
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// ["pull_request:opened", "pull_request:closed"] — only PR open/close
|
||||||
|
// ["push", "issues"] — all push and issue events
|
||||||
|
// ["pull_request:opened", "workflow_run"] — PR opens + all workflow runs
|
||||||
|
// [] — everything allowed (no filtering)
|
||||||
|
func ParseEventFilter(filters []string) EventFilter {
|
||||||
|
ef := EventFilter{rules: make(map[string]map[string]bool)}
|
||||||
|
|
||||||
|
for _, f := range filters {
|
||||||
|
f = strings.TrimSpace(f)
|
||||||
|
if f == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
event, action, hasAction := strings.Cut(f, ":")
|
||||||
|
event = strings.ToLower(strings.TrimSpace(event))
|
||||||
|
if event == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := ef.rules[event]; !ok {
|
||||||
|
ef.rules[event] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAction {
|
||||||
|
action = strings.ToLower(strings.TrimSpace(action))
|
||||||
|
if action != "" {
|
||||||
|
ef.rules[event][action] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ef
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if no filters are configured (allow everything).
|
||||||
|
func (ef EventFilter) IsEmpty() bool {
|
||||||
|
return len(ef.rules) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventEnabled returns true if the event type is in the filter.
|
||||||
|
func (ef EventFilter) EventEnabled(event string) bool {
|
||||||
|
if ef.IsEmpty() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, ok := ef.rules[strings.ToLower(event)]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowed returns true if the event/action combination should be forwarded.
|
||||||
|
func (ef EventFilter) Allowed(event, action string) bool {
|
||||||
|
if ef.IsEmpty() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
event = strings.ToLower(event)
|
||||||
|
actions, ok := ef.rules[event]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// No specific actions configured = all actions allowed
|
||||||
|
if len(actions) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions[strings.ToLower(action)]
|
||||||
|
}
|
||||||
32
filter_test.go
Normal file
32
filter_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEventFilter(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
filters []string
|
||||||
|
event string
|
||||||
|
action string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"empty allows all", nil, "push", "", true},
|
||||||
|
{"event only", []string{"push"}, "push", "", true},
|
||||||
|
{"event only blocks other", []string{"push"}, "issues", "opened", false},
|
||||||
|
{"event:action match", []string{"pull_request:opened"}, "pull_request", "opened", true},
|
||||||
|
{"event:action no match", []string{"pull_request:opened"}, "pull_request", "closed", false},
|
||||||
|
{"multiple actions", []string{"pull_request:opened", "pull_request:closed"}, "pull_request", "closed", true},
|
||||||
|
{"mixed event and action", []string{"push", "pull_request:opened"}, "push", "", true},
|
||||||
|
{"mixed blocks filtered", []string{"push", "pull_request:opened"}, "pull_request", "closed", false},
|
||||||
|
{"case insensitive", []string{"Pull_Request:Opened"}, "pull_request", "opened", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ef := ParseEventFilter(tt.filters)
|
||||||
|
if got := ef.Allowed(tt.event, tt.action); got != tt.want {
|
||||||
|
t.Errorf("Allowed(%q, %q) = %v, want %v", tt.event, tt.action, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
97
main.go
97
main.go
@@ -26,25 +26,57 @@ func main() {
|
|||||||
signal := signalcli.NewClient(cfg.SignalURL, cfg.SignalAccount)
|
signal := signalcli.NewClient(cfg.SignalURL, cfg.SignalAccount)
|
||||||
handle := githubevents.New(cfg.WebhookSecret)
|
handle := githubevents.New(cfg.WebhookSecret)
|
||||||
|
|
||||||
notifier := ¬ifier{
|
n := ¬ifier{
|
||||||
signal: signal,
|
signal: signal,
|
||||||
recipient: cfg.SignalRecipient,
|
recipient: cfg.SignalRecipient,
|
||||||
groupID: cfg.SignalGroupID,
|
groupID: cfg.SignalGroupID,
|
||||||
|
filter: cfg.Events,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register event handlers.
|
// Register event handlers (only for enabled events).
|
||||||
handle.OnPushEventAny(notifier.onPush)
|
f := cfg.Events
|
||||||
handle.OnIssuesEventAny(notifier.onIssue)
|
if f.EventEnabled("push") {
|
||||||
handle.OnIssueCommentEventAny(notifier.onIssueComment)
|
handle.OnPushEventAny(n.onPush)
|
||||||
handle.OnPullRequestEventAny(notifier.onPR)
|
}
|
||||||
handle.OnPullRequestReviewEventAny(notifier.onPRReview)
|
if f.EventEnabled("issues") {
|
||||||
handle.OnPullRequestReviewCommentEventAny(notifier.onPRReviewComment)
|
handle.OnIssuesEventAny(n.onIssue)
|
||||||
handle.OnReleaseEventAny(notifier.onRelease)
|
}
|
||||||
handle.OnStarEventAny(notifier.onStar)
|
if f.EventEnabled("issue_comment") {
|
||||||
handle.OnForkEventAny(notifier.onFork)
|
handle.OnIssueCommentEventAny(n.onIssueComment)
|
||||||
handle.OnWorkflowRunEventAny(notifier.onWorkflowRun)
|
}
|
||||||
handle.OnCreateEventAny(notifier.onCreate)
|
if f.EventEnabled("pull_request") {
|
||||||
handle.OnDeleteEventAny(notifier.onDelete)
|
handle.OnPullRequestEventAny(n.onPR)
|
||||||
|
}
|
||||||
|
if f.EventEnabled("pull_request_review") {
|
||||||
|
handle.OnPullRequestReviewEventAny(n.onPRReview)
|
||||||
|
}
|
||||||
|
if f.EventEnabled("pull_request_review_comment") {
|
||||||
|
handle.OnPullRequestReviewCommentEventAny(n.onPRReviewComment)
|
||||||
|
}
|
||||||
|
if f.EventEnabled("release") {
|
||||||
|
handle.OnReleaseEventAny(n.onRelease)
|
||||||
|
}
|
||||||
|
if f.EventEnabled("star") {
|
||||||
|
handle.OnStarEventAny(n.onStar)
|
||||||
|
}
|
||||||
|
if f.EventEnabled("fork") {
|
||||||
|
handle.OnForkEventAny(n.onFork)
|
||||||
|
}
|
||||||
|
if f.EventEnabled("workflow_run") {
|
||||||
|
handle.OnWorkflowRunEventAny(n.onWorkflowRun)
|
||||||
|
}
|
||||||
|
if f.EventEnabled("create") {
|
||||||
|
handle.OnCreateEventAny(n.onCreate)
|
||||||
|
}
|
||||||
|
if f.EventEnabled("delete") {
|
||||||
|
handle.OnDeleteEventAny(n.onDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.IsEmpty() {
|
||||||
|
log.Println("event filter: all events enabled")
|
||||||
|
} else {
|
||||||
|
log.Printf("event filter: %v", f.rules)
|
||||||
|
}
|
||||||
|
|
||||||
handle.OnError(func(_ context.Context, _ string, _ string, _ interface{}, err error) error {
|
handle.OnError(func(_ context.Context, _ string, _ string, _ interface{}, err error) error {
|
||||||
log.Printf("webhook error: %v", err)
|
log.Printf("webhook error: %v", err)
|
||||||
@@ -75,6 +107,7 @@ type notifier struct {
|
|||||||
signal *signalcli.Client
|
signal *signalcli.Client
|
||||||
recipient string
|
recipient string
|
||||||
groupID string
|
groupID string
|
||||||
|
filter EventFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) send(ctx context.Context, msg string) {
|
func (n *notifier) send(ctx context.Context, msg string) {
|
||||||
@@ -94,61 +127,97 @@ func (n *notifier) send(ctx context.Context, msg string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onPush(ctx context.Context, _ string, _ string, event *github.PushEvent) error {
|
func (n *notifier) onPush(ctx context.Context, _ string, _ string, event *github.PushEvent) error {
|
||||||
|
if !n.filter.Allowed("push", "") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatPush(event))
|
n.send(ctx, formatPush(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onIssue(ctx context.Context, _ string, _ string, event *github.IssuesEvent) error {
|
func (n *notifier) onIssue(ctx context.Context, _ string, _ string, event *github.IssuesEvent) error {
|
||||||
|
if !n.filter.Allowed("issues", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatIssue(event))
|
n.send(ctx, formatIssue(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onIssueComment(ctx context.Context, _ string, _ string, event *github.IssueCommentEvent) error {
|
func (n *notifier) onIssueComment(ctx context.Context, _ string, _ string, event *github.IssueCommentEvent) error {
|
||||||
|
if !n.filter.Allowed("issue_comment", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatIssueComment(event))
|
n.send(ctx, formatIssueComment(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onPR(ctx context.Context, _ string, _ string, event *github.PullRequestEvent) error {
|
func (n *notifier) onPR(ctx context.Context, _ string, _ string, event *github.PullRequestEvent) error {
|
||||||
|
if !n.filter.Allowed("pull_request", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatPR(event))
|
n.send(ctx, formatPR(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onPRReview(ctx context.Context, _ string, _ string, event *github.PullRequestReviewEvent) error {
|
func (n *notifier) onPRReview(ctx context.Context, _ string, _ string, event *github.PullRequestReviewEvent) error {
|
||||||
|
if !n.filter.Allowed("pull_request_review", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatPRReview(event))
|
n.send(ctx, formatPRReview(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onPRReviewComment(ctx context.Context, _ string, _ string, event *github.PullRequestReviewCommentEvent) error {
|
func (n *notifier) onPRReviewComment(ctx context.Context, _ string, _ string, event *github.PullRequestReviewCommentEvent) error {
|
||||||
|
if !n.filter.Allowed("pull_request_review_comment", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatPRReviewComment(event))
|
n.send(ctx, formatPRReviewComment(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onRelease(ctx context.Context, _ string, _ string, event *github.ReleaseEvent) error {
|
func (n *notifier) onRelease(ctx context.Context, _ string, _ string, event *github.ReleaseEvent) error {
|
||||||
|
if !n.filter.Allowed("release", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatRelease(event))
|
n.send(ctx, formatRelease(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onStar(ctx context.Context, _ string, _ string, event *github.StarEvent) error {
|
func (n *notifier) onStar(ctx context.Context, _ string, _ string, event *github.StarEvent) error {
|
||||||
|
if !n.filter.Allowed("star", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatStar(event))
|
n.send(ctx, formatStar(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onFork(ctx context.Context, _ string, _ string, event *github.ForkEvent) error {
|
func (n *notifier) onFork(ctx context.Context, _ string, _ string, event *github.ForkEvent) error {
|
||||||
|
if !n.filter.Allowed("fork", "") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatFork(event))
|
n.send(ctx, formatFork(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onWorkflowRun(ctx context.Context, _ string, _ string, event *github.WorkflowRunEvent) error {
|
func (n *notifier) onWorkflowRun(ctx context.Context, _ string, _ string, event *github.WorkflowRunEvent) error {
|
||||||
|
if !n.filter.Allowed("workflow_run", event.GetAction()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatWorkflowRun(event))
|
n.send(ctx, formatWorkflowRun(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onCreate(ctx context.Context, _ string, _ string, event *github.CreateEvent) error {
|
func (n *notifier) onCreate(ctx context.Context, _ string, _ string, event *github.CreateEvent) error {
|
||||||
|
if !n.filter.Allowed("create", "") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatCreate(event))
|
n.send(ctx, formatCreate(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onDelete(ctx context.Context, _ string, _ string, event *github.DeleteEvent) error {
|
func (n *notifier) onDelete(ctx context.Context, _ string, _ string, event *github.DeleteEvent) error {
|
||||||
|
if !n.filter.Allowed("delete", "") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
n.send(ctx, formatDelete(event))
|
n.send(ctx, formatDelete(event))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user