Architecture Overview

Architecture Overview

Architecture Overview

Update-Watcher follows a modular plugin architecture. Checkers and notifiers register themselves at startup, and a central runner orchestrates parallel execution.

Project Structure

    • main.go
      • root.go
      • run.go
      • setup.go
      • watch_*.go
      • checker.go
      • registry.go
            • notifier.go
            • registry.go
                  • runner.go
                  • config.go
                  • wizard.go
                  • terminal.go
                  • table.go
                  • cron.go
                                • install.sh
                                • uninstall.sh
                            • Key Design Patterns

                              Registry Pattern

                              Checkers and notifiers use a registry pattern with factory functions. Each implementation registers itself during package initialization via init().

                              Checker registry (checker/registry.go):

                              checker/registry.go
                              // FactoryFunc creates a Checker from a watcher configuration.
                              type FactoryFunc func(cfg config.WatcherConfig) (Checker, error)
                              
                              var registry = map[string]FactoryFunc{}
                              
                              // Register adds a checker factory to the global registry.
                              func Register(name string, factory FactoryFunc) {
                                  registry[name] = factory
                              }

                              Notifier registry (notifier/registry.go):

                              notifier/registry.go
                              // Register adds a notifier factory to the global registry.
                              func Register(name string, factory FactoryFunc)
                              
                              // RegisterMeta adds display metadata for the setup wizard.
                              func RegisterMeta(meta NotifierMeta)

                              Blank Imports for Registration

                              The runner imports all checker and notifier packages as blank imports to trigger their init() functions.
                              runner/runner.go
                              // runner/runner.go
                              import (
                                  _ "github.com/mahype/update-watcher/checker/apt"
                                  _ "github.com/mahype/update-watcher/checker/docker"
                                  // ... all other checker packages
                              
                                  _ "github.com/mahype/update-watcher/notifier/slack"
                                  _ "github.com/mahype/update-watcher/notifier/discord"
                                  // ... all other notifier packages
                              )

                              Interface-Based Design

                              Both checkers and notifiers are defined by simple interfaces, allowing new implementations to be added without modifying existing code.
                              checker/checker.go
                              // checker/checker.go
                              type Checker interface {
                                  Name() string
                                  Check(ctx context.Context) (*CheckResult, error)
                              }
                              notifier/notifier.go
                              // notifier/notifier.go
                              type Notifier interface {
                                  Name() string
                                  Send(ctx context.Context, hostname string, results []*checker.CheckResult) error
                              }

                              Functional Options

                              The runner uses the functional options pattern for configuration:

                              runner/runner.go
                              runner.New(cfg, runner.WithNotify(&notify), runner.WithOnly("apt"))

                              Data Flow

                              The execution flow during update-watcher run:

                              1. cmd/run.go
                                 ├── config.Load()           # Read YAML, resolve ${ENV_VAR} references
                                 └── runner.New(cfg).Run()
                                     ├── For each enabled watcher (parallel):
                                     │   ├── checker.Create(type, cfg)   # Registry lookup + factory
                                     │   └── checker.Check(ctx)          # Execute check
                                     ├── Self-update check               # Query GitHub Releases API
                                     ├── Aggregate results               # Count updates, detect security
                                     └── Notify (sequential):
                                         ├── Apply send_policy           # Skip if no updates + only-on-updates
                                         └── For each enabled notifier:
                                             ├── notifier.Create(type, cfg)
                                             └── notifier.Send(ctx, hostname, results)

                              Key points:

                              • Checkers run in parallel using sync.WaitGroup with a shared mutex for results
                              • Notifiers run sequentially after all checkers complete
                              • The self-update check always runs (unless --only filters to a specific checker)
                              • Errors from individual checkers do not abort other checkers

                              Core Types

                              CheckResult

                              checker/checker.go
                              type CheckResult struct {
                                  CheckerName string    `json:"checker_name"`
                                  Updates     []Update  `json:"updates"`
                                  Summary     string    `json:"summary"`
                                  CheckedAt   time.Time `json:"checked_at"`
                                  Error       string    `json:"error,omitempty"`
                                  Notes       []string  `json:"notes,omitempty"`
                              }

                              Update

                              checker/checker.go
                              type Update struct {
                                  Name           string `json:"name"`
                                  CurrentVersion string `json:"current_version"`
                                  NewVersion     string `json:"new_version"`
                                  Type           string `json:"type"`      // security, regular, plugin, theme, core, image, distro
                                  Priority       string `json:"priority"`  // critical, high, normal, low
                                  Source         string `json:"source,omitempty"`
                                  Phasing        string `json:"phasing,omitempty"`
                              }

                              RunResult

                              runner/runner.go
                              type RunResult struct {
                                  Results      []*checker.CheckResult
                                  TotalUpdates int
                                  HasSecurity  bool
                                  Errors       []error
                              }

                              Dependencies

                              PackagePurpose
                              cobraCLI framework
                              viperConfiguration management
                              charmbracelet/huhInteractive TUI forms
                              charmbracelet/lipglossTerminal styling
                              goreleaserRelease automation