refactor(store): migration files + embed.FS

- migrations/001_init.sql + 002_target_extras.sql extraidos de schema inline
- store.go: applyMigrations() con embed.FS, splitSQLStatements, isIdempotentError
- aplica regla db_migrations.md (fn_registry/.claude/rules/)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 00:44:52 +02:00
parent 8e30e8cf29
commit ff5c17f7ff
3 changed files with 89 additions and 37 deletions
+26
View File
@@ -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
);
+6
View File
@@ -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 '';
+57 -37
View File
@@ -2,13 +2,20 @@ package main
import ( import (
"database/sql" "database/sql"
"embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"sort"
"strings"
"time" "time"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
//go:embed migrations/*.sql
var migrationsFS embed.FS
// DeployTarget represents a deploy configuration stored in operations.db. // DeployTarget represents a deploy configuration stored in operations.db.
type DeployTarget struct { type DeployTarget struct {
App string `json:"app"` App string `json:"app"`
@@ -62,50 +69,63 @@ func (s *Store) Close() error {
} }
func (s *Store) migrate() error { func (s *Store) migrate() error {
_, err := s.db.Exec(` files, err := fs.Glob(migrationsFS, "migrations/*.sql")
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
);
`)
if err != nil { if err != nil {
return err return err
} }
// Idempotent column additions for existing databases sort.Strings(files)
for _, col := range []string{ for _, f := range files {
"ALTER TABLE deploy_targets ADD COLUMN strategy TEXT NOT NULL DEFAULT 'systemd'", b, err := migrationsFS.ReadFile(f)
"ALTER TABLE deploy_targets ADD COLUMN source_dir TEXT NOT NULL DEFAULT ''", if err != nil {
"ALTER TABLE deploy_targets ADD COLUMN branch TEXT NOT NULL DEFAULT 'main'", return err
"ALTER TABLE deploy_targets ADD COLUMN compose_files TEXT NOT NULL DEFAULT ''", }
} { // Split por sentencia para que cada ALTER pueda fallar idempotentemente
s.db.Exec(col) // ignore "duplicate column" errors // 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 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 { func (s *Store) AddTarget(t DeployTarget) error {
envJSON, _ := json.Marshal(t.Env) envJSON, _ := json.Marshal(t.Env)
if t.Strategy == "" { if t.Strategy == "" {