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:
@@ -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/")
|
||||
|
||||
Reference in New Issue
Block a user