// Package git implements the "git" trigger: matches inbound git push or // tag-create events from Gitea, GitHub, or GitLab against a repo + ref // filter. package git import ( "context" "encoding/json" "fmt" "path" "strings" "time" "github.com/alexei/tinyforge/internal/workload/plugin" ) // Config is the per-workload trigger config. Repo is "owner/name" (must // match the event repo). Mode controls whether branch pushes or tag // pushes fire the deploy. Branch is exact-matched when Mode=="push"; // TagPattern is glob-matched when Mode=="tag". type Config struct { Repo string `json:"repo"` Mode string `json:"mode"` // "push" | "tag" Branch string `json:"branch"` TagPattern string `json:"tag_pattern"` } type trigger struct{} func init() { plugin.RegisterTrigger(&trigger{}) } func (*trigger) Kind() string { return "git" } func (*trigger) SchemaSample() any { return Config{ Repo: "owner/repo", Mode: "push", Branch: "main", } } func (*trigger) Validate(cfg json.RawMessage) error { var c Config if len(cfg) == 0 { return fmt.Errorf("git trigger: config is required") } if err := json.Unmarshal(cfg, &c); err != nil { return fmt.Errorf("git trigger: invalid json: %w", err) } switch c.Mode { case "push": // Branch is optional ("" means any branch). case "tag": pattern := c.TagPattern if pattern == "" { pattern = "*" } if _, err := path.Match(pattern, "probe"); err != nil { return fmt.Errorf("git trigger: invalid tag_pattern %q: %w", pattern, err) } default: return fmt.Errorf("git trigger: mode must be \"push\" or \"tag\"") } return nil } func (*trigger) Match(ctx context.Context, deps plugin.Deps, w plugin.Workload, evt plugin.InboundEvent) (*plugin.DeploymentIntent, error) { if evt.Git == nil { return nil, nil } cfg, err := plugin.TriggerConfigOf[Config](w) if err != nil { return nil, fmt.Errorf("git trigger: decode config: %w", err) } if cfg.Repo != "" && !strings.EqualFold(cfg.Repo, evt.Git.Repo) { return nil, nil } if !refMatches(cfg, evt.Git.Ref) { return nil, nil } meta := map[string]string{ "repo": evt.Git.Repo, "vendor": evt.Git.Vendor, "ref": evt.Git.Ref, "pusher": evt.Git.Pusher, } if evt.Git.Branch != "" { meta["branch"] = evt.Git.Branch } if evt.Git.Tag != "" { meta["tag"] = evt.Git.Tag } return &plugin.DeploymentIntent{ Reason: "git-push", Reference: evt.Git.CommitSHA, Metadata: meta, TriggeredAt: time.Now().UTC(), TriggeredBy: "git-webhook", }, nil } func refMatches(cfg Config, ref string) bool { switch cfg.Mode { case "push": branch, ok := strings.CutPrefix(ref, "refs/heads/") if !ok { return false } return cfg.Branch == "" || cfg.Branch == branch case "tag": tag, ok := strings.CutPrefix(ref, "refs/tags/") if !ok { return false } pattern := cfg.TagPattern if pattern == "" { pattern = "*" } matched, err := path.Match(pattern, tag) return err == nil && matched } return false }