From 840dc4879823dad85a5f2b9bca6e68ce74a491b2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 06:08:03 +0000 Subject: [PATCH 1/2] Respect verification status in environment progression Co-authored-by: Justin Brooks --- apps/api/openapi/openapi.json | 5 + apps/api/openapi/schemas/policies.jsonnet | 5 + apps/api/src/types/openapi.ts | 5 + apps/web/app/api/openapi.ts | 5 + apps/workspace-engine/oapi/openapi.json | 5 + .../oapi/spec/schemas/policy.jsonnet | 5 + apps/workspace-engine/pkg/oapi/oapi.gen.go | 8 +- .../environment_progression_action.go | 34 ++++- .../environmentprogression.go | 6 +- .../environmentprogression/jobtracker.go | 66 +++++++-- .../environmentprogression/jobtracker_test.go | 137 ++++++++++++++++++ .../workspace-engine/test/integration/opts.go | 7 + packages/workspace-engine-sdk/src/schema.ts | 5 + 13 files changed, 277 insertions(+), 16 deletions(-) diff --git a/apps/api/openapi/openapi.json b/apps/api/openapi/openapi.json index 0a15e9328..971a306e4 100644 --- a/apps/api/openapi/openapi.json +++ b/apps/api/openapi/openapi.json @@ -633,6 +633,11 @@ "$ref": "#/components/schemas/JobStatus" }, "type": "array" + }, + "requireVerificationPassed": { + "default": false, + "description": "If true, jobs must pass verification to count toward the success percentage", + "type": "boolean" } }, "required": [ diff --git a/apps/api/openapi/schemas/policies.jsonnet b/apps/api/openapi/schemas/policies.jsonnet index 0a3048c60..b6e0905c4 100644 --- a/apps/api/openapi/schemas/policies.jsonnet +++ b/apps/api/openapi/schemas/policies.jsonnet @@ -126,6 +126,11 @@ local openapi = import '../lib/openapi.libsonnet'; minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 }, successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') }, + requireVerificationPassed: { + type: 'boolean', + default: false, + description: 'If true, jobs must pass verification to count toward the success percentage', + }, minimumSockTimeMinutes: { type: 'integer', diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index 35a42b56b..a79671e13 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -1030,6 +1030,11 @@ export interface components { */ minimumSuccessPercentage: number; successStatuses?: components["schemas"]["JobStatus"][]; + /** + * @description If true, jobs must pass verification to count toward the success percentage + * @default false + */ + requireVerificationPassed: boolean; }; Error: { /** @description Error code */ diff --git a/apps/web/app/api/openapi.ts b/apps/web/app/api/openapi.ts index 600fde064..d95b0cdee 100644 --- a/apps/web/app/api/openapi.ts +++ b/apps/web/app/api/openapi.ts @@ -1066,6 +1066,11 @@ export interface components { */ minimumSuccessPercentage: number; successStatuses?: components["schemas"]["JobStatus"][]; + /** + * @description If true, jobs must pass verification to count toward the success percentage + * @default false + */ + requireVerificationPassed: boolean; }; Error: { /** @description Error code */ diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 9542daa37..5c6ab9739 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -494,6 +494,11 @@ "$ref": "#/components/schemas/JobStatus" }, "type": "array" + }, + "requireVerificationPassed": { + "default": false, + "description": "If true, jobs must pass verification to count toward the success percentage", + "type": "boolean" } }, "required": [ diff --git a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet index 41b4e900d..f9614008b 100644 --- a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet @@ -159,6 +159,11 @@ local openapi = import '../lib/openapi.libsonnet'; minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 }, successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') }, + requireVerificationPassed: { + type: 'boolean', + default: false, + description: 'If true, jobs must pass verification to count toward the success percentage', + }, minimumSockTimeMinutes: { type: 'integer', diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 983163cf4..c74a82e1b 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -394,9 +394,11 @@ type EnvironmentProgressionRule struct { MaximumAgeHours *int32 `json:"maximumAgeHours,omitempty"` // MinimumSockTimeMinutes Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed - MinimumSockTimeMinutes *int32 `json:"minimumSockTimeMinutes,omitempty"` - MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage,omitempty"` - SuccessStatuses *[]JobStatus `json:"successStatuses,omitempty"` + MinimumSockTimeMinutes *int32 `json:"minimumSockTimeMinutes,omitempty"` + MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage,omitempty"` + // RequireVerificationPassed If true, jobs must pass verification to count as successful. + RequireVerificationPassed *bool `json:"requireVerificationPassed,omitempty"` + SuccessStatuses *[]JobStatus `json:"successStatuses,omitempty"` } // ErrorResponse defines model for ErrorResponse. diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go index 0db7298ad..bb48ebc35 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go @@ -210,7 +210,8 @@ func (a *EnvironmentProgressionAction) didThresholdJustCross( minPercentage = *rule.MinimumSuccessPercentage } - tracker := NewReleaseTargetJobTracker(ctx, a.store, dependencyEnv, version, successStatuses) + requireVerificationPassed := rule.RequireVerificationPassed != nil && *rule.RequireVerificationPassed + tracker := NewReleaseTargetJobTracker(ctx, a.store, dependencyEnv, version, successStatuses, requireVerificationPassed) if len(tracker.ReleaseTargets) == 0 { return false @@ -221,7 +222,21 @@ func (a *EnvironmentProgressionAction) didThresholdJustCross( return false } - return satisfiedAt.Equal(*job.CompletedAt) + successCompletedAt := job.CompletedAt + if requireVerificationPassed { + if a.store.JobVerifications.GetJobVerificationStatus(job.Id) != oapi.JobVerificationStatusPassed { + return false + } + if verificationCompletedAt := a.getLatestVerificationCompletedAt(job.Id); verificationCompletedAt != nil { + successCompletedAt = verificationCompletedAt + } + } + + if successCompletedAt == nil { + return false + } + + return satisfiedAt.Equal(*successCompletedAt) } func (a *EnvironmentProgressionAction) getThresholdSatisfiedAt( @@ -233,3 +248,18 @@ func (a *EnvironmentProgressionAction) getThresholdSatisfiedAt( } return tracker.GetSuccessPercentageSatisfiedAt(minPercentage) } + +func (a *EnvironmentProgressionAction) getLatestVerificationCompletedAt(jobId string) *time.Time { + verifications := a.store.JobVerifications.GetByJobId(jobId) + var latest *time.Time + for _, verification := range verifications { + completedAt := verification.CompletedAt() + if completedAt == nil { + continue + } + if latest == nil || completedAt.After(*latest) { + latest = completedAt + } + } + return latest +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go index ff32516a1..2f64d6a35 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go @@ -194,6 +194,8 @@ func (e *EnvironmentProgressionEvaluator) checkDependencyEnvironments( } } + requireVerificationPassed := e.rule.RequireVerificationPassed != nil && *e.rule.RequireVerificationPassed + var minSuccessPercentage float32 = 0.0 // Default: require at least one successful job (> 0%) if e.rule.MinimumSuccessPercentage != nil { minSuccessPercentage = *e.rule.MinimumSuccessPercentage @@ -223,6 +225,7 @@ func (e *EnvironmentProgressionEvaluator) checkDependencyEnvironments( version, depEnv, successStatuses, + requireVerificationPassed, passRateEvaluator, soakTimeEvaluator, ).WithDetail("environment", depEnv) @@ -278,13 +281,14 @@ func (e *EnvironmentProgressionEvaluator) evaluateJobSuccessCriteria( version *oapi.DeploymentVersion, environment *oapi.Environment, successStatuses map[oapi.JobStatus]bool, + requireVerificationPassed bool, passRateEvaluator *PassRateEvaluator, soakTimeEvaluator *SoakTimeEvaluator, ) *oapi.RuleEvaluation { ctx, span := tracer.Start(ctx, "EnvironmentProgressionEvaluator.evaluateJobSuccessCriteria") defer span.End() - tracker := NewReleaseTargetJobTracker(ctx, e.store, environment, version, successStatuses) + tracker := NewReleaseTargetJobTracker(ctx, e.store, environment, version, successStatuses, requireVerificationPassed) if len(tracker.ReleaseTargets) == 0 { return results.NewAllowedResult("No release targets in dependency environment, defaulting to allowed").WithSatisfiedAt(version.CreatedAt) } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker.go index beefda4d4..04282fe2d 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker.go @@ -40,6 +40,9 @@ type ReleaseTargetJobTracker struct { Version *oapi.DeploymentVersion ReleaseTargets []*oapi.ReleaseTarget SuccessStatuses map[oapi.JobStatus]bool + // RequireVerificationPassed controls whether jobs must have passed verification + // before being counted as successful for progression. + RequireVerificationPassed bool // Cached computed values jobsByStatus map[oapi.JobStatus]int @@ -56,6 +59,7 @@ func NewReleaseTargetJobTracker( environment *oapi.Environment, version *oapi.DeploymentVersion, successStatuses map[oapi.JobStatus]bool, + requireVerificationPassed ...bool, ) *ReleaseTargetJobTracker { ctx, span := jobTrackerTracer.Start(ctx, "NewReleaseTargetJobTracker", trace.WithAttributes( attribute.String("environment.id", environment.Id), @@ -64,6 +68,11 @@ func NewReleaseTargetJobTracker( )) defer span.End() + requireVerification := false + if len(requireVerificationPassed) > 0 { + requireVerification = requireVerificationPassed[0] + } + // Default success statuses if successStatuses == nil { successStatuses = map[oapi.JobStatus]bool{ @@ -76,11 +85,12 @@ func NewReleaseTargetJobTracker( span.SetAttributes(attribute.Int("release_targets.count", len(releaseTargets))) rtt := &ReleaseTargetJobTracker{ - store: store, - Environment: environment, - Version: version, - ReleaseTargets: releaseTargets, - SuccessStatuses: successStatuses, + store: store, + Environment: environment, + Version: version, + ReleaseTargets: releaseTargets, + SuccessStatuses: successStatuses, + RequireVerificationPassed: requireVerification, jobs: make([]*oapi.Job, 0), jobsByStatus: make(map[oapi.JobStatus]int, 0), @@ -118,14 +128,14 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { continue } - if t.SuccessStatuses[job.Status] && job.CompletedAt != nil { + if completedAt, ok := t.successCompletionTime(job); ok { targetKey := rt.Key() // Store the oldest successful completion time for this release target - if existingTime, exists := t.successfulReleaseTargets[targetKey]; !exists || job.CompletedAt.Before(existingTime) { - t.successfulReleaseTargets[targetKey] = *job.CompletedAt + if existingTime, exists := t.successfulReleaseTargets[targetKey]; !exists || completedAt.Before(existingTime) { + t.successfulReleaseTargets[targetKey] = *completedAt } - if t.mostRecentSuccess.Before(*job.CompletedAt) { - t.mostRecentSuccess = *job.CompletedAt + if t.mostRecentSuccess.Before(*completedAt) { + t.mostRecentSuccess = *completedAt } } @@ -142,6 +152,42 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { return t.jobs } +func (t *ReleaseTargetJobTracker) successCompletionTime(job *oapi.Job) (*time.Time, bool) { + if !t.SuccessStatuses[job.Status] || job.CompletedAt == nil { + return nil, false + } + + if !t.RequireVerificationPassed { + return job.CompletedAt, true + } + + verificationStatus := t.store.JobVerifications.GetJobVerificationStatus(job.Id) + if verificationStatus != oapi.JobVerificationStatusPassed { + return nil, false + } + + if completedAt := t.getLatestVerificationCompletedAt(job.Id); completedAt != nil { + return completedAt, true + } + + return job.CompletedAt, true +} + +func (t *ReleaseTargetJobTracker) getLatestVerificationCompletedAt(jobId string) *time.Time { + verifications := t.store.JobVerifications.GetByJobId(jobId) + var latest *time.Time + for _, verification := range verifications { + completedAt := verification.CompletedAt() + if completedAt == nil { + continue + } + if latest == nil || completedAt.After(*latest) { + latest = completedAt + } + } + return latest +} + // GetSuccessPercentage returns the percentage of release targets that have at least one successful job (0-100) func (t *ReleaseTargetJobTracker) GetSuccessPercentage() float32 { _, span := jobTrackerTracer.Start(context.Background(), "GetSuccessPercentage") diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go index 7d7ae36fc..6db7b0080 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go @@ -67,6 +67,71 @@ func setupTestStoreForJobTracker() *store.Store { return st } +func upsertVerificationWithStatus( + st *store.Store, + ctx context.Context, + jobId string, + status oapi.JobVerificationStatus, + createdAt time.Time, +) *oapi.JobVerification { + successCondition := "result.statusCode == 200" + metrics := []oapi.VerificationMetricStatus{} + + switch status { + case oapi.JobVerificationStatusPassed: + metrics = []oapi.VerificationMetricStatus{ + { + Name: "health-check", + IntervalSeconds: 30, + Count: 2, + SuccessCondition: successCondition, + Provider: oapi.MetricProvider{}, + Measurements: []oapi.VerificationMeasurement{ + {Status: oapi.Passed, MeasuredAt: createdAt}, + {Status: oapi.Passed, MeasuredAt: createdAt.Add(30 * time.Second)}, + }, + }, + } + case oapi.JobVerificationStatusFailed: + metrics = []oapi.VerificationMetricStatus{ + { + Name: "health-check", + IntervalSeconds: 30, + Count: 2, + SuccessCondition: successCondition, + Provider: oapi.MetricProvider{}, + Measurements: []oapi.VerificationMeasurement{ + {Status: oapi.Failed, MeasuredAt: createdAt}, + {Status: oapi.Failed, MeasuredAt: createdAt.Add(30 * time.Second)}, + }, + }, + } + default: + metrics = []oapi.VerificationMetricStatus{ + { + Name: "health-check", + IntervalSeconds: 30, + Count: 2, + SuccessCondition: successCondition, + Provider: oapi.MetricProvider{}, + Measurements: []oapi.VerificationMeasurement{ + {Status: oapi.Passed, MeasuredAt: createdAt}, + }, + }, + } + } + + verification := &oapi.JobVerification{ + Id: "verification-" + jobId, + JobId: jobId, + CreatedAt: createdAt, + Metrics: metrics, + } + + st.JobVerifications.Upsert(ctx, verification) + return verification +} + func TestGetReleaseTargets(t *testing.T) { st := setupTestStoreForJobTracker() ctx := context.Background() @@ -216,6 +281,78 @@ func TestReleaseTargetJobTracker_GetSuccessPercentage_WithSuccesses(t *testing.T assert.InDelta(t, expected, percentage, 0.1, "expected ~33.33%% success") } +func TestReleaseTargetJobTracker_RequireVerificationPassed(t *testing.T) { + st := setupTestStoreForJobTracker() + ctx := context.Background() + + env, _ := st.Environments.Get("env-1") + version, _ := st.DeploymentVersions.Get("version-1") + + rt := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + _ = st.ReleaseTargets.Upsert(ctx, rt) + + release := &oapi.Release{ + ReleaseTarget: *rt, + Version: *version, + Variables: map[string]oapi.LiteralValue{}, + CreatedAt: time.Now().Format(time.RFC3339), + } + _ = st.Releases.Upsert(ctx, release) + + completedAt := time.Now().Add(-10 * time.Minute) + job1 := &oapi.Job{ + Id: "job-1", + ReleaseId: release.ID(), + JobAgentId: "agent-1", + Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt.Add(-1 * time.Minute), + UpdatedAt: completedAt, + CompletedAt: &completedAt, + JobAgentConfig: oapi.JobAgentConfig{}, + } + st.Jobs.Upsert(ctx, job1) + + upsertVerificationWithStatus(st, ctx, job1.Id, oapi.JobVerificationStatusFailed, completedAt.Add(1*time.Minute)) + + tracker := NewReleaseTargetJobTracker(ctx, st, env, version, nil, true) + assert.Equal(t, float32(0.0), tracker.GetSuccessPercentage(), "expected 0%% when verification failed") + + release2 := &oapi.Release{ + ReleaseTarget: *rt, + Version: *version, + Variables: map[string]oapi.LiteralValue{}, + CreatedAt: time.Now().Format(time.RFC3339), + } + _ = st.Releases.Upsert(ctx, release2) + + completedAt2 := time.Now().Add(-5 * time.Minute) + job2 := &oapi.Job{ + Id: "job-2", + ReleaseId: release2.ID(), + JobAgentId: "agent-1", + Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt2.Add(-1 * time.Minute), + UpdatedAt: completedAt2, + CompletedAt: &completedAt2, + JobAgentConfig: oapi.JobAgentConfig{}, + } + st.Jobs.Upsert(ctx, job2) + + verification := upsertVerificationWithStatus(st, ctx, job2.Id, oapi.JobVerificationStatusPassed, completedAt2.Add(1*time.Minute)) + + tracker2 := NewReleaseTargetJobTracker(ctx, st, env, version, nil, true) + assert.Equal(t, float32(100.0), tracker2.GetSuccessPercentage(), "expected 100%% with passed verification") + + expectedCompletedAt := verification.CompletedAt() + if assert.NotNil(t, expectedCompletedAt) { + assert.True(t, tracker2.GetEarliestSuccess().Equal(*expectedCompletedAt)) + } +} + func TestReleaseTargetJobTracker_GetSuccessPercentage_AllSuccessful(t *testing.T) { st := setupTestStoreForJobTracker() ctx := context.Background() diff --git a/apps/workspace-engine/test/integration/opts.go b/apps/workspace-engine/test/integration/opts.go index 94283c820..15c886069 100644 --- a/apps/workspace-engine/test/integration/opts.go +++ b/apps/workspace-engine/test/integration/opts.go @@ -1023,6 +1023,13 @@ func EnvironmentProgressionSuccessStatuses(statuses ...oapi.JobStatus) Environme } } +func EnvironmentProgressionRequireVerificationPassed(require bool) EnvironmentProgressionRuleOption { + return func(_ *TestWorkspace, r *oapi.EnvironmentProgressionRule) error { + r.RequireVerificationPassed = &require + return nil + } +} + // ===== RelationshipRule Options ===== func RelationshipRuleName(name string) RelationshipRuleOption { diff --git a/packages/workspace-engine-sdk/src/schema.ts b/packages/workspace-engine-sdk/src/schema.ts index 9091c0700..cff8731ac 100644 --- a/packages/workspace-engine-sdk/src/schema.ts +++ b/packages/workspace-engine-sdk/src/schema.ts @@ -1312,6 +1312,11 @@ export interface components { */ minimumSuccessPercentage: number; successStatuses?: components["schemas"]["JobStatus"][]; + /** + * @description If true, jobs must pass verification to count toward the success percentage + * @default false + */ + requireVerificationPassed: boolean; }; ErrorResponse: { /** @example Workspace not found */ From 3cb59b7be3bcd67c86c7ef8837549a87b45679a8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 15:34:20 +0000 Subject: [PATCH 2/2] Require verification for environment progression Co-authored-by: Justin Brooks --- apps/api/openapi/openapi.json | 5 --- apps/api/openapi/schemas/policies.jsonnet | 5 --- apps/api/src/types/openapi.ts | 5 --- apps/web/app/api/openapi.ts | 5 --- apps/workspace-engine/oapi/openapi.json | 5 --- .../oapi/spec/schemas/policy.jsonnet | 5 --- apps/workspace-engine/pkg/oapi/oapi.gen.go | 8 ++--- .../environment_progression_action.go | 12 +++---- .../environmentprogression.go | 6 +--- .../environmentprogression/jobtracker.go | 32 ++++++------------- .../environmentprogression/jobtracker_test.go | 25 ++++++--------- .../workspace-engine/test/integration/opts.go | 7 ---- packages/workspace-engine-sdk/src/schema.ts | 5 --- 13 files changed, 30 insertions(+), 95 deletions(-) diff --git a/apps/api/openapi/openapi.json b/apps/api/openapi/openapi.json index 971a306e4..0a15e9328 100644 --- a/apps/api/openapi/openapi.json +++ b/apps/api/openapi/openapi.json @@ -633,11 +633,6 @@ "$ref": "#/components/schemas/JobStatus" }, "type": "array" - }, - "requireVerificationPassed": { - "default": false, - "description": "If true, jobs must pass verification to count toward the success percentage", - "type": "boolean" } }, "required": [ diff --git a/apps/api/openapi/schemas/policies.jsonnet b/apps/api/openapi/schemas/policies.jsonnet index b6e0905c4..0a3048c60 100644 --- a/apps/api/openapi/schemas/policies.jsonnet +++ b/apps/api/openapi/schemas/policies.jsonnet @@ -126,11 +126,6 @@ local openapi = import '../lib/openapi.libsonnet'; minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 }, successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') }, - requireVerificationPassed: { - type: 'boolean', - default: false, - description: 'If true, jobs must pass verification to count toward the success percentage', - }, minimumSockTimeMinutes: { type: 'integer', diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index a79671e13..35a42b56b 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -1030,11 +1030,6 @@ export interface components { */ minimumSuccessPercentage: number; successStatuses?: components["schemas"]["JobStatus"][]; - /** - * @description If true, jobs must pass verification to count toward the success percentage - * @default false - */ - requireVerificationPassed: boolean; }; Error: { /** @description Error code */ diff --git a/apps/web/app/api/openapi.ts b/apps/web/app/api/openapi.ts index d95b0cdee..600fde064 100644 --- a/apps/web/app/api/openapi.ts +++ b/apps/web/app/api/openapi.ts @@ -1066,11 +1066,6 @@ export interface components { */ minimumSuccessPercentage: number; successStatuses?: components["schemas"]["JobStatus"][]; - /** - * @description If true, jobs must pass verification to count toward the success percentage - * @default false - */ - requireVerificationPassed: boolean; }; Error: { /** @description Error code */ diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 5c6ab9739..9542daa37 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -494,11 +494,6 @@ "$ref": "#/components/schemas/JobStatus" }, "type": "array" - }, - "requireVerificationPassed": { - "default": false, - "description": "If true, jobs must pass verification to count toward the success percentage", - "type": "boolean" } }, "required": [ diff --git a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet index f9614008b..41b4e900d 100644 --- a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet @@ -159,11 +159,6 @@ local openapi = import '../lib/openapi.libsonnet'; minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 }, successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') }, - requireVerificationPassed: { - type: 'boolean', - default: false, - description: 'If true, jobs must pass verification to count toward the success percentage', - }, minimumSockTimeMinutes: { type: 'integer', diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index c74a82e1b..983163cf4 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -394,11 +394,9 @@ type EnvironmentProgressionRule struct { MaximumAgeHours *int32 `json:"maximumAgeHours,omitempty"` // MinimumSockTimeMinutes Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed - MinimumSockTimeMinutes *int32 `json:"minimumSockTimeMinutes,omitempty"` - MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage,omitempty"` - // RequireVerificationPassed If true, jobs must pass verification to count as successful. - RequireVerificationPassed *bool `json:"requireVerificationPassed,omitempty"` - SuccessStatuses *[]JobStatus `json:"successStatuses,omitempty"` + MinimumSockTimeMinutes *int32 `json:"minimumSockTimeMinutes,omitempty"` + MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage,omitempty"` + SuccessStatuses *[]JobStatus `json:"successStatuses,omitempty"` } // ErrorResponse defines model for ErrorResponse. diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go index bb48ebc35..593c9da5b 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go @@ -210,8 +210,7 @@ func (a *EnvironmentProgressionAction) didThresholdJustCross( minPercentage = *rule.MinimumSuccessPercentage } - requireVerificationPassed := rule.RequireVerificationPassed != nil && *rule.RequireVerificationPassed - tracker := NewReleaseTargetJobTracker(ctx, a.store, dependencyEnv, version, successStatuses, requireVerificationPassed) + tracker := NewReleaseTargetJobTracker(ctx, a.store, dependencyEnv, version, successStatuses) if len(tracker.ReleaseTargets) == 0 { return false @@ -223,10 +222,11 @@ func (a *EnvironmentProgressionAction) didThresholdJustCross( } successCompletedAt := job.CompletedAt - if requireVerificationPassed { - if a.store.JobVerifications.GetJobVerificationStatus(job.Id) != oapi.JobVerificationStatusPassed { - return false - } + verificationStatus := a.store.JobVerifications.GetJobVerificationStatus(job.Id) + if verificationStatus != "" && verificationStatus != oapi.JobVerificationStatusPassed { + return false + } + if verificationStatus == oapi.JobVerificationStatusPassed { if verificationCompletedAt := a.getLatestVerificationCompletedAt(job.Id); verificationCompletedAt != nil { successCompletedAt = verificationCompletedAt } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go index 2f64d6a35..ff32516a1 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go @@ -194,8 +194,6 @@ func (e *EnvironmentProgressionEvaluator) checkDependencyEnvironments( } } - requireVerificationPassed := e.rule.RequireVerificationPassed != nil && *e.rule.RequireVerificationPassed - var minSuccessPercentage float32 = 0.0 // Default: require at least one successful job (> 0%) if e.rule.MinimumSuccessPercentage != nil { minSuccessPercentage = *e.rule.MinimumSuccessPercentage @@ -225,7 +223,6 @@ func (e *EnvironmentProgressionEvaluator) checkDependencyEnvironments( version, depEnv, successStatuses, - requireVerificationPassed, passRateEvaluator, soakTimeEvaluator, ).WithDetail("environment", depEnv) @@ -281,14 +278,13 @@ func (e *EnvironmentProgressionEvaluator) evaluateJobSuccessCriteria( version *oapi.DeploymentVersion, environment *oapi.Environment, successStatuses map[oapi.JobStatus]bool, - requireVerificationPassed bool, passRateEvaluator *PassRateEvaluator, soakTimeEvaluator *SoakTimeEvaluator, ) *oapi.RuleEvaluation { ctx, span := tracer.Start(ctx, "EnvironmentProgressionEvaluator.evaluateJobSuccessCriteria") defer span.End() - tracker := NewReleaseTargetJobTracker(ctx, e.store, environment, version, successStatuses, requireVerificationPassed) + tracker := NewReleaseTargetJobTracker(ctx, e.store, environment, version, successStatuses) if len(tracker.ReleaseTargets) == 0 { return results.NewAllowedResult("No release targets in dependency environment, defaulting to allowed").WithSatisfiedAt(version.CreatedAt) } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker.go index 04282fe2d..c5549c394 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker.go @@ -40,9 +40,6 @@ type ReleaseTargetJobTracker struct { Version *oapi.DeploymentVersion ReleaseTargets []*oapi.ReleaseTarget SuccessStatuses map[oapi.JobStatus]bool - // RequireVerificationPassed controls whether jobs must have passed verification - // before being counted as successful for progression. - RequireVerificationPassed bool // Cached computed values jobsByStatus map[oapi.JobStatus]int @@ -59,7 +56,6 @@ func NewReleaseTargetJobTracker( environment *oapi.Environment, version *oapi.DeploymentVersion, successStatuses map[oapi.JobStatus]bool, - requireVerificationPassed ...bool, ) *ReleaseTargetJobTracker { ctx, span := jobTrackerTracer.Start(ctx, "NewReleaseTargetJobTracker", trace.WithAttributes( attribute.String("environment.id", environment.Id), @@ -68,11 +64,6 @@ func NewReleaseTargetJobTracker( )) defer span.End() - requireVerification := false - if len(requireVerificationPassed) > 0 { - requireVerification = requireVerificationPassed[0] - } - // Default success statuses if successStatuses == nil { successStatuses = map[oapi.JobStatus]bool{ @@ -85,12 +76,11 @@ func NewReleaseTargetJobTracker( span.SetAttributes(attribute.Int("release_targets.count", len(releaseTargets))) rtt := &ReleaseTargetJobTracker{ - store: store, - Environment: environment, - Version: version, - ReleaseTargets: releaseTargets, - SuccessStatuses: successStatuses, - RequireVerificationPassed: requireVerification, + store: store, + Environment: environment, + Version: version, + ReleaseTargets: releaseTargets, + SuccessStatuses: successStatuses, jobs: make([]*oapi.Job, 0), jobsByStatus: make(map[oapi.JobStatus]int, 0), @@ -157,17 +147,15 @@ func (t *ReleaseTargetJobTracker) successCompletionTime(job *oapi.Job) (*time.Ti return nil, false } - if !t.RequireVerificationPassed { - return job.CompletedAt, true - } - verificationStatus := t.store.JobVerifications.GetJobVerificationStatus(job.Id) - if verificationStatus != oapi.JobVerificationStatusPassed { + if verificationStatus != "" && verificationStatus != oapi.JobVerificationStatusPassed { return nil, false } - if completedAt := t.getLatestVerificationCompletedAt(job.Id); completedAt != nil { - return completedAt, true + if verificationStatus == oapi.JobVerificationStatusPassed { + if completedAt := t.getLatestVerificationCompletedAt(job.Id); completedAt != nil { + return completedAt, true + } } return job.CompletedAt, true diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go index 6db7b0080..de8b72829 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go @@ -281,7 +281,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentage_WithSuccesses(t *testing.T assert.InDelta(t, expected, percentage, 0.1, "expected ~33.33%% success") } -func TestReleaseTargetJobTracker_RequireVerificationPassed(t *testing.T) { +func TestReleaseTargetJobTracker_VerificationStatus(t *testing.T) { st := setupTestStoreForJobTracker() ctx := context.Background() @@ -316,23 +316,18 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed(t *testing.T) { } st.Jobs.Upsert(ctx, job1) - upsertVerificationWithStatus(st, ctx, job1.Id, oapi.JobVerificationStatusFailed, completedAt.Add(1*time.Minute)) + tracker := NewReleaseTargetJobTracker(ctx, st, env, version, nil) + assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), "expected 100%% with no verification") - tracker := NewReleaseTargetJobTracker(ctx, st, env, version, nil, true) - assert.Equal(t, float32(0.0), tracker.GetSuccessPercentage(), "expected 0%% when verification failed") + upsertVerificationWithStatus(st, ctx, job1.Id, oapi.JobVerificationStatusFailed, completedAt.Add(1*time.Minute)) - release2 := &oapi.Release{ - ReleaseTarget: *rt, - Version: *version, - Variables: map[string]oapi.LiteralValue{}, - CreatedAt: time.Now().Format(time.RFC3339), - } - _ = st.Releases.Upsert(ctx, release2) + tracker2 := NewReleaseTargetJobTracker(ctx, st, env, version, nil) + assert.Equal(t, float32(0.0), tracker2.GetSuccessPercentage(), "expected 0%% when verification failed") completedAt2 := time.Now().Add(-5 * time.Minute) job2 := &oapi.Job{ Id: "job-2", - ReleaseId: release2.ID(), + ReleaseId: release.ID(), JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, CreatedAt: completedAt2.Add(-1 * time.Minute), @@ -344,12 +339,12 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed(t *testing.T) { verification := upsertVerificationWithStatus(st, ctx, job2.Id, oapi.JobVerificationStatusPassed, completedAt2.Add(1*time.Minute)) - tracker2 := NewReleaseTargetJobTracker(ctx, st, env, version, nil, true) - assert.Equal(t, float32(100.0), tracker2.GetSuccessPercentage(), "expected 100%% with passed verification") + tracker3 := NewReleaseTargetJobTracker(ctx, st, env, version, nil) + assert.Equal(t, float32(100.0), tracker3.GetSuccessPercentage(), "expected 100%% with passed verification") expectedCompletedAt := verification.CompletedAt() if assert.NotNil(t, expectedCompletedAt) { - assert.True(t, tracker2.GetEarliestSuccess().Equal(*expectedCompletedAt)) + assert.True(t, tracker3.GetEarliestSuccess().Equal(*expectedCompletedAt)) } } diff --git a/apps/workspace-engine/test/integration/opts.go b/apps/workspace-engine/test/integration/opts.go index 15c886069..94283c820 100644 --- a/apps/workspace-engine/test/integration/opts.go +++ b/apps/workspace-engine/test/integration/opts.go @@ -1023,13 +1023,6 @@ func EnvironmentProgressionSuccessStatuses(statuses ...oapi.JobStatus) Environme } } -func EnvironmentProgressionRequireVerificationPassed(require bool) EnvironmentProgressionRuleOption { - return func(_ *TestWorkspace, r *oapi.EnvironmentProgressionRule) error { - r.RequireVerificationPassed = &require - return nil - } -} - // ===== RelationshipRule Options ===== func RelationshipRuleName(name string) RelationshipRuleOption { diff --git a/packages/workspace-engine-sdk/src/schema.ts b/packages/workspace-engine-sdk/src/schema.ts index cff8731ac..9091c0700 100644 --- a/packages/workspace-engine-sdk/src/schema.ts +++ b/packages/workspace-engine-sdk/src/schema.ts @@ -1312,11 +1312,6 @@ export interface components { */ minimumSuccessPercentage: number; successStatuses?: components["schemas"]["JobStatus"][]; - /** - * @description If true, jobs must pass verification to count toward the success percentage - * @default false - */ - requireVerificationPassed: boolean; }; ErrorResponse: { /** @example Workspace not found */