// Package static implements the "static" source: a git-folder-backed // deployable that can serve plain files or run a Deno backend. Builds // an image from the cloned folder and runs one container. // // The full deploy pipeline is implemented inline in this package // (deploy.go / teardown.go / reconcile.go). It operates directly on // plugin.Workload + the containers / workload_env tables — there is no // longer a synthetic static_sites row backing each workload. // // The legacy internal/staticsite package remains alive to serve the // /api/sites/* HTTP routes and the existing static_sites table; this // plugin does not depend on it for state, only for git-provider // helpers and Deno scaffolding generation. package static import ( "context" "encoding/json" "fmt" "strings" "github.com/alexei/tinyforge/internal/workload/plugin" ) // Config is the per-workload source config blob. Mirrors the fields // that used to live on the static_sites table, less anything moved to // Workload (notification config, webhook secrets, public_face). type Config struct { Provider string `json:"provider"` // "gitea" | "github" | "gitlab"; "" = autodetect BaseURL string `json:"base_url"` // e.g. https://git.example.com RepoOwner string `json:"repo_owner"` RepoName string `json:"repo_name"` Branch string `json:"branch"` FolderPath string `json:"folder_path"` // path within repo AccessToken string `json:"access_token"` // encrypted; optional for public repos Mode string `json:"mode"` // "static" | "deno" RenderMarkdown bool `json:"render_markdown"` StorageEnabled bool `json:"storage_enabled"` StorageLimitMB int `json:"storage_limit_mb"` // ReportCommitStatus, when true, pushes the deploy outcome back to the // git provider as a commit status (pending/success/failure) on the // deployed SHA. Best-effort — a reporting failure never fails a deploy. ReportCommitStatus bool `json:"report_commit_status"` // DeployStrategy selects how a redeploy cuts over. "" (default) and // "recreate" stop the old container before the new one comes up (a brief // downtime window). "blue-green" starts the new container alongside the // old, gates it, swaps the proxy route in place, then reaps the old — // zero-downtime under NPM. Validated via plugin.ValidateStrategy. DeployStrategy string `json:"deploy_strategy,omitempty"` } // effectiveStrategy resolves the configured strategy for the static source. // Empty maps to recreate — the source's historical behavior. Storage-backed // deno sites are forced to recreate even when blue-green is requested: a // blue-green overlap would mount the same RW named volume into BOTH // containers at once (a concurrent-writer window recreate never has, since // recreate stops blue before green starts). func effectiveStrategy(cfg Config) string { s := cfg.DeployStrategy if s == "" { s = plugin.StrategyRecreate } if s == plugin.StrategyBlueGreen && cfg.StorageEnabled && cfg.Mode == "deno" { return plugin.StrategyRecreate } return s } type source struct{} // Eager registration — the deploy pipeline lives entirely inside this // package now, so the kind is usable as soon as init() fires. No more // "backend not wired" failure mode at deploy time. func init() { plugin.RegisterSource(&source{}) } func (*source) Kind() string { return "static" } func (*source) SchemaSample() any { return Config{ Provider: "gitea", BaseURL: "https://git.example.com", RepoOwner: "owner", RepoName: "pages", Branch: "main", FolderPath: "", Mode: "static", } } func (*source) Validate(cfg json.RawMessage) error { var c Config if len(cfg) == 0 { return fmt.Errorf("static source: config is required") } if err := json.Unmarshal(cfg, &c); err != nil { return fmt.Errorf("static source: invalid json: %w", err) } if strings.TrimSpace(c.RepoOwner) == "" || strings.TrimSpace(c.RepoName) == "" { return fmt.Errorf("static source: repo_owner and repo_name are required") } if c.Mode != "" && c.Mode != "static" && c.Mode != "deno" { return fmt.Errorf("static source: mode must be \"static\" or \"deno\"") } if err := plugin.ValidateStrategy(c.DeployStrategy, true); err != nil { return fmt.Errorf("static source: %w", err) } return nil } func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload, intent plugin.DeploymentIntent) error { return deploy(ctx, deps, w, intent) } func (*source) Teardown(ctx context.Context, deps plugin.Deps, w plugin.Workload) error { return teardown(ctx, deps, w) } func (*source) Reconcile(ctx context.Context, deps plugin.Deps, w plugin.Workload) error { return reconcile(ctx, deps, w) }