d63c831d15
Initialize Go module, directory structure, and full SQLite store layer: - 7-table schema (projects, stages, registries, settings, instances, deploys, deploy_logs) with auto-migration - CRUD operations for all entities with proper error handling - ErrNotFound sentinel for distinguishing 404 from 500 in handlers - WAL mode, foreign keys, busy timeout pragmas
168 lines
4.8 KiB
Go
168 lines
4.8 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// CreateDeploy inserts a new deploy record.
|
|
func (s *Store) CreateDeploy(d Deploy) (Deploy, error) {
|
|
d.ID = uuid.New().String()
|
|
d.StartedAt = now()
|
|
if d.Status == "" {
|
|
d.Status = "pending"
|
|
}
|
|
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO deploys (id, project_id, stage_id, instance_id, image_tag, status, started_at, finished_at, error)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
d.ID, d.ProjectID, d.StageID, d.InstanceID, d.ImageTag, d.Status, d.StartedAt, d.FinishedAt, d.Error,
|
|
)
|
|
if err != nil {
|
|
return Deploy{}, fmt.Errorf("insert deploy: %w", err)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// GetDeployByID returns a single deploy by its ID.
|
|
func (s *Store) GetDeployByID(id string) (Deploy, error) {
|
|
var d Deploy
|
|
err := s.db.QueryRow(
|
|
`SELECT id, project_id, stage_id, instance_id, image_tag, status, started_at, finished_at, error
|
|
FROM deploys WHERE id = ?`, id,
|
|
).Scan(&d.ID, &d.ProjectID, &d.StageID, &d.InstanceID, &d.ImageTag, &d.Status, &d.StartedAt, &d.FinishedAt, &d.Error)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return Deploy{}, fmt.Errorf("deploy %s: %w", id, ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return Deploy{}, fmt.Errorf("query deploy: %w", err)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// GetDeploysByProjectID returns all deploys for a project, newest first.
|
|
func (s *Store) GetDeploysByProjectID(projectID string) ([]Deploy, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, project_id, stage_id, instance_id, image_tag, status, started_at, finished_at, error
|
|
FROM deploys WHERE project_id = ? ORDER BY started_at DESC`, projectID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query deploys: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
return scanDeploys(rows)
|
|
}
|
|
|
|
// GetRecentDeploys returns the most recent deploys across all projects.
|
|
func (s *Store) GetRecentDeploys(limit int) ([]Deploy, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, project_id, stage_id, instance_id, image_tag, status, started_at, finished_at, error
|
|
FROM deploys ORDER BY started_at DESC LIMIT ?`, limit,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query recent deploys: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
return scanDeploys(rows)
|
|
}
|
|
|
|
// UpdateDeployStatus sets the status (and optionally error and finished_at) on a deploy.
|
|
func (s *Store) UpdateDeployStatus(id string, status string, deployErr string) error {
|
|
ts := now()
|
|
var finishedAt string
|
|
if isTerminalDeployStatus(status) {
|
|
finishedAt = ts
|
|
}
|
|
|
|
result, err := s.db.Exec(
|
|
`UPDATE deploys SET status=?, error=?, finished_at=? WHERE id=?`,
|
|
status, deployErr, finishedAt, id,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update deploy status: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("deploy %s: %w", id, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetDeployInstanceID links a deploy to the instance it created.
|
|
func (s *Store) SetDeployInstanceID(deployID string, instanceID string) error {
|
|
result, err := s.db.Exec(`UPDATE deploys SET instance_id=? WHERE id=?`, instanceID, deployID)
|
|
if err != nil {
|
|
return fmt.Errorf("set deploy instance: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("deploy %s: %w", deployID, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AppendDeployLog adds a log entry for a deploy.
|
|
func (s *Store) AppendDeployLog(deployID string, message string, level string) error {
|
|
if level == "" {
|
|
level = "info"
|
|
}
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO deploy_logs (deploy_id, message, level, created_at) VALUES (?, ?, ?, ?)`,
|
|
deployID, message, level, now(),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("append deploy log: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetDeployLogs returns all log entries for a deploy, ordered chronologically.
|
|
func (s *Store) GetDeployLogs(deployID string) ([]DeployLog, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, deploy_id, message, level, created_at
|
|
FROM deploy_logs WHERE deploy_id = ? ORDER BY id`, deployID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query deploy logs: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var logs []DeployLog
|
|
for rows.Next() {
|
|
var l DeployLog
|
|
if err := rows.Scan(&l.ID, &l.DeployID, &l.Message, &l.Level, &l.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("scan deploy log: %w", err)
|
|
}
|
|
logs = append(logs, l)
|
|
}
|
|
return logs, rows.Err()
|
|
}
|
|
|
|
// scanDeploys is a helper that scans deploy rows from a cursor.
|
|
func scanDeploys(rows *sql.Rows) ([]Deploy, error) {
|
|
var deploys []Deploy
|
|
for rows.Next() {
|
|
var d Deploy
|
|
if err := rows.Scan(&d.ID, &d.ProjectID, &d.StageID, &d.InstanceID, &d.ImageTag, &d.Status, &d.StartedAt, &d.FinishedAt, &d.Error); err != nil {
|
|
return nil, fmt.Errorf("scan deploy: %w", err)
|
|
}
|
|
deploys = append(deploys, d)
|
|
}
|
|
return deploys, rows.Err()
|
|
}
|
|
|
|
// isTerminalDeployStatus returns true if the status indicates the deploy is finished.
|
|
func isTerminalDeployStatus(status string) bool {
|
|
switch status {
|
|
case "success", "failed", "rolled_back":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|