410a131cec
This session (frontend focus):
- Rebuild /apps/new as a 4-step wizard (Basics → Configure → Trigger → Review):
WizardRail, SourceKindPicker card grid, AppManifest review, per-step validation,
ConfirmDialog-based unsaved-changes guard.
- Extract lib/workload/sourceForms.ts (single source of truth for source_config)
+ {Image,Compose,Static,Dockerfile}SourceForm + StaticDiscoveryWizard; fold the
/apps/[id] edit form onto the same components (removes the duplication). Add
vitest + sourceForms unit tests.
- Branch preview environments UI: /chain is_preview/preview_branch + a Preview
environments panel on /apps/[id] (per-branch URLs, ConfirmDialog teardown, armed
state); RegistryImagePicker on the registry trigger and the image source.
- Fixes: image-inspect 404 -> admin-gated POST /api/discovery/image/inspect;
conflict-panel blur flicker; friendly localized discovery errors; CPU/Memory
label hints; dashboard + /apps "Total workloads" count only source_kind workloads
(drop stale trigger_kind gate); NPM cert/access-list name cache; EntityPicker
empty-list guard.
- Update CLAUDE.md frontend conventions + add a Build & Test section.
Also captures pre-existing in-progress platform work (not from this session):
workload notifications, Prometheus metrics export, store lockfile, health probes,
backup hardening, and related store/webhook/scheduler changes.
167 lines
4.8 KiB
Go
167 lines
4.8 KiB
Go
// 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".
|
|
//
|
|
// BranchPattern is the preview-deploy escape hatch: when non-empty in
|
|
// "push" mode it overrides Branch and matches the event branch as a glob
|
|
// (`feat/*`, `release-*`, `*` for "any branch"). The trigger returns an
|
|
// intent whose Metadata["preview_branch"] holds the matched branch — the
|
|
// dispatcher uses that signal to materialize an ephemeral per-branch
|
|
// child workload rather than redeploying the parent.
|
|
type Config struct {
|
|
Repo string `json:"repo"`
|
|
Mode string `json:"mode"` // "push" | "tag"
|
|
Branch string `json:"branch"`
|
|
BranchPattern string `json:"branch_pattern"`
|
|
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). BranchPattern is
|
|
// validated as a path.Match glob if present; misconfigured
|
|
// patterns are rejected at the boundary rather than letting them
|
|
// fail silently inside Match.
|
|
if c.BranchPattern != "" {
|
|
if _, err := path.Match(c.BranchPattern, "probe"); err != nil {
|
|
return fmt.Errorf("git trigger: invalid branch_pattern %q: %w", c.BranchPattern, err)
|
|
}
|
|
}
|
|
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
|
|
}
|
|
// Preview-deploy signal: when BranchPattern is set AND the matched
|
|
// branch is NOT the configured baseline Branch, flag this dispatch
|
|
// for materialization as a per-branch child workload. The dispatcher
|
|
// reads preview_branch and decides whether to spawn a preview row;
|
|
// a baseline-branch push falls through to a normal redeploy of the
|
|
// template itself.
|
|
if cfg.Mode == "push" && cfg.BranchPattern != "" && evt.Git.Branch != "" && evt.Git.Branch != cfg.Branch {
|
|
meta["preview_branch"] = evt.Git.Branch
|
|
if evt.Git.Deleted {
|
|
meta["preview_deleted"] = "1"
|
|
}
|
|
}
|
|
reason := "git-push"
|
|
if meta["preview_deleted"] == "1" {
|
|
reason = "git-branch-deleted"
|
|
}
|
|
return &plugin.DeploymentIntent{
|
|
Reason: reason,
|
|
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
|
|
}
|
|
// Pattern-mode preview filter: any branch whose name matches the
|
|
// glob is in scope. The baseline `cfg.Branch` is also allowed so
|
|
// pushes to the template's primary branch keep redeploying the
|
|
// template itself.
|
|
if cfg.BranchPattern != "" {
|
|
if cfg.Branch != "" && cfg.Branch == branch {
|
|
return true
|
|
}
|
|
matched, err := path.Match(cfg.BranchPattern, branch)
|
|
return err == nil && matched
|
|
}
|
|
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
|
|
}
|