Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/api/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions apps/api/openapi/schemas/policies.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
},

Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/routes/v1/workspaces/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -196,6 +198,7 @@ const formatPolicy = (p: PolicyRow) => {
...(r.successStatuses != null && {
successStatuses: r.successStatuses,
}),
requireVerificationPassed: r.requireVerificationPassed,
},
}),
),
Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +1413 to +1417
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make requireVerificationPassed optional in the API contract.

Line 1417 defines requireVerificationPassed as required, but this feature is described and implemented as defaulting to false when omitted. Keeping it required here can break generated clients and conflicts with server-side defaulting behavior.

Suggested contract shape
-            requireVerificationPassed: boolean;
+            requireVerificationPassed?: boolean;

Since this file is generated, apply the fix in the OpenAPI source schema and regenerate artifacts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/types/openapi.ts` around lines 1413 - 1417, The
OpenAPI-generated type marks requireVerificationPassed as required but the
server treats it as optional with a default of false; update the OpenAPI source
schema to make the property optional (remove it from required[] or mark it with
nullable/optional) for the relevant schema that contains
requireVerificationPassed, then regenerate the TypeScript artifacts so the
generated type (requireVerificationPassed) is optional and clients won't be
forced to send it.

successStatuses?: components["schemas"]["JobStatus"][];
};
EnvironmentRequestAccepted: {
Expand Down
5 changes: 5 additions & 0 deletions apps/web/app/api/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
5 changes: 5 additions & 0 deletions apps/workspace-engine/oapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions apps/workspace-engine/oapi/spec/schemas/policy.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
},

Expand Down
2 changes: 2 additions & 0 deletions apps/workspace-engine/pkg/db/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down
53 changes: 53 additions & 0 deletions apps/workspace-engine/pkg/db/job_verification_metric.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/workspace-engine/pkg/db/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 11 additions & 5 deletions apps/workspace-engine/pkg/db/policies.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions apps/workspace-engine/pkg/db/queries/job_verification_metric.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions apps/workspace-engine/pkg/db/queries/policies.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions apps/workspace-engine/pkg/db/queries/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);

Expand Down
9 changes: 6 additions & 3 deletions apps/workspace-engine/pkg/oapi/oapi.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,16 @@ 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)
Expand Down
Loading
Loading