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:
@@ -1083,7 +1083,9 @@
|
||||
"rotateConfirm": "Сменить",
|
||||
"unbindTitle": "Отвязать нагрузку?",
|
||||
"unbindMessage": "Нагрузка «{name}» перестанет передеплоиваться при срабатывании этого триггера. Сама нагрузка не удаляется.",
|
||||
"unbindConfirm": "Отвязать"
|
||||
"unbindConfirm": "Отвязать",
|
||||
"lastFired": "Последний запуск",
|
||||
"lastFiredNever": "Ни разу не срабатывал"
|
||||
},
|
||||
"form": {
|
||||
"kindLabel": "Вид",
|
||||
@@ -1108,6 +1110,18 @@
|
||||
"branchPlaceholder": "main",
|
||||
"branchHint": "Только push'и, продвигающие эту ветку, дёргают триггер.",
|
||||
"manualNote": "У ручных триггеров нет конфига. Они срабатывают только через кнопку Deploy на странице нагрузки или POST /workloads/{id}/deploy.",
|
||||
"scheduleNote": "Срабатывает по фиксированному интервалу, который ведёт внутренний планировщик Tinyforge. Внешний webhook не нужен — включите его ниже только если CI тоже должен запускать триггер вручную.",
|
||||
"intervalPresets": "Быстрые пресеты",
|
||||
"intervalPreset": {
|
||||
"hourly": "Каждый час",
|
||||
"daily": "Каждый день",
|
||||
"weekly": "Каждую неделю"
|
||||
},
|
||||
"interval": "Интервал",
|
||||
"intervalHint": "Длительность в формате Go (например «30m», «6h», «24h», «168h»). Минимум 1 минута.",
|
||||
"scheduleReference": "Фиксированная ссылка (опционально)",
|
||||
"scheduleReferencePlaceholder": "stable",
|
||||
"scheduleReferenceHint": "Опциональный тег, ветка или ревизия, которые источник будет подтягивать на каждом срабатывании. Оставьте пустым, чтобы использовать значение по умолчанию.",
|
||||
"unknownNote": "У этого вида ещё нет встроенной формы. Используйте JSON-редактор ниже; сервер валидирует форму.",
|
||||
"advancedToggle": "Расширенный JSON",
|
||||
"advancedHint": "Запасной вариант для опытных пользователей — заменяет структурированную форму сырым payload'ом.",
|
||||
|
||||
Reference in New Issue
Block a user