diff --git a/internal/drivers/api_gateway.go b/internal/drivers/api_gateway.go index 0282daf..5abbf6e 100644 --- a/internal/drivers/api_gateway.go +++ b/internal/drivers/api_gateway.go @@ -89,8 +89,7 @@ func (d *APIGatewayDriver) HealthCheck(ctx context.Context, ref interfaces.Resou if err != nil { return &interfaces.HealthResult{Healthy: false, Message: err.Error()}, nil } - healthy := app.ActiveDeployment != nil && app.ActiveDeployment.Phase == godo.DeploymentPhase_Active - return &interfaces.HealthResult{Healthy: healthy}, nil + return appHealthResult(app), nil } func (d *APIGatewayDriver) Scale(_ context.Context, _ interfaces.ResourceRef, _ int) (*interfaces.ResourceOutput, error) { diff --git a/internal/drivers/api_gateway_test.go b/internal/drivers/api_gateway_test.go index 49b06c0..ebede1e 100644 --- a/internal/drivers/api_gateway_test.go +++ b/internal/drivers/api_gateway_test.go @@ -3,6 +3,7 @@ package drivers_test import ( "context" "fmt" + "strings" "testing" "github.com/GoCodeAlone/workflow-plugin-digitalocean/internal/drivers" @@ -208,3 +209,89 @@ func TestAPIGatewayDriver_HealthCheck_Unhealthy(t *testing.T) { t.Errorf("expected unhealthy when no active deployment") } } + +// ── APIGateway HealthCheck deployment-phase tests ──────────────────────────── + +func gwAppWithPhases(active, inProgress, pending *godo.DeploymentPhase) *godo.App { + app := &godo.App{ID: "app-gw-999", Spec: &godo.AppSpec{Name: "phased-gw"}} + if active != nil { + app.ActiveDeployment = &godo.Deployment{Phase: *active} + } + if inProgress != nil { + app.InProgressDeployment = &godo.Deployment{Phase: *inProgress} + } + if pending != nil { + app.PendingDeployment = &godo.Deployment{Phase: *pending} + } + return app +} + +func gwPhasePtr(p godo.DeploymentPhase) *godo.DeploymentPhase { return &p } + +func TestAPIGatewayDriver_HealthCheck_InProgress_Building(t *testing.T) { + d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{ + app: gwAppWithPhases(nil, gwPhasePtr(godo.DeploymentPhase_Building), nil), + }, "nyc3") + result, err := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Healthy { + t.Error("expected Healthy=false while BUILDING") + } + if !strings.Contains(result.Message, "in progress") { + t.Errorf("message should contain 'in progress', got: %q", result.Message) + } +} + +func TestAPIGatewayDriver_HealthCheck_InProgress_Deploying(t *testing.T) { + d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{ + app: gwAppWithPhases(nil, gwPhasePtr(godo.DeploymentPhase_Deploying), nil), + }, "nyc3") + result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"}) + if result.Healthy { + t.Error("expected Healthy=false while DEPLOYING") + } + if !strings.Contains(result.Message, "in progress") { + t.Errorf("message should contain 'in progress', got: %q", result.Message) + } +} + +func TestAPIGatewayDriver_HealthCheck_InProgress_Failed(t *testing.T) { + d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{ + app: gwAppWithPhases(nil, gwPhasePtr(godo.DeploymentPhase_Error), nil), + }, "nyc3") + result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"}) + if result.Healthy { + t.Error("expected Healthy=false for ERROR phase") + } + if !strings.Contains(result.Message, "failed") { + t.Errorf("message should contain 'failed', got: %q", result.Message) + } +} + +func TestAPIGatewayDriver_HealthCheck_PendingDeployment(t *testing.T) { + d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{ + app: gwAppWithPhases(nil, nil, gwPhasePtr(godo.DeploymentPhase_PendingBuild)), + }, "nyc3") + result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"}) + if result.Healthy { + t.Error("expected Healthy=false with only a pending deployment") + } + if !strings.Contains(result.Message, "queued") { + t.Errorf("message should contain 'queued', got: %q", result.Message) + } +} + +func TestAPIGatewayDriver_HealthCheck_NoDeployment(t *testing.T) { + d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{ + app: gwAppWithPhases(nil, nil, nil), + }, "nyc3") + result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"}) + if result.Healthy { + t.Error("expected Healthy=false with no deployments") + } + if !strings.Contains(result.Message, "no deployment") { + t.Errorf("message should contain 'no deployment', got: %q", result.Message) + } +} diff --git a/internal/drivers/app_platform.go b/internal/drivers/app_platform.go index 7b52abc..527716d 100644 --- a/internal/drivers/app_platform.go +++ b/internal/drivers/app_platform.go @@ -170,12 +170,50 @@ func (d *AppPlatformDriver) HealthCheck(ctx context.Context, ref interfaces.Reso if err != nil { return &interfaces.HealthResult{Healthy: false, Message: err.Error()}, nil } - healthy := app.ActiveDeployment != nil && app.ActiveDeployment.Phase == godo.DeploymentPhase_Active - msg := "" - if !healthy { - msg = fmt.Sprintf("phase: %v", app.ActiveDeployment) + return appHealthResult(app), nil +} + +// appHealthResult evaluates all three DO deployment slots in priority order and +// returns an accurate HealthResult: +// +// - ActiveDeployment ACTIVE → Healthy=true +// - InProgressDeployment (building) → Healthy=false, "deployment in progress: " +// - InProgressDeployment (failed) → Healthy=false, "deployment failed: " +// - PendingDeployment → Healthy=false, "deployment queued" +// - none of the above → Healthy=false, "no deployment found" +func appHealthResult(app *godo.App) *interfaces.HealthResult { + // 1. Active and healthy. + if app.ActiveDeployment != nil && app.ActiveDeployment.Phase == godo.DeploymentPhase_Active { + return &interfaces.HealthResult{Healthy: true} } - return &interfaces.HealthResult{Healthy: healthy, Message: msg}, nil + + // 2. Deployment currently in progress — inspect its phase. + if dep := app.InProgressDeployment; dep != nil { + switch dep.Phase { + case godo.DeploymentPhase_PendingBuild, + godo.DeploymentPhase_Building, + godo.DeploymentPhase_PendingDeploy, + godo.DeploymentPhase_Deploying: + return &interfaces.HealthResult{ + Healthy: false, + Message: fmt.Sprintf("deployment in progress: %s", dep.Phase), + } + default: + // ERROR, CANCELED, SUPERSEDED, etc. + return &interfaces.HealthResult{ + Healthy: false, + Message: fmt.Sprintf("deployment failed: %s", dep.Phase), + } + } + } + + // 3. Deployment queued but not yet started. + if app.PendingDeployment != nil { + return &interfaces.HealthResult{Healthy: false, Message: "deployment queued"} + } + + // 4. No deployment at all (first-deploy not yet kicked off, or app never deployed). + return &interfaces.HealthResult{Healthy: false, Message: "no deployment found"} } func (d *AppPlatformDriver) Scale(ctx context.Context, ref interfaces.ResourceRef, replicas int) (*interfaces.ResourceOutput, error) { diff --git a/internal/drivers/app_platform_test.go b/internal/drivers/app_platform_test.go index 22af4f2..23826fe 100644 --- a/internal/drivers/app_platform_test.go +++ b/internal/drivers/app_platform_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "testing" "github.com/GoCodeAlone/workflow-plugin-digitalocean/internal/drivers" @@ -334,6 +335,105 @@ func TestAppPlatformDriver_HealthCheck_Unhealthy(t *testing.T) { } } +// ── HealthCheck deployment-phase tests ─────────────────────────────────────── + +func appWithPhases(active, inProgress, pending *godo.DeploymentPhase) *godo.App { + app := &godo.App{ID: "app-999", Spec: &godo.AppSpec{Name: "phased-app"}} + if active != nil { + app.ActiveDeployment = &godo.Deployment{Phase: *active} + } + if inProgress != nil { + app.InProgressDeployment = &godo.Deployment{Phase: *inProgress} + } + if pending != nil { + app.PendingDeployment = &godo.Deployment{Phase: *pending} + } + return app +} + +func phasePtr(p godo.DeploymentPhase) *godo.DeploymentPhase { return &p } + +func TestAppPlatformDriver_HealthCheck_Active(t *testing.T) { + d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{ + app: appWithPhases(phasePtr(godo.DeploymentPhase_Active), nil, nil), + }, "nyc3") + result, err := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result.Healthy { + t.Errorf("expected Healthy=true for ACTIVE phase, got message: %q", result.Message) + } +} + +func TestAppPlatformDriver_HealthCheck_InProgress_Building(t *testing.T) { + d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{ + app: appWithPhases(nil, phasePtr(godo.DeploymentPhase_Building), nil), + }, "nyc3") + result, err := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Healthy { + t.Error("expected Healthy=false while BUILDING") + } + if !strings.Contains(result.Message, "in progress") { + t.Errorf("message should contain 'in progress', got: %q", result.Message) + } +} + +func TestAppPlatformDriver_HealthCheck_InProgress_Deploying(t *testing.T) { + d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{ + app: appWithPhases(nil, phasePtr(godo.DeploymentPhase_Deploying), nil), + }, "nyc3") + result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"}) + if result.Healthy { + t.Error("expected Healthy=false while DEPLOYING") + } + if !strings.Contains(result.Message, "in progress") { + t.Errorf("message should contain 'in progress', got: %q", result.Message) + } +} + +func TestAppPlatformDriver_HealthCheck_InProgress_Failed(t *testing.T) { + d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{ + app: appWithPhases(nil, phasePtr(godo.DeploymentPhase_Error), nil), + }, "nyc3") + result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"}) + if result.Healthy { + t.Error("expected Healthy=false for ERROR phase") + } + if !strings.Contains(result.Message, "failed") { + t.Errorf("message should contain 'failed', got: %q", result.Message) + } +} + +func TestAppPlatformDriver_HealthCheck_PendingDeployment(t *testing.T) { + d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{ + app: appWithPhases(nil, nil, phasePtr(godo.DeploymentPhase_PendingBuild)), + }, "nyc3") + result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"}) + if result.Healthy { + t.Error("expected Healthy=false with only a pending deployment") + } + if !strings.Contains(result.Message, "queued") { + t.Errorf("message should contain 'queued', got: %q", result.Message) + } +} + +func TestAppPlatformDriver_HealthCheck_NoDeployment(t *testing.T) { + d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{ + app: appWithPhases(nil, nil, nil), + }, "nyc3") + result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"}) + if result.Healthy { + t.Error("expected Healthy=false with no deployments") + } + if !strings.Contains(result.Message, "no deployment") { + t.Errorf("message should contain 'no deployment', got: %q", result.Message) + } +} + // ── ParseImageRef unit tests ────────────────────────────────────────────────── func TestParseImageRef_DOCR(t *testing.T) {