cli/pkg/php/deploy.go

407 lines
10 KiB
Go

package php
import (
"context"
"time"
"github.com/host-uk/core/pkg/cli"
)
// Environment represents a deployment environment.
type Environment string
const (
// EnvProduction is the production environment.
EnvProduction Environment = "production"
// EnvStaging is the staging environment.
EnvStaging Environment = "staging"
)
// DeployOptions configures a deployment.
type DeployOptions struct {
// Dir is the project directory containing .env config.
Dir string
// Environment is the target environment (production or staging).
Environment Environment
// Force triggers a deployment even if no changes are detected.
Force bool
// Wait blocks until deployment completes.
Wait bool
// WaitTimeout is the maximum time to wait for deployment.
// Defaults to 10 minutes.
WaitTimeout time.Duration
// PollInterval is how often to check deployment status when waiting.
// Defaults to 5 seconds.
PollInterval time.Duration
}
// StatusOptions configures a status check.
type StatusOptions struct {
// Dir is the project directory containing .env config.
Dir string
// Environment is the target environment (production or staging).
Environment Environment
// DeploymentID is a specific deployment to check.
// If empty, returns the latest deployment.
DeploymentID string
}
// RollbackOptions configures a rollback.
type RollbackOptions struct {
// Dir is the project directory containing .env config.
Dir string
// Environment is the target environment (production or staging).
Environment Environment
// DeploymentID is the deployment to rollback to.
// If empty, rolls back to the previous successful deployment.
DeploymentID string
// Wait blocks until rollback completes.
Wait bool
// WaitTimeout is the maximum time to wait for rollback.
WaitTimeout time.Duration
}
// DeploymentStatus represents the status of a deployment.
type DeploymentStatus struct {
// ID is the deployment identifier.
ID string
// Status is the current deployment status.
// Values: queued, building, deploying, finished, failed, cancelled
Status string
// URL is the deployed application URL.
URL string
// Commit is the git commit SHA.
Commit string
// CommitMessage is the git commit message.
CommitMessage string
// Branch is the git branch.
Branch string
// StartedAt is when the deployment started.
StartedAt time.Time
// CompletedAt is when the deployment completed.
CompletedAt time.Time
// Log contains deployment logs.
Log string
}
// Deploy triggers a deployment to Coolify.
func Deploy(ctx context.Context, opts DeployOptions) (*DeploymentStatus, error) {
if opts.Dir == "" {
opts.Dir = "."
}
if opts.Environment == "" {
opts.Environment = EnvProduction
}
if opts.WaitTimeout == 0 {
opts.WaitTimeout = 10 * time.Minute
}
if opts.PollInterval == 0 {
opts.PollInterval = 5 * time.Second
}
// Load config
config, err := LoadCoolifyConfig(opts.Dir)
if err != nil {
return nil, cli.WrapVerb(err, "load", "Coolify config")
}
// Get app ID for environment
appID := getAppIDForEnvironment(config, opts.Environment)
if appID == "" {
return nil, cli.Err("no app ID configured for %s environment", opts.Environment)
}
// Create client
client := NewCoolifyClient(config.URL, config.Token)
// Trigger deployment
deployment, err := client.TriggerDeploy(ctx, appID, opts.Force)
if err != nil {
return nil, cli.WrapVerb(err, "trigger", "deployment")
}
status := convertDeployment(deployment)
// Wait for completion if requested
if opts.Wait && deployment.ID != "" {
status, err = waitForDeployment(ctx, client, appID, deployment.ID, opts.WaitTimeout, opts.PollInterval)
if err != nil {
return status, err
}
}
// Get app info for URL
app, err := client.GetApp(ctx, appID)
if err == nil && app.FQDN != "" {
status.URL = app.FQDN
}
return status, nil
}
// DeployStatus retrieves the status of a deployment.
func DeployStatus(ctx context.Context, opts StatusOptions) (*DeploymentStatus, error) {
if opts.Dir == "" {
opts.Dir = "."
}
if opts.Environment == "" {
opts.Environment = EnvProduction
}
// Load config
config, err := LoadCoolifyConfig(opts.Dir)
if err != nil {
return nil, cli.WrapVerb(err, "load", "Coolify config")
}
// Get app ID for environment
appID := getAppIDForEnvironment(config, opts.Environment)
if appID == "" {
return nil, cli.Err("no app ID configured for %s environment", opts.Environment)
}
// Create client
client := NewCoolifyClient(config.URL, config.Token)
var deployment *CoolifyDeployment
if opts.DeploymentID != "" {
// Get specific deployment
deployment, err = client.GetDeployment(ctx, appID, opts.DeploymentID)
if err != nil {
return nil, cli.WrapVerb(err, "get", "deployment")
}
} else {
// Get latest deployment
deployments, err := client.ListDeployments(ctx, appID, 1)
if err != nil {
return nil, cli.WrapVerb(err, "list", "deployments")
}
if len(deployments) == 0 {
return nil, cli.Err("no deployments found")
}
deployment = &deployments[0]
}
status := convertDeployment(deployment)
// Get app info for URL
app, err := client.GetApp(ctx, appID)
if err == nil && app.FQDN != "" {
status.URL = app.FQDN
}
return status, nil
}
// Rollback triggers a rollback to a previous deployment.
func Rollback(ctx context.Context, opts RollbackOptions) (*DeploymentStatus, error) {
if opts.Dir == "" {
opts.Dir = "."
}
if opts.Environment == "" {
opts.Environment = EnvProduction
}
if opts.WaitTimeout == 0 {
opts.WaitTimeout = 10 * time.Minute
}
// Load config
config, err := LoadCoolifyConfig(opts.Dir)
if err != nil {
return nil, cli.WrapVerb(err, "load", "Coolify config")
}
// Get app ID for environment
appID := getAppIDForEnvironment(config, opts.Environment)
if appID == "" {
return nil, cli.Err("no app ID configured for %s environment", opts.Environment)
}
// Create client
client := NewCoolifyClient(config.URL, config.Token)
// Find deployment to rollback to
deploymentID := opts.DeploymentID
if deploymentID == "" {
// Find previous successful deployment
deployments, err := client.ListDeployments(ctx, appID, 10)
if err != nil {
return nil, cli.WrapVerb(err, "list", "deployments")
}
// Skip the first (current) deployment, find the last successful one
for i, d := range deployments {
if i == 0 {
continue // Skip current deployment
}
if d.Status == "finished" || d.Status == "success" {
deploymentID = d.ID
break
}
}
if deploymentID == "" {
return nil, cli.Err("no previous successful deployment found to rollback to")
}
}
// Trigger rollback
deployment, err := client.Rollback(ctx, appID, deploymentID)
if err != nil {
return nil, cli.WrapVerb(err, "trigger", "rollback")
}
status := convertDeployment(deployment)
// Wait for completion if requested
if opts.Wait && deployment.ID != "" {
status, err = waitForDeployment(ctx, client, appID, deployment.ID, opts.WaitTimeout, 5*time.Second)
if err != nil {
return status, err
}
}
return status, nil
}
// ListDeployments retrieves recent deployments.
func ListDeployments(ctx context.Context, dir string, env Environment, limit int) ([]DeploymentStatus, error) {
if dir == "" {
dir = "."
}
if env == "" {
env = EnvProduction
}
if limit == 0 {
limit = 10
}
// Load config
config, err := LoadCoolifyConfig(dir)
if err != nil {
return nil, cli.WrapVerb(err, "load", "Coolify config")
}
// Get app ID for environment
appID := getAppIDForEnvironment(config, env)
if appID == "" {
return nil, cli.Err("no app ID configured for %s environment", env)
}
// Create client
client := NewCoolifyClient(config.URL, config.Token)
deployments, err := client.ListDeployments(ctx, appID, limit)
if err != nil {
return nil, cli.WrapVerb(err, "list", "deployments")
}
result := make([]DeploymentStatus, len(deployments))
for i, d := range deployments {
result[i] = *convertDeployment(&d)
}
return result, nil
}
// getAppIDForEnvironment returns the app ID for the given environment.
func getAppIDForEnvironment(config *CoolifyConfig, env Environment) string {
switch env {
case EnvStaging:
if config.StagingAppID != "" {
return config.StagingAppID
}
return config.AppID // Fallback to production
default:
return config.AppID
}
}
// convertDeployment converts a CoolifyDeployment to DeploymentStatus.
func convertDeployment(d *CoolifyDeployment) *DeploymentStatus {
return &DeploymentStatus{
ID: d.ID,
Status: d.Status,
URL: d.DeployedURL,
Commit: d.CommitSHA,
CommitMessage: d.CommitMsg,
Branch: d.Branch,
StartedAt: d.CreatedAt,
CompletedAt: d.FinishedAt,
Log: d.Log,
}
}
// waitForDeployment polls for deployment completion.
func waitForDeployment(ctx context.Context, client *CoolifyClient, appID, deploymentID string, timeout, interval time.Duration) (*DeploymentStatus, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
deployment, err := client.GetDeployment(ctx, appID, deploymentID)
if err != nil {
return nil, cli.WrapVerb(err, "get", "deployment status")
}
status := convertDeployment(deployment)
// Check if deployment is complete
switch deployment.Status {
case "finished", "success":
return status, nil
case "failed", "error":
return status, cli.Err("deployment failed: %s", deployment.Status)
case "cancelled":
return status, cli.Err("deployment was cancelled")
}
// Still in progress, wait and retry
select {
case <-ctx.Done():
return status, ctx.Err()
case <-time.After(interval):
}
}
return nil, cli.Err("deployment timed out after %v", timeout)
}
// IsDeploymentComplete returns true if the status indicates completion.
func IsDeploymentComplete(status string) bool {
switch status {
case "finished", "success", "failed", "error", "cancelled":
return true
default:
return false
}
}
// IsDeploymentSuccessful returns true if the status indicates success.
func IsDeploymentSuccessful(status string) bool {
return status == "finished" || status == "success"
}