mirror of
https://github.com/tiennm99/claude-code-routine-cron.git
synced 2026-05-20 14:23:49 +00:00
105 lines
2.5 KiB
Go
105 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/robfig/cron/v3"
|
|
)
|
|
|
|
const defaultTextTemplate = "Scheduled trigger at {{.LocalTime}}"
|
|
|
|
// Config is the validated runtime configuration assembled from env vars.
|
|
type Config struct {
|
|
FireURL string
|
|
Token string
|
|
Schedules []string
|
|
Location *time.Location
|
|
Template *template.Template
|
|
LogLevel string
|
|
}
|
|
|
|
// Load reads env vars and returns a fully validated Config or an error.
|
|
// All required vars must be present and all parsed values must pass validation.
|
|
func Load() (*Config, error) {
|
|
cfg := &Config{}
|
|
|
|
cfg.FireURL = strings.TrimSpace(os.Getenv("ROUTINE_FIRE_URL"))
|
|
if cfg.FireURL == "" {
|
|
return nil, errors.New("missing required env var ROUTINE_FIRE_URL")
|
|
}
|
|
|
|
cfg.Token = strings.TrimSpace(os.Getenv("ROUTINE_FIRE_TOKEN"))
|
|
if cfg.Token == "" {
|
|
return nil, errors.New("missing required env var ROUTINE_FIRE_TOKEN")
|
|
}
|
|
|
|
rawCron := os.Getenv("CRON_SCHEDULE")
|
|
schedules, err := parseSchedules(rawCron)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.Schedules = schedules
|
|
|
|
tz := strings.TrimSpace(os.Getenv("TZ"))
|
|
if tz == "" {
|
|
cfg.Location = time.UTC
|
|
} else {
|
|
loc, err := time.LoadLocation(tz)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid TZ %q: %w", tz, err)
|
|
}
|
|
cfg.Location = loc
|
|
}
|
|
|
|
rawTpl := os.Getenv("TEXT_TEMPLATE")
|
|
if rawTpl == "" {
|
|
rawTpl = defaultTextTemplate
|
|
}
|
|
tpl, err := template.New("text").Parse(rawTpl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid TEXT_TEMPLATE: %w", err)
|
|
}
|
|
cfg.Template = tpl
|
|
|
|
cfg.LogLevel = strings.TrimSpace(os.Getenv("LOG_LEVEL"))
|
|
if cfg.LogLevel == "" {
|
|
cfg.LogLevel = "info"
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
var scheduleSeparators = regexp.MustCompile(`[;\n]+`)
|
|
|
|
// parseSchedules splits CRON_SCHEDULE on `;` or newlines, trims, drops empties,
|
|
// and validates each expression against the standard 5-field parser.
|
|
func parseSchedules(raw string) ([]string, error) {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return nil, errors.New("missing required env var CRON_SCHEDULE")
|
|
}
|
|
parts := scheduleSeparators.Split(raw, -1)
|
|
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
|
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
s := strings.TrimSpace(p)
|
|
if s == "" {
|
|
continue
|
|
}
|
|
if _, err := parser.Parse(s); err != nil {
|
|
return nil, fmt.Errorf("invalid cron %q: %w", s, err)
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, errors.New("CRON_SCHEDULE has no non-empty expressions")
|
|
}
|
|
return out, nil
|
|
}
|