Adding a New Notifier
Adding a New Notifier
This guide walks you through adding a new notification backend to Update-Watcher. A notifier delivers update check results to a specific service (chat platform, push service, webhook, etc.).
Overview
Adding a new notifier involves these steps:
- Create a new package under
notifier/ - Implement the
Notifierinterface - Register the notifier and its metadata in
init() - Add a blank import in
runner/runner.go - (Optional) Add to the setup wizard
Step 1: Create the Package
Create a new directory under notifier/:
- myservice.go
- myservice_test.go
Step 2: Implement the Notifier Interface
Your notifier must implement the Notifier interface:
notifier/notifier.go
type Notifier interface {
Name() string
Send(ctx context.Context, hostname string, results []*checker.CheckResult) error
}Here is a real-world example based on the ntfy notifier:
notifier/myservice/myservice.go
package myservice
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/mahype/update-watcher/checker"
"github.com/mahype/update-watcher/config"
"github.com/mahype/update-watcher/notifier"
"github.com/mahype/update-watcher/notifier/formatting"
)
func init() {
notifier.Register("myservice", NewFromConfig)
notifier.RegisterMeta(notifier.NotifierMeta{
Type: "myservice",
DisplayName: "My Service",
Description: "Send notifications via My Service",
})
}
// MyServiceNotifier sends update reports to My Service.
type MyServiceNotifier struct {
webhookURL string
httpClient *http.Client
}
// NewFromConfig creates a MyServiceNotifier from a notifier configuration.
func NewFromConfig(cfg config.NotifierConfig) (notifier.Notifier, error) {
url := cfg.Options.GetString("webhook_url", "")
if url == "" {
return nil, fmt.Errorf("myservice: webhook_url is required")
}
return &MyServiceNotifier{
webhookURL: url,
httpClient: &http.Client{Timeout: 30 * time.Second},
}, nil
}
func (n *MyServiceNotifier) Name() string { return "myservice" }
func (n *MyServiceNotifier) Send(ctx context.Context, hostname string, results []*checker.CheckResult) error {
// Use the shared formatting helpers
title, body := formatting.BuildMarkdownMessage(hostname, results, formatting.DefaultOptions())
// Send to your service
req, err := http.NewRequest("POST", n.webhookURL, strings.NewReader(body))
if err != nil {
return fmt.Errorf("myservice: %w", err)
}
req.Header.Set("Content-Type", "text/plain")
resp, err := n.httpClient.Do(req)
if err != nil {
return fmt.Errorf("myservice: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("myservice: server returned %d", resp.StatusCode)
}
return nil
}Key points:
init()registers both the factory (notifier.Register) and display metadata (notifier.RegisterMeta). The metadata is used by the setup wizard.NewFromConfigreads options fromcfg.Optionsusing typed accessors. Validate required options and return errors for missing ones.Send()receives the hostname and all check results. Use theformattingpackage to build messages.
Formatting helpers available in notifier/formatting/:
- Use
formatting.BuildMarkdownMessage()for Markdown-formatted messages. - Use
formatting.BuildPlainTextMessage()for plain text. - Use
formatting.SummarizeResults()to get aggregate counts (total updates, security count).
Step 3: Message Formatting
The notifier/formatting/ package provides shared helpers:
notifier/formatting/formatting.go
// Build a Markdown-formatted message
title, body := formatting.BuildMarkdownMessage(hostname, results, formatting.DefaultOptions())
// Build a plain text message
title, body := formatting.BuildPlainTextMessage(hostname, results)
// Get aggregate stats
summary := formatting.SummarizeResults(results)
// summary.TotalUpdates, summary.SecurityCount, summary.CheckerCount💡
For services that require structured data (like Slack Block Kit or Teams Adaptive Cards), build the payload manually using the
results data directly.Step 4: Register in runner.go
Add a blank import in runner/runner.go:
runner/runner.go
import (
// ... existing imports
_ "github.com/mahype/update-watcher/notifier/myservice"
)Step 5: Test Your Notifier
Create notifier/myservice/myservice_test.go:
notifier/myservice/myservice_test.go
package myservice
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/mahype/update-watcher/checker"
)
func TestSend(t *testing.T) {
// Mock HTTP server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
n := &MyServiceNotifier{
webhookURL: server.URL,
httpClient: &http.Client{Timeout: 5 * time.Second},
}
results := []*checker.CheckResult{
{
CheckerName: "apt",
Updates: []checker.Update{
{Name: "openssl", CurrentVersion: "3.0.1", NewVersion: "3.0.2", Type: checker.UpdateTypeSecurity},
},
Summary: "1 package (1 security)",
CheckedAt: time.Now(),
},
}
err := n.Send(context.Background(), "test-server", results)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}Add to Config Example
Update config.example.yaml with your new notifier:
config.yaml
notifiers:
# Secrets: webhook_url
- type: myservice
enabled: false
options:
webhook_url: "${MYSERVICE_WEBHOOK_URL}"Checklist
Before submitting a PR for a new notifier:
- Implements
Notifierinterface (Name(),Send()) - Registered via
init()withnotifier.Register()andnotifier.RegisterMeta() - Blank import added to
runner/runner.go - Uses
formattingpackage for message building - Validates required options in
NewFromConfig - Secrets use
${ENV_VAR}references in config example - Tests written and passing (
make test) - Code formatted (
make fmt) and linted (make lint) - Documentation updated (notifier options in README)