Skip to content

feat: gradual rollouts #572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

feat: gradual rollouts #572

wants to merge 7 commits into from

Conversation

adityachoudhari26
Copy link
Contributor

@adityachoudhari26 adityachoudhari26 commented May 20, 2025

Summary by CodeRabbit

  • New Features
    • Introduced support for "Gradual Rollout" policies, allowing controlled, incremental deployment of releases based on configurable rollout rates and windows.
    • Added a new API endpoint to fetch all releases for a specific deployment version, including detailed release and variable information.
    • Extended policy creation and update APIs to accept gradual rollout configuration.
  • Bug Fixes
    • None.
  • Documentation
    • Updated OpenAPI specifications to document the new gradual rollout policy options and the new releases endpoint.
  • Tests
    • Added comprehensive end-to-end tests for gradual rollout policy behavior and deployment scenarios.
  • Chores
    • Refined and extended API and schema definitions to support new features and maintain consistency.

Copy link
Contributor

coderabbitai bot commented May 20, 2025

Walkthrough

This update introduces the "Gradual Rollout" policy rule across the backend, API, database schema, and end-to-end tests. It adds new database tables and relations, OpenAPI schema extensions, route handlers, and core rule engine logic to support gradual rollout of releases. Comprehensive tests and documentation for these features are also included.

Changes

File(s) / Area Change Summary
apps/jobs/src/policy-checker/index.ts Refactored release target query to multi-table join for gradual rollout policies; enabled bulk queue addition.
apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/openapi.ts
apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/route.ts
Added OpenAPI spec and route handler for fetching releases by deployment version.
apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts
apps/webservice/src/app/api/v1/policies/openapi.ts
Extended OpenAPI schemas to include gradualRollout property in Policy.
e2e/api/schema.ts
openapi.v1.json
Added new API endpoint, schemas for Release and GradualRollout, and updated Policy schemas.
e2e/tests/api/policies/gradual-rollout.spec.ts
e2e/tests/api/policies/gradual-rollout.spec.yaml
Added end-to-end test and YAML spec for gradual rollout policy feature.
packages/api/src/router/policy/evaluate.ts
packages/rule-engine/src/db/get-applicable-policies.ts
Included gradualRollout in policy queries.
packages/db/src/schema/policy-relations.ts
packages/db/src/schema/rules/rule-relations.ts
Added relations for gradual rollout between policy and rule tables.
packages/db/src/schema/policy.ts
packages/db/src/schema/rules/gradual-rollout.ts
packages/db/src/schema/rules/index.ts
Introduced schema, Zod validation, and exports for gradual rollout rule.
packages/rule-engine/src/db/create-policy.ts
packages/rule-engine/src/db/update-policy.ts
Handled creation and update of gradual rollout rules in transactional logic.
packages/rule-engine/src/manager/version-manager-rules.ts
packages/rule-engine/src/rules/environment-version-rollout-rule.ts
Implemented the gradual rollout rule logic and supporting classes/functions.
packages/rule-engine/src/rules/version-approval-rule.ts Added approvedAt timestamp to approval record type.
packages/rule-engine/src/types.ts Added gradualRollout property to Policy type.
packages/db/drizzle/meta/_journal.json Removed trailing newline.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant DB
    participant RuleEngine

    Client->>API: PATCH /v1/policies/{policyId} (with gradualRollout)
    API->>DB: Update policy and gradualRollout rule
    DB-->>API: Confirmation
    API-->>Client: Updated policy

    Client->>API: GET /v1/deployment-versions/{deploymentVersionId}/releases
    API->>DB: Query releases and variables for deployment version
    DB-->>API: Release data
    API-->>Client: Release list (with gradual rollout info)

    Note over RuleEngine,DB: On policy evaluation:
    RuleEngine->>DB: Fetch policy (with gradualRollout)
    RuleEngine->>DB: Fetch release targets and approvals
    RuleEngine->>RuleEngine: Compute rollout start, position, offset
    RuleEngine-->>DB: Update release status as per rollout rule
Loading

Possibly related PRs

  • init filter rule #435: Modifies release fetching and evaluation logic for policies, closely related to the new gradual rollout policy evaluation.
  • evaluate on cron #467: Refactors the same policy-checker logic, but with a simpler query; both touch release target evaluation for policies.
  • feat: policy target computation #490: Introduces computedPolicyTargetReleaseTarget table, which is now used in the new release target query for gradual rollout.

Poem

A rabbit hops through rolling code,
Gradual rollouts now bestowed!
Policies bloom with careful pace,
Each release finds its rightful place.
Database and engine in sync,
Tests and docs—quick as a wink!
🐇✨ Onward, features, hop and grow!


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0044116 and 4b0b191.

📒 Files selected for processing (1)
  • packages/db/drizzle/meta/_journal.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/db/drizzle/meta/_journal.json

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🔭 Outside diff range comments (1)
e2e/api/schema.ts (1)

1368-1386: ⚠️ Potential issue

versionAnyApprovals changed to an object, but endpoints are inconsistent

Policy1.versionAnyApprovals is now a single object (good), yet:

  • updatePolicy (lines 2693-2696) still expects an array
  • upsertPolicy (lines 2801-2802) correctly expects a single object

This mismatch will generate incompatible server & client stubs and fail runtime validation.

- versionAnyApprovals?: { requiredApprovalsCount?: number; }[];
+ versionAnyApprovals?: components["schemas"]["VersionAnyApproval"];

Please align the request bodies across all operations and add contract tests.

🧹 Nitpick comments (13)
packages/rule-engine/src/db/create-policy.ts (1)

95-97: Return the new rule to keep the API symmetric

gradualRollout is stripped out of rest (👍) but never added back to the object that the function returns, whereas every other rule block echoes its input back.
Caller code that relies on the create API to “round-trip” the full payload will now receive undefined.

     versionUserApprovals,
     versionRoleApprovals,
+    gradualRollout,
   };
apps/jobs/src/policy-checker/index.ts (1)

50-57: Provide a stable jobId to BullMQ to avoid queuing duplicates

addBulk does not deduplicate on name; without a jobId identical jobs can be queued multiple times (especially given the duplication risk above).

         releaseTargets.map((rt) => ({
-          name: `${rt.resourceId}-${rt.environmentId}-${rt.deploymentId}`,
+          name: "evaluate-release-target",
+          jobId: `${rt.resourceId}:${rt.environmentId}:${rt.deploymentId}`,
           data: rt,
           priority: 10,
         })),
apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/openapi.ts (2)

10-21: Declare required properties for stronger client contracts

All fields are currently optional, which weakens generated typings. Mark them as required:

         variables: { type: "object", additionalProperties: true },
-        },
+        },
+        required: [
+          "resource",
+          "environment",
+          "deployment",
+          "version",
+          "variables",
+        ],

24-78: Add tags for better grouping in Swagger / Redoc

       get: {
         summary: "Get all releases for a deployment version",
+        tags: ["Releases"],
e2e/tests/api/policies/gradual-rollout.spec.ts (2)

53-73: Consider testing edge cases for gradual rollout configuration.

The test only covers a simple case with deployRate=1 and windowSizeMinutes=1. Consider adding tests with different values to verify the feature works across various configurations.


86-104: Test might increase CI pipeline duration.

The test waits for 60 seconds between checks and runs 5 iterations, adding at least 4 minutes to the test runtime. Consider parameterizing these values for faster execution in CI environments.

- await page.waitForTimeout(60_000);
+ const waitTime = process.env.CI ? 1_000 : 60_000; // Use shorter wait time in CI
+ await page.waitForTimeout(waitTime);
apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/route.ts (1)

98-109: Avoid O(n) re-serialization of static version metadata

deploymentVersion.metadata is identical for every row in releases.
Re-computing the object inside the map incurs unnecessary CPU & memory allocation.

-      const fullReleases = releases.map((release) => ({
-        resource: release.resource,
-        environment: release.environment,
-        deployment: release.deployment,
-        version: {
-          ...deploymentVersion,
-          metadata: Object.fromEntries(
-            deploymentVersion.metadata.map((m) => [m.key, m.value]),
-          ),
-        },
-        variables: release.variableRelease.variables,
-      }));
+      const versionMetadata = Object.fromEntries(
+        deploymentVersion.metadata.map((m) => [m.key, m.value]),
+      );
+
+      const fullReleases = releases.map((release) => ({
+        resource: release.resource,
+        environment: release.environment,
+        deployment: release.deployment,
+        version: { ...deploymentVersion, metadata: versionMetadata },
+        variables: release.variableRelease.variables,
+      }));

Single calculation keeps the response identical while trimming per-row overhead.

packages/rule-engine/src/rules/gradual-rollout-rule.ts (1)

40-57: Run eligibility checks in parallel to cut latency

filter currently awaits getDeploymentTime serially for every candidate.
When the helpers perform DB/IO, the total latency scales linearly with the
number of versions.

-    for (const candidate of candidates) {
-      const deploymentTime = await this.getDeploymentTime(candidate);
-      const isEligible =
-        isAfter(now, deploymentTime) || isEqual(now, deploymentTime);
-      if (!isEligible) rejectionReasons.set(candidate.id, skip);
-    }
+    const evaluated = await Promise.all(
+      candidates.map(async (candidate) => ({
+        candidate,
+        deploymentTime: await this.getDeploymentTime(candidate),
+      })),
+    );
+
+    for (const { candidate, deploymentTime } of evaluated) {
+      const isEligible =
+        isAfter(now, deploymentTime) || isEqual(now, deploymentTime);
+      if (!isEligible) rejectionReasons.set(candidate.id, skip);
+    }

This keeps complexity O(n) but reduces wall-clock time to the slowest single
getDeploymentTime call.

packages/rule-engine/src/manager/version-manager-rules.ts (1)

106-137: Reduce duplicate DB work in getReleaseTargetPosition

getReleaseTargetPosition performs two queries per version.
Within a single filter invocation this can explode into dozens of hits.

Suggestions:

  1. Cache the releaseTarget (it is constant across versions) in the outer
    closure.
  2. Memoise position look-ups per version.id – the deterministic md5
    expression guarantees identical results when called twice for the same
    version.

Indicative refactor:

const releaseTargetPromise = db.query.releaseTarget.findFirst({
  where: eq(schema.releaseTarget.id, releaseTargetId),
}).then(takeFirst);

const positionCache = new Map<string, number>();

const getReleaseTargetPosition = async (version: Version) => {
  if (positionCache.has(version.id)) return positionCache.get(version.id)!;

  const releaseTarget = await releaseTargetPromise;
  // ... build orderedTargetsSubquery as today
  const position = await db
    .select()
    .from(orderedTargetsSubquery)
    .where(eq(orderedTargetsSubquery.id, releaseTargetId))
    .then(takeFirst)
    .then((r) => r.position);

  positionCache.set(version.id, position);
  return position;
};

This keeps correctness while dramatically cutting DB round-trips for large
candidate sets.

e2e/api/schema.ts (1)

1089-1126: Naming collision (Release vs Release1) will confuse every typed consumer

Auto-generated suffixes (Release1) are a code smell that leaks generation details into the public contract.
Pick deterministic, semantically-meaningful names (e.g. ReleaseDetailed, ReleaseSummary) and regenerate the spec so downstream packages don’t end up with unstable import paths on every minor change.

openapi.v1.json (3)

2189-2202: Make gradualRollout nullable in POST upsertPolicy
In the POST /v1/policies request schema you added gradualRollout but omitted "nullable": true. To match the PATCH endpoint and allow callers to omit or explicitly null this field, please add nullability:

   "gradualRollout": {
     "$ref": "#/components/schemas/GradualRollout"
+    ,"nullable": true
   }

5232-5276: Renamed old Release schema to Release1—consider a more descriptive name
Release1 works technically but is not self-descriptive. Consider renaming to something like ReleaseBase or ReleaseDetail to convey intent. Don’t forget to update all $ref usages accordingly.


5907-5928: Define GradualRollout schema—add examples and descriptions
The core properties (deployRate, windowSizeMinutes, name, description) are in place. For better client guidance, consider adding "description" and an "example" to each property. For example:

 "GradualRollout": {
   "type": "object",
   "properties": {
     "deployRate": {
       "type": "number",
+      "description": "Proportion of targets to deploy per window (0–1)",
+      "example": 0.1
     },
     "windowSizeMinutes": {
       "type": "number",
+      "description": "Duration of each rollout window in minutes",
+      "example": 15
     },
     "name": {
       "type": "string",
+      "description": "Identifier for this rollout rule",
+      "example": "canary-10-percent"
     },
     "description": {
       "type": "string",
+      "description": "Optional human-readable detail about this rollout"
     }
   },
   "required": ["deployRate","windowSizeMinutes","name"]
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3010c36 and 9419949.

📒 Files selected for processing (24)
  • apps/jobs/src/policy-checker/index.ts (3 hunks)
  • apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/openapi.ts (1 hunks)
  • apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/route.ts (1 hunks)
  • apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts (1 hunks)
  • apps/webservice/src/app/api/v1/policies/openapi.ts (4 hunks)
  • e2e/api/schema.ts (8 hunks)
  • e2e/tests/api/policies/gradual-rollout.spec.ts (1 hunks)
  • e2e/tests/api/policies/gradual-rollout.spec.yaml (1 hunks)
  • openapi.v1.json (8 hunks)
  • packages/api/src/router/policy/evaluate.ts (1 hunks)
  • packages/db/drizzle/0106_wonderful_iron_fist.sql (1 hunks)
  • packages/db/drizzle/meta/_journal.json (1 hunks)
  • packages/db/src/schema/policy-relations.ts (2 hunks)
  • packages/db/src/schema/policy.ts (3 hunks)
  • packages/db/src/schema/rules/gradual-rollout.ts (1 hunks)
  • packages/db/src/schema/rules/index.ts (1 hunks)
  • packages/db/src/schema/rules/rule-relations.ts (2 hunks)
  • packages/rule-engine/src/db/create-policy.ts (2 hunks)
  • packages/rule-engine/src/db/get-applicable-policies.ts (1 hunks)
  • packages/rule-engine/src/db/update-policy.ts (4 hunks)
  • packages/rule-engine/src/manager/version-manager-rules.ts (2 hunks)
  • packages/rule-engine/src/rules/gradual-rollout-rule.ts (1 hunks)
  • packages/rule-engine/src/rules/version-approval-rule.ts (1 hunks)
  • packages/rule-engine/src/types.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{ts,tsx}`: **Note on Error Handling:** Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error...

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

  • packages/api/src/router/policy/evaluate.ts
  • packages/rule-engine/src/types.ts
  • packages/rule-engine/src/db/get-applicable-policies.ts
  • packages/rule-engine/src/rules/version-approval-rule.ts
  • packages/db/src/schema/policy-relations.ts
  • apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts
  • packages/db/src/schema/rules/index.ts
  • apps/jobs/src/policy-checker/index.ts
  • packages/db/src/schema/policy.ts
  • packages/db/src/schema/rules/rule-relations.ts
  • apps/webservice/src/app/api/v1/policies/openapi.ts
  • packages/rule-engine/src/db/create-policy.ts
  • e2e/tests/api/policies/gradual-rollout.spec.ts
  • packages/rule-engine/src/db/update-policy.ts
  • apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/route.ts
  • packages/db/src/schema/rules/gradual-rollout.ts
  • packages/rule-engine/src/rules/gradual-rollout-rule.ts
  • apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/openapi.ts
  • e2e/api/schema.ts
  • packages/rule-engine/src/manager/version-manager-rules.ts
🧬 Code Graph Analysis (8)
packages/db/src/schema/policy-relations.ts (1)
packages/db/src/schema/rules/gradual-rollout.ts (1)
  • policyRuleGradualRollout (7-19)
apps/jobs/src/policy-checker/index.ts (2)
packages/db/src/client.ts (1)
  • db (15-15)
packages/events/src/index.ts (1)
  • getQueue (28-34)
packages/db/src/schema/policy.ts (1)
packages/db/src/schema/rules/gradual-rollout.ts (1)
  • createPolicyRuleGradualRollout (21-28)
e2e/tests/api/policies/gradual-rollout.spec.ts (3)
e2e/tests/fixtures.ts (1)
  • test (11-26)
e2e/api/yaml-loader.ts (3)
  • ImportedEntities (73-123)
  • importEntitiesFromYaml (133-368)
  • cleanupImportedEntities (375-423)
packages/db/src/schema/workspace.ts (1)
  • workspace (18-27)
packages/rule-engine/src/db/update-policy.ts (2)
packages/db/src/common.ts (1)
  • Tx (22-22)
packages/db/src/schema/policy.ts (2)
  • UpdatePolicy (185-185)
  • policy (43-63)
packages/db/src/schema/rules/gradual-rollout.ts (1)
packages/db/src/schema/policy.ts (1)
  • policy (43-63)
packages/rule-engine/src/rules/gradual-rollout-rule.ts (2)
packages/rule-engine/src/manager/version-rule-engine.ts (1)
  • Version (11-17)
packages/rule-engine/src/types.ts (2)
  • FilterRule (31-36)
  • RuleEngineRuleResult (23-26)
packages/rule-engine/src/manager/version-manager-rules.ts (7)
packages/rule-engine/src/types.ts (2)
  • Policy (66-73)
  • FilterRule (31-36)
packages/rule-engine/src/manager/version-rule-engine.ts (1)
  • Version (11-17)
packages/rule-engine/src/rules/version-approval-rule.ts (3)
  • getAnyApprovalRecords (68-81)
  • getUserApprovalRecords (98-111)
  • getRoleApprovalRecords (83-96)
packages/db/src/schema/release.ts (1)
  • releaseTarget (20-42)
packages/db/src/client.ts (1)
  • db (15-15)
packages/db/src/common.ts (1)
  • takeFirst (9-13)
packages/rule-engine/src/rules/gradual-rollout-rule.ts (1)
  • GradualRolloutRule (14-59)
🪛 Checkov (3.2.334)
openapi.v1.json

[MEDIUM] 350-355: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Lint
🔇 Additional comments (36)
packages/db/src/schema/rules/index.ts (1)

12-12: LGTM! Clean export addition for the gradual rollout module.

This export makes the new gradual rollout functionality accessible to other parts of the application, which is essential for implementing the gradual rollout feature.

packages/api/src/router/policy/evaluate.ts (1)

62-62: LGTM! Correctly includes gradual rollout data in policy evaluation.

Adding the gradualRollout: true property ensures that the gradual rollout relation is included when policies are fetched for evaluation. This is necessary for the policy evaluation logic to access and apply gradual rollout rules.

packages/db/drizzle/meta/_journal.json (1)

746-753: LGTM! Properly added migration for the gradual rollout feature.

The new migration entry with tag "0106_wonderful_iron_fist" is correctly added to the journal to track the database schema changes for the gradual rollout feature.

packages/rule-engine/src/rules/version-approval-rule.ts (1)

15-15: LGTM! Added necessary timestamp for approval tracking.

Adding the approvedAt field to the Record type is a good enhancement that enables tracking when approvals occur. This timestamp is essential for implementing time-based gradual rollout logic that depends on approval timing.

packages/rule-engine/src/db/get-applicable-policies.ts (1)

43-43: LGTM! Gradual rollout data is now included in policy queries.

The addition of gradualRollout: true to the query ensures that when fetching applicable policies, the gradual rollout configuration will be included. This change properly integrates the new feature into the existing query structure.

packages/rule-engine/src/types.ts (1)

72-72: LGTM! Type definition properly updated for gradual rollout.

The Policy type has been correctly extended to include the optional gradualRollout field of type schema.PolicyGradualRollout | null. This change ensures type safety when working with policies that may include gradual rollout configurations.

packages/db/src/schema/policy-relations.ts (2)

13-13: LGTM! Import for gradual rollout schema added.

The import of policyRuleGradualRollout from the rules index has been properly added to support the new relation.


28-29: LGTM! One-to-one relation established for gradual rollout.

The one-to-one relation between policy and gradual rollout has been correctly defined. The blank line after the relation maintains consistent code formatting with other similar relations.

apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts (1)

80-83: LGTM! OpenAPI schema updated to include gradual rollout.

The PATCH endpoint for updating policies has been properly extended to include the gradualRollout field, with the correct reference to the GradualRollout schema component and appropriate nullable setting.

packages/db/src/schema/rules/rule-relations.ts (2)

20-20: Import for gradual rollout added correctly.

The import is properly placed among other rule imports, maintaining the organization of the file.


132-140: Relation definition follows established pattern.

The policyRuleGradualRolloutRelations follows the same pattern as other rule relations, correctly establishing a one-to-one relationship between the gradual rollout rule and its parent policy.

packages/db/drizzle/0106_wonderful_iron_fist.sql (2)

1-9: Table structure is well-designed with appropriate constraints.

The policy_rule_gradual_rollout table has all necessary fields for implementing gradual rollout functionality:

  • Primary key with UUID generation
  • One-to-one relationship with policy (via unique constraint)
  • Required fields for deployment rate and window size
  • Optional description field

This structure supports the gradual rollout feature requirements effectively.


11-11: Foreign key constraint properly configured.

The foreign key to the policy table includes cascade delete, which ensures that when a policy is deleted, its associated gradual rollout rule is also removed. This maintains referential integrity in the database.

packages/db/src/schema/policy.ts (3)

40-40: Import added in appropriate location.

The import statement for the gradual rollout schema is correctly placed with other rule imports.


151-155: Schema extension properly implements gradual rollout in create policy.

The gradualRollout field is correctly added to the create policy schema:

  • Properly omits the policyId as it will be set during creation
  • Made optional and nullable since not all policies will use gradual rollout
  • Follows the same pattern used for other rule types

This implementation aligns with the established pattern in the codebase.


180-184: Schema extension properly implements gradual rollout in update policy.

The gradualRollout field is correctly added to the update policy schema with the same configuration as in the create policy schema, maintaining consistency between the two.

e2e/tests/api/policies/gradual-rollout.spec.yaml (2)

1-14: Test system and environment setup is well structured.

The test fixture correctly sets up a system and environment with appropriate naming using template variables. The resource selector is properly configured to select resources by identifier, which will work well with the test resources defined below.


15-104: Comprehensive test resource setup for gradual rollout testing.

The test fixture creates 10 service resources with:

  • Systematic naming using the prefix variable
  • Consistent versioning (1.0.0)
  • Alternating environments (prod/qa) in metadata
  • Enabled status for all services

This provides an excellent foundation for testing various gradual rollout scenarios with different resource subsets.

e2e/tests/api/policies/gradual-rollout.spec.ts (3)

12-12: Good use of constants for readability.

The constant TEN_MINUTES clearly communicates the timeout value and its purpose.


16-33: Test setup follows good practices.

The test setup loads entities from a YAML file and cleans them up afterward, ensuring proper test isolation.


41-51: Good use of randomization for test isolation.

Using a random deployment slug prevents potential conflicts with other tests.

packages/db/src/schema/rules/gradual-rollout.ts (3)

7-19: Well-structured database schema design.

The table schema follows best practices with proper foreign key constraints, uniqueness, and field definitions.


21-28: Appropriate validation constraints.

The validation ensures that deployRate and windowSizeMinutes are at least 1, preventing invalid configurations.


30-30: Good type export pattern.

Exporting the inferred type promotes type safety throughout the application.

packages/rule-engine/src/db/update-policy.ts (4)

139-154: Well-implemented update function.

The updateGradualRollout function follows the same pattern as other update functions in the file, handling all cases (undefined, null, update) correctly.


168-169: Consistent parameter destructuring.

The gradualRollout parameter is properly added to the destructured input parameters.


193-194: Parallel update execution.

Including updateGradualRollout in the Promise.all call maintains the efficient parallel update pattern.


205-206: Complete query response.

Including gradualRollout in the with clause ensures it's included in the query response.

apps/webservice/src/app/api/v1/policies/openapi.ts (4)

70-79: Well-defined OpenAPI schema for GradualRollout.

The schema properly defines all required properties and types, matching the database schema constraints.


112-116: Correctly referenced GradualRollout schema.

The reference to the GradualRollout schema component is properly set up with nullable option.


128-129: Properly added to required fields.

Including gradualRollout in the required fields ensures API contract consistency.


184-187: Request body schema properly updated.

The POST endpoint schema is correctly updated to accept the gradualRollout property.

e2e/api/schema.ts (2)

2064-2073: Good simplification of values payload

Switching to VariableValue[] removes an unnecessary nested selector union and keeps the request body leaner. Nice clean-up!


61-120:

❓ Verification inconclusive

Release path returns a different “Release” representation than the rest of the API

/v1/deployment-versions/{deploymentVersionId}/releases advertises a 200 payload of
components["schemas"]["Release"][], but elsewhere PATCH/POST /v1/releases still rely on Release1.

Because openapi-typescript had to auto-rename the original schema to Release1, client code that was written against the earlier contract (Release) will now silently break (type import no longer exists) or, worse, accept the new abbreviated object without id, name, status, etc.

Action items

  1. Keep only one canonical “Release” schema—rename the newly introduced lightweight object (e.g. ReleaseSummary) or revert the original and adapt this path.
  2. Run rg -t ts '"Release1"' (or a similar query) to update service / UI code that still imports the old name.
  3. Add an integration test that fetches this endpoint and asserts the presence of an id field so regressions are caught early.

#!/bin/bash
# Search for occurrences of Release1 in the repository
rg -n "Release1"

#!/usr/bin/env bash
set -e

# Search for occurrences of the auto-renamed Release1 schema in TypeScript files
rg -n '"Release1"' --glob '*.{ts,tsx}'

# Search for occurrences of the original Release schema in TypeScript files
rg -n '"Release"' --glob '*.{ts,tsx}'

# Search for TypeScript type definitions of Release1 and Release
rg -n 'type Release1' --glob '*.{ts,tsx}'
rg -n 'type Release =' --glob '*.{ts,tsx}'

#!/usr/bin/env bash
set -e

# Search for occurrences of the auto-renamed Release1 schema in TypeScript files
rg -n '"Release1"' --glob '*.{ts,tsx}'

# Search for usage of the original Release schema in the OpenAPI client
rg -n '"Release"' e2e/api/schema.ts

# Check for any lightweight summary type introduced alongside Release
rg -n 'ReleaseSummary' --glob '*.{ts,tsx}'

🏁 Script executed:

#!/usr/bin/env bash
set -e

# Locate occurrences of the auto-renamed Release1 schema
rg -n '"Release1"' --glob '*.{ts,tsx}'

# Check for any lightweight summary type alongside Release
rg -n 'ReleaseSummary' --glob '*.{ts,tsx}'

# Extract the releases endpoint definition for verification
sed -n '61,120p' e2e/api/schema.ts

Length of output: 49


#!/usr/bin/env bash
# Search for any occurrences of the auto-renamed Release1 schema
rg -n '"Release1"' --glob '*.{ts,tsx}' || true

# Search for any lightweight summary type like ReleaseSummary
rg -n 'ReleaseSummary' --glob '*.{ts,tsx}' || true

# Show the /v1/releases endpoint definition in schema.ts
rg -n '"/v1/releases"' -C 5 e2e/api/schema.ts || true

# Re-extract the /v1/deployment-versions/{deploymentVersionId}/releases snippet to reconfirm
sed -n '60,140p' e2e/api/schema.ts

Double-check “Release” schema consistency across endpoints

It looks like the /v1/deployment-versions/{deploymentVersionId}/releases endpoint is returning components["schemas"]["Release"][], while elsewhere the client may be importing a Release1 type (the auto-renamed original schema). This mismatch can lead to missing fields (id, name, status, etc.) or broken imports.

Action items

  • Verify whether openapi-typescript has generated both Release and Release1 types by searching your codebase for Release1.
  • Decide on a single canonical schema:
    • Rename the lightweight version to something like ReleaseSummary and update the path definition accordingly
    • Or revert the change and ensure /v1/deployment-versions/.../releases returns the full Release object.
  • Search for existing imports of Release1 (e.g. rg '"Release1"') and update consumer code to use the chosen schema name.
  • Add an integration test for this endpoint that asserts the presence of required fields (e.g. id) to prevent silent regressions.
openapi.v1.json (2)

1971-1975: Gradual rollout property added to PATCH policy schema
Good addition of the gradualRollout field to the PATCH /v1/policies/{policyId} request schema. This aligns the API contract with the new feature.


5985-5989: Ensure gradualRollout is returned in policy responses
You’ve added gradualRollout to the Policy1 schema and marked it nullable. Please verify that the backend controllers and rule-engine logic populate this field in the responses (and in the E2E tests), otherwise clients will receive null or miss the property entirely.

Comment on lines +181 to +185
if (gradualRollout != null)
await tx
.insert(SCHEMA.policyRuleGradualRollout)
.values({ ...gradualRollout, policyId });

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Upsert & cleanup logic missing – duplicate rows or constraint errors ahead

All other rule inserts use onConflictDoUpdate, making the operation idempotent.
policyRuleGradualRollout is the sole exception, so:

  1. Re-creating an existing policy will either violate a unique/PK constraint or create duplicates (depending on the table definition).
  2. Passing gradualRollout: null never removes the stale row, unlike the behaviour implemented in update-policy.ts.

Suggested fix:

-  if (gradualRollout != null)
-    await tx
-      .insert(SCHEMA.policyRuleGradualRollout)
-      .values({ ...gradualRollout, policyId });
+  if (gradualRollout != null) {
+    await tx
+      .insert(SCHEMA.policyRuleGradualRollout)
+      .values({ ...gradualRollout, policyId })
+      .onConflictDoUpdate({
+        target: [SCHEMA.policyRuleGradualRollout.id],
+        set: buildConflictUpdateColumns(
+          SCHEMA.policyRuleGradualRollout,
+          Object.keys(gradualRollout as Record<string, unknown>),
+        ),
+      });
+  } else {
+    // remove an existing gradual-rollout rule when the caller omits it
+    await tx
+      .delete(SCHEMA.policyRuleGradualRollout)
+      .where(eq(SCHEMA.policyRuleGradualRollout.policyId, policyId));
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (gradualRollout != null)
await tx
.insert(SCHEMA.policyRuleGradualRollout)
.values({ ...gradualRollout, policyId });
if (gradualRollout != null) {
await tx
.insert(SCHEMA.policyRuleGradualRollout)
.values({ ...gradualRollout, policyId })
.onConflictDoUpdate({
target: [SCHEMA.policyRuleGradualRollout.id],
set: buildConflictUpdateColumns(
SCHEMA.policyRuleGradualRollout,
Object.keys(gradualRollout as Record<string, unknown>),
),
});
} else {
// remove an existing gradual-rollout rule when the caller omits it
await tx
.delete(SCHEMA.policyRuleGradualRollout)
.where(eq(SCHEMA.policyRuleGradualRollout.policyId, policyId));
}
🤖 Prompt for AI Agents
In packages/rule-engine/src/db/create-policy.ts around lines 181 to 185, the
insert into policyRuleGradualRollout lacks upsert logic, risking duplicate rows
or constraint violations and does not handle removal when gradualRollout is
null. Modify the code to use onConflictDoUpdate for idempotent inserts and add
logic to delete the existing row if gradualRollout is null, matching the
behavior in update-policy.ts.

Comment on lines +17 to +37
const ct = alias(schema.computedPolicyTargetReleaseTarget, "ct");

const releaseTargets = await db
.select()
.from(schema.policy)
.innerJoin(
schema.policyTarget,
eq(schema.policyTarget.policyId, schema.policy.id),
)
.innerJoin(ct, eq(ct.policyTargetId, schema.policyTarget.id))
.innerJoin(
schema.releaseTarget,
eq(ct.releaseTargetId, schema.releaseTarget.id),
)
.innerJoin(
schema.policyRuleGradualRollout,
eq(schema.policyRuleGradualRollout.policyId, schema.policy.id),
)
.limit(PAGE_SIZE)
.offset(offset)
.then((rows) => rows.map((row) => row.release_target));
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Non-deterministic pagination + join fan-out risks missing or duplicate work

The query paginates with limit/offset but has no orderBy, so PostgreSQL can change row order between calls, causing duplicates or gaps.
Because the joins create a one-to-many relation (multiple policyTargets per policy, etc.) a single release_target can emerge multiple times.

-      const releaseTargets = await db
-        .select()
+      const releaseTargets = await db
+        .selectDistinctOn([schema.releaseTarget.id])
         .from(schema.policy)
 ...
-        .offset(offset)
+        .orderBy(schema.releaseTarget.id)
+        .offset(offset)
         .then((rows) => rows.map((row) => row.release_target));

This makes pagination deterministic and collapses duplicates.

🤖 Prompt for AI Agents
In apps/jobs/src/policy-checker/index.ts around lines 17 to 37, the query uses
limit and offset for pagination without an orderBy clause, causing
non-deterministic results and potential duplicates due to join fan-out. To fix
this, add a deterministic orderBy clause on a unique or stable column (e.g.,
primary key) to ensure consistent row ordering, and modify the query to collapse
duplicates by using distinct or grouping on release_target to avoid processing
the same release_target multiple times.

Comment on lines +65 to +97
const releases = await db
.select()
.from(SCHEMA.release)
.innerJoin(
SCHEMA.versionRelease,
eq(SCHEMA.release.versionReleaseId, SCHEMA.versionRelease.id),
)
.innerJoin(
variableReleaseSubquery,
eq(
SCHEMA.release.variableReleaseId,
variableReleaseSubquery.variableSetReleaseId,
),
)
.innerJoin(
SCHEMA.releaseTarget,
eq(SCHEMA.versionRelease.releaseTargetId, SCHEMA.releaseTarget.id),
)
.innerJoin(
SCHEMA.resource,
eq(SCHEMA.releaseTarget.resourceId, SCHEMA.resource.id),
)
.innerJoin(
SCHEMA.environment,
eq(SCHEMA.releaseTarget.environmentId, SCHEMA.environment.id),
)
.innerJoin(
SCHEMA.deployment,
eq(SCHEMA.releaseTarget.deploymentId, SCHEMA.deployment.id),
)
.where(eq(SCHEMA.versionRelease.versionId, deploymentVersionId))
.limit(500);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add deterministic ordering & cursor-friendly pagination to the releases query

Today the query limits the result set to 500 rows, but returns them in an undefined order.
This can cause:

  • Non-deterministic responses across identical requests (hard to cache & page).
  • Starvation of older releases if new ones constantly push them out of the first 500.

Consider ordering by a monotonic column (createdAt, id, etc.) and exposing limit/cursor query-params so consumers can reliably traverse the full history.

-      .where(eq(SCHEMA.versionRelease.versionId, deploymentVersionId))
-      .limit(500);
+      .where(eq(SCHEMA.versionRelease.versionId, deploymentVersionId))
+      .orderBy(sql`${SCHEMA.release.createdAt} DESC`)  // deterministic
+      .limit(limit)                                    // value from searchParams
+      .offset(offset);                                 // cursor/offset

This small change prevents pagination gaps and improves cacheability.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const releases = await db
.select()
.from(SCHEMA.release)
.innerJoin(
SCHEMA.versionRelease,
eq(SCHEMA.release.versionReleaseId, SCHEMA.versionRelease.id),
)
.innerJoin(
variableReleaseSubquery,
eq(
SCHEMA.release.variableReleaseId,
variableReleaseSubquery.variableSetReleaseId,
),
)
.innerJoin(
SCHEMA.releaseTarget,
eq(SCHEMA.versionRelease.releaseTargetId, SCHEMA.releaseTarget.id),
)
.innerJoin(
SCHEMA.resource,
eq(SCHEMA.releaseTarget.resourceId, SCHEMA.resource.id),
)
.innerJoin(
SCHEMA.environment,
eq(SCHEMA.releaseTarget.environmentId, SCHEMA.environment.id),
)
.innerJoin(
SCHEMA.deployment,
eq(SCHEMA.releaseTarget.deploymentId, SCHEMA.deployment.id),
)
.where(eq(SCHEMA.versionRelease.versionId, deploymentVersionId))
.limit(500);
const releases = await db
.select()
.from(SCHEMA.release)
.innerJoin(
SCHEMA.versionRelease,
eq(SCHEMA.release.versionReleaseId, SCHEMA.versionRelease.id),
)
.innerJoin(
variableReleaseSubquery,
eq(
SCHEMA.release.variableReleaseId,
variableReleaseSubquery.variableSetReleaseId,
),
)
.innerJoin(
SCHEMA.releaseTarget,
eq(SCHEMA.versionRelease.releaseTargetId, SCHEMA.releaseTarget.id),
)
.innerJoin(
SCHEMA.resource,
eq(SCHEMA.releaseTarget.resourceId, SCHEMA.resource.id),
)
.innerJoin(
SCHEMA.environment,
eq(SCHEMA.releaseTarget.environmentId, SCHEMA.environment.id),
)
.innerJoin(
SCHEMA.deployment,
eq(SCHEMA.releaseTarget.deploymentId, SCHEMA.deployment.id),
)
.where(eq(SCHEMA.versionRelease.versionId, deploymentVersionId))
.orderBy(sql`${SCHEMA.release.createdAt} DESC`) // deterministic order
.limit(limit) // from request params
.offset(offset); // for cursor/offset pagination
🤖 Prompt for AI Agents
In
apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/releases/route.ts
between lines 65 and 97, the releases query limits results to 500 without any
ordering, causing non-deterministic and inconsistent pagination. Fix this by
adding an orderBy clause on a monotonic column like createdAt or id to ensure
deterministic ordering. Additionally, modify the API to accept limit and cursor
query parameters to enable cursor-based pagination, allowing consumers to
reliably page through the full release history without gaps or starvation.

Comment on lines 94 to 104
const latestRecord = _.chain([
...anyApprovalRecords,
...userApprovalRecords,
...roleApprovalRecords,
])
.filter((r) => r.approvedAt != null)
.maxBy((r) => r.approvedAt)
.value();

return latestRecord.approvedAt ?? version.createdAt;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against missing approval records to avoid runtime crash

When a policy has approval rules but zero approval records (e.g., a rule
requires 0 approvals or DB inconsistency), latestRecord is undefined,
causing latestRecord.approvedAt to throw.

-    const latestRecord = _.chain([
+    const latestRecord: { approvedAt: Date } | undefined = _.chain([
       ...anyApprovalRecords,
       ...userApprovalRecords,
       ...roleApprovalRecords,
     ])
       .filter((r) => r.approvedAt != null)
       .maxBy((r) => r.approvedAt)
       .value();
-
-    return latestRecord.approvedAt ?? version.createdAt;
+    return latestRecord?.approvedAt ?? version.createdAt;

This preserves the intended fallback to version.createdAt without risking a
TypeError.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const latestRecord = _.chain([
...anyApprovalRecords,
...userApprovalRecords,
...roleApprovalRecords,
])
.filter((r) => r.approvedAt != null)
.maxBy((r) => r.approvedAt)
.value();
return latestRecord.approvedAt ?? version.createdAt;
};
const latestRecord: { approvedAt: Date } | undefined = _.chain([
...anyApprovalRecords,
...userApprovalRecords,
...roleApprovalRecords,
])
.filter((r) => r.approvedAt != null)
.maxBy((r) => r.approvedAt)
.value();
return latestRecord?.approvedAt ?? version.createdAt;
};
🤖 Prompt for AI Agents
In packages/rule-engine/src/manager/version-manager-rules.ts around lines 94 to
104, the code assumes latestRecord is always defined, but if there are no
approval records, latestRecord is undefined causing a runtime error when
accessing latestRecord.approvedAt. Fix this by adding a guard to check if
latestRecord exists before accessing approvedAt, and if not, return
version.createdAt as the fallback to avoid the TypeError.

Comment on lines +1362 to +1367
GradualRollout: {
deployRate: number;
windowSizeMinutes: number;
name: string;
description?: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

GradualRollout needs domain constraints

The schema currently allows any positive/negative/NaN number:

  • deployRate – is this 0-100 %? 0-1? integer only?
  • windowSizeMinutes – should be a positive integer.
  • name – max length? pattern?

Consider adding minimum, maximum, and pattern/maxLength to prevent invalid policies from reaching the backend.

🤖 Prompt for AI Agents
In e2e/api/schema.ts around lines 1362 to 1367, the GradualRollout schema
properties lack domain constraints, allowing invalid values. Add validation
rules: set deployRate to a number between 0 and 100 (or clarify if 0-1),
windowSizeMinutes to a positive integer with minimum 1, and add maxLength and
optionally a pattern to name to restrict its format and length. Use JSON schema
keywords like minimum, maximum, maxLength, and pattern to enforce these
constraints.

Comment on lines +973 to 982
Release: {
resource?: components["schemas"]["Resource"];
environment?: components["schemas"]["Environment"];
deployment?: components["schemas"]["Deployment"];
version?: components["schemas"]["DeploymentVersion"];
variables?: {
[key: string]: unknown;
};
};
BaseVariableValue: {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

New Release schema is probably incomplete

A release returned from any endpoint normally needs at least an identifier and timestamps so that UI & automation can reference it.
The newly added schema only contains optional relations + a variables map – no id, name, createdAt, or status.

Unless this is intentional (e.g. “embedded release snippet”), please extend the schema or introduce a derivative type that reflects its truncated shape explicitly.

+id: string;        // uuid
+name: string;
+createdAt: string; // date-time
+status?: "ready" | "building" | "failed";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Release: {
resource?: components["schemas"]["Resource"];
environment?: components["schemas"]["Environment"];
deployment?: components["schemas"]["Deployment"];
version?: components["schemas"]["DeploymentVersion"];
variables?: {
[key: string]: unknown;
};
};
BaseVariableValue: {
Release: {
id: string; // uuid
name: string;
createdAt: string; // date-time
status?: "ready" | "building" | "failed";
resource?: components["schemas"]["Resource"];
environment?: components["schemas"]["Environment"];
deployment?: components["schemas"]["Deployment"];
version?: components["schemas"]["DeploymentVersion"];
variables?: {
[key: string]: unknown;
};
};
🤖 Prompt for AI Agents
In e2e/api/schema.ts around lines 973 to 982, the Release schema lacks essential
fields like id, name, createdAt, and status, which are typically required for UI
and automation references. To fix this, add these missing fields to the Release
schema with appropriate types, or create a new derived type that explicitly
represents this truncated version if the omission is intentional. Ensure the
schema accurately reflects the data structure returned by the endpoints.

Comment on lines +4819 to +4839
"Release": {
"type": "object",
"properties": {
"resource": {
"$ref": "#/components/schemas/Resource"
},
"environment": {
"$ref": "#/components/schemas/Environment"
},
"deployment": {
"$ref": "#/components/schemas/Deployment"
},
"version": {
"$ref": "#/components/schemas/DeploymentVersion"
},
"variables": {
"type": "object",
"additionalProperties": true
}
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

New Release schema introduced—ensure references update correctly
You’ve defined a fresh Release schema for the new endpoint, but the existing endpoints still reference a Release model (formerly the old one). Because the old schema was renamed to Release1, you must update all endpoint schemas (e.g., POST /v1/releases, PATCH /v1/releases/{releaseId}) to reference Release1 instead of Release. Otherwise, clients will receive the wrong payload structure.

🤖 Prompt for AI Agents
In openapi.v1.json around lines 4819 to 4839, you introduced a new Release
schema but renamed the old Release schema to Release1. Update all endpoint
schemas that reference the old Release model, such as POST /v1/releases and
PATCH /v1/releases/{releaseId}, to use Release1 instead of Release to ensure
clients receive the correct payload structure.

Comment on lines +331 to +397
"/v1/deployment-versions/{deploymentVersionId}/releases": {
"get": {
"summary": "Get all releases for a deployment version",
"parameters": [
{
"name": "deploymentVersionId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Release"
}
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
}
}
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Missing operationId and tagging for the new endpoint
The GET /v1/deployment-versions/{deploymentVersionId}/releases operation is missing an operationId (and a tags array for grouping). Having a consistent operationId helps code generation and future maintenance.

Apply this diff to introduce an operationId and group under a logical tag:

 "/v1/deployment-versions/{deploymentVersionId}/releases": {
   "get": {
+    "operationId": "getDeploymentVersionReleases",
+    "tags": ["DeploymentVersions"],
     "summary": "Get all releases for a deployment version",
     "parameters": [
       {
         "name": "deploymentVersionId",
         "in": "path",
         "required": true,
         "schema": {
           "type": "string",
           "format": "uuid"
         }
       }
     ],
     "responses": {
       "200": { ... }
     }
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"/v1/deployment-versions/{deploymentVersionId}/releases": {
"get": {
"summary": "Get all releases for a deployment version",
"parameters": [
{
"name": "deploymentVersionId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Release"
}
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
}
}
}
},
"/v1/deployment-versions/{deploymentVersionId}/releases": {
"get": {
"operationId": "getDeploymentVersionReleases",
"tags": ["DeploymentVersions"],
"summary": "Get all releases for a deployment version",
"parameters": [
{
"name": "deploymentVersionId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Release"
}
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
}
}
}
},
🧰 Tools
🪛 Checkov (3.2.334)

[MEDIUM] 350-355: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🤖 Prompt for AI Agents
In openapi.v1.json between lines 331 and 397, the GET endpoint for
/v1/deployment-versions/{deploymentVersionId}/releases lacks an operationId and
tags array. Add a unique operationId to identify this operation clearly and
include a tags array with a relevant tag name to group this endpoint logically
for better code generation and maintenance.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/rule-engine/src/manager/version-manager-rules.ts (1)

106-111: ⚠️ Potential issue

Fix potential null reference exception

Similar to a previously identified issue, if there are no approval records or none with an approvedAt value, latestApprovalRecord will be undefined, causing a runtime error when accessing latestApprovalRecord.approvedAt.

Apply this diff to fix the issue:

-  const latestApprovalRecord = _.chain(allApprovalRecords)
+  const latestApprovalRecord: { approvedAt: Date } | undefined = _.chain(allApprovalRecords)
     .filter((record) => record.approvedAt != null)
     .maxBy((record) => record.approvedAt)
     .value();

-  return latestApprovalRecord.approvedAt ?? version.createdAt;
+  return latestApprovalRecord?.approvedAt ?? version.createdAt;
🧹 Nitpick comments (2)
packages/rule-engine/src/manager/version-manager-rules.ts (2)

139-145: Add error handling for the database query

The takeFirst function will throw an error if the query returns zero or multiple results. While this shouldn't happen with a properly configured database, it's good practice to add error handling.

Consider adding more specific error handling:

  return db
    .select()
    .from(orderedTargetsSubquery)
    .where(eq(orderedTargetsSubquery.id, releaseTargetId))
-    .then(takeFirst)
-    .then((r) => r.position);
+    .then(results => {
+      try {
+        return takeFirst(results).position;
+      } catch (error) {
+        throw new Error(`Failed to determine position for release target ${releaseTargetId}: ${error.message}`);
+      }
+    });

126-128: Consider adding a comment explaining the deterministic ordering logic

The SQL query uses an MD5 hash of concatenated IDs to create a deterministic but pseudo-random ordering of release targets. A comment explaining this design choice would help future maintainers understand the reasoning.

      position:
        sql<number>`ROW_NUMBER() OVER (ORDER BY md5(id || ${version.id}) ASC) - 1`.as(
          "position",
+         // Creates a deterministic but pseudo-random ordering of release targets
+         // based on a hash of their IDs combined with the version ID
        ),
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between b7f8ce3 and 607a6ca.

📒 Files selected for processing (1)
  • packages/rule-engine/src/manager/version-manager-rules.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{ts,tsx}`: **Note on Error Handling:** Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error...

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

  • packages/rule-engine/src/manager/version-manager-rules.ts
🧬 Code Graph Analysis (1)
packages/rule-engine/src/manager/version-manager-rules.ts (7)
packages/rule-engine/src/types.ts (2)
  • Policy (66-73)
  • FilterRule (31-36)
packages/rule-engine/src/manager/version-rule-engine.ts (1)
  • Version (11-17)
packages/rule-engine/src/rules/version-approval-rule.ts (3)
  • getAnyApprovalRecords (68-81)
  • getUserApprovalRecords (98-111)
  • getRoleApprovalRecords (83-96)
packages/db/src/schema/release.ts (1)
  • releaseTarget (20-42)
packages/db/src/client.ts (1)
  • db (15-15)
packages/db/src/common.ts (1)
  • takeFirst (9-13)
packages/rule-engine/src/rules/gradual-rollout-rule.ts (1)
  • GradualRolloutRule (14-67)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (3)
packages/rule-engine/src/manager/version-manager-rules.ts (3)

83-97: Good implementation of approval rules validation

The approach to verifying that a version passes all approval rules is well-implemented. Using reduce with an async function to short-circuit at the first failing rule is efficient and clean.


161-162: Well-integrated gradual rollout rule

The gradual rollout rule is correctly added to the array of rules and properly filtered to remove any null entries. This maintains backward compatibility with policies that don't have gradual rollout configured.


147-152: Clean implementation of GradualRolloutRule instantiation

The GradualRolloutRule is instantiated with the policy's gradual rollout configuration and the necessary helper functions, following the dependency injection pattern. This separation of concerns enhances testability and maintainability.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/rule-engine/src/manager/version-manager-rules.ts (1)

109-115: ⚠️ Potential issue

Guard against missing approval records to avoid runtime crash

When a policy has approval rules but zero approval records (e.g., a rule requires 0 approvals or DB inconsistency), latestApprovalRecord could be undefined, causing latestApprovalRecord.approvedAt to throw.

 const latestApprovalRecord = _.chain(allApprovalRecords)
   .filter((record) => record.approvedAt != null)
   .maxBy((record) => record.approvedAt)
   .value();

-return latestApprovalRecord.approvedAt ?? version.createdAt;
+return latestApprovalRecord?.approvedAt ?? version.createdAt;
🧹 Nitpick comments (5)
packages/rule-engine/src/rules/environment-version-rollout-rule.ts (3)

8-22: Consider adding input validation for deployment offset functions

The linear and exponential deployment offset functions don't validate their inputs. Negative values for positionGrowthFactor or timeScaleMinutes could lead to unexpected behavior.

 export const linearDeploymentOffset =
   (
     positionGrowthFactor: number,
     timeScaleMinutes: number,
   ): GetDeploymentOffsetMinutes =>
-  (x: number) =>
-    timeScaleMinutes * (x / positionGrowthFactor);
+  (x: number) => {
+    if (positionGrowthFactor <= 0 || timeScaleMinutes < 0) {
+      throw new Error("Invalid parameters: positionGrowthFactor must be positive and timeScaleMinutes must be non-negative");
+    }
+    return timeScaleMinutes * (x / positionGrowthFactor);
+  };

 export const exponentialDeploymentOffset =
   (
     positionGrowthFactor: number,
     timeScaleMinutes: number,
   ): GetDeploymentOffsetMinutes =>
-  (x: number) =>
-    timeScaleMinutes * (Math.exp(x / positionGrowthFactor) - 1);
+  (x: number) => {
+    if (positionGrowthFactor <= 0 || timeScaleMinutes < 0) {
+      throw new Error("Invalid parameters: positionGrowthFactor must be positive and timeScaleMinutes must be non-negative");
+    }
+    return timeScaleMinutes * (Math.exp(x / positionGrowthFactor) - 1);
+  };

46-73: The filter method lacks error handling for async operations

The filter method contains several await operations but doesn't handle potential exceptions from the async calls to getRolloutStartTime, getDeploymentTime, or getReleaseTargetPosition. Consider adding try/catch blocks.

 async filter(candidates: Version[]): Promise<RuleEngineRuleResult<Version>> {
   const now = this.getCurrentTime();
   const rejectionReasons = new Map<string, string>();
   const skip =
     this.options.skipReason ??
     "Version not eligible for deployment in the current time window";

   for (const candidate of candidates) {
+    try {
       const startTime = await this.options.getRolloutStartTime(candidate);
       if (startTime == null) {
         rejectionReasons.set(
           candidate.id,
           "Rollout has not started for this version",
         );
         continue;
       }
       const deploymentTime = await this.getDeploymentTime(candidate, startTime);

       const isEligible =
         isAfter(now, deploymentTime) || isEqual(now, deploymentTime);
       if (!isEligible) rejectionReasons.set(candidate.id, skip);
+    } catch (error) {
+      rejectionReasons.set(
+        candidate.id,
+        `Error determining eligibility: ${error instanceof Error ? error.message : String(error)}`
+      );
+    }
   }

   return {
     allowedCandidates: candidates.filter((c) => !rejectionReasons.has(c.id)),
     rejectionReasons,
   };
 }

24-74: Add JSDoc comments to improve code documentation

The class and its methods lack documentation comments. Adding JSDoc comments would improve readability and make the purpose of each component clearer.

+/**
+ * Options for configuring the environment version rollout rule.
+ */
 type EnvironmentVersionRolloutRuleOptions = {
   getRolloutStartTime: (version: Version) => Date | Promise<Date | null> | null;
   getReleaseTargetPosition: (version: Version) => number | Promise<number>;
   getDeploymentOffsetMinutes: GetDeploymentOffsetMinutes;
   skipReason?: string;
 };

+/**
+ * Rule that filters versions based on their calculated deployment time.
+ * Versions are allowed if the current time is after or equal to their deployment time.
+ */
 export class EnvironmentVersionRolloutRule implements FilterRule<Version> {
   public readonly name = "EnvironmentVersionRolloutRule";

   constructor(private readonly options: EnvironmentVersionRolloutRuleOptions) {}

+  /**
+   * Returns the current time. Protected for easier testing.
+   */
   protected getCurrentTime() {
     return new Date();
   }

+  /**
+   * Calculates the deployment time for a version based on its start time and target position.
+   * @param version The version to calculate deployment time for
+   * @param startTime The rollout start time
+   * @returns The calculated deployment time
+   */
   async getDeploymentTime(version: Version, startTime: Date) {
     const targetPosition = await this.options.getReleaseTargetPosition(version);
     const minutes = this.options.getDeploymentOffsetMinutes(targetPosition);
     return addMinutes(startOfMinute(startTime), minutes);
   }
packages/rule-engine/src/manager/version-manager-rules.ts (2)

117-148: Improve error handling in database operations

The database query in getReleaseTargetPosition has minimal error handling. Consider adding try/catch blocks to provide more context in case of database errors.

 const getReleaseTargetPosition = async (version: Version) => {
+  try {
     const releaseTarget = await db.query.releaseTarget.findFirst({
       where: eq(schema.releaseTarget.id, releaseTargetId),
     });
 
     if (releaseTarget == null)
       throw new Error(`Release target ${releaseTargetId} not found`);
 
     const orderedTargetsSubquery = db
       .select({
         id: schema.releaseTarget.id,
         position:
           sql<number>`ROW_NUMBER() OVER (ORDER BY md5(id || ${version.id}) ASC) - 1`.as(
             "position",
           ),
       })
       .from(schema.releaseTarget)
       .where(
         and(
           eq(schema.releaseTarget.environmentId, releaseTarget.environmentId),
           eq(schema.releaseTarget.deploymentId, releaseTarget.deploymentId),
         ),
       )
       .as("ordered_targets");
 
     return db
       .select()
       .from(orderedTargetsSubquery)
       .where(eq(orderedTargetsSubquery.id, releaseTargetId))
       .then(takeFirst)
       .then((r) => r.position);
+  } catch (error) {
+    throw new Error(`Failed to determine release target position: ${error instanceof Error ? error.message : String(error)}`);
+  }
 };

86-115: Consider refactoring the approval check logic

The logic to check if all approval rules pass (lines 91-98) can be simplified using Promise.all or a more straightforward approach.

-// Check if version passes all approval rules
-const allApprovalsPassed = await versionApprovalRules.reduce(
-  async (passedSoFar, rule) => {
-    if (!(await passedSoFar)) return false;
-    const { allowedCandidates } = await rule.filter([version]);
-    return allowedCandidates.length > 0;
-  },
-  Promise.resolve(true),
-);
+// Check if version passes all approval rules
+const approvalResults = await Promise.all(
+  versionApprovalRules.map(rule => rule.filter([version]))
+);
+const allApprovalsPassed = approvalResults.every(
+  result => result.allowedCandidates.length > 0
+);
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 607a6ca and 8503871.

📒 Files selected for processing (2)
  • packages/rule-engine/src/manager/version-manager-rules.ts (2 hunks)
  • packages/rule-engine/src/rules/environment-version-rollout-rule.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{ts,tsx}`: **Note on Error Handling:** Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error...

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

  • packages/rule-engine/src/rules/environment-version-rollout-rule.ts
  • packages/rule-engine/src/manager/version-manager-rules.ts

Comment on lines 150 to 153
const getDeploymentOffsetMinutes = linearDeploymentOffset(
policy.environmentVersionRollou.positionGrowthFactor,
policy.environmentVersionRollou.timeScaleMinutes,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typo in property name

There's a typo in the property name environmentVersionRollou which should be environmentVersionRollout.

 const getDeploymentOffsetMinutes = linearDeploymentOffset(
-  policy.environmentVersionRollou.positionGrowthFactor,
-  policy.environmentVersionRollou.timeScaleMinutes,
+  policy.environmentVersionRollout.positionGrowthFactor,
+  policy.environmentVersionRollout.timeScaleMinutes,
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getDeploymentOffsetMinutes = linearDeploymentOffset(
policy.environmentVersionRollou.positionGrowthFactor,
policy.environmentVersionRollou.timeScaleMinutes,
);
const getDeploymentOffsetMinutes = linearDeploymentOffset(
policy.environmentVersionRollout.positionGrowthFactor,
policy.environmentVersionRollout.timeScaleMinutes,
);
🤖 Prompt for AI Agents
In packages/rule-engine/src/manager/version-manager-rules.ts around lines 150 to
153, fix the typo in the property name by changing all instances of
`environmentVersionRollou` to `environmentVersionRollout` to correctly reference
the intended property.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/rule-engine/src/manager/version-manager-rules.ts (1)

114-114: ⚠️ Potential issue

Guard against missing approval records to avoid runtime crash

When there are no approval records found (either empty list or all records have null approvedAt), latestApprovalRecord will be undefined, causing a TypeError when accessing latestApprovalRecord.approvedAt.

Apply this fix to prevent runtime errors:

-    return latestApprovalRecord.approvedAt ?? version.createdAt;
+    return latestApprovalRecord?.approvedAt ?? version.createdAt;
🧹 Nitpick comments (3)
packages/rule-engine/src/manager/version-manager-rules.ts (3)

91-98: Consider simplifying approval validation logic

The current approval validation uses a reduce with an async callback, which can be harder to reason about. Consider using a more straightforward approach with a for loop.

-    const allApprovalsPassed = await versionApprovalRules.reduce(
-      async (passedSoFar, rule) => {
-        if (!(await passedSoFar)) return false;
-        const { allowedCandidates } = await rule.filter([version]);
-        return allowedCandidates.length > 0;
-      },
-      Promise.resolve(true),
-    );
+    let allApprovalsPassed = true;
+    for (const rule of versionApprovalRules) {
+      const { allowedCandidates } = await rule.filter([version]);
+      if (allowedCandidates.length === 0) {
+        allApprovalsPassed = false;
+        break;
+      }
+    }

103-107: Optimize approval records fetching

The current implementation fetches approval records from three separate sources, which could be inefficient. Consider combining these queries if possible.

-    const allApprovalRecords = [
-      ...(await getAnyApprovalRecords([version.id])),
-      ...(await getUserApprovalRecords([version.id])),
-      ...(await getRoleApprovalRecords([version.id])),
-    ];
+    // Fetch all approval records in parallel for better performance
+    const [anyApprovals, userApprovals, roleApprovals] = await Promise.all([
+      getAnyApprovalRecords([version.id]),
+      getUserApprovalRecords([version.id]),
+      getRoleApprovalRecords([version.id]),
+    ]);
+    
+    const allApprovalRecords = [
+      ...anyApprovals,
+      ...userApprovals,
+      ...roleApprovals,
+    ];

109-113: Add type annotation for latestApprovalRecord

Adding a type annotation would make it clearer that latestApprovalRecord might be undefined and help prevent potential bugs.

-    const latestApprovalRecord = _.chain(allApprovalRecords)
+    const latestApprovalRecord: { approvedAt: Date | null } | undefined = _.chain(allApprovalRecords)
       .filter((record) => record.approvedAt != null)
       .maxBy((record) => record.approvedAt)
       .value();
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8503871 and a465c77.

📒 Files selected for processing (2)
  • packages/rule-engine/src/manager/version-manager-rules.ts (2 hunks)
  • packages/rule-engine/src/rules/environment-version-rollout-rule.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rule-engine/src/rules/environment-version-rollout-rule.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{ts,tsx}`: **Note on Error Handling:** Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error...

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

  • packages/rule-engine/src/manager/version-manager-rules.ts
🧬 Code Graph Analysis (1)
packages/rule-engine/src/manager/version-manager-rules.ts (7)
packages/rule-engine/src/types.ts (2)
  • Policy (66-73)
  • FilterRule (31-36)
packages/rule-engine/src/manager/version-rule-engine.ts (1)
  • Version (11-17)
packages/rule-engine/src/rules/version-approval-rule.ts (3)
  • getAnyApprovalRecords (68-81)
  • getUserApprovalRecords (98-111)
  • getRoleApprovalRecords (83-96)
packages/db/src/schema/release.ts (1)
  • releaseTarget (20-42)
packages/db/src/client.ts (1)
  • db (15-15)
packages/db/src/common.ts (1)
  • takeFirst (9-13)
packages/rule-engine/src/rules/environment-version-rollout-rule.ts (2)
  • linearDeploymentOffset (8-14)
  • EnvironmentVersionRolloutRule (31-74)
🔇 Additional comments (3)
packages/rule-engine/src/manager/version-manager-rules.ts (3)

169-171: LGTM: Properly integrates the gradual rollout rule

The gradual rollout rule is correctly added to the list of rules and filtered to remove any nulls using isPresent. This is a clean integration with the existing rule system.


86-115: Comprehensive rollout start time determination

The implementation to determine the rollout start time is well-structured:

  1. First checks if all approval rules are satisfied
  2. If so, finds the latest approval timestamp
  3. Falls back to version creation time if no approvals exist

This ensures gradual rollouts begin at an appropriate time based on the approval workflow.


143-147: 🛠️ Refactor suggestion

Add error handling for the takeFirst function

The takeFirst function will throw an error if the query doesn't return exactly one result, according to its implementation in packages/db/src/common.ts. Consider adding error handling to gracefully handle this case.

-    return db
-      .select()
-      .from(orderedTargetsSubquery)
-      .where(eq(orderedTargetsSubquery.id, releaseTargetId))
-      .then(takeFirst)
-      .then((r) => r.position);
+    return db
+      .select()
+      .from(orderedTargetsSubquery)
+      .where(eq(orderedTargetsSubquery.id, releaseTargetId))
+      .then((results) => {
+        if (results.length !== 1) {
+          throw new Error(`Expected exactly one result for release target position, got ${results.length}`);
+        }
+        return results[0].position;
+      });
⛔ Skipped due to learnings
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#395
File: packages/api/src/router/environment-page/resources/router.ts:40-45
Timestamp: 2025-03-24T18:46:38.894Z
Learning: The `takeFirst` utility function in the codebase (from `@ctrlplane/db`) throws an Error with message "Found non unique or inexistent value" if the result array doesn't contain exactly one element, making additional null/undefined checks unnecessary after its use.

Comment on lines +129 to +131
sql<number>`ROW_NUMBER() OVER (ORDER BY md5(id || ${version.id}) ASC) - 1`.as(
"position",
),
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Review the deterministic ordering mechanism

The SQL query is using an MD5 hash of the release target ID and version ID to create a deterministic random ordering. Make sure this approach meets your requirements for consistent but unpredictable ordering.

This deterministic ordering is important for gradual rollouts to ensure:

  1. The same targets get the same position for a given version
  2. Different versions produce different orderings of targets

🏁 Script executed:

#!/bin/bash
# Check if there are any tests for this deterministic ordering
rg -A 5 -B 5 "md5\(id \|\| " --type ts

Length of output: 971


Add tests for the MD5-based deterministic ordering

I didn’t find any existing tests covering the ROW_NUMBER() OVER (ORDER BY md5(id || ${version.id}) ASC) logic. To ensure gradual rollouts remain consistent and unpredictable as intended, please add unit tests in packages/rule-engine/src/manager/version-manager-rules.ts that verify:

  • For a fixed set of release-target IDs and a given version.id, the computed position array is identical on repeated runs.
  • Using the same target IDs with a different version.id produces a different ordering.
  • (Optional) Edge cases, e.g., single target or empty set, behave as expected.

Targets for new tests:

  • File: packages/rule-engine/src/manager/version-manager-rules.ts
  • SQL snippet at lines 129–131:
    sql<number>`ROW_NUMBER() OVER (ORDER BY md5(id || ${version.id}) ASC) - 1`.as(
      "position",
    ),
🤖 Prompt for AI Agents
In packages/rule-engine/src/manager/version-manager-rules.ts around lines 129 to
131, add unit tests to verify the deterministic ordering logic using the MD5
hash of target IDs concatenated with version.id. Write tests that confirm the
position array remains consistent for the same set of target IDs and a fixed
version.id across multiple runs, and that changing the version.id results in a
different ordering. Also include tests for edge cases like a single target or an
empty target set to ensure the ordering behaves as expected in those scenarios.

Comment on lines +150 to +153
const getDeploymentOffsetMinutes = linearDeploymentOffset(
policy.environmentVersionRollout.positionGrowthFactor,
policy.environmentVersionRollout.timeScaleInterval,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify property path policy.environmentVersionRollout

The property environmentVersionRollout is accessed but doesn't appear in the Policy type definition from the context provided. Make sure this property exists and is properly defined in the schema.


🏁 Script executed:

#!/bin/bash
# Check if environmentVersionRollout property exists in the Policy type or schema
rg -A 3 -B 3 "environmentVersionRollout" --type ts

Length of output: 747


I’ll verify the Policy definition and its use in version-manager-rules.ts to confirm whether environmentVersionRollout is declared.


🏁 Script executed:

#!/bin/bash
# 1. Locate the Policy interface or type across the repo
rg -n "interface Policy" --type ts
rg -n "type Policy" --type ts

# 2. Show how `policy` is typed where used in version-manager-rules.ts
rg -n "policy\s*:" -C3 packages/rule-engine/src/manager/version-manager-rules.ts

Length of output: 3952


I’ll inspect the Policy type in packages/rule-engine/src/types.ts to confirm whether environmentVersionRollout is declared.


🏁 Script executed:

#!/bin/bash
# 1. Check for environmentVersionRollout in the Policy extension
rg -n "environmentVersionRollout" -C5 packages/rule-engine/src/types.ts

# 2. Show the definition of Policy in this file
sed -n '1,120p' packages/rule-engine/src/types.ts

Length of output: 2573


Missing environmentVersionRollout on Policy

The code in packages/rule-engine/src/manager/version-manager-rules.ts is calling

policy.environmentVersionRollout.positionGrowthFactor

but your Policy extension (in packages/rule-engine/src/types.ts) only includes:

  • denyWindows
  • deploymentVersionSelector
  • versionAnyApprovals
  • versionUserApprovals
  • versionRoleApprovals
  • gradualRollout

There is no environmentVersionRollout property declared. Please address this by:

  • Adding the new field
    In packages/rule-engine/src/types.ts:
     export type Policy = schema.Policy & {
       denyWindows: schema.PolicyRuleDenyWindow[];
       deploymentVersionSelector: schema.PolicyDeploymentVersionSelector | null;
       versionAnyApprovals: schema.PolicyRuleAnyApproval | null;
       versionUserApprovals: schema.PolicyRuleUserApproval[];
       versionRoleApprovals: schema.PolicyRuleRoleApproval[];
       gradualRollout: schema.PolicyGradualRollout | null;
  • environmentVersionRollout: schema.PolicyEnvironmentVersionRollout; // ← add this
    };
- **Verifying the DB schema**  
Make sure `@ctrlplane/db/schema.Policy` (in `packages/db/src/schema/policy.ts`) actually defines `environmentVersionRollout` (or rename your code to use an existing field, e.g. `gradualRollout`).
- **Running validators/schemas** to ensure TypeScript picks up the new property.

…so that `policy.environmentVersionRollout` compiles and reflects the intended rollout settings.

<details>
<summary>🤖 Prompt for AI Agents</summary>

In packages/rule-engine/src/manager/version-manager-rules.ts around lines 150 to
153, the code accesses policy.environmentVersionRollout, but this property is
missing from the Policy type definition in packages/rule-engine/src/types.ts. To
fix this, add the environmentVersionRollout field with the correct type to the
Policy interface in packages/rule-engine/src/types.ts. Also verify that the
database schema in packages/db/src/schema/policy.ts includes this field or
rename the code to use an existing property like gradualRollout. Finally, run
any schema validators or TypeScript checks to ensure the new property is
recognized and the code compiles correctly.


</details>

<!-- This is an auto-generated comment by CodeRabbit -->

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants