Files
ghtracker/main.go
T
lukaszraczylo c79ecb728f feat(web): add automatic actions for open pull requests
A new pr_open alert carries the head commit SHA of each open, non-draft, same-repository pull request. An action with auto enabled sends its webhook once for each pull request and head commit. An authors list limits which pull requests trigger it. The alert never changes repository health.
2026-10-05 00:41:08 +01:00

110 lines
3.0 KiB
Go

// Command ghtracker serves a dashboard and Prometheus metrics for a list of GitHub repositories.
package main
import (
"context"
"embed"
"errors"
"flag"
"fmt"
"io/fs"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/lukaszraczylo/ghtracker/internal/collector"
"github.com/lukaszraczylo/ghtracker/internal/config"
"github.com/lukaszraczylo/ghtracker/internal/gh"
"github.com/lukaszraczylo/ghtracker/internal/metrics"
"github.com/lukaszraczylo/ghtracker/internal/web"
)
// distFS holds the built UI; dist/.gitkeep keeps the pattern valid before the first UI build.
//
//go:embed all:dist
var distFS embed.FS
// version is set at release time with -ldflags "-X main.version=...".
var version = "dev"
const (
httpTimeout = 30 * time.Second
shutdownTimeout = 10 * time.Second
headerTimeout = 10 * time.Second
)
func main() {
cfgPath := flag.String("config", "config.yaml", "path to the YAML config file")
showVersion := flag.Bool("version", false, "print the version and exit")
flag.Parse()
if *showVersion {
fmt.Println(version)
return
}
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, *cfgPath, log); err != nil {
stop()
log.Error("fatal", "err", err)
os.Exit(1)
}
}
func run(ctx context.Context, cfgPath string, log *slog.Logger) error {
cfg, err := config.Load(cfgPath)
if err != nil {
return err
}
hc := &http.Client{Timeout: httpTimeout}
auth, err := gh.NewAppAuth(cfg.GitHub.AppID, cfg.GitHub.PrivateKeyPEM, cfg.GitHub.APIURL, cfg.GitHub.InstallationID, hc)
if err != nil {
return err
}
client := gh.NewClient(cfg.GitHub.APIURL, auth, hc, cfg.MaxItems)
coll := collector.New(client, cfg, log)
reg := prometheus.NewRegistry()
reg.MustRegister(collectors.NewGoCollector(), collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
metrics.New(coll, client.RateLimitRemaining))
ui, err := fs.Sub(distFS, "dist")
if err != nil {
return err
}
srv := web.New(coll, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), ui, *cfg.ManualRefresh, log)
if len(cfg.Actions) > 0 {
actions := web.NewActions(cfg.Actions, log, reg)
srv.WithActions(actions)
if auto := web.NewAutoRunner(actions); auto != nil {
coll.OnRefresh(auto.Run)
}
}
go coll.Run(ctx)
hs := &http.Server{Addr: cfg.Listen, Handler: srv.Handler(), ReadHeaderTimeout: headerTimeout}
errc := make(chan error, 1)
go func() { errc <- hs.ListenAndServe() }()
log.Info("listening", "version", version, "addr", cfg.Listen, "repos", len(cfg.Repos), "refresh", cfg.RefreshInterval)
select {
case err := <-errc:
return err
case <-ctx.Done():
}
sctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := hs.Shutdown(sctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}