feat(apps): stepped creation wizard, branch previews, and app-creation fixes

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.
This commit is contained in:
2026-05-29 02:09:54 +03:00
parent 956943edbb
commit 410a131cec
112 changed files with 13285 additions and 2765 deletions
+49 -6
View File
@@ -18,11 +18,19 @@ import (
// 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"`
TagPattern string `json:"tag_pattern"`
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{}
@@ -49,7 +57,15 @@ func (*trigger) Validate(cfg json.RawMessage) error {
}
switch c.Mode {
case "push":
// Branch is optional ("" means any branch).
// 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 == "" {
@@ -90,8 +106,24 @@ func (*trigger) Match(ctx context.Context, deps plugin.Deps, w plugin.Workload,
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: "git-push",
Reason: reason,
Reference: evt.Git.CommitSHA,
Metadata: meta,
TriggeredAt: time.Now().UTC(),
@@ -106,6 +138,17 @@ func refMatches(cfg Config, ref string) bool {
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/")