326 lines
7.6 KiB
Go
326 lines
7.6 KiB
Go
package models
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/upaas/internal/database"
|
|
)
|
|
|
|
// DeploymentStatus represents the status of a deployment.
|
|
type DeploymentStatus string
|
|
|
|
// Deployment status constants.
|
|
const (
|
|
DeploymentStatusBuilding DeploymentStatus = "building"
|
|
DeploymentStatusDeploying DeploymentStatus = "deploying"
|
|
DeploymentStatusSuccess DeploymentStatus = "success"
|
|
DeploymentStatusFailed DeploymentStatus = "failed"
|
|
)
|
|
|
|
// Display constants.
|
|
const (
|
|
// secondsPerMinute is used for duration formatting.
|
|
secondsPerMinute = 60
|
|
// shortCommitLength is the number of characters to show for commit SHA.
|
|
shortCommitLength = 12
|
|
)
|
|
|
|
// Deployment represents a deployment attempt for an app.
|
|
type Deployment struct {
|
|
db *database.Database
|
|
|
|
ID int64
|
|
AppID string
|
|
WebhookEventID sql.NullInt64
|
|
CommitSHA sql.NullString
|
|
ImageID sql.NullString
|
|
ContainerID sql.NullString
|
|
Status DeploymentStatus
|
|
Logs sql.NullString
|
|
StartedAt time.Time
|
|
FinishedAt sql.NullTime
|
|
}
|
|
|
|
// NewDeployment creates a new Deployment with a database reference.
|
|
func NewDeployment(db *database.Database) *Deployment {
|
|
return &Deployment{
|
|
db: db,
|
|
Status: DeploymentStatusBuilding,
|
|
}
|
|
}
|
|
|
|
// Save inserts or updates the deployment in the database.
|
|
func (d *Deployment) Save(ctx context.Context) error {
|
|
if d.ID == 0 {
|
|
return d.insert(ctx)
|
|
}
|
|
|
|
return d.update(ctx)
|
|
}
|
|
|
|
// Reload refreshes the deployment from the database.
|
|
func (d *Deployment) Reload(ctx context.Context) error {
|
|
query := `
|
|
SELECT id, app_id, webhook_event_id, commit_sha, image_id,
|
|
container_id, status, logs, started_at, finished_at
|
|
FROM deployments WHERE id = ?`
|
|
|
|
row := d.db.QueryRow(ctx, query, d.ID)
|
|
|
|
return d.scan(row)
|
|
}
|
|
|
|
// AppendLog appends a log line to the deployment logs.
|
|
func (d *Deployment) AppendLog(ctx context.Context, line string) error {
|
|
var currentLogs string
|
|
|
|
if d.Logs.Valid {
|
|
currentLogs = d.Logs.String
|
|
}
|
|
|
|
d.Logs = sql.NullString{String: currentLogs + line + "\n", Valid: true}
|
|
|
|
return d.Save(ctx)
|
|
}
|
|
|
|
// MarkFinished marks the deployment as finished with the given status.
|
|
func (d *Deployment) MarkFinished(
|
|
ctx context.Context,
|
|
status DeploymentStatus,
|
|
) error {
|
|
d.Status = status
|
|
d.FinishedAt = sql.NullTime{Time: time.Now(), Valid: true}
|
|
|
|
return d.Save(ctx)
|
|
}
|
|
|
|
// Duration returns the duration of the deployment as a formatted string.
|
|
// Returns empty string if deployment is not finished.
|
|
func (d *Deployment) Duration() string {
|
|
if !d.FinishedAt.Valid {
|
|
return ""
|
|
}
|
|
|
|
duration := d.FinishedAt.Time.Sub(d.StartedAt)
|
|
|
|
// Format as minutes and seconds
|
|
minutes := int(duration.Minutes())
|
|
seconds := int(duration.Seconds()) % secondsPerMinute
|
|
|
|
if minutes > 0 {
|
|
return fmt.Sprintf("%dm %ds", minutes, seconds)
|
|
}
|
|
|
|
return fmt.Sprintf("%ds", seconds)
|
|
}
|
|
|
|
// ShortCommit returns a truncated commit SHA for display.
|
|
// Returns "-" if no commit SHA is set.
|
|
func (d *Deployment) ShortCommit() string {
|
|
if !d.CommitSHA.Valid || d.CommitSHA.String == "" {
|
|
return "-"
|
|
}
|
|
|
|
sha := d.CommitSHA.String
|
|
if len(sha) > shortCommitLength {
|
|
return sha[:shortCommitLength]
|
|
}
|
|
|
|
return sha
|
|
}
|
|
|
|
// FinishedAtISO returns the finished time in ISO format for JavaScript parsing.
|
|
// Returns empty string if not finished yet.
|
|
func (d *Deployment) FinishedAtISO() string {
|
|
if d.FinishedAt.Valid {
|
|
return d.FinishedAt.Time.Format(time.RFC3339)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// FinishedAtFormatted returns the finished time formatted for display.
|
|
// Returns empty string if not finished yet.
|
|
func (d *Deployment) FinishedAtFormatted() string {
|
|
if d.FinishedAt.Valid {
|
|
return d.FinishedAt.Time.Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (d *Deployment) insert(ctx context.Context) error {
|
|
query := `
|
|
INSERT INTO deployments (
|
|
app_id, webhook_event_id, commit_sha, image_id,
|
|
container_id, status, logs
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
result, err := d.db.Exec(ctx, query,
|
|
d.AppID, d.WebhookEventID, d.CommitSHA, d.ImageID,
|
|
d.ContainerID, d.Status, d.Logs,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
insertID, err := result.LastInsertId()
|
|
if err != nil {
|
|
return fmt.Errorf("getting last insert id: %w", err)
|
|
}
|
|
|
|
d.ID = insertID
|
|
|
|
return d.Reload(ctx)
|
|
}
|
|
|
|
func (d *Deployment) update(ctx context.Context) error {
|
|
query := `
|
|
UPDATE deployments SET
|
|
image_id = ?, container_id = ?, status = ?, logs = ?, finished_at = ?
|
|
WHERE id = ?`
|
|
|
|
_, err := d.db.Exec(ctx, query,
|
|
d.ImageID, d.ContainerID, d.Status, d.Logs, d.FinishedAt, d.ID,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
func (d *Deployment) scan(row *sql.Row) error {
|
|
return row.Scan(
|
|
&d.ID, &d.AppID, &d.WebhookEventID, &d.CommitSHA, &d.ImageID,
|
|
&d.ContainerID, &d.Status, &d.Logs, &d.StartedAt, &d.FinishedAt,
|
|
)
|
|
}
|
|
|
|
// FindDeployment finds a deployment by ID.
|
|
//
|
|
//nolint:nilnil // returning nil,nil is idiomatic for "not found" in Active Record
|
|
func FindDeployment(
|
|
ctx context.Context,
|
|
deployDB *database.Database,
|
|
deployID int64,
|
|
) (*Deployment, error) {
|
|
deploy := NewDeployment(deployDB)
|
|
deploy.ID = deployID
|
|
|
|
row := deployDB.QueryRow(ctx, `
|
|
SELECT id, app_id, webhook_event_id, commit_sha, image_id,
|
|
container_id, status, logs, started_at, finished_at
|
|
FROM deployments WHERE id = ?`,
|
|
deployID,
|
|
)
|
|
|
|
err := deploy.scan(row)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("scanning deployment: %w", err)
|
|
}
|
|
|
|
return deploy, nil
|
|
}
|
|
|
|
// FindDeploymentsByAppID finds recent deployments for an app.
|
|
func FindDeploymentsByAppID(
|
|
ctx context.Context,
|
|
deployDB *database.Database,
|
|
appID string,
|
|
limit int,
|
|
) ([]*Deployment, error) {
|
|
query := `
|
|
SELECT id, app_id, webhook_event_id, commit_sha, image_id,
|
|
container_id, status, logs, started_at, finished_at
|
|
FROM deployments WHERE app_id = ?
|
|
ORDER BY started_at DESC, id DESC LIMIT ?`
|
|
|
|
rows, err := deployDB.Query(ctx, query, appID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying deployments by app: %w", err)
|
|
}
|
|
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var deployments []*Deployment
|
|
|
|
for rows.Next() {
|
|
deploy := NewDeployment(deployDB)
|
|
|
|
scanErr := rows.Scan(
|
|
&deploy.ID, &deploy.AppID, &deploy.WebhookEventID,
|
|
&deploy.CommitSHA, &deploy.ImageID, &deploy.ContainerID,
|
|
&deploy.Status, &deploy.Logs, &deploy.StartedAt, &deploy.FinishedAt,
|
|
)
|
|
if scanErr != nil {
|
|
return nil, fmt.Errorf("scanning deployment row: %w", scanErr)
|
|
}
|
|
|
|
deployments = append(deployments, deploy)
|
|
}
|
|
|
|
rowsErr := rows.Err()
|
|
if rowsErr != nil {
|
|
return nil, fmt.Errorf("iterating deployment rows: %w", rowsErr)
|
|
}
|
|
|
|
return deployments, nil
|
|
}
|
|
|
|
// LatestDeploymentForApp finds the most recent deployment for an app.
|
|
//
|
|
//nolint:nilnil // returning nil,nil is idiomatic for "not found" in Active Record
|
|
func LatestDeploymentForApp(
|
|
ctx context.Context,
|
|
deployDB *database.Database,
|
|
appID string,
|
|
) (*Deployment, error) {
|
|
deploy := NewDeployment(deployDB)
|
|
|
|
row := deployDB.QueryRow(ctx, `
|
|
SELECT id, app_id, webhook_event_id, commit_sha, image_id,
|
|
container_id, status, logs, started_at, finished_at
|
|
FROM deployments WHERE app_id = ?
|
|
ORDER BY started_at DESC, id DESC LIMIT 1`,
|
|
appID,
|
|
)
|
|
|
|
err := deploy.scan(row)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("scanning latest deployment: %w", err)
|
|
}
|
|
|
|
return deploy, nil
|
|
}
|
|
|
|
// CountDeploymentsByAppID returns the total number of deployments for an app.
|
|
func CountDeploymentsByAppID(
|
|
ctx context.Context,
|
|
deployDB *database.Database,
|
|
appID string,
|
|
) (int, error) {
|
|
var count int
|
|
|
|
row := deployDB.QueryRow(ctx,
|
|
"SELECT COUNT(*) FROM deployments WHERE app_id = ?",
|
|
appID,
|
|
)
|
|
|
|
err := row.Scan(&count)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("counting deployments: %w", err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|