Files
tiny-forge/internal/workload/plugin/trigger.go
alexei.dolgolyov 8d6a527a2b refactor(workload): plugin architecture wave + apps UI + volume scopes
Completes the workload-first refactor's plugin layer:

- internal/workload/plugin/ — Source/Trigger plugin contract,
  registry, types (Workload, DeploymentIntent, InboundEvent,
  PublicFace). Self-registering init() pattern + blank-import
  in cmd/server/main.go.
- Source plugins: image (blue-green with multi-face proxy routing),
  compose, static. Trigger plugins: registry, git, manual.
- internal/deployer/dispatch.go — DispatchPlugin/Teardown/Reconcile
  seam routing the legacy deployer through plugins.
- internal/api/workload_*.go — REST surface: workloads, env,
  volumes, chain (parent/children), promote-from. hooks.go
  serves /api/hooks/kinds/{kind}/schema for the wizard.
- internal/store: workload_env (encrypt-at-rest secrets) and
  workload_volumes tables, keyed on workload_id.
- cmd/server/static_backend.go — phantom-row adapter delegating
  the static source plugin to the legacy staticsite.Manager
  (deleted at hard cutover once the static inline port lands).
- web/src/routes/apps/ — /apps list + /apps/new wizard +
  /apps/[id] detail with kind-aware compose / image / static
  forms (Advanced JSON toggle), env panel, volumes panel,
  webhook panel, chain panel, manual deploy.

Volume scope generalization (v2 resolver):

- internal/volume.ResolveWorkloadPath (workload-keyed, sits
  next to legacy ResolvePath). Honors all VolumeScope values:
  absolute, ephemeral, instance, stage, project, project_named,
  named. internal/workload/plugin/source/image/image.go
  computeMounts wires settings + imageTag through. Coverage in
  internal/volume/resolver_test.go (portable Linux/Windows via
  t.TempDir).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:17:41 +03:00

75 lines
2.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package plugin
import (
"context"
"encoding/json"
"fmt"
"sort"
"sync"
)
// Trigger is the contract for one redeploy signal source (registry push,
// git push, manual, cron, ...). A Trigger has one job: given an inbound
// event and a workload's TriggerConfig, decide whether a deploy should
// fire and shape the resulting DeploymentIntent.
//
// Triggers do not perform deploys themselves — they hand the intent back
// to the deployer, which routes it to the matching Source. This keeps
// the (M sources × N triggers) cross-product code-free.
type Trigger interface {
// Kind is the registration key (e.g. "registry", "git", "manual", "cron").
Kind() string
// Validate type-checks a raw trigger config blob before it is persisted.
Validate(cfg json.RawMessage) error
// Match decides whether evt fires a deploy of w. Returning (nil, nil)
// means "not interested, skip silently"; an error is reserved for
// configuration or signature problems the operator should see.
Match(ctx context.Context, deps Deps, w Workload, evt InboundEvent) (*DeploymentIntent, error)
}
var (
triggersMu sync.RWMutex
triggers = map[string]Trigger{}
)
// RegisterTrigger installs t under t.Kind(). Panics on duplicate
// registration (init-time bug, never a runtime condition).
func RegisterTrigger(t Trigger) {
triggersMu.Lock()
defer triggersMu.Unlock()
k := t.Kind()
if _, dup := triggers[k]; dup {
panic(fmt.Sprintf("plugin: trigger %q already registered", k))
}
triggers[k] = t
}
// GetTrigger returns the Trigger for kind. Errors carry the missing kind
// for diagnostics.
func GetTrigger(kind string) (Trigger, error) {
triggersMu.RLock()
defer triggersMu.RUnlock()
t, ok := triggers[kind]
if !ok {
return nil, fmt.Errorf("plugin: no trigger registered for kind %q", kind)
}
return t, nil
}
// TriggerKinds returns all registered trigger kinds, sorted.
func TriggerKinds() []string {
triggersMu.RLock()
defer triggersMu.RUnlock()
out := make([]string, 0, len(triggers))
for k := range triggers {
out = append(out, k)
}
sortStrings(out)
return out
}
// sortStrings is shared by SourceKinds / TriggerKinds.
func sortStrings(s []string) { sort.Strings(s) }