feat(triggers): add schedule trigger kind + internal scheduler
Build / build (push) Successful in 10m42s
Build / build (push) Successful in 10m42s
Fourth trigger kind alongside registry/git/manual. Recurring time-interval fires driven by a new internal/scheduler tick loop (default 30s, clamped to 5m). Goes through the same webhook.Handler.FanOutForTrigger seam as inbound HTTP webhooks, so per-binding concurrency, outcome accounting, and config-merge semantics are identical. Schema: triggers.last_fired_at TEXT column (additive ALTER for existing DBs). Scheduler persists last_fired_at BEFORE dispatch so a panicking Match cannot wedge a tight loop; failed deploys wait one full interval before retry — correct trade-off for a periodic refresh trigger. Frontend: TriggerKindForm + /triggers/new + /triggers/[id] gain the schedule kind (4-col card grid, preset chips Hourly/Daily/Weekly, custom interval input matched to Go time.ParseDuration syntax, optional pinned reference). /triggers/[id] surfaces "last fired" on schedule rows. EN+RU i18n in parity. Review fixes from go-reviewer / security-reviewer / typescript-reviewer: - Scheduler Start/Stop wrapped in sync.Once (no goroutine leak / double- cancel panic on shutdown re-entry). - shouldFire rejects sub-MinInterval as defense-in-depth against hand-inserted rows that bypassed Validate. - fire() asserts trigger Kind=="schedule" before dispatching. - Aligned isValidInterval regex across all three frontend sites; reject the unsupported "d" unit (Go time.ParseDuration doesn't accept it). - formatLastFired falls back to lastFiredNever on malformed timestamps rather than leaking raw bytes into the UI. - main.go scheduler closure logs per-fire deployed/errored counts.
This commit is contained in:
@@ -32,10 +32,12 @@ import (
|
||||
"github.com/alexei/tinyforge/internal/npm"
|
||||
"github.com/alexei/tinyforge/internal/proxy"
|
||||
"github.com/alexei/tinyforge/internal/reconciler"
|
||||
"github.com/alexei/tinyforge/internal/scheduler"
|
||||
"github.com/alexei/tinyforge/internal/stale"
|
||||
"github.com/alexei/tinyforge/internal/stats"
|
||||
"github.com/alexei/tinyforge/internal/store"
|
||||
"github.com/alexei/tinyforge/internal/webhook"
|
||||
"github.com/alexei/tinyforge/internal/workload/plugin"
|
||||
|
||||
// Plugin registrations: each blank-import runs its init() and registers
|
||||
// itself with internal/workload/plugin. Adding a new Source or Trigger
|
||||
@@ -46,6 +48,7 @@ import (
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/trigger/git"
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/trigger/manual"
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/trigger/registry"
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/trigger/schedule"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -178,6 +181,36 @@ func main() {
|
||||
webhookHandler := webhook.NewHandler(db)
|
||||
webhookHandler.SetPluginDispatcher(dep)
|
||||
|
||||
// Scheduler ticks every 30s and dispatches "schedule"-kind triggers
|
||||
// through the same FanOutForTrigger path as the inbound webhook. Boot
|
||||
// runs one sweep immediately so a daily schedule does not idle 24h
|
||||
// after a restart before catching up.
|
||||
sched := scheduler.New(db, func(ctx context.Context, trg store.Trigger, evt plugin.InboundEvent) error {
|
||||
results, err := webhookHandler.FanOutForTrigger(ctx, trg, evt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Log per-fire summary so a schedule that quietly fails on N
|
||||
// of M bindings is visible without parsing per-binding rows.
|
||||
var deployed, errored int
|
||||
for _, r := range results {
|
||||
switch {
|
||||
case r.Deployed:
|
||||
deployed++
|
||||
case r.Reason == "binding disabled", r.Reason == "no match":
|
||||
// not a failure — silent
|
||||
default:
|
||||
errored++
|
||||
}
|
||||
}
|
||||
slog.Info("scheduler dispatch summary",
|
||||
"trigger", trg.Name, "bindings", len(results),
|
||||
"deployed", deployed, "errored", errored)
|
||||
return nil
|
||||
}, 30*time.Second)
|
||||
sched.Start(context.Background())
|
||||
defer sched.Stop()
|
||||
|
||||
// Initialize stale container scanner.
|
||||
staleScanner := stale.New(db, dockerClient, eventBus)
|
||||
if err := staleScanner.Start("1h"); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user