Skip to content

feat: job retry policy via api#601

Merged
adityachoudhari26 merged 5 commits intomainfrom
job-retry-policy
Jun 25, 2025
Merged

feat: job retry policy via api#601
adityachoudhari26 merged 5 commits intomainfrom
job-retry-policy

Conversation

@adityachoudhari26
Copy link
Copy Markdown
Member

@adityachoudhari26 adityachoudhari26 commented Jun 24, 2025

Summary by CodeRabbit

  • New Features
    • Introduced a "maxRetries" setting for policies, allowing specification of maximum retry attempts for jobs.
    • Policies now display and accept the "maxRetries" value in API responses, requests, and UI forms.
    • Jobs automatically retry up to the specified "maxRetries" value upon failure.
  • Bug Fixes
    • The "reason" field is now optional when approving or rejecting deployment versions via the API.
  • Tests
    • Added end-to-end tests to verify job retry behavior according to policy settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jun 24, 2025

Walkthrough

This change introduces a "retry policy" feature across the API, database, and job dispatch layers. It adds a new maxRetries property to policy schemas, OpenAPI specs, and database tables, enabling jobs to be retried automatically up to a specified limit. End-to-end tests and validation logic are included.

Changes

File(s) Change Summary
apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts,
openapi.v1.json,
apps/webservice/src/app/api/v1/policies/openapi.ts
Added maxRetries property to policy schemas and request/response bodies in OpenAPI specs; defined new MaxRetries schema component.
apps/webservice/src/app/api/v1/policies/[policyId]/route.ts GET handler updated to include maxRetries in policy response.
e2e/api/entity-fixtures.ts,
e2e/api/schema.ts
Added optional maxRetries field to policy fixtures and API schema; made reason field optional in certain deployment version approval/rejection operations.
e2e/tests/api/policies/retry-policy.spec.ts,
e2e/tests/api/policies/retry-policy.spec.yaml
Introduced new end-to-end test suite and YAML spec for retry policy, validating job retry behavior up to the max retry count.
packages/db/drizzle/0118_aromatic_polaris.sql,
packages/db/src/schema/rules/retry.ts
Created new policy_rule_retry table and schema for associating max retries with policies.
packages/db/src/schema/policy-relations.ts,
packages/db/src/schema/rules/rule-relations.ts
Added relations linking policies to their retry rules in the ORM schema.
packages/db/src/schema/policy.ts Added maxRetries field to policy creation and update schemas with validation.
packages/db/src/schema/rules/index.ts Exported new retry rule schema module.
packages/db/src/utils/policies/index.ts Added maxRetries to the set of all recognized policy rules.
packages/db/drizzle/meta/_journal.json Added migration journal entry for retry rule table.
packages/events/src/dispatch-jobs.ts Added toDispatch function for queueing jobs for retry dispatch.
packages/job-dispatch/src/job-update.ts Refactored job update logic; implemented job retry mechanism based on policy; added helpers for policy matching and retry counting.
packages/rule-engine/src/db/create-policy.ts,
packages/rule-engine/src/db/update-policy.ts
Updated policy creation and update functions to handle maxRetries (insert/update/delete retry rules as needed); return maxRetries in API responses.
apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx,
apps/webservice/src/app/[workspaceSlug]/(app)/policies/create/_components/PolicyContext.tsx
Added maxRetries field to policy form contexts and default form values.

Sequence Diagram(s)

sequenceDiagram
    participant API
    participant DB
    participant JobDispatcher
    participant EventsQueue

    API->>DB: Create/Update Policy (with maxRetries)
    DB-->>API: Policy stored (maxRetries in policy_rule_retry table)
    API->>JobDispatcher: Job marked as failure
    JobDispatcher->>DB: Fetch policy (get maxRetries)
    JobDispatcher->>DB: Count previous retries for job
    alt Retries remaining
        JobDispatcher->>DB: Create new job for retry
        JobDispatcher->>EventsQueue: Dispatch retry job
    else Retries exhausted
        JobDispatcher->>EventsQueue: Dispatch evaluation for release target
    end
Loading

Possibly related PRs

  • ctrlplanedev/ctrlplane#284: Also modifies job update logic in job-update.ts, focusing on timestamp autofill; related as both touch job update and dispatch flows.

Suggested reviewers

  • jsbroks

Poem

In the meadow of code, where the job bunnies hop,
A retry is born—no more will they stop!
If a job fails, it gets another chance,
Up to maxRetries, it continues its dance.
The tests all agree: this feature’s a treat—
Now jobs are persistent, and failure’s defeat!
🐇✨


📜 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 f2efddf and 757db5f.

📒 Files selected for processing (1)
  • packages/db/src/schema/policy.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/db/src/schema/policy.ts
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
✨ Finishing Touches
  • 📝 Generate Docstrings

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
Copy Markdown
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: 6

♻️ Duplicate comments (1)
openapi.v1.json (1)

2696-2701: Same validation / defaulting concerns for create endpoint

See previous comment – creation path should perform identical checks & defaulting rules as update.

🧹 Nitpick comments (7)
e2e/tests/api/policies/retry-policy.spec.yaml (1)

24-31: Comprehensive test specification with minor formatting issue.

The test setup is well-structured with appropriate entities for testing retry policy functionality. The maxRetries: 3 value is suitable for testing.

Fix the missing newline at the end of the file:

     maxRetries: 3
+
e2e/tests/api/policies/retry-policy.spec.ts (3)

85-85: Consider making timeout values configurable.

Hard-coded timeout values make the tests less flexible and may cause flakiness in different environments.

+const SETUP_TIMEOUT = 1_000;
+const RETRY_TIMEOUT = 2_000;
+
 test("should retry job until max attempts is reached", async ({
   api,
   page,
 }) => {
   const agentId = await initTestEntities(api, builder);

-  await page.waitForTimeout(1_000);
+  await page.waitForTimeout(SETUP_TIMEOUT);
   // ... later in the loop
-  await page.waitForTimeout(2_000);
+  await page.waitForTimeout(RETRY_TIMEOUT);

Also applies to: 132-132


97-97: Remove debug console.log statements.

Console.log statements should be removed from production test code to avoid cluttering test output.

   expect(policyResponse.data?.maxRetries).toBeDefined();
-  console.log({ policyResponse });
   const maxRetries = policyResponse.data?.maxRetries!;
   
   // ... and later
-  console.log({ i, jobResponse });

Also applies to: 110-110


79-147: Enhance test robustness with better error handling.

The test logic is sound but could benefit from better error handling and more descriptive assertions to improve debugging when tests fail.

Consider wrapping API calls in try-catch blocks or using more descriptive error messages in expect statements to make test failures easier to diagnose:

expect(jobResponse.response.ok).toBe(true);
// Could be enhanced to:
if (!jobResponse.response.ok) {
  throw new Error(`Failed to fetch next job: ${jobResponse.response.status} ${jobResponse.response.statusText}`);
}
openapi.v1.json (2)

6553-6558: Add descriptive context & allow zero-retry scenarios

The MaxRetries schema is well-formed but a few small tweaks will improve expressiveness and future-proofing:

  1. A short description + example makes the field self-documenting in generated SDKs & UIs.
  2. minimum: 1 forbids an explicit “no retries” value (0). If null → “inherit default” and 0 → “do not retry” is a desirable distinction, consider minimum: 0.
       "MaxRetries": {
         "type": "integer",
         "nullable": true,
-        "minimum": 1,
+        "description": "Maximum number of attempts the system will make to run a job. \
+Null to inherit the global default, 0 to disable retries.",
+        "example": 3,
+        "minimum": 0,
         "format": "int32"
       },

Please confirm whether 0 should be an allowed value or if null already covers “no retry”.


6650-6652: Property ordering / grouping

maxRetries now sits between rollout & the end of the map. To keep diffs tidy in future, consider grouping numeric limit fields (concurrency, maxRetries) together or keeping alphabetical order, but this is purely cosmetic.

packages/job-dispatch/src/job-update.ts (1)

303-309: Consider documenting the retry policy selection behavior.

The code only uses the first retry policy found. While this appears intentional, it would be helpful to document this behavior for future maintainers.

Add a clarifying comment:

   const matchedPolicies = await getMatchedPolicies(db, releaseTarget);
   const isJobFailed = failedStatuses.includes(updatedJob.status as JobStatus);
+  // Use the first retry policy found (policies are ordered by priority)
   const firstRetryPolicy = matchedPolicies.find((p) => p.retry != null)?.retry;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between bff7ca6 and c9699b7.

📒 Files selected for processing (20)
  • apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts (1 hunks)
  • apps/webservice/src/app/api/v1/policies/[policyId]/route.ts (2 hunks)
  • apps/webservice/src/app/api/v1/policies/openapi.ts (3 hunks)
  • e2e/api/entity-fixtures.ts (1 hunks)
  • e2e/api/schema.ts (8 hunks)
  • e2e/tests/api/policies/retry-policy.spec.ts (1 hunks)
  • e2e/tests/api/policies/retry-policy.spec.yaml (1 hunks)
  • openapi.v1.json (4 hunks)
  • packages/db/drizzle/0118_aromatic_polaris.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 (2 hunks)
  • packages/db/src/schema/rules/index.ts (1 hunks)
  • packages/db/src/schema/rules/retry.ts (1 hunks)
  • packages/db/src/schema/rules/rule-relations.ts (2 hunks)
  • packages/db/src/utils/policies/index.ts (1 hunks)
  • packages/events/src/dispatch-jobs.ts (1 hunks)
  • packages/job-dispatch/src/job-update.ts (8 hunks)
  • packages/rule-engine/src/db/create-policy.ts (3 hunks)
  • packages/rule-engine/src/db/update-policy.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
`**/*.ts`: Use TypeScript with explicit types and prefer interfaces for public APIs. Use named imports and group imports by source: standard library first, then external, then inte...

**/*.ts: Use TypeScript with explicit types and prefer interfaces for public APIs.
Use named imports and group imports by source: standard library first, then external, then internal.
Use consistent type imports: 'import type { Type } from "module"'.

  • packages/db/src/schema/rules/index.ts
  • packages/db/src/utils/policies/index.ts
  • apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts
  • packages/events/src/dispatch-jobs.ts
  • e2e/api/entity-fixtures.ts
  • packages/db/src/schema/policy-relations.ts
  • apps/webservice/src/app/api/v1/policies/[policyId]/route.ts
  • packages/rule-engine/src/db/update-policy.ts
  • packages/db/src/schema/rules/rule-relations.ts
  • packages/db/src/schema/rules/retry.ts
  • packages/rule-engine/src/db/create-policy.ts
  • apps/webservice/src/app/api/v1/policies/openapi.ts
  • packages/db/src/schema/policy.ts
  • e2e/api/schema.ts
  • packages/job-dispatch/src/job-update.ts
  • e2e/tests/api/policies/retry-policy.spec.ts
`**/*.{js,ts,tsx}`: Formatting must follow Prettier with '@ctrlplane/prettier-config'.

**/*.{js,ts,tsx}: Formatting must follow Prettier with '@ctrlplane/prettier-config'.

  • packages/db/src/schema/rules/index.ts
  • packages/db/src/utils/policies/index.ts
  • apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts
  • packages/events/src/dispatch-jobs.ts
  • e2e/api/entity-fixtures.ts
  • packages/db/src/schema/policy-relations.ts
  • apps/webservice/src/app/api/v1/policies/[policyId]/route.ts
  • packages/rule-engine/src/db/update-policy.ts
  • packages/db/src/schema/rules/rule-relations.ts
  • packages/db/src/schema/rules/retry.ts
  • packages/rule-engine/src/db/create-policy.ts
  • apps/webservice/src/app/api/v1/policies/openapi.ts
  • packages/db/src/schema/policy.ts
  • e2e/api/schema.ts
  • packages/job-dispatch/src/job-update.ts
  • e2e/tests/api/policies/retry-policy.spec.ts
`packages/**`: Adhere to file and directory naming conventions specific to each package.

packages/**: Adhere to file and directory naming conventions specific to each package.

  • packages/db/src/schema/rules/index.ts
  • packages/db/src/utils/policies/index.ts
  • packages/events/src/dispatch-jobs.ts
  • packages/db/drizzle/meta/_journal.json
  • packages/db/drizzle/0118_aromatic_polaris.sql
  • packages/db/src/schema/policy-relations.ts
  • packages/rule-engine/src/db/update-policy.ts
  • packages/db/src/schema/rules/rule-relations.ts
  • packages/db/src/schema/rules/retry.ts
  • packages/rule-engine/src/db/create-policy.ts
  • packages/db/src/schema/policy.ts
  • packages/job-dispatch/src/job-update.ts
`**/*.{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/db/src/schema/rules/index.ts
  • packages/db/src/utils/policies/index.ts
  • apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts
  • packages/events/src/dispatch-jobs.ts
  • e2e/api/entity-fixtures.ts
  • packages/db/src/schema/policy-relations.ts
  • apps/webservice/src/app/api/v1/policies/[policyId]/route.ts
  • packages/rule-engine/src/db/update-policy.ts
  • packages/db/src/schema/rules/rule-relations.ts
  • packages/db/src/schema/rules/retry.ts
  • packages/rule-engine/src/db/create-policy.ts
  • apps/webservice/src/app/api/v1/policies/openapi.ts
  • packages/db/src/schema/policy.ts
  • e2e/api/schema.ts
  • packages/job-dispatch/src/job-update.ts
  • e2e/tests/api/policies/retry-policy.spec.ts
`apps/**`: Adhere to file and directory naming conventions specific to each package.

apps/**: Adhere to file and directory naming conventions specific to each package.

  • apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts
  • apps/webservice/src/app/api/v1/policies/[policyId]/route.ts
  • apps/webservice/src/app/api/v1/policies/openapi.ts
🧬 Code Graph Analysis (7)
packages/db/src/schema/policy-relations.ts (1)
packages/db/src/schema/rules/retry.ts (1)
  • policyRuleRetry (7-16)
packages/rule-engine/src/db/update-policy.ts (2)
packages/db/src/common.ts (2)
  • Tx (22-22)
  • buildConflictUpdateColumns (30-46)
packages/db/src/schema/policy.ts (2)
  • UpdatePolicy (223-223)
  • policy (43-63)
packages/db/src/schema/rules/rule-relations.ts (2)
packages/db/src/schema/rules/retry.ts (1)
  • policyRuleRetry (7-16)
packages/db/src/schema/policy.ts (1)
  • policy (43-63)
packages/db/src/schema/rules/retry.ts (1)
packages/db/src/schema/policy.ts (1)
  • policy (43-63)
packages/rule-engine/src/db/create-policy.ts (1)
packages/db/src/common.ts (1)
  • buildConflictUpdateColumns (30-46)
e2e/api/schema.ts (1)
packages/node-sdk/src/schema.ts (1)
  • components (892-1420)
packages/job-dispatch/src/job-update.ts (7)
packages/db/src/client.ts (1)
  • db (15-15)
packages/db/src/common.ts (2)
  • Tx (22-22)
  • takeFirst (9-13)
packages/db/src/schema/job.ts (3)
  • Job (144-144)
  • UpdateJob (154-154)
  • JobStatus (145-145)
packages/db/src/schema/release.ts (2)
  • releaseTarget (21-43)
  • ReleaseTarget (45-45)
packages/db/src/queries/create-release-job.ts (1)
  • createReleaseJob (19-95)
packages/events/src/dispatch-jobs.ts (1)
  • dispatchQueueJob (161-166)
packages/validators/src/jobs/index.ts (1)
  • failedStatuses (54-59)
🪛 YAMLlint (1.37.1)
e2e/tests/api/policies/retry-policy.spec.yaml

[error] 31-31: no new line character at the end of file

(new-line-at-end-of-file)

⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (32)
packages/db/drizzle/meta/_journal.json (1)

831-837: Migration journal entry looks correct.

The new journal entry follows the established pattern with proper incremental indexing (118), chronological timestamp, and consistent naming convention. The breakpoints: true flag correctly indicates this migration introduces schema changes.

packages/db/src/schema/rules/index.ts (1)

14-14: LGTM: Proper barrel export for retry rule.

The export statement follows the established pattern and correctly exposes the new retry rule module through the index.

e2e/api/entity-fixtures.ts (1)

135-135: LGTM: Appropriate schema extension for retry policy.

The maxRetries field is correctly typed as an optional number, which aligns with the retry policy feature and follows the established pattern for optional policy fields.

packages/db/src/utils/policies/index.ts (1)

10-10: LGTM: Proper rule registration.

The addition of maxRetries: _true follows the established pattern and correctly registers the retry rule as an available policy rule type.

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

18-18: LGTM: Proper import for retry rule.

The import statement follows the established pattern and correctly imports the policyRuleRetry entity needed for the relation definition.


36-36: LGTM: Correct one-to-one relation definition.

The maxRetries relation is properly defined as a one-to-one relationship with policyRuleRetry. This aligns with the database schema where policyRuleRetry has a unique constraint on policyId, ensuring the one-to-one relationship integrity.

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

49-49: LGTM: Consistent with existing policy rule extraction pattern.

The maxRetries extraction follows the same nested property access pattern used for concurrency and environmentVersionRollout. The implementation correctly extracts the value and includes it in the response.

Also applies to: 61-61

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

135-137: LGTM: Proper OpenAPI schema documentation.

The maxRetries property is correctly added to the PATCH request body schema using the established $ref pattern, maintaining consistency with other policy rule properties.

packages/events/src/dispatch-jobs.ts (1)

154-159: LGTM: Clean implementation following established patterns.

The new toDispatch function provides a clean interface for dispatching retry jobs and follows the same architectural pattern as existing toEvaluate and toCompute functions.

Also applies to: 165-165

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

22-22: LGTM: Consistent database relation definition.

The policyRuleRetryRelations follows the exact same pattern as other policy rule relations, correctly establishing the one-to-one relationship between policyRuleRetry and policy tables.

Also applies to: 154-162

packages/rule-engine/src/db/create-policy.ts (1)

96-96: LGTM: Proper implementation of retry rule creation.

The maxRetries handling follows the established pattern for policy rules:

  • Correctly destructured from input
  • Uses appropriate upsert logic with conflict resolution on policyId
  • Consistently uses buildConflictUpdateColumns for update behavior
  • Properly included in the return object

The implementation is consistent with other single-value policy rules like concurrency.

Also applies to: 215-222, 234-234

packages/db/src/schema/rules/retry.ts (2)

7-16: Well-designed retry policy schema.

The table structure is solid with appropriate constraints:

  • Unique constraint on policyId ensures one retry rule per policy
  • Cascade delete maintains referential integrity
  • Non-null maxRetries enforces explicit retry limits

18-20: Clean Zod schema definition.

The insert schema correctly omits the auto-generated id field and validates policyId as a UUID string.

packages/db/drizzle/0118_aromatic_polaris.sql (1)

1-8: Migration correctly implements the retry policy schema.

The SQL structure matches the TypeScript schema definition with proper constraints and foreign key relationships.

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

209-216: Correct validation implementation for update schema.

The validation logic and error message path are properly implemented for the update schema.

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

78-83: Well-defined MaxRetries schema component.

The schema follows the established pattern used by other similar fields like PolicyConcurrency, with appropriate nullable integer type and minimum validation.


144-146: Properly integrated maxRetries into OpenAPI specification.

The maxRetries field is correctly added to both the Policy response schema and request body schema using proper $ref references.

Also applies to: 220-222

e2e/tests/api/policies/retry-policy.spec.ts (2)

1-10: LGTM - Clean imports and setup.

The imports follow the coding guidelines with proper grouping (standard library, external, internal) and the path setup is appropriate for E2E tests.


66-77: LGTM - Proper test setup and cleanup.

The beforeEach and afterEach hooks are properly structured to ensure test isolation and cleanup.

openapi.v1.json (1)

2466-2469: Ensure validation parity for PATCH payload

maxRetries is now accepted on update, but no additional request-side validation is declared beyond the shared schema reference.
Double-check that server-side code:

  • rejects negative integers / non-integers,
  • correctly interprets null vs missing vs 0 (if permitted).

Otherwise clients could silently send invalid values.

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

202-220: Excellent implementation following established patterns!

The updateMaxRetries function correctly implements the three-state pattern (undefined/null/value) consistent with other policy rule update functions. The upsert logic with conflict resolution is appropriate for atomic updates.


235-236: Proper integration of maxRetries extraction.

The destructuring correctly extracts maxRetries from the input alongside other policy rule properties.


263-263: Correct concurrent execution of update operations.

Adding updateMaxRetries to the Promise.all array ensures it executes concurrently with other policy rule updates within the same transaction.


284-284: Proper extraction of nested maxRetries value.

The optional chaining updatedPolicy.maxRetries?.maxRetries correctly handles the database relation structure, matching the pattern used for other nested properties like concurrency.


289-289: Complete integration of maxRetries in return object.

The returned object properly includes the extracted maxRetries value, maintaining consistency with the API contract.

e2e/api/schema.ts (5)

1418-1419: Well-structured schema definition for MaxRetries.

The new MaxRetries schema follows the same pattern as PolicyConcurrency, ensuring consistency in the API schema. The int32 format and nullable type are appropriate for retry count values.


1454-1454: Proper integration of maxRetries into Policy schema.

The optional maxRetries property is correctly added to the Policy schema, following the established patterns and positioned logically with other deployment control properties.


1695-1695: Verify the business impact of making approval/rejection reasons optional.

The changes make the reason field optional and allow empty request bodies for approval/rejection endpoints. While this improves API flexibility, it may reduce audit trail quality if reasons become less frequently provided.

Please verify that making approval/rejection reasons optional aligns with business requirements and audit/compliance needs.

Also applies to: 1756-1756, 1941-1941, 2000-2000


3138-3138: Consistent integration into updatePolicy operation.

The maxRetries property is properly integrated into the updatePolicy request body, maintaining consistency with the Policy schema and following established patterns for optional update fields.


3243-3243: Complete feature integration in upsertPolicy operation.

The maxRetries property integration in upsertPolicy ensures complete API coverage for the retry policy feature, maintaining consistency across all policy-related operations.

packages/job-dispatch/src/job-update.ts (2)

99-115: Good refactoring to extract database update logic.

The extraction of the database update logic into dbUpdateJob improves modularity and makes the code more maintainable.


146-183: Well-structured query for fetching policies with their rules.

The updated getMatchedPolicies function correctly uses left joins for optional policy rules and maintains proper ordering by priority.

Comment on lines +166 to +173
maxRetries: z
.number()
.optional()
.nullable()
.refine((data) => data == null || data > 0, {
message: "Retry must be greater than 0 or null",
path: ["retry"],
}),
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

Fix inconsistent error message path.

The validation logic is correct, but the error message path should match the field name.

     .refine((data) => data == null || data > 0, {
       message: "Retry must be greater than 0 or null",
-      path: ["retry"],
+      path: ["maxRetries"],
     }),
🤖 Prompt for AI Agents
In packages/db/src/schema/policy.ts around lines 166 to 173, the error message
path in the refine method is set to "retry" which is inconsistent with the field
name "maxRetries". Update the path property in the refine method to
["maxRetries"] to match the field name for accurate error reporting.

Comment on lines +23 to +24
expect(agentResponse.data?.id).toBeDefined();
return agentResponse.data?.id!;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jun 24, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add null safety check for agent ID.

Using non-null assertion without proper validation could lead to runtime errors if the API response doesn't contain an ID.

-  expect(agentResponse.data?.id).toBeDefined();
-  return agentResponse.data?.id!;
+  expect(agentResponse.data?.id).toBeDefined();
+  const agentId = agentResponse.data?.id;
+  if (!agentId) {
+    throw new Error("Failed to create agent: no ID returned");
+  }
+  return agentId;
📝 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
expect(agentResponse.data?.id).toBeDefined();
return agentResponse.data?.id!;
expect(agentResponse.data?.id).toBeDefined();
const agentId = agentResponse.data?.id;
if (!agentId) {
throw new Error("Failed to create agent: no ID returned");
}
return agentId;
🤖 Prompt for AI Agents
In e2e/tests/api/policies/retry-policy.spec.ts around lines 23 to 24, the code
uses a non-null assertion on agentResponse.data?.id without verifying its
presence, which risks runtime errors if the ID is missing. Add a null safety
check before returning the ID by validating that agentResponse.data?.id is
defined; if not, handle the error appropriately or throw an informative
exception to prevent unsafe access.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

since this is just a test, i'm not really concerned with this

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.

@adityachoudhari26 Understood! You're right that tests often prioritize simplicity and readability over defensive programming. I'll keep that in mind for future test reviews.

(\ /)
( .. )
o
(")(") acknowledging your preference


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#601
File: e2e/tests/api/policies/retry-policy.spec.ts:23-24
Timestamp: 2025-06-24T23:52:50.706Z
Learning: The user adityachoudhari26 prefers not to add null safety checks or defensive programming in test code, particularly in e2e tests, as they prioritize simplicity and focus on the main functionality being tested rather than comprehensive error handling within the test itself.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +117 to +130
const nextJob = nextJobs?.[0];
expect(nextJob).toBeDefined();
expect(nextJob?.status).toBe("pending");

await api.PATCH("/v1/jobs/{jobId}", {
params: {
path: {
jobId: nextJob?.id!,
},
},
body: {
status: "failure",
},
});
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jun 24, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add null safety check for job operations.

The job processing logic uses non-null assertions that should be replaced with proper validation.

   const nextJob = nextJobs?.[0];
   expect(nextJob).toBeDefined();
+  if (!nextJob?.id) {
+    throw new Error("Job missing required ID");
+  }
   expect(nextJob?.status).toBe("pending");

   await api.PATCH("/v1/jobs/{jobId}", {
     params: {
       path: {
-        jobId: nextJob?.id!,
+        jobId: nextJob.id,
       },
     },
📝 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 nextJob = nextJobs?.[0];
expect(nextJob).toBeDefined();
expect(nextJob?.status).toBe("pending");
await api.PATCH("/v1/jobs/{jobId}", {
params: {
path: {
jobId: nextJob?.id!,
},
},
body: {
status: "failure",
},
});
const nextJob = nextJobs?.[0];
expect(nextJob).toBeDefined();
if (!nextJob?.id) {
throw new Error("Job missing required ID");
}
expect(nextJob?.status).toBe("pending");
await api.PATCH("/v1/jobs/{jobId}", {
params: {
path: {
jobId: nextJob.id,
},
},
body: {
status: "failure",
},
});
🤖 Prompt for AI Agents
In e2e/tests/api/policies/retry-policy.spec.ts around lines 117 to 130, replace
the non-null assertion on nextJob.id with a proper null safety check. Before
using nextJob.id in the PATCH request, add a conditional check to ensure nextJob
is defined and handle the case where it is undefined to avoid runtime errors.
This will make the job operations safer and more robust.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

same as above, I dont' really care about type safety in a test because if something is off the test will fail anyways

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.

@adityachoudhari26 That's a fair point! You're right that in e2e tests, if the job ID is missing or undefined, the test failure itself would indicate a problem with the system being tested. I'll respect your preference to keep the non-null assertions in test code for simplicity.


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#601
File: e2e/tests/api/policies/retry-policy.spec.ts:117-130
Timestamp: 2025-06-24T23:53:25.389Z
Learning: User adityachoudhari26 prefers to keep non-null assertions in e2e test code without extensive null safety checks, reasoning that test failures serve the same purpose of catching issues and the extra validation doesn't add much value in test contexts.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +42 to +51
expect(deploymentResponse.data?.id).toBeDefined();

const versionTag = faker.string.alphanumeric(10);
const versionResponse = await api.POST("/v1/deployment-versions", {
body: {
deploymentId: deploymentResponse.data?.id!,
tag: versionTag,
},
});
expect(versionResponse.data?.id).toBeDefined();
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jun 24, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add null safety checks for deployment creation.

Similar to the agent creation, these non-null assertions should be replaced with proper validation.

   expect(deploymentResponse.data?.id).toBeDefined();
+  const deploymentId = deploymentResponse.data?.id;
+  if (!deploymentId) {
+    throw new Error("Failed to create deployment: no ID returned");
+  }

   const versionTag = faker.string.alphanumeric(10);
   const versionResponse = await api.POST("/v1/deployment-versions", {
     body: {
-      deploymentId: deploymentResponse.data?.id!,
+      deploymentId,
       tag: versionTag,
     },
   });
   expect(versionResponse.data?.id).toBeDefined();
📝 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
expect(deploymentResponse.data?.id).toBeDefined();
const versionTag = faker.string.alphanumeric(10);
const versionResponse = await api.POST("/v1/deployment-versions", {
body: {
deploymentId: deploymentResponse.data?.id!,
tag: versionTag,
},
});
expect(versionResponse.data?.id).toBeDefined();
expect(deploymentResponse.data?.id).toBeDefined();
const deploymentId = deploymentResponse.data?.id;
if (!deploymentId) {
throw new Error("Failed to create deployment: no ID returned");
}
const versionTag = faker.string.alphanumeric(10);
const versionResponse = await api.POST("/v1/deployment-versions", {
body: {
deploymentId,
tag: versionTag,
},
});
expect(versionResponse.data?.id).toBeDefined();
🤖 Prompt for AI Agents
In e2e/tests/api/policies/retry-policy.spec.ts around lines 42 to 51, the code
uses non-null assertions on deploymentResponse.data?.id without validating if
deploymentResponse.data or its id is actually defined. Replace these non-null
assertions with explicit null or undefined checks before using the id, and
handle the case where deploymentResponse.data or id is missing by throwing an
error or failing the test early to ensure null safety.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

same as above

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.

@adityachoudhari26 Exactly! The same null safety pattern should be applied consistently throughout the test file. This approach of explicit validation and early error throwing will make the tests more robust and provide clearer failure messages when API calls don't return expected data.

The pattern helps avoid runtime errors from non-null assertions and makes the test failures more debuggable by showing exactly which API call failed to return the expected data.

Comment on lines +96 to +99
expect(policyResponse.data?.maxRetries).toBeDefined();
console.log({ policyResponse });
const maxRetries = policyResponse.data?.maxRetries!;
expect(maxRetries).toBeGreaterThan(0);
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jun 24, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add null safety check for maxRetries.

The non-null assertion should be replaced with proper validation and error handling.

   expect(policyResponse.data?.maxRetries).toBeDefined();
-  console.log({ policyResponse });
-  const maxRetries = policyResponse.data?.maxRetries!;
+  const maxRetries = policyResponse.data?.maxRetries;
+  if (typeof maxRetries !== 'number' || maxRetries <= 0) {
+    throw new Error(`Invalid maxRetries value: ${maxRetries}`);
+  }
   expect(maxRetries).toBeGreaterThan(0);
📝 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
expect(policyResponse.data?.maxRetries).toBeDefined();
console.log({ policyResponse });
const maxRetries = policyResponse.data?.maxRetries!;
expect(maxRetries).toBeGreaterThan(0);
expect(policyResponse.data?.maxRetries).toBeDefined();
const maxRetries = policyResponse.data?.maxRetries;
if (typeof maxRetries !== 'number' || maxRetries <= 0) {
throw new Error(`Invalid maxRetries value: ${maxRetries}`);
}
expect(maxRetries).toBeGreaterThan(0);
🤖 Prompt for AI Agents
In e2e/tests/api/policies/retry-policy.spec.ts around lines 96 to 99, replace
the non-null assertion on maxRetries with a proper null safety check. Add a
conditional to verify maxRetries is not null or undefined before using it, and
handle the case where it might be missing by throwing an error or failing the
test with a clear message. This ensures robust validation without assuming
maxRetries is always present.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

same as above

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.

@adityachoudhari26 Perfect! I understand you agree with the null safety approach for handling the maxRetries validation. Replacing the non-null assertion with proper type checking and error handling makes the test more robust and provides clearer failure messages when the API response doesn't contain the expected data.

The suggested change improves the test by:

  • Removing assumptions about API response structure
  • Providing clear error messages when maxRetries is missing or invalid
  • Following better testing practices with explicit validation

Thanks for confirming this approach!

🐰✨


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#601
File: e2e/tests/api/policies/retry-policy.spec.ts:96-99
Timestamp: 2025-06-24T23:53:36.318Z
Learning: In e2e test files, prefer explicit null safety checks and validation over non-null assertions (!). When validating API responses in tests, use type guards and throw descriptive errors rather than assuming values are always present.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +264 to +270
const newReleaseJob = await db.transaction((tx) =>
createReleaseJob(tx, {
id: releaseId,
versionReleaseId,
variableReleaseId,
}),
);
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jun 24, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unnecessary transaction wrapper.

The db parameter is already of type Tx (transaction context), and createReleaseJob is designed to work within a transaction. The additional transaction wrapper is redundant.

Apply this fix:

-  const newReleaseJob = await db.transaction((tx) =>
-    createReleaseJob(tx, {
-      id: releaseId,
-      versionReleaseId,
-      variableReleaseId,
-    }),
-  );
+  const newReleaseJob = await createReleaseJob(db, {
+    id: releaseId,
+    versionReleaseId,
+    variableReleaseId,
+  });
📝 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 newReleaseJob = await db.transaction((tx) =>
createReleaseJob(tx, {
id: releaseId,
versionReleaseId,
variableReleaseId,
}),
);
const newReleaseJob = await createReleaseJob(db, {
id: releaseId,
versionReleaseId,
variableReleaseId,
});
🤖 Prompt for AI Agents
In packages/job-dispatch/src/job-update.ts around lines 264 to 270, remove the
unnecessary db.transaction wrapper since the db parameter is already a
transaction context (Tx). Instead, call createReleaseJob directly with the db
parameter and the job details object, eliminating the redundant transaction
nesting.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the tX is just a type meaning db client. if you look at the callers its not actually happening within a tx. even if it was, drizzle supports breakpoints in tx so we would be good either way I believe

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for callers of updateJob function to understand the context
rg "updateJob\(" -A 3 -B 3 --type ts

# Also search for the Tx type definition
rg "type Tx" -A 2 -B 2 --type ts

# Look for createReleaseJob implementation
rg "createReleaseJob" -A 5 -B 2 --type ts

Length of output: 12005


@adityachoudhari26 You're absolutely right, and thank you for the correction! I can see from the codebase that:

  1. Tx is indeed just a type alias (export type Tx = Omit<typeof db, "$client">) representing the database client, not necessarily an active transaction context
  2. Looking at the callers of updateJob, they pass the regular db client, not within transactions
  3. Other usages of createReleaseJob in the codebase (like in evaluate-release-target.ts and redeploy.ts) also wrap it with db.transaction(), confirming that createReleaseJob expects to be called within a transaction context
  4. You're also correct that Drizzle supports nested transactions via breakpoints, so even if this were already in a transaction, it would work fine

My apologies for the confusion - the transaction wrapper is indeed necessary here. The code is correct as written.

🐰 ears perk up with curiosity
Thanks for teaching me something new about the codebase architecture!


🧬 Code Graph Analysis Results

Relevant snippets for understanding packages/job-dispatch/src/job-update.ts and the user comment about Tx type usage:


File: packages/db/src/common.ts
Lines: 22-22

export type Tx = Omit<typeof db, "$client">;
  • Defines the Tx type as the database client type without the $client property.
  • This type alias is used in job-update.ts to type the db parameter, indicating it represents a database client instance (not necessarily a transaction).
  • This supports the user's comment that Tx is just a type meaning "db client" and does not imply an actual transaction context.

File: packages/db/src/client.ts
Line: 15

export const db = drizzle(pool, { schema });
  • Exports the main database client instance db.
  • This instance is passed around as db or tx in other files.
  • The Tx type is derived from this client, reinforcing that Tx is a client type, not necessarily a transaction.

File: packages/db/src/queries/create-release-job.ts
Lines: 19-95 (summary)

  • Defines createReleaseJob(tx: Tx, release: {...}) which creates a job within a database context typed as Tx.
  • Uses tx to perform multiple queries and inserts.
  • The function is called inside a transaction in retryJob in job-update.ts via db.transaction((tx) => createReleaseJob(tx, {...})).
  • This shows that Tx can be a transaction context but is also used as a general db client type.
  • Important for understanding how Tx is used in job-update.ts and the user comment about transaction breakpoints.

File: packages/events/src/dispatch-jobs.ts
Lines: 161-166

export const dispatchQueueJob = () => ({
  toUpdatedResource: dispatchUpdatedResourceJob,
  toEvaluate,
  toCompute,
  toDispatch,
});
  • Provides the dispatchQueueJob function used in job-update.ts to dispatch jobs to various queues.
  • Relevant for understanding the dispatching logic after job updates.

File: packages/validators/src/jobs/index.ts
Lines: 35-43 and 54-59

export const exitedStatus = [
  JobStatus.Successful,
  JobStatus.InvalidJobAgent,
  JobStatus.InvalidIntegration,
  JobStatus.ExternalRunNotFound,
  JobStatus.Failure,
  JobStatus.Cancelled,
  JobStatus.Skipped,
];

export const failedStatuses = [
  JobStatus.Failure,
  JobStatus.InvalidJobAgent,
  JobStatus.InvalidIntegration,
  JobStatus.ExternalRunNotFound,
];
  • Defines arrays of job statuses used in job-update.ts to determine if a job has exited or failed.
  • Used in logic to decide whether to retry a job or dispatch further evaluation.

File: packages/db/src/schema/job.ts
Lines: 140-154 (summary)

  • Defines types JobMetadata, Job, UpdateJob, and JobStatus based on the database schema.
  • These types are used extensively in job-update.ts for typing job data and updates.

These snippets provide the necessary context to understand the Tx type usage in job-update.ts, the database client and transaction distinction, and the job update and dispatch logic.


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#601
File: packages/job-dispatch/src/job-update.ts:264-270
Timestamp: 2025-06-24T23:56:54.778Z
Learning: In this codebase, the `Tx` type is just an alias for the database client type (`Omit<typeof db, "$client">`) and does not necessarily indicate an active transaction context. Functions like `createReleaseJob` need to be called within a transaction, which is why they are wrapped with `db.transaction()` even when the parameter is typed as `Tx`. Drizzle supports nested transactions via breakpoints, so additional transaction wrappers are safe even if already within a transaction.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

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.

1 participant