diff --git a/migrations/001_init.sql b/migrations/001_init.sql new file mode 100644 index 0000000..a20bb9e --- /dev/null +++ b/migrations/001_init.sql @@ -0,0 +1,26 @@ +-- deploy_server initial schema. Tracks deploy targets (per app+host) and execution history. + +CREATE TABLE IF NOT EXISTS deploy_targets ( + app TEXT NOT NULL, + host TEXT NOT NULL, + remote_dir TEXT NOT NULL DEFAULT '', + binary_name TEXT NOT NULL DEFAULT '', + build_cmd TEXT NOT NULL DEFAULT '', + service_user TEXT NOT NULL DEFAULT '', + port INTEGER NOT NULL DEFAULT 0, + health_path TEXT NOT NULL DEFAULT '', + env TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + PRIMARY KEY (app, host) +); + +CREATE TABLE IF NOT EXISTS deploy_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + app TEXT NOT NULL, + host TEXT NOT NULL, + status TEXT NOT NULL, + trigger TEXT NOT NULL DEFAULT 'manual', + error TEXT NOT NULL DEFAULT '', + duration_ms INTEGER NOT NULL DEFAULT 0, + started_at TEXT NOT NULL +); diff --git a/migrations/002_target_extras.sql b/migrations/002_target_extras.sql new file mode 100644 index 0000000..e5bd40d --- /dev/null +++ b/migrations/002_target_extras.sql @@ -0,0 +1,6 @@ +-- Extra deploy_targets columns: strategy, source_dir, branch, compose_files. +-- Aplicacion idempotente: store.go captura "duplicate column" para DBs preexistentes. +ALTER TABLE deploy_targets ADD COLUMN strategy TEXT NOT NULL DEFAULT 'systemd'; +ALTER TABLE deploy_targets ADD COLUMN source_dir TEXT NOT NULL DEFAULT ''; +ALTER TABLE deploy_targets ADD COLUMN branch TEXT NOT NULL DEFAULT 'main'; +ALTER TABLE deploy_targets ADD COLUMN compose_files TEXT NOT NULL DEFAULT ''; diff --git a/store.go b/store.go index b125932..f834d89 100644 --- a/store.go +++ b/store.go @@ -2,13 +2,20 @@ package main import ( "database/sql" + "embed" "encoding/json" "fmt" + "io/fs" + "sort" + "strings" "time" _ "github.com/mattn/go-sqlite3" ) +//go:embed migrations/*.sql +var migrationsFS embed.FS + // DeployTarget represents a deploy configuration stored in operations.db. type DeployTarget struct { App string `json:"app"` @@ -62,50 +69,63 @@ func (s *Store) Close() error { } func (s *Store) migrate() error { - _, err := s.db.Exec(` - CREATE TABLE IF NOT EXISTS deploy_targets ( - app TEXT NOT NULL, - host TEXT NOT NULL, - remote_dir TEXT NOT NULL DEFAULT '', - binary_name TEXT NOT NULL DEFAULT '', - build_cmd TEXT NOT NULL DEFAULT '', - service_user TEXT NOT NULL DEFAULT '', - port INTEGER NOT NULL DEFAULT 0, - health_path TEXT NOT NULL DEFAULT '', - env TEXT NOT NULL DEFAULT '{}', - strategy TEXT NOT NULL DEFAULT 'systemd', - source_dir TEXT NOT NULL DEFAULT '', - branch TEXT NOT NULL DEFAULT 'main', - compose_files TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL, - PRIMARY KEY (app, host) - ); - CREATE TABLE IF NOT EXISTS deploy_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - app TEXT NOT NULL, - host TEXT NOT NULL, - status TEXT NOT NULL, - trigger TEXT NOT NULL DEFAULT 'manual', - error TEXT NOT NULL DEFAULT '', - duration_ms INTEGER NOT NULL DEFAULT 0, - started_at TEXT NOT NULL - ); - `) + files, err := fs.Glob(migrationsFS, "migrations/*.sql") if err != nil { return err } - // Idempotent column additions for existing databases - for _, col := range []string{ - "ALTER TABLE deploy_targets ADD COLUMN strategy TEXT NOT NULL DEFAULT 'systemd'", - "ALTER TABLE deploy_targets ADD COLUMN source_dir TEXT NOT NULL DEFAULT ''", - "ALTER TABLE deploy_targets ADD COLUMN branch TEXT NOT NULL DEFAULT 'main'", - "ALTER TABLE deploy_targets ADD COLUMN compose_files TEXT NOT NULL DEFAULT ''", - } { - s.db.Exec(col) // ignore "duplicate column" errors + sort.Strings(files) + for _, f := range files { + b, err := migrationsFS.ReadFile(f) + if err != nil { + return err + } + // Split por sentencia para que cada ALTER pueda fallar idempotentemente + // sin abortar el resto. SQLite acepta multiples sentencias en Exec(), + // pero no ignora duplicados — ejecutamos una a una. + stmts := splitSQLStatements(string(b)) + for _, stmt := range stmts { + s2 := strings.TrimSpace(stmt) + if s2 == "" { + continue + } + if _, err := s.db.Exec(s2); err != nil { + if isIdempotentError(err) { + continue + } + return fmt.Errorf("migrate %s: %w", f, err) + } + } } return nil } +func splitSQLStatements(s string) []string { + out := []string{} + cur := strings.Builder{} + for _, line := range strings.Split(s, "\n") { + trim := strings.TrimSpace(line) + if strings.HasPrefix(trim, "--") || trim == "" { + continue + } + cur.WriteString(line) + cur.WriteString("\n") + if strings.HasSuffix(trim, ";") { + out = append(out, cur.String()) + cur.Reset() + } + } + if cur.Len() > 0 { + out = append(out, cur.String()) + } + return out +} + +func isIdempotentError(err error) bool { + msg := err.Error() + return strings.Contains(msg, "duplicate column") || + strings.Contains(msg, "already exists") +} + func (s *Store) AddTarget(t DeployTarget) error { envJSON, _ := json.Marshal(t.Env) if t.Strategy == "" {