From bcdc95848276d871b72d497437e85b45048d36b7 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:59:46 +0000 Subject: [PATCH 1/8] feat: add requireVerificationPassed to EnvironmentProgressionRule Fixes issue #742 by adding a new `requireVerificationPassed` flag to the `EnvironmentProgressionRule`. When enabled, jobs with a failed or running verification status are excluded from the success rate calculation, preventing progression to dependent environments when verification has not passed. Changes: - Add `require_verification_passed` column to DB schema with migration - Add `requireVerificationPassed` field to EnvironmentProgressionRule in all API schemas - Update SQL query to compute aggregate job verification status inline - Update ReleaseTargetJob struct and tracker to check verification status - Add tests for the new verification-aware success tracking Co-authored-by: Aditya Choudhari --- apps/api/openapi/openapi.json | 5 + apps/api/openapi/schemas/policies.jsonnet | 6 + apps/api/src/routes/v1/workspaces/policies.ts | 3 + apps/api/src/types/openapi.ts | 5 + .../oapi/spec/schemas/policy.jsonnet | 6 + apps/workspace-engine/pkg/db/convert.go | 2 + apps/workspace-engine/pkg/db/jobs.sql.go | 38 +++- apps/workspace-engine/pkg/db/models.go | 1 + apps/workspace-engine/pkg/db/policies.sql.go | 16 +- apps/workspace-engine/pkg/db/queries/jobs.sql | 24 +- .../pkg/db/queries/policies.sql | 13 +- .../pkg/db/queries/schema.sql | 1 + apps/workspace-engine/pkg/oapi/oapi.gen.go | 3 + .../environmentprogression.go | 3 +- .../environmentprogression/getters.go | 24 +- .../environmentprogression/jobtracker.go | 26 ++- .../environmentprogression/jobtracker_test.go | 212 ++++++++++++++++-- .../environmentprogression/mock_test.go | 47 ++-- .../environmentprogression/passrate.go | 1 + .../environmentprogression/soaktime.go | 2 +- .../test/controllers/harness/mocks.go | 19 +- .../0186_add_require_verification_passed.sql | 1 + packages/db/src/schema/policy.ts | 3 + 23 files changed, 371 insertions(+), 90 deletions(-) create mode 100644 packages/db/drizzle/0186_add_require_verification_passed.sql diff --git a/apps/api/openapi/openapi.json b/apps/api/openapi/openapi.json index 6611b8c7da..ba594efe17 100644 --- a/apps/api/openapi/openapi.json +++ b/apps/api/openapi/openapi.json @@ -1007,6 +1007,11 @@ "minimum": 0, "type": "number" }, + "requireVerificationPassed": { + "default": false, + "description": "If true, jobs must also have passed verification to count toward the success percentage", + "type": "boolean" + }, "successStatuses": { "items": { "$ref": "#/components/schemas/JobStatus" diff --git a/apps/api/openapi/schemas/policies.jsonnet b/apps/api/openapi/schemas/policies.jsonnet index a75bc30fd8..1bb2f5d3ed 100644 --- a/apps/api/openapi/schemas/policies.jsonnet +++ b/apps/api/openapi/schemas/policies.jsonnet @@ -181,6 +181,12 @@ local openapi = import '../lib/openapi.libsonnet'; minimum: 0, description: 'Maximum age of dependency deployment before blocking progression (prevents stale promotions)', }, + + requireVerificationPassed: { + type: 'boolean', + default: false, + description: 'If true, jobs must also have passed verification to count toward the success percentage', + }, }, }, diff --git a/apps/api/src/routes/v1/workspaces/policies.ts b/apps/api/src/routes/v1/workspaces/policies.ts index 76a51a37ae..39868724d4 100644 --- a/apps/api/src/routes/v1/workspaces/policies.ts +++ b/apps/api/src/routes/v1/workspaces/policies.ts @@ -83,6 +83,8 @@ const insertPolicyRules = async (tx: Tx, policyId: string, rules: any[]) => { minimumSuccessPercentage: rule.environmentProgression.minimumSuccessPercentage, successStatuses: rule.environmentProgression.successStatuses, + requireVerificationPassed: + rule.environmentProgression.requireVerificationPassed ?? false, }); if (rule.gradualRollout != null) @@ -196,6 +198,7 @@ const formatPolicy = (p: PolicyRow) => { ...(r.successStatuses != null && { successStatuses: r.successStatuses, }), + requireVerificationPassed: r.requireVerificationPassed, }, }), ), diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index 07730889a6..e5b19990be 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -1410,6 +1410,11 @@ export interface components { * @default 100 */ minimumSuccessPercentage: number; + /** + * @default false + * @description If true, jobs must also have passed verification to count toward the success percentage + */ + requireVerificationPassed?: boolean; successStatuses?: components["schemas"]["JobStatus"][]; }; EnvironmentRequestAccepted: { diff --git a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet index 48cedec2d5..b371668909 100644 --- a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet @@ -163,6 +163,12 @@ local openapi = import '../lib/openapi.libsonnet'; minimum: 0, description: 'Maximum age of dependency deployment before blocking progression (prevents stale promotions)', }, + + requireVerificationPassed: { + type: 'boolean', + default: false, + description: 'If true, jobs must also have passed verification to count toward the success percentage', + }, }, }, diff --git a/apps/workspace-engine/pkg/db/convert.go b/apps/workspace-engine/pkg/db/convert.go index aeac018739..2428ba3cc1 100644 --- a/apps/workspace-engine/pkg/db/convert.go +++ b/apps/workspace-engine/pkg/db/convert.go @@ -141,6 +141,7 @@ func ToOapiPolicyWithRules(row ListPoliciesWithRulesByWorkspaceIDRow) *oapi.Poli MinimumSoakTimeMinutes *int32 `json:"minimumSoakTimeMinutes"` MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage"` SuccessStatuses *[]string `json:"successStatuses"` + RequireVerificationPassed *bool `json:"requireVerificationPassed"` } var progs []progressionJSON _ = json.Unmarshal(row.EnvironmentProgressionRules, &progs) @@ -150,6 +151,7 @@ func ToOapiPolicyWithRules(row ListPoliciesWithRulesByWorkspaceIDRow) *oapi.Poli MaximumAgeHours: pr.MaximumAgeHours, MinimumSoakTimeMinutes: pr.MinimumSoakTimeMinutes, MinimumSuccessPercentage: pr.MinimumSuccessPercentage, + RequireVerificationPassed: pr.RequireVerificationPassed, } if pr.SuccessStatuses != nil { statuses := make([]oapi.JobStatus, len(*pr.SuccessStatuses)) diff --git a/apps/workspace-engine/pkg/db/jobs.sql.go b/apps/workspace-engine/pkg/db/jobs.sql.go index 3a6939d079..e31f865451 100644 --- a/apps/workspace-engine/pkg/db/jobs.sql.go +++ b/apps/workspace-engine/pkg/db/jobs.sql.go @@ -396,7 +396,29 @@ SELECT j.completed_at, r.deployment_id, r.environment_id, - r.resource_id + r.resource_id, + COALESCE( + ( + SELECT + CASE + WHEN COUNT(*) = 0 THEN '' + WHEN bool_or(COALESCE(mc.failures, 0) > COALESCE(jvm.failure_threshold, 0)) THEN 'failed' + WHEN bool_or(COALESCE(mc.total, 0) < jvm.count + AND COALESCE(mc.failures, 0) <= COALESCE(jvm.failure_threshold, 0)) THEN 'running' + ELSE 'passed' + END + FROM job_verification_metric jvm + LEFT JOIN LATERAL ( + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE mm.status = 'failed')::int AS failures + FROM job_verification_metric_measurement mm + WHERE mm.job_verification_metric_status_id = jvm.id + ) mc ON true + WHERE jvm.job_id = j.id + ), + '' + ) AS verification_status FROM job j JOIN release_job rj ON rj.job_id = j.id JOIN release r ON r.id = rj.release_id @@ -410,12 +432,13 @@ type ListJobsByEnvironmentAndVersionParams struct { } type ListJobsByEnvironmentAndVersionRow struct { - ID uuid.UUID - Status JobStatus - CompletedAt pgtype.Timestamptz - DeploymentID uuid.UUID - EnvironmentID uuid.UUID - ResourceID uuid.UUID + ID uuid.UUID + Status JobStatus + CompletedAt pgtype.Timestamptz + DeploymentID uuid.UUID + EnvironmentID uuid.UUID + ResourceID uuid.UUID + VerificationStatus string } // Returns all jobs for a given environment and version in a single query, @@ -436,6 +459,7 @@ func (q *Queries) ListJobsByEnvironmentAndVersion(ctx context.Context, arg ListJ &i.DeploymentID, &i.EnvironmentID, &i.ResourceID, + &i.VerificationStatus, ); err != nil { return nil, err } diff --git a/apps/workspace-engine/pkg/db/models.go b/apps/workspace-engine/pkg/db/models.go index df3deeda65..c6f84753e8 100644 --- a/apps/workspace-engine/pkg/db/models.go +++ b/apps/workspace-engine/pkg/db/models.go @@ -511,6 +511,7 @@ type PolicyRuleEnvironmentProgression struct { MinimumSoakTimeMinutes pgtype.Int4 MinimumSuccessPercentage pgtype.Float4 SuccessStatuses []string + RequireVerificationPassed bool CreatedAt pgtype.Timestamptz } diff --git a/apps/workspace-engine/pkg/db/policies.sql.go b/apps/workspace-engine/pkg/db/policies.sql.go index aa4c8591a0..1d4d7c136a 100644 --- a/apps/workspace-engine/pkg/db/policies.sql.go +++ b/apps/workspace-engine/pkg/db/policies.sql.go @@ -245,7 +245,8 @@ func (q *Queries) ListDeploymentWindowRulesByPolicyID(ctx context.Context, polic const listEnvironmentProgressionRulesByPolicyID = `-- name: ListEnvironmentProgressionRulesByPolicyID :many SELECT id, policy_id, depends_on_environment_selector, maximum_age_hours, - minimum_soak_time_minutes, minimum_success_percentage, success_statuses, created_at + minimum_soak_time_minutes, minimum_success_percentage, success_statuses, + require_verification_passed, created_at FROM policy_rule_environment_progression WHERE policy_id = $1 ` @@ -270,6 +271,7 @@ func (q *Queries) ListEnvironmentProgressionRulesByPolicyID(ctx context.Context, &i.MinimumSoakTimeMinutes, &i.MinimumSuccessPercentage, &i.SuccessStatuses, + &i.RequireVerificationPassed, &i.CreatedAt, ); err != nil { return nil, err @@ -367,7 +369,7 @@ SELECT COALESCE((SELECT json_agg(json_build_object('id', r.id, 'minApprovals', r.min_approvals)) FROM policy_rule_any_approval r WHERE r.policy_id = p.id), '[]'::json) AS approval_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'allowWindow', r.allow_window, 'durationMinutes', r.duration_minutes, 'rrule', r.rrule, 'timezone', r.timezone)) FROM policy_rule_deployment_window r WHERE r.policy_id = p.id), '[]'::json) AS deployment_window_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOn', r.depends_on)) FROM policy_rule_deployment_dependency r WHERE r.policy_id = p.id), '[]'::json) AS deployment_dependency_rules, - COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOnEnvironmentSelector', r.depends_on_environment_selector, 'maximumAgeHours', r.maximum_age_hours, 'minimumSoakTimeMinutes', r.minimum_soak_time_minutes, 'minimumSuccessPercentage', r.minimum_success_percentage, 'successStatuses', r.success_statuses)) FROM policy_rule_environment_progression r WHERE r.policy_id = p.id), '[]'::json) AS environment_progression_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOnEnvironmentSelector', r.depends_on_environment_selector, 'maximumAgeHours', r.maximum_age_hours, 'minimumSoakTimeMinutes', r.minimum_soak_time_minutes, 'minimumSuccessPercentage', r.minimum_success_percentage, 'successStatuses', r.success_statuses, 'requireVerificationPassed', r.require_verification_passed)) FROM policy_rule_environment_progression r WHERE r.policy_id = p.id), '[]'::json) AS environment_progression_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'rolloutType', r.rollout_type, 'timeScaleInterval', r.time_scale_interval)) FROM policy_rule_gradual_rollout r WHERE r.policy_id = p.id), '[]'::json) AS gradual_rollout_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'intervalSeconds', r.interval_seconds)) FROM policy_rule_version_cooldown r WHERE r.policy_id = p.id), '[]'::json) AS version_cooldown_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'description', r.description, 'selector', r.selector)) FROM policy_rule_version_selector r WHERE r.policy_id = p.id), '[]'::json) AS version_selector_rules @@ -730,14 +732,16 @@ func (q *Queries) UpsertDeploymentWindowRule(ctx context.Context, arg UpsertDepl const upsertEnvironmentProgressionRule = `-- name: UpsertEnvironmentProgressionRule :exec INSERT INTO policy_rule_environment_progression ( id, policy_id, depends_on_environment_selector, maximum_age_hours, - minimum_soak_time_minutes, minimum_success_percentage, success_statuses, created_at -) VALUES ($1, $2, $3, $4, $5, $6, $7, COALESCE($8::timestamptz, NOW())) + minimum_soak_time_minutes, minimum_success_percentage, success_statuses, + require_verification_passed, created_at +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, COALESCE($9::timestamptz, NOW())) ON CONFLICT (id) DO UPDATE SET depends_on_environment_selector = EXCLUDED.depends_on_environment_selector, maximum_age_hours = EXCLUDED.maximum_age_hours, minimum_soak_time_minutes = EXCLUDED.minimum_soak_time_minutes, minimum_success_percentage = EXCLUDED.minimum_success_percentage, - success_statuses = EXCLUDED.success_statuses + success_statuses = EXCLUDED.success_statuses, + require_verification_passed = EXCLUDED.require_verification_passed ` type UpsertEnvironmentProgressionRuleParams struct { @@ -748,6 +752,7 @@ type UpsertEnvironmentProgressionRuleParams struct { MinimumSoakTimeMinutes pgtype.Int4 MinimumSuccessPercentage pgtype.Float4 SuccessStatuses []string + RequireVerificationPassed bool CreatedAt pgtype.Timestamptz } @@ -760,6 +765,7 @@ func (q *Queries) UpsertEnvironmentProgressionRule(ctx context.Context, arg Upse arg.MinimumSoakTimeMinutes, arg.MinimumSuccessPercentage, arg.SuccessStatuses, + arg.RequireVerificationPassed, arg.CreatedAt, ) return err diff --git a/apps/workspace-engine/pkg/db/queries/jobs.sql b/apps/workspace-engine/pkg/db/queries/jobs.sql index 99aec0718e..c4601cc495 100644 --- a/apps/workspace-engine/pkg/db/queries/jobs.sql +++ b/apps/workspace-engine/pkg/db/queries/jobs.sql @@ -235,7 +235,29 @@ SELECT j.completed_at, r.deployment_id, r.environment_id, - r.resource_id + r.resource_id, + COALESCE( + ( + SELECT + CASE + WHEN COUNT(*) = 0 THEN '' + WHEN bool_or(COALESCE(mc.failures, 0) > COALESCE(jvm.failure_threshold, 0)) THEN 'failed' + WHEN bool_or(COALESCE(mc.total, 0) < jvm.count + AND COALESCE(mc.failures, 0) <= COALESCE(jvm.failure_threshold, 0)) THEN 'running' + ELSE 'passed' + END + FROM job_verification_metric jvm + LEFT JOIN LATERAL ( + SELECT + COUNT(*)::int AS total, + COUNT(*) FILTER (WHERE mm.status = 'failed')::int AS failures + FROM job_verification_metric_measurement mm + WHERE mm.job_verification_metric_status_id = jvm.id + ) mc ON true + WHERE jvm.job_id = j.id + ), + '' + ) AS verification_status FROM job j JOIN release_job rj ON rj.job_id = j.id JOIN release r ON r.id = rj.release_id diff --git a/apps/workspace-engine/pkg/db/queries/policies.sql b/apps/workspace-engine/pkg/db/queries/policies.sql index a23b7f1148..693c9763e3 100644 --- a/apps/workspace-engine/pkg/db/queries/policies.sql +++ b/apps/workspace-engine/pkg/db/queries/policies.sql @@ -30,7 +30,7 @@ SELECT COALESCE((SELECT json_agg(json_build_object('id', r.id, 'minApprovals', r.min_approvals)) FROM policy_rule_any_approval r WHERE r.policy_id = p.id), '[]'::json) AS approval_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'allowWindow', r.allow_window, 'durationMinutes', r.duration_minutes, 'rrule', r.rrule, 'timezone', r.timezone)) FROM policy_rule_deployment_window r WHERE r.policy_id = p.id), '[]'::json) AS deployment_window_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOn', r.depends_on)) FROM policy_rule_deployment_dependency r WHERE r.policy_id = p.id), '[]'::json) AS deployment_dependency_rules, - COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOnEnvironmentSelector', r.depends_on_environment_selector, 'maximumAgeHours', r.maximum_age_hours, 'minimumSoakTimeMinutes', r.minimum_soak_time_minutes, 'minimumSuccessPercentage', r.minimum_success_percentage, 'successStatuses', r.success_statuses)) FROM policy_rule_environment_progression r WHERE r.policy_id = p.id), '[]'::json) AS environment_progression_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOnEnvironmentSelector', r.depends_on_environment_selector, 'maximumAgeHours', r.maximum_age_hours, 'minimumSoakTimeMinutes', r.minimum_soak_time_minutes, 'minimumSuccessPercentage', r.minimum_success_percentage, 'successStatuses', r.success_statuses, 'requireVerificationPassed', r.require_verification_passed)) FROM policy_rule_environment_progression r WHERE r.policy_id = p.id), '[]'::json) AS environment_progression_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'rolloutType', r.rollout_type, 'timeScaleInterval', r.time_scale_interval)) FROM policy_rule_gradual_rollout r WHERE r.policy_id = p.id), '[]'::json) AS gradual_rollout_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'intervalSeconds', r.interval_seconds)) FROM policy_rule_version_cooldown r WHERE r.policy_id = p.id), '[]'::json) AS version_cooldown_rules, COALESCE((SELECT json_agg(json_build_object('id', r.id, 'description', r.description, 'selector', r.selector)) FROM policy_rule_version_selector r WHERE r.policy_id = p.id), '[]'::json) AS version_selector_rules @@ -99,21 +99,24 @@ DELETE FROM policy_rule_deployment_window WHERE policy_id = $1; -- name: ListEnvironmentProgressionRulesByPolicyID :many SELECT id, policy_id, depends_on_environment_selector, maximum_age_hours, - minimum_soak_time_minutes, minimum_success_percentage, success_statuses, created_at + minimum_soak_time_minutes, minimum_success_percentage, success_statuses, + require_verification_passed, created_at FROM policy_rule_environment_progression WHERE policy_id = $1; -- name: UpsertEnvironmentProgressionRule :exec INSERT INTO policy_rule_environment_progression ( id, policy_id, depends_on_environment_selector, maximum_age_hours, - minimum_soak_time_minutes, minimum_success_percentage, success_statuses, created_at -) VALUES ($1, $2, $3, $4, $5, $6, $7, COALESCE(sqlc.narg('created_at')::timestamptz, NOW())) + minimum_soak_time_minutes, minimum_success_percentage, success_statuses, + require_verification_passed, created_at +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, COALESCE(sqlc.narg('created_at')::timestamptz, NOW())) ON CONFLICT (id) DO UPDATE SET depends_on_environment_selector = EXCLUDED.depends_on_environment_selector, maximum_age_hours = EXCLUDED.maximum_age_hours, minimum_soak_time_minutes = EXCLUDED.minimum_soak_time_minutes, minimum_success_percentage = EXCLUDED.minimum_success_percentage, - success_statuses = EXCLUDED.success_statuses; + success_statuses = EXCLUDED.success_statuses, + require_verification_passed = EXCLUDED.require_verification_passed; -- name: DeleteEnvironmentProgressionRulesByPolicyID :exec DELETE FROM policy_rule_environment_progression WHERE policy_id = $1; diff --git a/apps/workspace-engine/pkg/db/queries/schema.sql b/apps/workspace-engine/pkg/db/queries/schema.sql index e1617b97ec..e317258a91 100644 --- a/apps/workspace-engine/pkg/db/queries/schema.sql +++ b/apps/workspace-engine/pkg/db/queries/schema.sql @@ -178,6 +178,7 @@ CREATE TABLE policy_rule_environment_progression ( minimum_soak_time_minutes INTEGER, minimum_success_percentage REAL, success_statuses TEXT[], + require_verification_passed BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 9e9497e37e..a648e37557 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -449,6 +449,9 @@ type EnvironmentProgressionRule struct { MinimumSoakTimeMinutes *int32 `json:"minimumSoakTimeMinutes,omitempty"` MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage,omitempty"` SuccessStatuses *[]JobStatus `json:"successStatuses,omitempty"` + + // RequireVerificationPassed If true, jobs must also have passed verification to count toward the success percentage + RequireVerificationPassed *bool `json:"requireVerificationPassed,omitempty"` } // EnvironmentSummary defines model for EnvironmentSummary. 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 a87fcab65a..c8f198f4e9 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 @@ -304,7 +304,8 @@ func (e *EnvironmentProgressionEvaluator) evaluateJobSuccessCriteria( ctx, span := tracer.Start(ctx, "EnvironmentProgressionEvaluator.evaluateJobSuccessCriteria") defer span.End() - tracker := NewReleaseTargetJobTracker(ctx, e.getters, environment, version, successStatuses) + requireVerificationPassed := e.rule.RequireVerificationPassed != nil && *e.rule.RequireVerificationPassed + tracker := NewReleaseTargetJobTracker(ctx, e.getters, 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/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go index 3a3ee6796a..4c89efd3fb 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go @@ -52,12 +52,13 @@ type Getters interface { // ReleaseTargetJob holds the minimal job fields needed by the job tracker, // along with the release target triple identifying which target the job belongs to. type ReleaseTargetJob struct { - JobID string - Status oapi.JobStatus - CompletedAt *time.Time - DeploymentID string - EnvironmentID string - ResourceID string + JobID string + Status oapi.JobStatus + CompletedAt *time.Time + DeploymentID string + EnvironmentID string + ResourceID string + VerificationStatus string } // --------------------------------------------------------------------------- @@ -208,11 +209,12 @@ func (p *PostgresGetters) GetJobsForEnvironmentAndVersion( result := make([]ReleaseTargetJob, len(rows)) for i, row := range rows { rtj := ReleaseTargetJob{ - JobID: row.ID.String(), - Status: db.ToOapiJobStatus(row.Status), - DeploymentID: row.DeploymentID.String(), - EnvironmentID: row.EnvironmentID.String(), - ResourceID: row.ResourceID.String(), + JobID: row.ID.String(), + Status: db.ToOapiJobStatus(row.Status), + DeploymentID: row.DeploymentID.String(), + EnvironmentID: row.EnvironmentID.String(), + ResourceID: row.ResourceID.String(), + VerificationStatus: row.VerificationStatus, } if row.CompletedAt.Valid { t := row.CompletedAt.Time 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 6a082ea371..3385875331 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 @@ -35,10 +35,11 @@ func getReleaseTargets( type ReleaseTargetJobTracker struct { getters Getters - Environment *oapi.Environment - Version *oapi.DeploymentVersion - ReleaseTargets []oapi.ReleaseTarget - SuccessStatuses map[oapi.JobStatus]bool + Environment *oapi.Environment + Version *oapi.DeploymentVersion + ReleaseTargets []oapi.ReleaseTarget + SuccessStatuses map[oapi.JobStatus]bool + RequireVerificationPassed bool // Cached computed values jobsByStatus map[oapi.JobStatus]int @@ -55,6 +56,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), @@ -75,11 +77,12 @@ func NewReleaseTargetJobTracker( span.SetAttributes(attribute.Int("release_targets.count", len(releaseTargets))) rtt := &ReleaseTargetJobTracker{ - getters: getters, - Environment: environment, - Version: version, - ReleaseTargets: releaseTargets, - SuccessStatuses: successStatuses, + getters: getters, + Environment: environment, + Version: version, + ReleaseTargets: releaseTargets, + SuccessStatuses: successStatuses, + RequireVerificationPassed: requireVerificationPassed, jobs: make([]*oapi.Job, 0), jobsByStatus: make(map[oapi.JobStatus]int, 0), @@ -137,7 +140,10 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { CompletedAt: row.CompletedAt, } - if t.SuccessStatuses[row.Status] && row.CompletedAt != nil { + isVerificationOk := !t.RequireVerificationPassed || + row.VerificationStatus == "" || + row.VerificationStatus == string(oapi.JobVerificationStatusPassed) + if t.SuccessStatuses[row.Status] && row.CompletedAt != nil && isVerificationOk { if existingTime, exists := t.successfulReleaseTargets[targetKey]; !exists || row.CompletedAt.Before(existingTime) { t.successfulReleaseTargets[targetKey] = *row.CompletedAt 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 c024d14cea..7e161f7ebb 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 @@ -37,7 +37,7 @@ func TestNewReleaseTargetJobTracker(t *testing.T) { env := mock.environments["env-1"] // Test with default success statuses - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) assert.NotNil(t, tracker, "expected non-nil tracker") assert.Equal(t, "env-1", tracker.Environment.Id) @@ -54,7 +54,7 @@ func TestNewReleaseTargetJobTracker(t *testing.T) { oapi.JobStatusSuccessful: true, oapi.JobStatusInProgress: true, } - tracker2 := NewReleaseTargetJobTracker(ctx, mock, env, version, customStatuses) + tracker2 := NewReleaseTargetJobTracker(ctx, mock, env, version, customStatuses, false) assert.True( t, @@ -74,7 +74,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentage_NoTargets(t *testing.T) { env := mock.environments["env-1"] - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) percentage := tracker.GetSuccessPercentage() assert.InDelta(t, float32(0.0), percentage, 0, "expected 0%% success with no targets") @@ -144,7 +144,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentage_WithSuccesses(t *testing.T } mock.addJob(rt2, job2, release2) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // Manually set the ReleaseTargets since we're not setting up the full resource/environment/deployment selectors tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2, *rt3} @@ -212,7 +212,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentage_AllSuccessful(t *testing.T mock.addJob(rt1, job1, release1) mock.addJob(rt2, job2, release2) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // Manually set the ReleaseTargets since we're not setting up the full resource/environment/deployment selectors tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2} @@ -227,7 +227,7 @@ func TestReleaseTargetJobTracker_MeetsSoakTimeRequirement_NoJobs(t *testing.T) { env := mock.environments["env-1"] - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // With no successful jobs, soak time requirement should return true (0 duration remaining) // Actually, looking at the code, with no successful jobs mostRecentSuccess is zero time @@ -280,7 +280,7 @@ func TestReleaseTargetJobTracker_MeetsSoakTimeRequirement_SoakTimeMet(t *testing } mock.addJob(rt1, job1, release1) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // Soak time of 10 minutes should be met (job completed 15 minutes ago) assert.True(t, tracker.MeetsSoakTimeRequirement(10*time.Minute), @@ -351,7 +351,7 @@ func TestReleaseTargetJobTracker_MeetsSoakTimeRequirement_MultipleJobs(t *testin mock.addJob(rt1, job1, release1) mock.addJob(rt2, job2, release2) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // Manually set the ReleaseTargets since we're not setting up the full resource/environment/deployment selectors tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2} @@ -399,7 +399,7 @@ func TestReleaseTargetJobTracker_GetSoakTimeRemaining(t *testing.T) { } mock.addJob(rt1, job1, release1) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // Test zero duration remaining := tracker.GetSoakTimeRemaining(0) @@ -427,7 +427,7 @@ func TestReleaseTargetJobTracker_GetMostRecentSuccess(t *testing.T) { env := mock.environments["env-1"] - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // With no successful jobs, should be zero time assert.True( @@ -465,7 +465,7 @@ func TestReleaseTargetJobTracker_GetMostRecentSuccess(t *testing.T) { } mock.addJob(rt1, job1, release1) - tracker2 := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker2 := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) mostRecent := tracker2.GetMostRecentSuccess() assert.False(t, mostRecent.IsZero(), "expected non-zero time with successful job") @@ -481,7 +481,7 @@ func TestReleaseTargetJobTracker_IsWithinMaxAge_NoSuccesses(t *testing.T) { env := mock.environments["env-1"] - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // With no successful jobs, should return false assert.False( @@ -525,7 +525,7 @@ func TestReleaseTargetJobTracker_IsWithinMaxAge_WithinAge(t *testing.T) { } mock.addJob(rt1, job1, release1) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // Should be within 10 minutes assert.True(t, tracker.IsWithinMaxAge(10*time.Minute), @@ -592,7 +592,7 @@ func TestReleaseTargetJobTracker_Jobs(t *testing.T) { mock.addJob(rt1, job1, release1) mock.addJob(rt2, job2, release2) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) jobs := tracker.Jobs() assert.Len(t, jobs, 2, "expected 2 jobs") @@ -731,7 +731,7 @@ func TestReleaseTargetJobTracker_MultipleJobsPerTarget_TracksOldestSuccess(t *te mock.addJob(rt1, job1, release1) mock.addJob(rt1, job2, release1) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) // Manually set the ReleaseTargets since we're not setting up the full resource/environment/deployment selectors tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} @@ -838,7 +838,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentageSatisfiedAt_Basic(t *testin } mock.addJob(rt3, job3, release3) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2, *rt3} // Test 50% requirement: need 2 successes (ceil(3 * 0.5) = 2) @@ -916,7 +916,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentageSatisfiedAt_NotEnoughSucces } mock.addJob(rt1, job1, release1) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2, *rt3} // Test 50% requirement: need 2 successes (ceil(3 * 0.5) = 2) @@ -938,7 +938,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentageSatisfiedAt_NoReleaseTarget env := mock.environments["env-1"] - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) tracker.ReleaseTargets = []oapi.ReleaseTarget{} // With no release targets, should return zero time @@ -964,7 +964,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentageSatisfiedAt_NoSuccessfulJob DeploymentId: "deploy-1", } - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2} // With no successful jobs, should return zero time @@ -1032,7 +1032,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentageSatisfiedAt_ZeroMinimumPerc mock.addJob(rt1, job1, release1) mock.addJob(rt2, job2, release2) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2} // With zero or negative minimum percentage, should default to 100% @@ -1151,7 +1151,7 @@ func TestReleaseTargetJobTracker_GetSuccessPercentageSatisfiedAt_OutOfOrderCompl } mock.addJob(rt3, job3, release3) - tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil) + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2, *rt3} // Test 50% requirement: need 2 successes (ceil(3 * 0.5) = 2) @@ -1167,3 +1167,173 @@ func TestReleaseTargetJobTracker_GetSuccessPercentageSatisfiedAt_OutOfOrderCompl "expected satisfiedAt to be the timestamp of the 2nd success chronologically (completedAt1)", ) } + +func TestReleaseTargetJobTracker_RequireVerificationPassed_ExcludesFailedVerification(t *testing.T) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, + Version: *version, + Variables: map[string]oapi.LiteralValue{}, + CreatedAt: time.Now().Format(time.RFC3339), + } + + completedAt := time.Now().Add(-10 * time.Minute) + job1 := &oapi.Job{ + Id: "job-1", + JobAgentId: "agent-1", + Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt, + UpdatedAt: completedAt, + CompletedAt: &completedAt, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + + // Set verification status to failed + mock.jobVerificationStatus["job-1"] = string(oapi.JobVerificationStatusFailed) + + // With RequireVerificationPassed=true, the failed verification job should not count + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} + + assert.Equal(t, float32(0.0), tracker.GetSuccessPercentage(), + "expected 0%% success when verification failed and RequireVerificationPassed=true") +} + +func TestReleaseTargetJobTracker_RequireVerificationPassed_IncludesPassedVerification(t *testing.T) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, + Version: *version, + Variables: map[string]oapi.LiteralValue{}, + CreatedAt: time.Now().Format(time.RFC3339), + } + + completedAt := time.Now().Add(-10 * time.Minute) + job1 := &oapi.Job{ + Id: "job-1", + JobAgentId: "agent-1", + Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt, + UpdatedAt: completedAt, + CompletedAt: &completedAt, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + + // Set verification status to passed + mock.jobVerificationStatus["job-1"] = string(oapi.JobVerificationStatusPassed) + + // With RequireVerificationPassed=true, the passed verification job should count + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} + + assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), + "expected 100%% success when verification passed and RequireVerificationPassed=true") +} + +func TestReleaseTargetJobTracker_RequireVerificationPassed_IncludesNoVerification(t *testing.T) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, + Version: *version, + Variables: map[string]oapi.LiteralValue{}, + CreatedAt: time.Now().Format(time.RFC3339), + } + + completedAt := time.Now().Add(-10 * time.Minute) + job1 := &oapi.Job{ + Id: "job-1", + JobAgentId: "agent-1", + Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt, + UpdatedAt: completedAt, + CompletedAt: &completedAt, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + + // No verification status set (empty string = no verification metrics) + // With RequireVerificationPassed=true, job with no verification should still count + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} + + assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), + "expected 100%% success when no verification metrics and RequireVerificationPassed=true") +} + +func TestReleaseTargetJobTracker_RequireVerificationPassed_False_IgnoresVerification(t *testing.T) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, + Version: *version, + Variables: map[string]oapi.LiteralValue{}, + CreatedAt: time.Now().Format(time.RFC3339), + } + + completedAt := time.Now().Add(-10 * time.Minute) + job1 := &oapi.Job{ + Id: "job-1", + JobAgentId: "agent-1", + Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt, + UpdatedAt: completedAt, + CompletedAt: &completedAt, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + + // Set verification status to failed + mock.jobVerificationStatus["job-1"] = string(oapi.JobVerificationStatusFailed) + + // With RequireVerificationPassed=false (default), failed verification should not block success + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} + + assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), + "expected 100%% success when RequireVerificationPassed=false even with failed verification") +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go index df1daa8aed..fa49857fd7 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go @@ -11,26 +11,28 @@ import ( var _ Getters = (*mockGetters)(nil) type mockGetters struct { - environments map[string]*oapi.Environment - deployments map[string]*oapi.Deployment - resources map[string]*oapi.Resource - releaseTargets []*oapi.ReleaseTarget - jobs map[string]map[string]*oapi.Job // releaseTarget.Key() -> jobID -> job - systemEnvs map[string][]string // envID -> systemIDs - releaseByJob map[string]*oapi.Release // jobID -> release - policies map[string]*oapi.Policy + environments map[string]*oapi.Environment + deployments map[string]*oapi.Deployment + resources map[string]*oapi.Resource + releaseTargets []*oapi.ReleaseTarget + jobs map[string]map[string]*oapi.Job // releaseTarget.Key() -> jobID -> job + systemEnvs map[string][]string // envID -> systemIDs + releaseByJob map[string]*oapi.Release // jobID -> release + policies map[string]*oapi.Policy + jobVerificationStatus map[string]string // jobID -> verification status } func newMockGetters() *mockGetters { return &mockGetters{ - environments: make(map[string]*oapi.Environment), - deployments: make(map[string]*oapi.Deployment), - resources: make(map[string]*oapi.Resource), - releaseTargets: make([]*oapi.ReleaseTarget, 0), - jobs: make(map[string]map[string]*oapi.Job), - systemEnvs: make(map[string][]string), - releaseByJob: make(map[string]*oapi.Release), - policies: make(map[string]*oapi.Policy), + environments: make(map[string]*oapi.Environment), + deployments: make(map[string]*oapi.Deployment), + resources: make(map[string]*oapi.Resource), + releaseTargets: make([]*oapi.ReleaseTarget, 0), + jobs: make(map[string]map[string]*oapi.Job), + systemEnvs: make(map[string][]string), + releaseByJob: make(map[string]*oapi.Release), + policies: make(map[string]*oapi.Policy), + jobVerificationStatus: make(map[string]string), } } @@ -144,12 +146,13 @@ func (m *mockGetters) GetJobsForEnvironmentAndVersion( continue } result = append(result, ReleaseTargetJob{ - JobID: job.Id, - Status: job.Status, - CompletedAt: job.CompletedAt, - DeploymentID: rel.ReleaseTarget.DeploymentId, - EnvironmentID: rel.ReleaseTarget.EnvironmentId, - ResourceID: rel.ReleaseTarget.ResourceId, + JobID: job.Id, + Status: job.Status, + CompletedAt: job.CompletedAt, + DeploymentID: rel.ReleaseTarget.DeploymentId, + EnvironmentID: rel.ReleaseTarget.EnvironmentId, + ResourceID: rel.ReleaseTarget.ResourceId, + VerificationStatus: m.jobVerificationStatus[job.Id], }) } } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/passrate.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/passrate.go index 61214822f5..53f34a4523 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/passrate.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/passrate.go @@ -57,6 +57,7 @@ func (e *PassRateEvaluator) Evaluate( scope.Environment, scope.Version, e.successStatuses, + false, ) return e.EvaluateWithTracker(tracker) } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/soaktime.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/soaktime.go index 367599d315..964134fd72 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/soaktime.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/soaktime.go @@ -66,7 +66,7 @@ func (e *SoakTimeEvaluator) Evaluate( environment := scope.Environment version := scope.Version - tracker := NewReleaseTargetJobTracker(ctx, e.getters, environment, version, e.successStatuses) + tracker := NewReleaseTargetJobTracker(ctx, e.getters, environment, version, e.successStatuses, false) return e.EvaluateWithTracker(tracker) } diff --git a/apps/workspace-engine/test/controllers/harness/mocks.go b/apps/workspace-engine/test/controllers/harness/mocks.go index 382a2fbb7d..a689f255d5 100644 --- a/apps/workspace-engine/test/controllers/harness/mocks.go +++ b/apps/workspace-engine/test/controllers/harness/mocks.go @@ -451,13 +451,20 @@ func (g *DesiredReleaseGetter) GetJobsForEnvironmentAndVersion( if !ok || rel.Version.Id != versionID { continue } + verificationStatus := "" + if g.JobVerificationStatuses != nil { + if vs, ok := g.JobVerificationStatuses[job.Id]; ok { + verificationStatus = string(vs) + } + } result = append(result, environmentprogression.ReleaseTargetJob{ - JobID: job.Id, - Status: job.Status, - CompletedAt: job.CompletedAt, - DeploymentID: depID, - EnvironmentID: envID, - ResourceID: resID, + JobID: job.Id, + Status: job.Status, + CompletedAt: job.CompletedAt, + DeploymentID: depID, + EnvironmentID: envID, + ResourceID: resID, + VerificationStatus: verificationStatus, }) } } diff --git a/packages/db/drizzle/0186_add_require_verification_passed.sql b/packages/db/drizzle/0186_add_require_verification_passed.sql new file mode 100644 index 0000000000..3ac98af0cc --- /dev/null +++ b/packages/db/drizzle/0186_add_require_verification_passed.sql @@ -0,0 +1 @@ +ALTER TABLE "policy_rule_environment_progression" ADD COLUMN "require_verification_passed" boolean DEFAULT false NOT NULL; diff --git a/packages/db/src/schema/policy.ts b/packages/db/src/schema/policy.ts index f6ca9936f2..376dae3e1e 100644 --- a/packages/db/src/schema/policy.ts +++ b/packages/db/src/schema/policy.ts @@ -139,6 +139,9 @@ export const policyRuleEnvironmentProgression = pgTable( minimumSoakTimeMinutes: integer("minimum_soak_time_minutes"), minimumSuccessPercentage: real("minimum_success_percentage"), successStatuses: text("success_statuses").array(), + requireVerificationPassed: boolean("require_verification_passed") + .notNull() + .default(false), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), From d28c4dc092fb0562bff1f9939bf77b698e938437 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Apr 2026 10:02:39 -0700 Subject: [PATCH 2/8] run generations --- apps/api/src/types/openapi.ts | 4 +- apps/web/app/api/openapi.ts | 5 + apps/workspace-engine/oapi/openapi.json | 5 + apps/workspace-engine/pkg/db/jobs.sql.go | 2 +- apps/workspace-engine/pkg/oapi/oapi.gen.go | 8 +- .../environmentprogression.go | 12 +- .../environmentprogression/jobtracker_test.go | 15 +- .../environmentprogression/mock_test.go | 18 +- .../environmentprogression/soaktime.go | 9 +- e2e/api/schema.ts | 6 +- ...on_passed.sql => 0186_cooing_doomsday.sql} | 2 +- packages/db/drizzle/meta/0186_snapshot.json | 6430 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/workspace-engine-sdk/src/schema.ts | 5 + 14 files changed, 6502 insertions(+), 26 deletions(-) rename packages/db/drizzle/{0186_add_require_verification_passed.sql => 0186_cooing_doomsday.sql} (52%) create mode 100644 packages/db/drizzle/meta/0186_snapshot.json diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index e5b19990be..0720f320ac 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -1411,10 +1411,10 @@ export interface components { */ minimumSuccessPercentage: number; /** - * @default false * @description If true, jobs must also have passed verification to count toward the success percentage + * @default false */ - requireVerificationPassed?: boolean; + requireVerificationPassed: boolean; successStatuses?: components["schemas"]["JobStatus"][]; }; EnvironmentRequestAccepted: { diff --git a/apps/web/app/api/openapi.ts b/apps/web/app/api/openapi.ts index 07730889a6..0720f320ac 100644 --- a/apps/web/app/api/openapi.ts +++ b/apps/web/app/api/openapi.ts @@ -1410,6 +1410,11 @@ export interface components { * @default 100 */ minimumSuccessPercentage: number; + /** + * @description If true, jobs must also have passed verification to count toward the success percentage + * @default false + */ + requireVerificationPassed: boolean; successStatuses?: components["schemas"]["JobStatus"][]; }; EnvironmentRequestAccepted: { diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 3e142fe54b..7090d678e9 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -621,6 +621,11 @@ "minimum": 0, "type": "number" }, + "requireVerificationPassed": { + "default": false, + "description": "If true, jobs must also have passed verification to count toward the success percentage", + "type": "boolean" + }, "successStatuses": { "items": { "$ref": "#/components/schemas/JobStatus" diff --git a/apps/workspace-engine/pkg/db/jobs.sql.go b/apps/workspace-engine/pkg/db/jobs.sql.go index e31f865451..bbd5333fba 100644 --- a/apps/workspace-engine/pkg/db/jobs.sql.go +++ b/apps/workspace-engine/pkg/db/jobs.sql.go @@ -438,7 +438,7 @@ type ListJobsByEnvironmentAndVersionRow struct { DeploymentID uuid.UUID EnvironmentID uuid.UUID ResourceID uuid.UUID - VerificationStatus string + VerificationStatus pgtype.Text } // Returns all jobs for a given environment and version in a single query, diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index a648e37557..7af1b7149c 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -446,12 +446,12 @@ type EnvironmentProgressionRule struct { MaximumAgeHours *int32 `json:"maximumAgeHours,omitempty"` // MinimumSoakTimeMinutes Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed - MinimumSoakTimeMinutes *int32 `json:"minimumSoakTimeMinutes,omitempty"` - MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage,omitempty"` - SuccessStatuses *[]JobStatus `json:"successStatuses,omitempty"` + MinimumSoakTimeMinutes *int32 `json:"minimumSoakTimeMinutes,omitempty"` + MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage,omitempty"` // RequireVerificationPassed If true, jobs must also have passed verification to count toward the success percentage - RequireVerificationPassed *bool `json:"requireVerificationPassed,omitempty"` + RequireVerificationPassed *bool `json:"requireVerificationPassed,omitempty"` + SuccessStatuses *[]JobStatus `json:"successStatuses,omitempty"` } // EnvironmentSummary defines model for EnvironmentSummary. 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 c8f198f4e9..19fe226ef2 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 @@ -304,8 +304,16 @@ func (e *EnvironmentProgressionEvaluator) evaluateJobSuccessCriteria( ctx, span := tracer.Start(ctx, "EnvironmentProgressionEvaluator.evaluateJobSuccessCriteria") defer span.End() - requireVerificationPassed := e.rule.RequireVerificationPassed != nil && *e.rule.RequireVerificationPassed - tracker := NewReleaseTargetJobTracker(ctx, e.getters, environment, version, successStatuses, requireVerificationPassed) + requireVerificationPassed := e.rule.RequireVerificationPassed != nil && + *e.rule.RequireVerificationPassed + tracker := NewReleaseTargetJobTracker( + ctx, + e.getters, + 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_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/jobtracker_test.go index 7e161f7ebb..b6415281f4 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 @@ -5,8 +5,9 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "workspace-engine/pkg/oapi" + + "github.com/stretchr/testify/assert" ) func TestGetReleaseTargets(t *testing.T) { @@ -666,7 +667,7 @@ func TestReleaseTargetJobTracker_FiltersByEnvironmentAndDeployment(t *testing.T) mock.addJob(rt2, job2, release2) // Tracker for env-1 should only see job-1 - tracker1 := NewReleaseTargetJobTracker(ctx, mock, env1, version, nil) + tracker1 := NewReleaseTargetJobTracker(ctx, mock, env1, version, nil, false) jobs1 := tracker1.Jobs() assert.Len(t, jobs1, 1, "expected 1 job for env-1") if len(jobs1) > 0 { @@ -674,7 +675,7 @@ func TestReleaseTargetJobTracker_FiltersByEnvironmentAndDeployment(t *testing.T) } // Tracker for env-2 should only see job-2 - tracker2 := NewReleaseTargetJobTracker(ctx, mock, env2, version, nil) + tracker2 := NewReleaseTargetJobTracker(ctx, mock, env2, version, nil, false) jobs2 := tracker2.Jobs() assert.Len(t, jobs2, 1, "expected 1 job for env-2") if len(jobs2) > 0 { @@ -1168,7 +1169,9 @@ func TestReleaseTargetJobTracker_GetSuccessPercentageSatisfiedAt_OutOfOrderCompl ) } -func TestReleaseTargetJobTracker_RequireVerificationPassed_ExcludesFailedVerification(t *testing.T) { +func TestReleaseTargetJobTracker_RequireVerificationPassed_ExcludesFailedVerification( + t *testing.T, +) { mock, version := setupMockForJobTracker() ctx := context.Background() @@ -1211,7 +1214,9 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed_ExcludesFailedVerific "expected 0%% success when verification failed and RequireVerificationPassed=true") } -func TestReleaseTargetJobTracker_RequireVerificationPassed_IncludesPassedVerification(t *testing.T) { +func TestReleaseTargetJobTracker_RequireVerificationPassed_IncludesPassedVerification( + t *testing.T, +) { mock, version := setupMockForJobTracker() ctx := context.Background() diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go index fa49857fd7..28167110f0 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go @@ -11,15 +11,15 @@ import ( var _ Getters = (*mockGetters)(nil) type mockGetters struct { - environments map[string]*oapi.Environment - deployments map[string]*oapi.Deployment - resources map[string]*oapi.Resource - releaseTargets []*oapi.ReleaseTarget - jobs map[string]map[string]*oapi.Job // releaseTarget.Key() -> jobID -> job - systemEnvs map[string][]string // envID -> systemIDs - releaseByJob map[string]*oapi.Release // jobID -> release - policies map[string]*oapi.Policy - jobVerificationStatus map[string]string // jobID -> verification status + environments map[string]*oapi.Environment + deployments map[string]*oapi.Deployment + resources map[string]*oapi.Resource + releaseTargets []*oapi.ReleaseTarget + jobs map[string]map[string]*oapi.Job // releaseTarget.Key() -> jobID -> job + systemEnvs map[string][]string // envID -> systemIDs + releaseByJob map[string]*oapi.Release // jobID -> release + policies map[string]*oapi.Policy + jobVerificationStatus map[string]string // jobID -> verification status } func newMockGetters() *mockGetters { diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/soaktime.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/soaktime.go index 964134fd72..71f41712c7 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/soaktime.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/soaktime.go @@ -66,7 +66,14 @@ func (e *SoakTimeEvaluator) Evaluate( environment := scope.Environment version := scope.Version - tracker := NewReleaseTargetJobTracker(ctx, e.getters, environment, version, e.successStatuses, false) + tracker := NewReleaseTargetJobTracker( + ctx, + e.getters, + environment, + version, + e.successStatuses, + false, + ) return e.EvaluateWithTracker(tracker) } diff --git a/e2e/api/schema.ts b/e2e/api/schema.ts index 6d74bcba8a..0720f320ac 100644 --- a/e2e/api/schema.ts +++ b/e2e/api/schema.ts @@ -1402,7 +1402,6 @@ export interface components { maximumAgeHours?: number; /** * Format: int32 - * @deprecated * @description Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed. Defaults to 0 if not provided. */ minimumSoakTimeMinutes?: number; @@ -1411,6 +1410,11 @@ export interface components { * @default 100 */ minimumSuccessPercentage: number; + /** + * @description If true, jobs must also have passed verification to count toward the success percentage + * @default false + */ + requireVerificationPassed: boolean; successStatuses?: components["schemas"]["JobStatus"][]; }; EnvironmentRequestAccepted: { diff --git a/packages/db/drizzle/0186_add_require_verification_passed.sql b/packages/db/drizzle/0186_cooing_doomsday.sql similarity index 52% rename from packages/db/drizzle/0186_add_require_verification_passed.sql rename to packages/db/drizzle/0186_cooing_doomsday.sql index 3ac98af0cc..75413ed62f 100644 --- a/packages/db/drizzle/0186_add_require_verification_passed.sql +++ b/packages/db/drizzle/0186_cooing_doomsday.sql @@ -1 +1 @@ -ALTER TABLE "policy_rule_environment_progression" ADD COLUMN "require_verification_passed" boolean DEFAULT false NOT NULL; +ALTER TABLE "policy_rule_environment_progression" ADD COLUMN "require_verification_passed" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0186_snapshot.json b/packages/db/drizzle/meta/0186_snapshot.json new file mode 100644 index 0000000000..11e21a3ddd --- /dev/null +++ b/packages/db/drizzle/meta/0186_snapshot.json @@ -0,0 +1,6430 @@ +{ + "id": "e54e0502-cd4c-42e1-8de8-7d3410c7a098", + "prevId": "a69f6421-a6ca-4b0f-b312-39429522d0c6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_session_token_unique": { + "name": "session_session_token_unique", + "nullsNotDistinct": false, + "columns": ["session_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "active_workspace_id": { + "name": "active_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "system_role": { + "name": "system_role", + "type": "system_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_active_workspace_id_workspace_id_fk": { + "name": "user_active_workspace_id_workspace_id_fk", + "tableFrom": "user", + "tableTo": "workspace", + "columnsFrom": ["active_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_api_key": { + "name": "user_api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key_preview": { + "name": "key_preview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_api_key_key_prefix_key_hash_index": { + "name": "user_api_key_key_prefix_key_hash_index", + "columns": [ + { + "expression": "key_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_api_key_user_id_user_id_fk": { + "name": "user_api_key_user_id_user_id_fk", + "tableFrom": "user_api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_entry": { + "name": "changelog_entry", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_data": { + "name": "entity_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "changelog_entry_workspace_id_workspace_id_fk": { + "name": "changelog_entry_workspace_id_workspace_id_fk", + "tableFrom": "changelog_entry", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "changelog_entry_workspace_id_entity_type_entity_id_pk": { + "name": "changelog_entry_workspace_id_entity_type_entity_id_pk", + "columns": ["workspace_id", "entity_type", "entity_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_widget": { + "name": "dashboard_widget", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "widget": { + "name": "widget", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "w": { + "name": "w", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "h": { + "name": "h", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_widget_dashboard_id_dashboard_id_fk": { + "name": "dashboard_widget_dashboard_id_dashboard_id_fk", + "tableFrom": "dashboard_widget", + "tableTo": "dashboard", + "columnsFrom": ["dashboard_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan": { + "name": "deployment_plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_tag": { + "name": "version_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_name": { + "name": "version_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_config": { + "name": "version_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version_job_agent_config": { + "name": "version_job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version_metadata": { + "name": "version_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deployment_plan_workspace_id_index": { + "name": "deployment_plan_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_deployment_id_index": { + "name": "deployment_plan_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_expires_at_index": { + "name": "deployment_plan_expires_at_index", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_workspace_id_workspace_id_fk": { + "name": "deployment_plan_workspace_id_workspace_id_fk", + "tableFrom": "deployment_plan", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_deployment_id_deployment_id_fk": { + "name": "deployment_plan_deployment_id_deployment_id_fk", + "tableFrom": "deployment_plan", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target": { + "name": "deployment_plan_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_release_id": { + "name": "current_release_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_plan_target_plan_id_index": { + "name": "deployment_plan_target_plan_id_index", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_target_plan_id_environment_id_resource_id_index": { + "name": "deployment_plan_target_plan_id_environment_id_resource_id_index", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_plan_id_deployment_plan_id_fk": { + "name": "deployment_plan_target_plan_id_deployment_plan_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "deployment_plan", + "columnsFrom": ["plan_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_environment_id_environment_id_fk": { + "name": "deployment_plan_target_environment_id_environment_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_resource_id_resource_id_fk": { + "name": "deployment_plan_target_resource_id_resource_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_current_release_id_release_id_fk": { + "name": "deployment_plan_target_current_release_id_release_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "release", + "columnsFrom": ["current_release_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target_result": { + "name": "deployment_plan_target_result", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dispatch_context": { + "name": "dispatch_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deployment_plan_target_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'computing'" + }, + "has_changes": { + "name": "has_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current": { + "name": "current", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed": { + "name": "proposed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_plan_target_result_target_id_index": { + "name": "deployment_plan_target_result_target_id_index", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_result_target_id_deployment_plan_target_id_fk": { + "name": "deployment_plan_target_result_target_id_deployment_plan_target_id_fk", + "tableFrom": "deployment_plan_target_result", + "tableTo": "deployment_plan_target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target_variable": { + "name": "deployment_plan_target_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "deployment_plan_target_variable_target_id_key_index": { + "name": "deployment_plan_target_variable_target_id_key_index", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_variable_target_id_deployment_plan_target_id_fk": { + "name": "deployment_plan_target_variable_target_id_deployment_plan_target_id_fk", + "tableFrom": "deployment_plan_target_variable", + "tableTo": "deployment_plan_target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_trace_span": { + "name": "deployment_trace_span", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "span_id": { + "name": "span_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_span_id": { + "name": "parent_span_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_target_key": { + "name": "release_target_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_trace_id": { + "name": "parent_trace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "node_type": { + "name": "node_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deployment_trace_span_trace_span_idx": { + "name": "deployment_trace_span_trace_span_idx", + "columns": [ + { + "expression": "trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "span_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_trace_id_idx": { + "name": "deployment_trace_span_trace_id_idx", + "columns": [ + { + "expression": "trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_parent_span_id_idx": { + "name": "deployment_trace_span_parent_span_id_idx", + "columns": [ + { + "expression": "parent_span_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_workspace_id_idx": { + "name": "deployment_trace_span_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_release_target_key_idx": { + "name": "deployment_trace_span_release_target_key_idx", + "columns": [ + { + "expression": "release_target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_release_id_idx": { + "name": "deployment_trace_span_release_id_idx", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_job_id_idx": { + "name": "deployment_trace_span_job_id_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_parent_trace_id_idx": { + "name": "deployment_trace_span_parent_trace_id_idx", + "columns": [ + { + "expression": "parent_trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_created_at_idx": { + "name": "deployment_trace_span_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_phase_idx": { + "name": "deployment_trace_span_phase_idx", + "columns": [ + { + "expression": "phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_node_type_idx": { + "name": "deployment_trace_span_node_type_idx", + "columns": [ + { + "expression": "node_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_status_idx": { + "name": "deployment_trace_span_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_trace_span_workspace_id_workspace_id_fk": { + "name": "deployment_trace_span_workspace_id_workspace_id_fk", + "tableFrom": "deployment_trace_span", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_variable": { + "name": "deployment_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_value": { + "name": "default_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_variable_deployment_id_index": { + "name": "deployment_variable_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_deployment_id_deployment_id_fk": { + "name": "deployment_variable_deployment_id_deployment_id_fk", + "tableFrom": "deployment_variable", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_variable_deployment_id_key_unique": { + "name": "deployment_variable_deployment_id_key_unique", + "nullsNotDistinct": false, + "columns": ["deployment_id", "key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_variable_value": { + "name": "deployment_variable_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_variable_id": { + "name": "deployment_variable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "deployment_variable_value_deployment_variable_id_index": { + "name": "deployment_variable_value_deployment_variable_id_index", + "columns": [ + { + "expression": "deployment_variable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_value_deployment_variable_id_deployment_variable_id_fk": { + "name": "deployment_variable_value_deployment_variable_id_deployment_variable_id_fk", + "tableFrom": "deployment_variable_value", + "tableTo": "deployment_variable", + "columnsFrom": ["deployment_variable_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_version": { + "name": "deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "deployment_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_version_deployment_id_tag_index": { + "name": "deployment_version_deployment_id_tag_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_version_created_at_idx": { + "name": "deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_version_workspace_id_workspace_id_fk": { + "name": "deployment_version_workspace_id_workspace_id_fk", + "tableFrom": "deployment_version", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_deployment_resource": { + "name": "computed_deployment_resource", + "schema": "", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "computed_deployment_resource_deployment_id_deployment_id_fk": { + "name": "computed_deployment_resource_deployment_id_deployment_id_fk", + "tableFrom": "computed_deployment_resource", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_deployment_resource_resource_id_resource_id_fk": { + "name": "computed_deployment_resource_resource_id_resource_id_fk", + "tableFrom": "computed_deployment_resource", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_deployment_resource_deployment_id_resource_id_pk": { + "name": "computed_deployment_resource_deployment_id_resource_id_pk", + "columns": ["deployment_id", "resource_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "job_agent_selector": { + "name": "job_agent_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'false'" + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_workspace_id_index": { + "name": "deployment_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_workspace_id_workspace_id_fk": { + "name": "deployment_workspace_id_workspace_id_fk", + "tableFrom": "deployment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_environment_resource": { + "name": "computed_environment_resource", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "computed_environment_resource_environment_id_environment_id_fk": { + "name": "computed_environment_resource_environment_id_environment_id_fk", + "tableFrom": "computed_environment_resource", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_environment_resource_resource_id_resource_id_fk": { + "name": "computed_environment_resource_resource_id_resource_id_fk", + "tableFrom": "computed_environment_resource", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_environment_resource_environment_id_resource_id_pk": { + "name": "computed_environment_resource_environment_id_resource_id_pk", + "columns": ["environment_id", "resource_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "environment_workspace_id_index": { + "name": "environment_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_workspace_id_workspace_id_fk": { + "name": "environment_workspace_id_workspace_id_fk", + "tableFrom": "environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event": { + "name": "event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "event_workspace_id_workspace_id_fk": { + "name": "event_workspace_id_workspace_id_fk", + "tableFrom": "event", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource": { + "name": "resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resource_identifier_workspace_id_index": { + "name": "resource_identifier_workspace_id_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_workspace_id_active_idx": { + "name": "resource_workspace_id_active_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_workspace_id_deleted_at_index": { + "name": "resource_workspace_id_deleted_at_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_provider_id_resource_provider_id_fk": { + "name": "resource_provider_id_resource_provider_id_fk", + "tableFrom": "resource", + "tableTo": "resource_provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_workspace_id_workspace_id_fk": { + "name": "resource_workspace_id_workspace_id_fk", + "tableFrom": "resource", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_aggregate": { + "name": "resource_aggregate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "filter": { + "name": "filter", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'true'" + }, + "group_by": { + "name": "group_by", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resource_aggregate_workspace_id_index": { + "name": "resource_aggregate_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_aggregate_workspace_id_workspace_id_fk": { + "name": "resource_aggregate_workspace_id_workspace_id_fk", + "tableFrom": "resource_aggregate", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "resource_aggregate_created_by_user_id_fk": { + "name": "resource_aggregate_created_by_user_id_fk", + "tableFrom": "resource_aggregate", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_schema": { + "name": "resource_schema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "json_schema": { + "name": "json_schema", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "resource_schema_version_kind_workspace_id_index": { + "name": "resource_schema_version_kind_workspace_id_index", + "columns": [ + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_schema_workspace_id_workspace_id_fk": { + "name": "resource_schema_workspace_id_workspace_id_fk", + "tableFrom": "resource_schema", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_provider": { + "name": "resource_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "resource_provider_workspace_id_name_index": { + "name": "resource_provider_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_provider_workspace_id_workspace_id_fk": { + "name": "resource_provider_workspace_id_workspace_id_fk", + "tableFrom": "resource_provider", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system": { + "name": "system", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "system_workspace_id_index": { + "name": "system_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "system_workspace_id_workspace_id_fk": { + "name": "system_workspace_id_workspace_id_fk", + "tableFrom": "system", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_deployment": { + "name": "system_deployment", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "system_deployment_system_id_system_id_fk": { + "name": "system_deployment_system_id_system_id_fk", + "tableFrom": "system_deployment", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_deployment_deployment_id_deployment_id_fk": { + "name": "system_deployment_deployment_id_deployment_id_fk", + "tableFrom": "system_deployment", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "system_deployment_system_id_deployment_id_pk": { + "name": "system_deployment_system_id_deployment_id_pk", + "columns": ["system_id", "deployment_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_environment": { + "name": "system_environment", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "system_environment_system_id_system_id_fk": { + "name": "system_environment_system_id_system_id_fk", + "tableFrom": "system_environment", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_environment_environment_id_environment_id_fk": { + "name": "system_environment_environment_id_environment_id_fk", + "tableFrom": "system_environment", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "system_environment_system_id_environment_id_pk": { + "name": "system_environment_system_id_environment_id_pk", + "columns": ["system_id", "environment_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_workspace_id_workspace_id_fk": { + "name": "team_workspace_id_workspace_id_fk", + "tableFrom": "team", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_member": { + "name": "team_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "team_member_team_id_user_id_index": { + "name": "team_member_team_id_user_id_index", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_member_team_id_team_id_fk": { + "name": "team_member_team_id_team_id_fk", + "tableFrom": "team_member", + "tableTo": "team", + "columnsFrom": ["team_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_member_user_id_user_id_fk": { + "name": "team_member_user_id_user_id_fk", + "tableFrom": "team_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trace_token": { + "name": "trace_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispatch_context": { + "name": "dispatch_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "job_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'policy_passing'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_created_at_idx": { + "name": "job_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_status_idx": { + "name": "job_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_external_id_idx": { + "name": "job_external_id_idx", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_job_agent_id_job_agent_id_fk": { + "name": "job_job_agent_id_job_agent_id_fk", + "tableFrom": "job", + "tableTo": "job_agent", + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_metadata": { + "name": "job_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_metadata_key_job_id_index": { + "name": "job_metadata_key_job_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_metadata_job_id_idx": { + "name": "job_metadata_job_id_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_metadata_job_id_job_id_fk": { + "name": "job_metadata_job_id_job_id_fk", + "tableFrom": "job_metadata", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_variable": { + "name": "job_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "job_variable_job_id_key_index": { + "name": "job_variable_job_id_key_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_variable_job_id_job_id_fk": { + "name": "job_variable_job_id_job_id_fk", + "tableFrom": "job_variable", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_email_domain_matching": { + "name": "workspace_email_domain_matching", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verification_code": { + "name": "verification_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_email": { + "name": "verification_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_email_domain_matching_workspace_id_domain_index": { + "name": "workspace_email_domain_matching_workspace_id_domain_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_email_domain_matching_workspace_id_workspace_id_fk": { + "name": "workspace_email_domain_matching_workspace_id_workspace_id_fk", + "tableFrom": "workspace_email_domain_matching", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_email_domain_matching_role_id_role_id_fk": { + "name": "workspace_email_domain_matching_role_id_role_id_fk", + "tableFrom": "workspace_email_domain_matching", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invite_token": { + "name": "workspace_invite_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invite_token_role_id_role_id_fk": { + "name": "workspace_invite_token_role_id_role_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_token_workspace_id_workspace_id_fk": { + "name": "workspace_invite_token_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_token_created_by_user_id_fk": { + "name": "workspace_invite_token_created_by_user_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invite_token_token_unique": { + "name": "workspace_invite_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_role": { + "name": "entity_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "scope_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "entity_role_role_id_entity_type_entity_id_scope_id_scope_type_index": { + "name": "entity_role_role_id_entity_type_entity_id_scope_id_scope_type_index", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entity_role_role_id_role_id_fk": { + "name": "entity_role_role_id_role_id_fk", + "tableFrom": "entity_role", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "role_workspace_id_workspace_id_fk": { + "name": "role_workspace_id_workspace_id_fk", + "tableFrom": "role", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permission": { + "name": "role_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "role_permission_role_id_permission_index": { + "name": "role_permission_role_id_permission_index", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_permission_role_id_role_id_fk": { + "name": "role_permission_role_id_role_id_fk", + "tableFrom": "role_permission", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release": { + "name": "release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "release_resource_id_environment_id_deployment_id_index": { + "name": "release_resource_id_environment_id_deployment_id_index", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_deployment_id_index": { + "name": "release_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_resource_id_resource_id_fk": { + "name": "release_resource_id_resource_id_fk", + "tableFrom": "release", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_environment_id_environment_id_fk": { + "name": "release_environment_id_environment_id_fk", + "tableFrom": "release", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_deployment_id_deployment_id_fk": { + "name": "release_deployment_id_deployment_id_fk", + "tableFrom": "release", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_version_id_deployment_version_id_fk": { + "name": "release_version_id_deployment_version_id_fk", + "tableFrom": "release", + "tableTo": "deployment_version", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_job": { + "name": "release_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "release_job_release_id_job_id_index": { + "name": "release_job_release_id_job_id_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_job_job_id_index": { + "name": "release_job_job_id_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_job_release_id_index": { + "name": "release_job_release_id_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_job_job_id_job_id_fk": { + "name": "release_job_job_id_job_id_fk", + "tableFrom": "release_job", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_job_release_id_release_id_fk": { + "name": "release_job_release_id_release_id_fk", + "tableFrom": "release_job", + "tableTo": "release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_target_desired_release": { + "name": "release_target_desired_release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "desired_release_id": { + "name": "desired_release_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "release_target_desired_release_resource_id_environment_id_deployment_id_index": { + "name": "release_target_desired_release_resource_id_environment_id_deployment_id_index", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_target_desired_release_resource_id_resource_id_fk": { + "name": "release_target_desired_release_resource_id_resource_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_environment_id_environment_id_fk": { + "name": "release_target_desired_release_environment_id_environment_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_deployment_id_deployment_id_fk": { + "name": "release_target_desired_release_deployment_id_deployment_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_desired_release_id_release_id_fk": { + "name": "release_target_desired_release_desired_release_id_release_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "release", + "columnsFrom": ["desired_release_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_variable": { + "name": "release_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "release_variable_release_id_key_index": { + "name": "release_variable_release_id_key_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_variable_release_id_release_id_fk": { + "name": "release_variable_release_id_release_id_fk", + "tableFrom": "release_variable", + "tableTo": "release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reconcile_work_scope": { + "name": "reconcile_work_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "reconcile_work_scope_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "event_ts": { + "name": "event_ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "not_before": { + "name": "not_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_until": { + "name": "claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reconcile_work_scope_workspace_id_kind_scope_type_scope_id_index": { + "name": "reconcile_work_scope_workspace_id_kind_scope_type_scope_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reconcile_work_scope_unclaimed_idx": { + "name": "reconcile_work_scope_unclaimed_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_ts", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reconcile_work_scope\".\"claimed_until\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "reconcile_work_scope_expired_claims_idx": { + "name": "reconcile_work_scope_expired_claims_idx", + "columns": [ + { + "expression": "claimed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reconcile_work_scope\".\"claimed_until\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy": { + "name": "policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'true'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "policy_workspace_id_index": { + "name": "policy_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_workspace_id_workspace_id_fk": { + "name": "policy_workspace_id_workspace_id_fk", + "tableFrom": "policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_any_approval": { + "name": "policy_rule_any_approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "min_approvals": { + "name": "min_approvals", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_any_approval_policy_id_policy_id_fk": { + "name": "policy_rule_any_approval_policy_id_policy_id_fk", + "tableFrom": "policy_rule_any_approval", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_deployment_dependency": { + "name": "policy_rule_deployment_dependency", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_dependency_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_dependency_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_dependency", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_deployment_window": { + "name": "policy_rule_deployment_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allow_window": { + "name": "allow_window", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_window_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_window_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_window", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_environment_progression": { + "name": "policy_rule_environment_progression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "depends_on_environment_selector": { + "name": "depends_on_environment_selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maximum_age_hours": { + "name": "maximum_age_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "minimum_soak_time_minutes": { + "name": "minimum_soak_time_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "minimum_success_percentage": { + "name": "minimum_success_percentage", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "success_statuses": { + "name": "success_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "require_verification_passed": { + "name": "require_verification_passed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_environment_progression_policy_id_policy_id_fk": { + "name": "policy_rule_environment_progression_policy_id_policy_id_fk", + "tableFrom": "policy_rule_environment_progression", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_gradual_rollout": { + "name": "policy_rule_gradual_rollout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rollout_type": { + "name": "rollout_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_scale_interval": { + "name": "time_scale_interval", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_gradual_rollout_policy_id_policy_id_fk": { + "name": "policy_rule_gradual_rollout_policy_id_policy_id_fk", + "tableFrom": "policy_rule_gradual_rollout", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_retry": { + "name": "policy_rule_retry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "max_retries": { + "name": "max_retries", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "backoff_seconds": { + "name": "backoff_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backoff_strategy": { + "name": "backoff_strategy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_backoff_seconds": { + "name": "max_backoff_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "retry_on_statuses": { + "name": "retry_on_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_retry_policy_id_policy_id_fk": { + "name": "policy_rule_retry_policy_id_policy_id_fk", + "tableFrom": "policy_rule_retry", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_rollback": { + "name": "policy_rule_rollback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "on_job_statuses": { + "name": "on_job_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "on_verification_failure": { + "name": "on_verification_failure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_rollback_policy_id_policy_id_fk": { + "name": "policy_rule_rollback_policy_id_policy_id_fk", + "tableFrom": "policy_rule_rollback", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_verification": { + "name": "policy_rule_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "trigger_on": { + "name": "trigger_on", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_verification_policy_id_policy_id_fk": { + "name": "policy_rule_verification_policy_id_policy_id_fk", + "tableFrom": "policy_rule_verification", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_version_cooldown": { + "name": "policy_rule_version_cooldown", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_version_cooldown_policy_id_policy_id_fk": { + "name": "policy_rule_version_cooldown_policy_id_policy_id_fk", + "tableFrom": "policy_rule_version_cooldown", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_version_selector": { + "name": "policy_rule_version_selector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_version_selector_policy_id_policy_id_fk": { + "name": "policy_rule_version_selector_policy_id_policy_id_fk", + "tableFrom": "policy_rule_version_selector", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_approval_record": { + "name": "user_approval_record", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_approval_record_version_id_user_id_environment_id_pk": { + "name": "user_approval_record_version_id_user_id_environment_id_pk", + "columns": ["version_id", "user_id", "environment_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_variable": { + "name": "resource_variable", + "schema": "", + "columns": { + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "resource_variable_resource_id_resource_id_fk": { + "name": "resource_variable_resource_id_resource_id_fk", + "tableFrom": "resource_variable", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "resource_variable_resource_id_key_pk": { + "name": "resource_variable_resource_id_key_pk", + "columns": ["resource_id", "key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "job_agents": { + "name": "job_agents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_job": { + "name": "workflow_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_job_workflow_run_id_workflow_run_id_fk": { + "name": "workflow_job_workflow_run_id_workflow_run_id_fk", + "tableFrom": "workflow_job", + "tableTo": "workflow_run", + "columnsFrom": ["workflow_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_job_job_id_job_id_fk": { + "name": "workflow_job_job_id_job_id_fk", + "tableFrom": "workflow_job", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run": { + "name": "workflow_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_workflow_id_workflow_id_fk": { + "name": "workflow_run_workflow_id_workflow_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_skip": { + "name": "policy_skip", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_policy_release_target": { + "name": "computed_policy_release_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "computed_policy_release_target_policy_id_environment_id_deployment_id_resource_id_index": { + "name": "computed_policy_release_target_policy_id_environment_id_deployment_id_resource_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_policy_release_target_policy_id_index": { + "name": "computed_policy_release_target_policy_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_policy_release_target_resource_id_environment_id_deployment_id_index": { + "name": "computed_policy_release_target_resource_id_environment_id_deployment_id_index", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "computed_policy_release_target_policy_id_policy_id_fk": { + "name": "computed_policy_release_target_policy_id_policy_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_environment_id_environment_id_fk": { + "name": "computed_policy_release_target_environment_id_environment_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_deployment_id_deployment_id_fk": { + "name": "computed_policy_release_target_deployment_id_deployment_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_resource_id_resource_id_fk": { + "name": "computed_policy_release_target_resource_id_resource_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_evaluation": { + "name": "policy_rule_evaluation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "rule_type": { + "name": "rule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "action_required": { + "name": "action_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "satisfied_at": { + "name": "satisfied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_evaluation_at": { + "name": "next_evaluation_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evaluated_at": { + "name": "evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "policy_rule_evaluation_rule_id_environment_id_version_id_resource_id_index": { + "name": "policy_rule_evaluation_rule_id_environment_id_version_id_resource_id_index", + "columns": [ + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "policy_rule_evaluation_environment_id_version_id_resource_id_rule_type_index": { + "name": "policy_rule_evaluation_environment_id_version_id_resource_id_rule_type_index", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_rule_evaluation_environment_id_environment_id_fk": { + "name": "policy_rule_evaluation_environment_id_environment_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_evaluation_version_id_deployment_version_id_fk": { + "name": "policy_rule_evaluation_version_id_deployment_version_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "deployment_version", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_evaluation_resource_id_resource_id_fk": { + "name": "policy_rule_evaluation_resource_id_resource_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_verification_metric_measurement": { + "name": "job_verification_metric_measurement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_verification_metric_status_id": { + "name": "job_verification_metric_status_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "measured_at": { + "name": "measured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "job_verification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_verification_metric_measurement_job_verification_metric_status_id_index": { + "name": "job_verification_metric_measurement_job_verification_metric_status_id_index", + "columns": [ + { + "expression": "job_verification_metric_status_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_verification_metric_measurement_job_verification_metric_status_id_job_verification_metric_id_fk": { + "name": "job_verification_metric_measurement_job_verification_metric_status_id_job_verification_metric_id_fk", + "tableFrom": "job_verification_metric_measurement", + "tableTo": "job_verification_metric", + "columnsFrom": ["job_verification_metric_status_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_verification_metric": { + "name": "job_verification_metric", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_rule_verification_metric_id": { + "name": "policy_rule_verification_metric_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success_threshold": { + "name": "success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "failure_condition": { + "name": "failure_condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "failure_threshold": { + "name": "failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": { + "job_verification_metric_job_id_index": { + "name": "job_verification_metric_job_id_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_verification_metric_policy_rule_verification_metric_id_index": { + "name": "job_verification_metric_policy_rule_verification_metric_id_index", + "columns": [ + { + "expression": "policy_rule_verification_metric_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_verification_metric_policy_rule_verification_metric_id_policy_rule_job_verification_metric_id_fk": { + "name": "job_verification_metric_policy_rule_verification_metric_id_policy_rule_job_verification_metric_id_fk", + "tableFrom": "job_verification_metric", + "tableTo": "policy_rule_job_verification_metric", + "columnsFrom": ["policy_rule_verification_metric_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_job_verification_metric": { + "name": "policy_rule_job_verification_metric", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger_on": { + "name": "trigger_on", + "type": "job_verification_trigger_on", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'jobSuccess'" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success_threshold": { + "name": "success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "failure_condition": { + "name": "failure_condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "failure_threshold": { + "name": "failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_job_verification_metric_policy_id_policy_id_fk": { + "name": "policy_rule_job_verification_metric_policy_id_policy_id_fk", + "tableFrom": "policy_rule_job_verification_metric", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_entity_relationship": { + "name": "computed_entity_relationship", + "schema": "", + "columns": { + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_entity_type": { + "name": "from_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_entity_id": { + "name": "from_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_entity_type": { + "name": "to_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_entity_id": { + "name": "to_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "computed_entity_relationship_from_idx": { + "name": "computed_entity_relationship_from_idx", + "columns": [ + { + "expression": "from_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_entity_relationship_to_idx": { + "name": "computed_entity_relationship_to_idx", + "columns": [ + { + "expression": "to_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "computed_entity_relationship_rule_id_relationship_rule_id_fk": { + "name": "computed_entity_relationship_rule_id_relationship_rule_id_fk", + "tableFrom": "computed_entity_relationship", + "tableTo": "relationship_rule", + "columnsFrom": ["rule_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_entity_relationship_rule_id_from_entity_type_from_entity_id_to_entity_type_to_entity_id_pk": { + "name": "computed_entity_relationship_rule_id_from_entity_type_from_entity_id_to_entity_type_to_entity_id_pk", + "columns": [ + "rule_id", + "from_entity_type", + "from_entity_id", + "to_entity_type", + "to_entity_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relationship_rule": { + "name": "relationship_rule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cel": { + "name": "cel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "relationship_rule_workspace_id_reference_index": { + "name": "relationship_rule_workspace_id_reference_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "relationship_rule_workspace_id_index": { + "name": "relationship_rule_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relationship_rule_workspace_id_workspace_id_fk": { + "name": "relationship_rule_workspace_id_workspace_id_fk", + "tableFrom": "relationship_rule", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_agent": { + "name": "job_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "job_agent_workspace_id_name_index": { + "name": "job_agent_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_agent_workspace_id_workspace_id_fk": { + "name": "job_agent_workspace_id_workspace_id_fk", + "tableFrom": "job_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set": { + "name": "variable_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_workspace_id_workspace_id_fk": { + "name": "variable_set_workspace_id_workspace_id_fk", + "tableFrom": "variable_set", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set_variable": { + "name": "variable_set_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_set_id": { + "name": "variable_set_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_variable_variable_set_id_variable_set_id_fk": { + "name": "variable_set_variable_variable_set_id_variable_set_id_fk", + "tableFrom": "variable_set_variable", + "tableTo": "variable_set", + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "variable_set_variable_variable_set_id_key_unique": { + "name": "variable_set_variable_variable_set_id_key_unique", + "nullsNotDistinct": false, + "columns": ["variable_set_id", "key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.system_role": { + "name": "system_role", + "schema": "public", + "values": ["user", "admin"] + }, + "public.deployment_plan_target_status": { + "name": "deployment_plan_target_status", + "schema": "public", + "values": ["computing", "completed", "errored", "unsupported"] + }, + "public.deployment_version_status": { + "name": "deployment_version_status", + "schema": "public", + "values": [ + "unspecified", + "building", + "ready", + "failed", + "rejected", + "paused" + ] + }, + "public.job_reason": { + "name": "job_reason", + "schema": "public", + "values": [ + "policy_passing", + "policy_override", + "env_policy_override", + "config_policy_override", + "redeploy" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "cancelled", + "skipped", + "in_progress", + "action_required", + "pending", + "failure", + "invalid_job_agent", + "invalid_integration", + "external_run_not_found", + "successful" + ] + }, + "public.entity_type": { + "name": "entity_type", + "schema": "public", + "values": ["user", "team"] + }, + "public.scope_type": { + "name": "scope_type", + "schema": "public", + "values": [ + "deploymentVersion", + "resource", + "resourceProvider", + "workspace", + "environment", + "system", + "deployment" + ] + }, + "public.job_verification_status": { + "name": "job_verification_status", + "schema": "public", + "values": ["failed", "inconclusive", "passed"] + }, + "public.job_verification_trigger_on": { + "name": "job_verification_trigger_on", + "schema": "public", + "values": ["jobCreated", "jobStarted", "jobSuccess", "jobFailure"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 013387a636..d9cf4289ea 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1303,6 +1303,13 @@ "when": 1775686833517, "tag": "0185_steep_scarecrow", "breakpoints": true + }, + { + "idx": 186, + "version": "7", + "when": 1776272131457, + "tag": "0186_cooing_doomsday", + "breakpoints": true } ] } diff --git a/packages/workspace-engine-sdk/src/schema.ts b/packages/workspace-engine-sdk/src/schema.ts index 740ffa8644..3202bdd337 100644 --- a/packages/workspace-engine-sdk/src/schema.ts +++ b/packages/workspace-engine-sdk/src/schema.ts @@ -400,6 +400,11 @@ export interface components { * @default 100 */ minimumSuccessPercentage: number; + /** + * @description If true, jobs must also have passed verification to count toward the success percentage + * @default false + */ + requireVerificationPassed: boolean; successStatuses?: components["schemas"]["JobStatus"][]; }; EnvironmentSummary: { From 6435843a53bf26d5c95a7f7dfc602257bd52c11e Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Apr 2026 10:58:03 -0700 Subject: [PATCH 3/8] cleanup --- .../pkg/db/job_verification_metric.sql.go | 53 +++ apps/workspace-engine/pkg/db/jobs.sql.go | 38 +- .../db/queries/job_verification_metric.sql | 16 + apps/workspace-engine/pkg/db/queries/jobs.sql | 24 +- .../environmentprogression/getters.go | 105 +++++- .../environmentprogression/jobtracker.go | 20 +- .../environmentprogression/jobtracker_test.go | 324 +++++++++++++++++- .../environmentprogression/mock_test.go | 26 +- .../gradualrollout/gradualrollout_test.go | 5 + .../policyeval/policyeval_test.go | 5 + .../desiredrelease/reconcile_test.go | 5 + .../test/controllers/harness/mocks.go | 35 +- 12 files changed, 564 insertions(+), 92 deletions(-) diff --git a/apps/workspace-engine/pkg/db/job_verification_metric.sql.go b/apps/workspace-engine/pkg/db/job_verification_metric.sql.go index ed4b512580..4a8def5d92 100644 --- a/apps/workspace-engine/pkg/db/job_verification_metric.sql.go +++ b/apps/workspace-engine/pkg/db/job_verification_metric.sql.go @@ -362,3 +362,56 @@ func (q *Queries) InsertJobVerificationMetricMeasurement(ctx context.Context, ar ) return err } + +const listVerificationMetricsWithMeasurementsByJobIDs = `-- name: ListVerificationMetricsWithMeasurementsByJobIDs :many +SELECT + jvm.job_id, + jvm.id AS metric_id, + jvm.count, + jvm.failure_threshold, + jvm.success_threshold, + mm.status AS measurement_status +FROM job_verification_metric jvm +LEFT JOIN job_verification_metric_measurement mm + ON mm.job_verification_metric_status_id = jvm.id +WHERE jvm.job_id = ANY($1::uuid[]) +ORDER BY jvm.job_id, jvm.id, mm.measured_at ASC +` + +type ListVerificationMetricsWithMeasurementsByJobIDsRow struct { + JobID uuid.UUID + MetricID uuid.UUID + Count int32 + FailureThreshold pgtype.Int4 + SuccessThreshold pgtype.Int4 + MeasurementStatus NullJobVerificationStatus +} + +// Returns verification metrics with their individual measurement statuses for a batch of jobs. +// Used to compute verification status in Go via JobVerification.Status(). +func (q *Queries) ListVerificationMetricsWithMeasurementsByJobIDs(ctx context.Context, jobIds []uuid.UUID) ([]ListVerificationMetricsWithMeasurementsByJobIDsRow, error) { + rows, err := q.db.Query(ctx, listVerificationMetricsWithMeasurementsByJobIDs, jobIds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListVerificationMetricsWithMeasurementsByJobIDsRow + for rows.Next() { + var i ListVerificationMetricsWithMeasurementsByJobIDsRow + if err := rows.Scan( + &i.JobID, + &i.MetricID, + &i.Count, + &i.FailureThreshold, + &i.SuccessThreshold, + &i.MeasurementStatus, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/apps/workspace-engine/pkg/db/jobs.sql.go b/apps/workspace-engine/pkg/db/jobs.sql.go index bbd5333fba..3a6939d079 100644 --- a/apps/workspace-engine/pkg/db/jobs.sql.go +++ b/apps/workspace-engine/pkg/db/jobs.sql.go @@ -396,29 +396,7 @@ SELECT j.completed_at, r.deployment_id, r.environment_id, - r.resource_id, - COALESCE( - ( - SELECT - CASE - WHEN COUNT(*) = 0 THEN '' - WHEN bool_or(COALESCE(mc.failures, 0) > COALESCE(jvm.failure_threshold, 0)) THEN 'failed' - WHEN bool_or(COALESCE(mc.total, 0) < jvm.count - AND COALESCE(mc.failures, 0) <= COALESCE(jvm.failure_threshold, 0)) THEN 'running' - ELSE 'passed' - END - FROM job_verification_metric jvm - LEFT JOIN LATERAL ( - SELECT - COUNT(*)::int AS total, - COUNT(*) FILTER (WHERE mm.status = 'failed')::int AS failures - FROM job_verification_metric_measurement mm - WHERE mm.job_verification_metric_status_id = jvm.id - ) mc ON true - WHERE jvm.job_id = j.id - ), - '' - ) AS verification_status + r.resource_id FROM job j JOIN release_job rj ON rj.job_id = j.id JOIN release r ON r.id = rj.release_id @@ -432,13 +410,12 @@ type ListJobsByEnvironmentAndVersionParams struct { } type ListJobsByEnvironmentAndVersionRow struct { - ID uuid.UUID - Status JobStatus - CompletedAt pgtype.Timestamptz - DeploymentID uuid.UUID - EnvironmentID uuid.UUID - ResourceID uuid.UUID - VerificationStatus pgtype.Text + ID uuid.UUID + Status JobStatus + CompletedAt pgtype.Timestamptz + DeploymentID uuid.UUID + EnvironmentID uuid.UUID + ResourceID uuid.UUID } // Returns all jobs for a given environment and version in a single query, @@ -459,7 +436,6 @@ func (q *Queries) ListJobsByEnvironmentAndVersion(ctx context.Context, arg ListJ &i.DeploymentID, &i.EnvironmentID, &i.ResourceID, - &i.VerificationStatus, ); err != nil { return nil, err } diff --git a/apps/workspace-engine/pkg/db/queries/job_verification_metric.sql b/apps/workspace-engine/pkg/db/queries/job_verification_metric.sql index efcbab4f62..32d636175f 100644 --- a/apps/workspace-engine/pkg/db/queries/job_verification_metric.sql +++ b/apps/workspace-engine/pkg/db/queries/job_verification_metric.sql @@ -130,6 +130,22 @@ FROM job_verification_metric jvm WHERE jvm.job_id = @job_id ORDER BY jvm.id; +-- name: ListVerificationMetricsWithMeasurementsByJobIDs :many +-- Returns verification metrics with their individual measurement statuses for a batch of jobs. +-- Used to compute verification status in Go via JobVerification.Status(). +SELECT + jvm.job_id, + jvm.id AS metric_id, + jvm.count, + jvm.failure_threshold, + jvm.success_threshold, + mm.status AS measurement_status +FROM job_verification_metric jvm +LEFT JOIN job_verification_metric_measurement mm + ON mm.job_verification_metric_status_id = jvm.id +WHERE jvm.job_id = ANY(@job_ids::uuid[]) +ORDER BY jvm.job_id, jvm.id, mm.measured_at ASC; + -- name: GetJobDispatchContext :one SELECT j.dispatch_context FROM job j diff --git a/apps/workspace-engine/pkg/db/queries/jobs.sql b/apps/workspace-engine/pkg/db/queries/jobs.sql index c4601cc495..99aec0718e 100644 --- a/apps/workspace-engine/pkg/db/queries/jobs.sql +++ b/apps/workspace-engine/pkg/db/queries/jobs.sql @@ -235,29 +235,7 @@ SELECT j.completed_at, r.deployment_id, r.environment_id, - r.resource_id, - COALESCE( - ( - SELECT - CASE - WHEN COUNT(*) = 0 THEN '' - WHEN bool_or(COALESCE(mc.failures, 0) > COALESCE(jvm.failure_threshold, 0)) THEN 'failed' - WHEN bool_or(COALESCE(mc.total, 0) < jvm.count - AND COALESCE(mc.failures, 0) <= COALESCE(jvm.failure_threshold, 0)) THEN 'running' - ELSE 'passed' - END - FROM job_verification_metric jvm - LEFT JOIN LATERAL ( - SELECT - COUNT(*)::int AS total, - COUNT(*) FILTER (WHERE mm.status = 'failed')::int AS failures - FROM job_verification_metric_measurement mm - WHERE mm.job_verification_metric_status_id = jvm.id - ) mc ON true - WHERE jvm.job_id = j.id - ), - '' - ) AS verification_status + r.resource_id FROM job j JOIN release_job rj ON rj.job_id = j.id JOIN release r ON r.id = rj.release_id diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go index 4c89efd3fb..dadccba5cb 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go @@ -47,18 +47,21 @@ type Getters interface { environmentID string, versionID string, ) ([]ReleaseTargetJob, error) + GetVerificationStatusForJobs( + ctx context.Context, + jobIDs []string, + ) (map[string]oapi.JobVerificationStatus, error) } // ReleaseTargetJob holds the minimal job fields needed by the job tracker, // along with the release target triple identifying which target the job belongs to. type ReleaseTargetJob struct { - JobID string - Status oapi.JobStatus - CompletedAt *time.Time - DeploymentID string - EnvironmentID string - ResourceID string - VerificationStatus string + JobID string + Status oapi.JobStatus + CompletedAt *time.Time + DeploymentID string + EnvironmentID string + ResourceID string } // --------------------------------------------------------------------------- @@ -209,12 +212,11 @@ func (p *PostgresGetters) GetJobsForEnvironmentAndVersion( result := make([]ReleaseTargetJob, len(rows)) for i, row := range rows { rtj := ReleaseTargetJob{ - JobID: row.ID.String(), - Status: db.ToOapiJobStatus(row.Status), - DeploymentID: row.DeploymentID.String(), - EnvironmentID: row.EnvironmentID.String(), - ResourceID: row.ResourceID.String(), - VerificationStatus: row.VerificationStatus, + JobID: row.ID.String(), + Status: db.ToOapiJobStatus(row.Status), + DeploymentID: row.DeploymentID.String(), + EnvironmentID: row.EnvironmentID.String(), + ResourceID: row.ResourceID.String(), } if row.CompletedAt.Valid { t := row.CompletedAt.Time @@ -225,6 +227,83 @@ func (p *PostgresGetters) GetJobsForEnvironmentAndVersion( return result, nil } +func (p *PostgresGetters) GetVerificationStatusForJobs( + ctx context.Context, + jobIDs []string, +) (map[string]oapi.JobVerificationStatus, error) { + ctx, span := gettersTracer.Start(ctx, "GetVerificationStatusForJobs") + defer span.End() + + uuids := make([]uuid.UUID, len(jobIDs)) + for i, id := range jobIDs { + uuids[i] = uuid.MustParse(id) + } + + rows, err := p.queries.ListVerificationMetricsWithMeasurementsByJobIDs(ctx, uuids) + if err != nil { + return nil, err + } + + // Group rows by job -> metric -> measurements + type metricData struct { + count int + failureThreshold *int + successThreshold *int + measurements []oapi.VerificationMeasurementStatus + } + // jobID -> metricID -> metricData + byJob := make(map[string]map[string]*metricData) + + for _, row := range rows { + jobKey := row.JobID.String() + metricKey := row.MetricID.String() + + if byJob[jobKey] == nil { + byJob[jobKey] = make(map[string]*metricData) + } + md, exists := byJob[jobKey][metricKey] + if !exists { + md = &metricData{count: int(row.Count)} + if row.FailureThreshold.Valid { + v := int(row.FailureThreshold.Int32) + md.failureThreshold = &v + } + if row.SuccessThreshold.Valid { + v := int(row.SuccessThreshold.Int32) + md.successThreshold = &v + } + byJob[jobKey][metricKey] = md + } + + if row.MeasurementStatus.Valid { + md.measurements = append(md.measurements, + oapi.VerificationMeasurementStatus(row.MeasurementStatus.JobVerificationStatus)) + } + } + + result := make(map[string]oapi.JobVerificationStatus, len(byJob)) + for jobID, metrics := range byJob { + jv := oapi.JobVerification{} + for _, md := range metrics { + vms := oapi.VerificationMetricStatus{ + Count: md.count, + FailureThreshold: md.failureThreshold, + SuccessThreshold: md.successThreshold, + } + for _, ms := range md.measurements { + vms.Measurements = append(vms.Measurements, oapi.VerificationMeasurement{ + Status: ms, + }) + } + jv.Metrics = append(jv.Metrics, vms) + } + result[jobID] = jv.Status() + } + + span.SetAttributes(attribute.Int("jobs_with_verification", len(result))) + return result, nil +} + func (p *PostgresGetters) GetReleaseByJobID( ctx context.Context, jobID string, 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 3385875331..0ad0c5efd0 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 @@ -123,6 +123,16 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { rtKeys[rt.Key()] = true } + // Fetch verification statuses only when required + var verificationStatuses map[string]oapi.JobVerificationStatus + if t.RequireVerificationPassed { + jobIDs := make([]string, len(rows)) + for i, row := range rows { + jobIDs[i] = row.JobID + } + verificationStatuses, _ = t.getters.GetVerificationStatusForJobs(ctx, jobIDs) + } + for _, row := range rows { rt := oapi.ReleaseTarget{ DeploymentId: row.DeploymentID, @@ -140,9 +150,13 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { CompletedAt: row.CompletedAt, } - isVerificationOk := !t.RequireVerificationPassed || - row.VerificationStatus == "" || - row.VerificationStatus == string(oapi.JobVerificationStatusPassed) + isVerificationOk := true + if t.RequireVerificationPassed && verificationStatuses != nil { + if status, exists := verificationStatuses[row.JobID]; exists { + isVerificationOk = status == oapi.JobVerificationStatusPassed + } + // Job not in the map means no verification metrics configured — ok + } if t.SuccessStatuses[row.Status] && row.CompletedAt != nil && isVerificationOk { if existingTime, exists := t.successfulReleaseTargets[targetKey]; !exists || row.CompletedAt.Before(existingTime) { 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 b6415281f4..804ff68c9a 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 @@ -5,9 +5,8 @@ import ( "testing" "time" - "workspace-engine/pkg/oapi" - "github.com/stretchr/testify/assert" + "workspace-engine/pkg/oapi" ) func TestGetReleaseTargets(t *testing.T) { @@ -1342,3 +1341,324 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed_False_IgnoresVerifica assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), "expected 100%% success when RequireVerificationPassed=false even with failed verification") } + +func TestReleaseTargetJobTracker_RequireVerificationPassed_MixedVerificationAcrossTargets( + t *testing.T, +) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + rt2 := &oapi.ReleaseTarget{ + ResourceId: "resource-2", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + rt3 := &oapi.ReleaseTarget{ + ResourceId: "resource-3", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + mock.addReleaseTarget(rt2) + mock.addReleaseTarget(rt3) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + release2 := &oapi.Release{ + ReleaseTarget: *rt2, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + release3 := &oapi.Release{ + ReleaseTarget: *rt3, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + + completedAt1 := time.Now().Add(-10 * time.Minute) + completedAt2 := time.Now().Add(-8 * time.Minute) + completedAt3 := time.Now().Add(-6 * time.Minute) + + job1 := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt1, UpdatedAt: completedAt1, CompletedAt: &completedAt1, + JobAgentConfig: oapi.JobAgentConfig{}, + } + job2 := &oapi.Job{ + Id: "job-2", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt2, UpdatedAt: completedAt2, CompletedAt: &completedAt2, + JobAgentConfig: oapi.JobAgentConfig{}, + } + job3 := &oapi.Job{ + Id: "job-3", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt3, UpdatedAt: completedAt3, CompletedAt: &completedAt3, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + mock.addJob(rt2, job2, release2) + mock.addJob(rt3, job3, release3) + + // rt1: passed verification, rt2: failed verification, rt3: no verification + mock.jobVerificationStatus["job-1"] = string(oapi.JobVerificationStatusPassed) + mock.jobVerificationStatus["job-2"] = string(oapi.JobVerificationStatusFailed) + // job-3 has no verification status (empty string) + + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2, *rt3} + + // 2 out of 3 targets should count (rt1=passed, rt3=no verification) + // rt2 should be excluded (failed verification) + expected := float32(2.0) / float32(3.0) * 100 + assert.InDelta(t, expected, tracker.GetSuccessPercentage(), 0.1, + "expected ~66.67%% success: passed and no-verification count, failed does not") +} + +func TestReleaseTargetJobTracker_RequireVerificationPassed_AffectsSuccessPercentageSatisfiedAt( + t *testing.T, +) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + rt2 := &oapi.ReleaseTarget{ + ResourceId: "resource-2", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + mock.addReleaseTarget(rt2) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + release2 := &oapi.Release{ + ReleaseTarget: *rt2, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + + completedAt1 := time.Date(2024, 1, 1, 10, 5, 0, 0, time.UTC) + completedAt2 := time.Date(2024, 1, 1, 10, 10, 0, 0, time.UTC) + + job1 := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt1, UpdatedAt: completedAt1, CompletedAt: &completedAt1, + JobAgentConfig: oapi.JobAgentConfig{}, + } + job2 := &oapi.Job{ + Id: "job-2", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt2, UpdatedAt: completedAt2, CompletedAt: &completedAt2, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + mock.addJob(rt2, job2, release2) + + // rt1 passed verification, rt2 failed + mock.jobVerificationStatus["job-1"] = string(oapi.JobVerificationStatusPassed) + mock.jobVerificationStatus["job-2"] = string(oapi.JobVerificationStatusFailed) + + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2} + + // Only 1 of 2 targets qualifies, so 100% requirement should never be satisfied + satisfiedAt := tracker.GetSuccessPercentageSatisfiedAt(100.0) + assert.True(t, satisfiedAt.IsZero(), + "100%% should not be satisfied when verification excludes one target") + + // 50% requirement needs 1 success — should be satisfied at completedAt1 + satisfiedAt50 := tracker.GetSuccessPercentageSatisfiedAt(50.0) + assert.False(t, satisfiedAt50.IsZero(), + "50%% should be satisfied with 1 of 2 targets passing") + assert.Equal(t, completedAt1, satisfiedAt50, + "satisfiedAt should be the completion time of the only qualifying job") +} + +func TestReleaseTargetJobTracker_RequireVerificationPassed_AffectsSoakTime(t *testing.T) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + rt2 := &oapi.ReleaseTarget{ + ResourceId: "resource-2", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + mock.addReleaseTarget(rt2) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + release2 := &oapi.Release{ + ReleaseTarget: *rt2, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + + // rt1: completed 30 min ago, failed verification + // rt2: completed 5 min ago, passed verification + completedAt1 := time.Now().Add(-30 * time.Minute) + completedAt2 := time.Now().Add(-5 * time.Minute) + + job1 := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt1, UpdatedAt: completedAt1, CompletedAt: &completedAt1, + JobAgentConfig: oapi.JobAgentConfig{}, + } + job2 := &oapi.Job{ + Id: "job-2", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt2, UpdatedAt: completedAt2, CompletedAt: &completedAt2, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + mock.addJob(rt2, job2, release2) + + mock.jobVerificationStatus["job-1"] = string(oapi.JobVerificationStatusFailed) + mock.jobVerificationStatus["job-2"] = string(oapi.JobVerificationStatusPassed) + + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2} + + // Most recent qualifying success should be rt2 (5 min ago), not rt1 (30 min ago) + mostRecent := tracker.GetMostRecentSuccess() + assert.InDelta(t, float64(completedAt2.Unix()), float64(mostRecent.Unix()), 1.0, + "most recent success should be the passed-verification job, not the failed one") + + // Soak time of 3 min should NOT be met (most recent qualifying success was 5 min ago, + // but we need the most recent across all successful targets for soak — 5 > 3 so it IS met) + assert.True(t, tracker.MeetsSoakTimeRequirement(3*time.Minute), + "3 min soak should be met (qualifying success was 5 min ago)") + + // Soak time of 10 min should NOT be met (most recent qualifying success was only 5 min ago) + assert.False(t, tracker.MeetsSoakTimeRequirement(10*time.Minute), + "10 min soak should not be met (qualifying success was only 5 min ago)") +} + +func TestReleaseTargetJobTracker_RequireVerificationPassed_RunningAndCancelledExcluded( + t *testing.T, +) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + rt2 := &oapi.ReleaseTarget{ + ResourceId: "resource-2", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + mock.addReleaseTarget(rt2) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + release2 := &oapi.Release{ + ReleaseTarget: *rt2, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + + completedAt1 := time.Now().Add(-10 * time.Minute) + completedAt2 := time.Now().Add(-8 * time.Minute) + + job1 := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt1, UpdatedAt: completedAt1, CompletedAt: &completedAt1, + JobAgentConfig: oapi.JobAgentConfig{}, + } + job2 := &oapi.Job{ + Id: "job-2", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt2, UpdatedAt: completedAt2, CompletedAt: &completedAt2, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + mock.addJob(rt2, job2, release2) + + // "running" verification is not "passed" — should not count + mock.jobVerificationStatus["job-1"] = string(oapi.JobVerificationStatusRunning) + // "cancelled" verification is not "passed" — should not count + mock.jobVerificationStatus["job-2"] = string(oapi.JobVerificationStatusCancelled) + + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2} + + assert.Equal(t, float32(0.0), tracker.GetSuccessPercentage(), + "running and cancelled verification statuses should not count as passed") +} + +func TestReleaseTargetJobTracker_RequireVerificationPassed_MultipleJobsPerTargetMixedVerification( + t *testing.T, +) { + mock, version := setupMockForJobTracker() + ctx := context.Background() + + env := mock.environments["env-1"] + + rt1 := &oapi.ReleaseTarget{ + ResourceId: "resource-1", + EnvironmentId: "env-1", + DeploymentId: "deploy-1", + } + mock.addReleaseTarget(rt1) + + release1 := &oapi.Release{ + ReleaseTarget: *rt1, Version: *version, + Variables: map[string]oapi.LiteralValue{}, CreatedAt: time.Now().Format(time.RFC3339), + } + + // First job: successful but verification failed + completedAt1 := time.Now().Add(-20 * time.Minute) + job1 := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt1, UpdatedAt: completedAt1, CompletedAt: &completedAt1, + JobAgentConfig: oapi.JobAgentConfig{}, + } + + // Second job: successful and verification passed + completedAt2 := time.Now().Add(-5 * time.Minute) + job2 := &oapi.Job{ + Id: "job-2", JobAgentId: "agent-1", Status: oapi.JobStatusSuccessful, + CreatedAt: completedAt2, UpdatedAt: completedAt2, CompletedAt: &completedAt2, + JobAgentConfig: oapi.JobAgentConfig{}, + } + mock.addJob(rt1, job1, release1) + mock.addJob(rt1, job2, release1) + + mock.jobVerificationStatus["job-1"] = string(oapi.JobVerificationStatusFailed) + mock.jobVerificationStatus["job-2"] = string(oapi.JobVerificationStatusPassed) + + tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) + tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} + + // The target should still count as successful because job-2 passed verification + assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), + "target should be successful when at least one job has passed verification") + + // Both jobs should still appear in the jobs list + assert.Len(t, tracker.Jobs(), 2, "both jobs should be tracked regardless of verification") +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go index 28167110f0..9fd58789f3 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go @@ -146,19 +146,31 @@ func (m *mockGetters) GetJobsForEnvironmentAndVersion( continue } result = append(result, ReleaseTargetJob{ - JobID: job.Id, - Status: job.Status, - CompletedAt: job.CompletedAt, - DeploymentID: rel.ReleaseTarget.DeploymentId, - EnvironmentID: rel.ReleaseTarget.EnvironmentId, - ResourceID: rel.ReleaseTarget.ResourceId, - VerificationStatus: m.jobVerificationStatus[job.Id], + JobID: job.Id, + Status: job.Status, + CompletedAt: job.CompletedAt, + DeploymentID: rel.ReleaseTarget.DeploymentId, + EnvironmentID: rel.ReleaseTarget.EnvironmentId, + ResourceID: rel.ReleaseTarget.ResourceId, }) } } return result, nil } +func (m *mockGetters) GetVerificationStatusForJobs( + _ context.Context, + jobIDs []string, +) (map[string]oapi.JobVerificationStatus, error) { + result := make(map[string]oapi.JobVerificationStatus) + for _, id := range jobIDs { + if status, ok := m.jobVerificationStatus[id]; ok { + result[id] = oapi.JobVerificationStatus(status) + } + } + return result, nil +} + func (m *mockGetters) addReleaseTarget(rt *oapi.ReleaseTarget) { m.releaseTargets = append(m.releaseTargets, rt) } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go index c4e4637dcf..400ae18108 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go @@ -159,6 +159,11 @@ func (m *mockGetters) GetJobsForEnvironmentAndVersion( ) ([]environmentprogression.ReleaseTargetJob, error) { return nil, nil } +func (m *mockGetters) GetVerificationStatusForJobs( + _ context.Context, _ []string, +) (map[string]oapi.JobVerificationStatus, error) { + return nil, nil +} // --------------------------------------------------------------------------- // Test helpers diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go index af4d275e67..08b631d416 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go @@ -186,6 +186,11 @@ func (m *mockGetter) GetJobsForEnvironmentAndVersion( ) ([]environmentprogression.ReleaseTargetJob, error) { return nil, nil } +func (m *mockGetter) GetVerificationStatusForJobs( + _ context.Context, _ []string, +) (map[string]oapi.JobVerificationStatus, error) { + return nil, nil +} // compile-time check. var _ Getter = (*mockGetter)(nil) diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go index f36f535406..66a1161ec3 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go @@ -221,6 +221,11 @@ func (m *mockReconcileGetter) GetJobsForEnvironmentAndVersion( ) ([]environmentprogression.ReleaseTargetJob, error) { return nil, nil } +func (m *mockReconcileGetter) GetVerificationStatusForJobs( + _ context.Context, _ []string, +) (map[string]oapi.JobVerificationStatus, error) { + return nil, nil +} func (m *mockReconcileGetter) GetVariableSetsWithVariables( ctx context.Context, diff --git a/apps/workspace-engine/test/controllers/harness/mocks.go b/apps/workspace-engine/test/controllers/harness/mocks.go index a689f255d5..4ef0ea080f 100644 --- a/apps/workspace-engine/test/controllers/harness/mocks.go +++ b/apps/workspace-engine/test/controllers/harness/mocks.go @@ -451,26 +451,35 @@ func (g *DesiredReleaseGetter) GetJobsForEnvironmentAndVersion( if !ok || rel.Version.Id != versionID { continue } - verificationStatus := "" - if g.JobVerificationStatuses != nil { - if vs, ok := g.JobVerificationStatuses[job.Id]; ok { - verificationStatus = string(vs) - } - } result = append(result, environmentprogression.ReleaseTargetJob{ - JobID: job.Id, - Status: job.Status, - CompletedAt: job.CompletedAt, - DeploymentID: depID, - EnvironmentID: envID, - ResourceID: resID, - VerificationStatus: verificationStatus, + JobID: job.Id, + Status: job.Status, + CompletedAt: job.CompletedAt, + DeploymentID: depID, + EnvironmentID: envID, + ResourceID: resID, }) } } return result, nil } +func (g *DesiredReleaseGetter) GetVerificationStatusForJobs( + _ context.Context, + jobIDs []string, +) (map[string]oapi.JobVerificationStatus, error) { + if g.JobVerificationStatuses == nil { + return nil, nil + } + result := make(map[string]oapi.JobVerificationStatus, len(jobIDs)) + for _, id := range jobIDs { + if s, ok := g.JobVerificationStatuses[id]; ok { + result[id] = s + } + } + return result, nil +} + func (g *DesiredReleaseGetter) GetVariableSetsWithVariables( ctx context.Context, workspaceID uuid.UUID, From b82e5bba2ae054c4229cb09afef45ebfbf65bd15 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Apr 2026 11:05:49 -0700 Subject: [PATCH 4/8] cleanup --- .../environmentprogression/getters.go | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go index dadccba5cb..fff9b3feef 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go @@ -227,6 +227,13 @@ func (p *PostgresGetters) GetJobsForEnvironmentAndVersion( return result, nil } +type metricData struct { + count int + failureThreshold *int + successThreshold *int + measurements []oapi.VerificationMeasurementStatus +} + func (p *PostgresGetters) GetVerificationStatusForJobs( ctx context.Context, jobIDs []string, @@ -234,26 +241,32 @@ func (p *PostgresGetters) GetVerificationStatusForJobs( ctx, span := gettersTracer.Start(ctx, "GetVerificationStatusForJobs") defer span.End() - uuids := make([]uuid.UUID, len(jobIDs)) - for i, id := range jobIDs { - uuids[i] = uuid.MustParse(id) - } + uuids := stringsToUUIDs(jobIDs) rows, err := p.queries.ListVerificationMetricsWithMeasurementsByJobIDs(ctx, uuids) if err != nil { return nil, err } - // Group rows by job -> metric -> measurements - type metricData struct { - count int - failureThreshold *int - successThreshold *int - measurements []oapi.VerificationMeasurementStatus + byJob := groupRowsByJobAndMetric(rows) + result := computeVerificationStatuses(byJob) + + span.SetAttributes(attribute.Int("jobs_with_verification", len(result))) + return result, nil +} + +func stringsToUUIDs(ids []string) []uuid.UUID { + uuids := make([]uuid.UUID, len(ids)) + for i, id := range ids { + uuids[i] = uuid.MustParse(id) } - // jobID -> metricID -> metricData - byJob := make(map[string]map[string]*metricData) + return uuids +} +func groupRowsByJobAndMetric( + rows []db.ListVerificationMetricsWithMeasurementsByJobIDsRow, +) map[string]map[string]*metricData { + byJob := make(map[string]map[string]*metricData) for _, row := range rows { jobKey := row.JobID.String() metricKey := row.MetricID.String() @@ -280,28 +293,35 @@ func (p *PostgresGetters) GetVerificationStatusForJobs( oapi.VerificationMeasurementStatus(row.MeasurementStatus.JobVerificationStatus)) } } + return byJob +} +func computeVerificationStatuses( + byJob map[string]map[string]*metricData, +) map[string]oapi.JobVerificationStatus { result := make(map[string]oapi.JobVerificationStatus, len(byJob)) for jobID, metrics := range byJob { - jv := oapi.JobVerification{} - for _, md := range metrics { - vms := oapi.VerificationMetricStatus{ - Count: md.count, - FailureThreshold: md.failureThreshold, - SuccessThreshold: md.successThreshold, - } - for _, ms := range md.measurements { - vms.Measurements = append(vms.Measurements, oapi.VerificationMeasurement{ - Status: ms, - }) - } - jv.Metrics = append(jv.Metrics, vms) - } - result[jobID] = jv.Status() + result[jobID] = buildJobVerification(metrics).Status() } + return result +} - span.SetAttributes(attribute.Int("jobs_with_verification", len(result))) - return result, nil +func buildJobVerification(metrics map[string]*metricData) *oapi.JobVerification { + jv := &oapi.JobVerification{} + for _, md := range metrics { + vms := oapi.VerificationMetricStatus{ + Count: md.count, + FailureThreshold: md.failureThreshold, + SuccessThreshold: md.successThreshold, + } + for _, ms := range md.measurements { + vms.Measurements = append(vms.Measurements, oapi.VerificationMeasurement{ + Status: ms, + }) + } + jv.Metrics = append(jv.Metrics, vms) + } + return jv } func (p *PostgresGetters) GetReleaseByJobID( From c9b1a74af067df30818c701ceb380c7f37c13a71 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Apr 2026 11:10:26 -0700 Subject: [PATCH 5/8] more cleanup --- .../environmentprogression/jobtracker.go | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) 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 0ad0c5efd0..b942463332 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 @@ -123,14 +123,12 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { rtKeys[rt.Key()] = true } - // Fetch verification statuses only when required - var verificationStatuses map[string]oapi.JobVerificationStatus - if t.RequireVerificationPassed { - jobIDs := make([]string, len(rows)) - for i, row := range rows { - jobIDs[i] = row.JobID - } - verificationStatuses, _ = t.getters.GetVerificationStatusForJobs(ctx, jobIDs) + verificationStatuses, err := t.fetchVerificationStatuses(ctx, rows) + if err != nil { + span.AddEvent("GetVerificationStatusForJobs error", + trace.WithAttributes(attribute.String("error", err.Error())), + ) + return t.jobs } for _, row := range rows { @@ -155,7 +153,6 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { if status, exists := verificationStatuses[row.JobID]; exists { isVerificationOk = status == oapi.JobVerificationStatusPassed } - // Job not in the map means no verification metrics configured — ok } if t.SuccessStatuses[row.Status] && row.CompletedAt != nil && isVerificationOk { if existingTime, exists := t.successfulReleaseTargets[targetKey]; !exists || @@ -179,6 +176,20 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { return t.jobs } +func (t *ReleaseTargetJobTracker) fetchVerificationStatuses( + ctx context.Context, + rows []ReleaseTargetJob, +) (map[string]oapi.JobVerificationStatus, error) { + if !t.RequireVerificationPassed { + return nil, nil + } + jobIDs := make([]string, len(rows)) + for i, row := range rows { + jobIDs[i] = row.JobID + } + return t.getters.GetVerificationStatusForJobs(ctx, jobIDs) +} + // 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") From d7c8c4bc4348444b7dfc9f8d1e3f46e89b68287c Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Apr 2026 11:22:51 -0700 Subject: [PATCH 6/8] lint --- .../environmentprogression/jobtracker_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 804ff68c9a..71136c941c 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 @@ -1209,7 +1209,7 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed_ExcludesFailedVerific tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} - assert.Equal(t, float32(0.0), tracker.GetSuccessPercentage(), + assert.InDelta(t, float32(0.0), tracker.GetSuccessPercentage(), 0, "expected 0%% success when verification failed and RequireVerificationPassed=true") } @@ -1254,7 +1254,7 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed_IncludesPassedVerific tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} - assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), + assert.InDelta(t, float32(100.0), tracker.GetSuccessPercentage(), 0, "expected 100%% success when verification passed and RequireVerificationPassed=true") } @@ -1295,7 +1295,7 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed_IncludesNoVerificatio tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} - assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), + assert.InDelta(t, float32(100.0), tracker.GetSuccessPercentage(), 0, "expected 100%% success when no verification metrics and RequireVerificationPassed=true") } @@ -1338,7 +1338,7 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed_False_IgnoresVerifica tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, false) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} - assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), + assert.InDelta(t, float32(100.0), tracker.GetSuccessPercentage(), 0, "expected 100%% success when RequireVerificationPassed=false even with failed verification") } @@ -1607,7 +1607,7 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed_RunningAndCancelledEx tracker := NewReleaseTargetJobTracker(ctx, mock, env, version, nil, true) tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1, *rt2} - assert.Equal(t, float32(0.0), tracker.GetSuccessPercentage(), + assert.InDelta(t, float32(0.0), tracker.GetSuccessPercentage(), 0, "running and cancelled verification statuses should not count as passed") } @@ -1656,7 +1656,7 @@ func TestReleaseTargetJobTracker_RequireVerificationPassed_MultipleJobsPerTarget tracker.ReleaseTargets = []oapi.ReleaseTarget{*rt1} // The target should still count as successful because job-2 passed verification - assert.Equal(t, float32(100.0), tracker.GetSuccessPercentage(), + assert.InDelta(t, float32(100.0), tracker.GetSuccessPercentage(), 0, "target should be successful when at least one job has passed verification") // Both jobs should still appear in the jobs list From 183f03c4e2799d6edcb016926c6dbb13e5970578 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Apr 2026 11:24:52 -0700 Subject: [PATCH 7/8] docs --- docs/policies/environment-progression.mdx | 33 +++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/policies/environment-progression.mdx b/docs/policies/environment-progression.mdx index a5bd54b5a7..847f129487 100644 --- a/docs/policies/environment-progression.mdx +++ b/docs/policies/environment-progression.mdx @@ -101,6 +101,17 @@ curl -X POST https://api.ctrlplane.com/v1/workspaces/{workspaceId}/policies \ deployment is older than this, progression is blocked until redeployed. + + When enabled, jobs must also have passed verification to count toward the + success percentage. Jobs with no verification metrics configured are still + counted. Jobs with verification in a running, failed, or cancelled state are + excluded from the success count. + + ## How It Works 1. **Find dependency environments** - Ctrlplane finds all environments matching @@ -108,11 +119,14 @@ curl -X POST https://api.ctrlplane.com/v1/workspaces/{workspaceId}/policies \ This prevents cross-system dependency issues. 2. **Evaluate pass rate** - For each dependency environment, Ctrlplane checks if the success percentage of release targets meets the threshold. -3. **Check soak time** - If `minimumSoakTimeMinutes` is configured, Ctrlplane +3. **Check verification** - If `requireVerificationPassed` is enabled, + only jobs that have passed verification count toward the success percentage. + Jobs without verification metrics are still counted. +4. **Check soak time** - If `minimumSoakTimeMinutes` is configured, Ctrlplane verifies that enough time has elapsed since the most recent successful job. -4. **Check freshness** - If `maximumAgeHours` is configured, Ctrlplane ensures +5. **Check freshness** - If `maximumAgeHours` is configured, Ctrlplane ensures the dependency deployment is not too old. -5. **OR logic across environments** - If the selector matches multiple +6. **OR logic across environments** - If the selector matches multiple environments, the rule passes if **at least one** of them satisfies all criteria. @@ -278,10 +292,11 @@ resource "ctrlplane_policy" "production_full_gate" { } environment_progression { - depends_on_environment_selector = "environment.name == 'staging'" - minimum_success_percentage = 100 - minimum_soak_time_minutes = 30 - maximum_age_hours = 48 + depends_on_environment_selector = "environment.name == 'staging'" + minimum_success_percentage = 100 + minimum_soak_time_minutes = 30 + maximum_age_hours = 48 + require_verification_passed = true } gradual_rollout { @@ -303,7 +318,8 @@ resource "ctrlplane_policy" "production_full_gate" { "dependsOnEnvironmentSelector": "environment.name == 'staging'", "minimumSuccessPercentage": 100, "minimumSoakTimeMinutes": 30, - "maximumAgeHours": 48 + "maximumAgeHours": 48, + "requireVerificationPassed": true } }, { @@ -334,6 +350,7 @@ Ctrlplane evaluates if the deployment meets success criteria: - Job status matches `successStatuses` - Success percentage meets `minimumSuccessPercentage` +- If `requireVerificationPassed` is enabled, verification must have passed ### 4. Soak Time (if configured) From 985d0b1aa7381053867a97caca71e1cb00659d59 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 15 Apr 2026 11:38:26 -0700 Subject: [PATCH 8/8] fix --- .../evaluator/environmentprogression/jobtracker.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 b942463332..226ad413a0 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 @@ -123,12 +123,11 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { rtKeys[rt.Key()] = true } - verificationStatuses, err := t.fetchVerificationStatuses(ctx, rows) - if err != nil { + verificationStatuses, verificationErr := t.fetchVerificationStatuses(ctx, rows) + if verificationErr != nil { span.AddEvent("GetVerificationStatusForJobs error", - trace.WithAttributes(attribute.String("error", err.Error())), + trace.WithAttributes(attribute.String("error", verificationErr.Error())), ) - return t.jobs } for _, row := range rows { @@ -148,8 +147,8 @@ func (t *ReleaseTargetJobTracker) compute(ctx context.Context) []*oapi.Job { CompletedAt: row.CompletedAt, } - isVerificationOk := true - if t.RequireVerificationPassed && verificationStatuses != nil { + isVerificationOk := !t.RequireVerificationPassed || verificationErr == nil + if t.RequireVerificationPassed && verificationErr == nil { if status, exists := verificationStatuses[row.JobID]; exists { isVerificationOk = status == oapi.JobVerificationStatusPassed }