Skip to content

Conversation

@adityachoudhari26
Copy link
Contributor

@adityachoudhari26 adityachoudhari26 commented Apr 28, 2025

Summary by CodeRabbit

  • New Features

    • Added a new API endpoint to retrieve the latest 100 releases for a specific release target, including deployment, version, and variable details.
    • Introduced OpenAPI documentation for the new releases endpoint.
    • Enhanced RBAC to support release target scopes and permissions.
  • Bug Fixes

    • Improved error handling and retry logic for release evaluation and job dispatching to handle concurrency and locking issues.
  • Tests

    • Added comprehensive end-to-end tests and supporting YAML specifications for release creation and retrieval workflows.
  • Documentation

    • Updated OpenAPI and API schema definitions to reflect the new releases endpoint and its responses.
  • Chores

    • Extended permission enums and scope types to support release target operations.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 28, 2025

Caution

Review failed

The pull request is closed.

"""

Walkthrough

This update introduces a new API endpoint to retrieve the latest 100 releases for a given release target, complete with OpenAPI documentation and end-to-end tests. The backend workers and utilities are refactored to improve transaction safety, row-level locking, and job dispatch logic. Permission and RBAC systems are extended to support the new "releaseTarget" scope and "releaseTarget.get" permission, ensuring secure access to the new endpoint. The YAML loader and test schema are updated to support deployment versions, and a comprehensive test suite validates release creation behaviors.

Changes

File(s) Change Summary
apps/event-worker/src/utils/dispatch-evaluate-jobs.ts Refactored function parameter type from schema.ReleaseTarget[] to ReleaseTargetIdentifier[]; updated import accordingly.
apps/event-worker/src/workers/compute-deployment-resource-selector.ts
apps/event-worker/src/workers/compute-environment-resource-selector.ts
Changed transaction locking from parent entity to computed resource tables; made job queue addition awaitable.
apps/event-worker/src/workers/compute-policy-target-release-target-selector.ts Simplified transaction logic, changed locking and filtering, updated job queueing to deduplicated individual adds.
apps/event-worker/src/workers/compute-systems-release-targets.ts Added early return if no IDs, revised transaction to select and lock, changed insertion and job dispatch logic, added delay on lock conflict.
apps/event-worker/src/workers/evaluate-release-target.ts Removed explicit mutex, wrapped logic in DB transaction, improved error handling and retry logic based on DB errors; updated function signatures.
apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/openapi.ts Added OpenAPI v3 specification for new endpoint to fetch releases by release target.
apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/route.ts Implemented GET route handler to fetch and return releases for a release target with authentication and authorization.
e2e/api/schema.ts
openapi.v1.json
Added new endpoint definition for /v1/release-targets/{releaseTargetId}/releases in API schema and OpenAPI spec.
e2e/api/yaml-loader.ts Extended YAML loader and imported entities to support deployment versions; added logic to create deployment versions from YAML.
e2e/tests/api/release.spec.ts
e2e/tests/api/release.spec.yaml
Added new end-to-end tests and YAML spec for release creation and retrieval scenarios.
packages/auth/src/utils/rbac.ts Added getReleaseTargetScopes function and registered it in scopeHandlers for RBAC.
packages/db/src/schema/rbac.ts Added "releaseTarget" to scopeType enum and schema.
packages/validators/src/auth/index.ts Added ReleaseTargetGet permission to the Permission enum.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant Auth
    participant DB

    Client->>API: GET /v1/release-targets/{releaseTargetId}/releases
    API->>Auth: Authenticate & authorize (ReleaseTargetGet)
    Auth-->>API: Permission granted
    API->>DB: Query release target existence
    DB-->>API: Release target found/not found
    API->>DB: Query latest 100 releases (with joins)
    DB-->>API: Releases data
    API->>Client: JSON array of releases (200 OK) or error (404/500)
Loading

Possibly related PRs

  • ctrlplanedev/ctrlplane#515: Changes dispatchEvaluateJobs parameter type and import, directly related to the same function modified here.
  • ctrlplanedev/ctrlplane#493: Modifies evaluate-release-target.ts to add checks preventing redundant release insertion, related to release creation logic changes in this PR.

Suggested reviewers

  • jsbroks
  • zacharyblasczyk

Poem

In the warren of code, a new path appears,
Release targets now serve their latest in cheers.
With locks held tight and permissions in tow,
The rabbits ensure only the right ones may know.
YAMLs are loaded, and tests all abound,
As releases hop swiftly, new features are found!
🐇✨
"""


📜 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 e263b95 and 63fc63f.

📒 Files selected for processing (1)
  • openapi.v1.json (1 hunks)

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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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: 9

🔭 Outside diff range comments (2)
apps/event-worker/src/workers/compute-policy-target-release-target-selector.ts (1)

70-99: 🛠️ Refactor suggestion

Limit job-fan-out to new release targets to avoid redundant queue traffic

After the transaction, you query all release targets for the whole workspace (lines 101-115) and enqueue an EvaluateReleaseTarget job for every one of them.
In the common case, only a handful of release-target rows were (re)computed inside the transaction. Dispatching jobs for the entire workspace:

  • needlessly floods the queue (potentially 1000s of jobs),
  • adds avoidable round-trips to Redis (even with the 500 ms dedup-TTL),
  • introduces extra load on workers that will immediately no-op.

A lighter alternative is to enqueue jobs only for the IDs you just inserted:

- const releaseTargets = await db             // huge select
-   .select()
-   .from(schema.releaseTarget)
-   ...
-   .then((rows) => rows.map((row) => row.release_target));

+ const releaseTargets = await db
+   .select()
+   .from(schema.releaseTarget)
+   .where(inArray(                       -- drizzle helper
+     schema.releaseTarget.id,
+     releaseTargetsIdsFromInsert,        -- ‹uuid[]› built from `releaseTargets`
+   ));

This keeps the lock-scope optimisation you added, but prevents unnecessary work.
Feel free to ping me if you’d like a full patch.

e2e/api/yaml-loader.ts (1)

218-223: ⚠️ Potential issue

Do not forward test-only versions field to the deployments API

deployment objects include a versions array that is only meaningful for the test harness, but the /v1/deployments endpoint is unlikely to understand or validate it.
Forwarding unknown attributes can lead to 400‐level errors or, worse, silent no-ops that mask test failures.

-const deploymentResponse = await api.POST("/v1/deployments", {
-  body: {
-    ...deployment,          // <- also forwards deployment.versions ❌
-    systemId: result.system.id,
-  },
-});
+const { versions: _ignored, ...deploymentBody } = deployment; // strip test-only field
+const deploymentResponse = await api.POST("/v1/deployments", {
+  body: {
+    ...deploymentBody,
+    systemId: result.system.id,
+  },
+});
🧹 Nitpick comments (8)
apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/openapi.ts (1)

1-86: Well-structured OpenAPI spec for the new endpoint.

The OpenAPI specification is clear and comprehensive, properly documenting the new endpoint for retrieving releases by release target ID.

Consider for future enhancement:

  1. Adding pagination support rather than a fixed limit of 100 releases
  2. Ensuring Deployment and DeploymentVersion component schemas are properly defined in the shared components
apps/event-worker/src/workers/compute-systems-release-targets.ts (1)

147-155: Await queue writes to guarantee at-least-once dispatch
getQueue(...).add() returns a Promise. Without await, the worker may acknowledge the
BullMQ job before the new job is actually enqueued, causing silent loss if the Node process
crashes immediately afterwards.

- getQueue(Channel.ComputePolicyTargetReleaseTargetSelector).add(
+ await getQueue(Channel.ComputePolicyTargetReleaseTargetSelector).add(

(Repeat inside the loop.)

apps/event-worker/src/workers/evaluate-release-target.ts (1)

234-239: Await the job-dispatch promise
As with the other worker, add an await to guarantee that the dispatch is persisted before
the worker finishes.

- getQueue(Channel.DispatchJob).add(job.id, { jobId: job.id });
+ await getQueue(Channel.DispatchJob).add(job.id, { jobId: job.id });
e2e/api/schema.ts (1)

2715-2743: Consider widening the variables[*].value type
The runtime allows variable values to be string, number, boolean, object, etc.
The schema narrows this to string only, which will cause client-side codegen to lose type
information and possibly reject valid payloads.

If the API intentionally stringifies everything, add a note; otherwise change to
value: string | number | boolean | object | null.

openapi.v1.json (1)

2396-2409: Consider documenting auth requirements for the new endpoint

Most sensitive endpoints in the file are protected via the
bearerAuth security scheme. The new release-retrieval route omits
security, 401, and 403 responses. If the handler does enforce
authentication/authorization (as stated in the PR description), the
spec should reflect that so client generation & docs stay accurate.

e2e/tests/api/release.spec.ts (2)

84-86: Replace page.waitForTimeout & sleeps with logical waits

The tests don’t use the browser; loading a page merely to sleep is
wasteful. Remove the page fixture and rely on the polling helper
recommended above. This will also speed up headless CI runs.

Also applies to: 128-130, 190-192


30-33: Fixture parameters include page when not required

Except for the artificial delay noted above, the tests never interact
with a browser page. You can drop page from the parameter list to
reduce Playwright’s startup time.

Also applies to: 105-108, 167-170

apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/route.ts (1)

43-69: Sub-query aggregation may explode memory on large variable sets

json_agg aggregates all variable key/values for every release row.
If a release target has many variables or a long history, this endpoint will:

  • Materialise a huge JSON array in Postgres memory for each join.
  • Transfer large payloads over the network.

Mitigations:

  1. Project only the variables you need (e.g., latest snapshot) or paginate at the SQL level.
  2. Fetch variables in a second, paginated call keyed by releaseId.
  3. Add a LIMIT or WHERE on variableValueSnapshot (e.g., latest createdAt).

Consider profiling with realistic data before this hits production.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 42d0902 and b7f4121.

📒 Files selected for processing (16)
  • apps/event-worker/src/utils/dispatch-evaluate-jobs.ts (1 hunks)
  • apps/event-worker/src/workers/compute-deployment-resource-selector.ts (2 hunks)
  • apps/event-worker/src/workers/compute-environment-resource-selector.ts (2 hunks)
  • apps/event-worker/src/workers/compute-policy-target-release-target-selector.ts (3 hunks)
  • apps/event-worker/src/workers/compute-systems-release-targets.ts (3 hunks)
  • apps/event-worker/src/workers/evaluate-release-target.ts (1 hunks)
  • apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/openapi.ts (1 hunks)
  • apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/route.ts (1 hunks)
  • e2e/api/schema.ts (2 hunks)
  • e2e/api/yaml-loader.ts (3 hunks)
  • e2e/tests/api/release.spec.ts (1 hunks)
  • e2e/tests/api/release.spec.yaml (1 hunks)
  • openapi.v1.json (1 hunks)
  • packages/auth/src/utils/rbac.ts (2 hunks)
  • packages/db/src/schema/rbac.ts (1 hunks)
  • packages/validators/src/auth/index.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.

  • apps/event-worker/src/workers/compute-deployment-resource-selector.ts
  • packages/db/src/schema/rbac.ts
  • packages/validators/src/auth/index.ts
  • apps/event-worker/src/workers/compute-environment-resource-selector.ts
  • apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/openapi.ts
  • apps/event-worker/src/utils/dispatch-evaluate-jobs.ts
  • packages/auth/src/utils/rbac.ts
  • apps/event-worker/src/workers/evaluate-release-target.ts
  • apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/route.ts
  • apps/event-worker/src/workers/compute-policy-target-release-target-selector.ts
  • apps/event-worker/src/workers/compute-systems-release-targets.ts
  • e2e/tests/api/release.spec.ts
  • e2e/api/schema.ts
  • e2e/api/yaml-loader.ts
🧠 Learnings (2)
packages/db/src/schema/rbac.ts (1)
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#181
File: packages/auth/src/utils/rbac.ts:102-118
Timestamp: 2024-10-29T02:05:46.185Z
Learning: The `releaseChannel` scope type is included in the `scopeType` enum in `packages/db/src/schema/rbac.ts`.
packages/auth/src/utils/rbac.ts (1)
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#181
File: packages/auth/src/utils/rbac.ts:102-118
Timestamp: 2024-10-29T02:05:46.185Z
Learning: The `releaseChannel` scope type is included in the `scopeType` enum in `packages/db/src/schema/rbac.ts`.
🧬 Code Graph Analysis (6)
apps/event-worker/src/workers/compute-deployment-resource-selector.ts (1)
packages/events/src/index.ts (1)
  • getQueue (28-34)
apps/event-worker/src/utils/dispatch-evaluate-jobs.ts (1)
packages/rule-engine/src/types.ts (1)
  • ReleaseTargetIdentifier (82-86)
apps/event-worker/src/workers/evaluate-release-target.ts (4)
packages/db/src/schema/release.ts (3)
  • release (113-124)
  • releaseTarget (20-42)
  • versionRelease (46-60)
packages/db/src/client.ts (1)
  • db (15-15)
packages/db/src/common.ts (1)
  • takeFirst (9-13)
packages/events/src/index.ts (1)
  • getQueue (28-34)
apps/event-worker/src/workers/compute-policy-target-release-target-selector.ts (3)
packages/db/src/client.ts (1)
  • db (15-15)
packages/db/src/schema/policy.ts (1)
  • policyTarget (64-78)
packages/events/src/index.ts (1)
  • getQueue (28-34)
apps/event-worker/src/workers/compute-systems-release-targets.ts (3)
packages/db/src/client.ts (1)
  • db (15-15)
packages/events/src/index.ts (1)
  • getQueue (28-34)
apps/event-worker/src/utils/dispatch-evaluate-jobs.ts (1)
  • dispatchEvaluateJobs (5-11)
e2e/tests/api/release.spec.ts (5)
e2e/tests/fixtures.ts (1)
  • test (11-26)
e2e/api/yaml-loader.ts (3)
  • ImportedEntities (62-99)
  • importEntitiesFromYaml (109-300)
  • cleanupImportedEntities (307-355)
packages/db/src/schema/workspace.ts (1)
  • workspace (18-27)
packages/db/src/schema/resource.ts (1)
  • resource (59-87)
packages/db/src/schema/release.ts (2)
  • releaseTarget (20-42)
  • release (113-124)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (18)
packages/db/src/schema/rbac.ts (1)

53-53: Appropriate extension of scope types to support release targets

The addition of the "releaseTarget" enum value to the scopeType enum follows the existing pattern and properly extends the RBAC system to support the new release target functionality. This change aligns with the learnings showing similar scope types (like releaseChannel) already exist in the system.

e2e/tests/api/release.spec.yaml (1)

1-32: Well-structured test specification for release functionality

This YAML test specification is well-organized with a clear hierarchy of entities (system → resources → environments → deployments). The use of template variables with {{ prefix }} allows for dynamic test setup and reuse across different test contexts.

packages/validators/src/auth/index.ts (1)

114-115: Correct extension of permission enum for release targets

The addition of ReleaseTargetGet = "releaseTarget.get" follows the established naming convention of [ResourceType].[Action] and properly supports the new API endpoint for retrieving release target releases. This permission will automatically be included in the admin role since it uses Object.values(Permission).

apps/event-worker/src/workers/compute-deployment-resource-selector.ts (2)

23-24: Improved row-level locking precision

The change to lock computedDeploymentResource rows rather than the entire deployment record is a good practice. This more granular locking approach reduces lock contention and improves database performance by only locking the specific rows being modified.


61-61: Enhanced transaction safety with awaited queue operation

Adding await to the getQueue().add() method ensures the queue operation completes before the worker proceeds. This prevents potential race conditions and ensures proper sequencing of asynchronous operations in the event-processing pipeline.

apps/event-worker/src/workers/compute-environment-resource-selector.ts (2)

41-42: Improved lock scope for reduced contention.

This change refines the locking strategy to target only the specific computed environment resource rows associated with the environment, rather than locking the entire environment record. This approach reduces database contention by narrowing the lock scope.


82-82: Proper sequencing with await for queue operations.

Adding the await keyword ensures that the job is successfully enqueued before the transaction completes, improving reliability of the job dispatch sequence.

apps/event-worker/src/utils/dispatch-evaluate-jobs.ts (1)

1-5: LGTM! Good type refinement.

Switching from the complete schema.ReleaseTarget to the more focused ReleaseTargetIdentifier type is a positive change. This makes the function signature more precise by specifically requiring only the identification fields needed for the operation.

packages/auth/src/utils/rbac.ts (2)

434-454: LGTM! Well-implemented scope resolution for release targets.

The implementation follows the established pattern for scope resolution functions in this file, properly joining all necessary tables to establish the complete scope hierarchy from release target down to workspace.


478-478: Proper registration of release target scope handler.

This addition ensures that the RBAC system correctly resolves permissions for the new release target scope, which is essential for the security of the new release targets endpoint.

apps/event-worker/src/workers/compute-systems-release-targets.ts (3)

80-81: Early-exit is a nice micro-optimisation
Short-circuiting when both id lists are empty prevents a DB round-trip – good catch.


157-158: 👍 Unified evaluate-job dispatcher
Delegating to dispatchEvaluateJobs keeps the worker slim and re-usable.


161-165: Retry delay lowered to 500 ms – double-check back-pressure
Halving the delay increases contention retries under high load. Monitor lock-wait logs to
ensure this does not flood the queue.

apps/event-worker/src/workers/evaluate-release-target.ts (1)

240-251: Good progressive retry strategy
Retrying on lock conflicts (55P03) and on FK-violation (23503) covers both concurrent
evaluations and race conditions during target creation. Nicely handled.

e2e/api/schema.ts (1)

426-442: Endpoint stub looks correct
Path and method are wired into the OpenAPI spec; no immediate issues spotted.

apps/event-worker/src/workers/compute-policy-target-release-target-selector.ts (2)

70-78: Lock may be a no-op when the association table is empty

FOR UPDATE NOWAIT on the join will lock rows only returned by the query.
If computedPolicyTargetReleaseTarget currently has zero rows for the
policyTarget, the select returns nothing → no rows are locked →
two workers can concurrently compute & insert, racing on the unique index.

The race is benign (on-conflict-do-nothing) but still wastes work and may starve the queue.
Consider additionally locking the policyTarget row itself (or using an advisory lock) to guarantee single-writer semantics.


116-129: Awaiting Promise.all is good 👍

Switching from queue.addBulk to per-item insertion with Promise.all maintains parallelism while preserving dedup keys – nice improvement!

e2e/tests/api/release.spec.ts (1)

159-160: Drop stray console.log statements

console.log in test output adds noise and makes CI logs harder to read.
[ suggest_nitpick ]

Also applies to: 221-222

Comment on lines +86 to +110
SELECT ${schema.releaseTarget.id} FROM ${schema.releaseTarget}
WHERE ${or(
inArray(schema.releaseTarget.deploymentId, deploymentIds),
inArray(schema.releaseTarget.environmentId, environmentIds),
)}
FOR UPDATE NOWAIT
`,
);

const previousReleaseTargets = await tx
.delete(schema.releaseTarget)
.where(
or(
inArray(schema.releaseTarget.deploymentId, deploymentIds),
inArray(schema.releaseTarget.environmentId, environmentIds),
),
)
.returning();
await tx.execute(
sql`
SELECT * FROM ${schema.computedEnvironmentResource}
WHERE ${inArray(schema.computedEnvironmentResource.environmentId, environmentIds)}
FOR UPDATE NOWAIT
`,
);

await tx.execute(
sql`
SELECT * FROM ${schema.computedDeploymentResource}
WHERE ${inArray(schema.computedDeploymentResource.deploymentId, deploymentIds)}
FOR UPDATE NOWAIT
`,
);

Copy link
Contributor

@coderabbitai coderabbitai bot Apr 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against empty IN () clauses when locking rows
inArray(_, deploymentIds) / inArray(_, environmentIds) will generate … IN () if the
corresponding list is empty. PostgreSQL rejects that syntax, so an empty list will make the
whole transaction fail before we reach the retry logic.

- await tx.execute(sql`SELECT * FROM ${schema.computedEnvironmentResource}
-   WHERE ${inArray(schema.computedEnvironmentResource.environmentId, environmentIds)}
-   FOR UPDATE NOWAIT`);
+ if (environmentIds.length > 0) {
+   await tx.execute(sql`
+     SELECT * FROM ${schema.computedEnvironmentResource}
+     WHERE ${inArray(schema.computedEnvironmentResource.environmentId, environmentIds)}
+     FOR UPDATE NOWAIT
+   `);
+ }

Apply the same conditional guard to all three lock statements (releaseTarget,
computedEnvironmentResource, computedDeploymentResource).

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if (deploymentIds.length === 0 && environmentIds.length === 0) return;
we do an early return befdore the tx even happens if either is empty

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 You're right that there's an early return if both arrays are empty. However, there's still a potential issue if only one of the arrays is empty:

  • If deploymentIds is empty but environmentIds is not, the lock statement for computedDeploymentResource would have an empty IN () clause
  • If environmentIds is empty but deploymentIds is not, the lock statement for computedEnvironmentResource would have an empty IN () clause

PostgreSQL rejects empty IN () syntax, so additional checks before each lock statement would prevent SQL errors in these cases. Something like:

if (environmentIds.length > 0) {
  await tx.execute(sql`
    SELECT * FROM ${schema.computedEnvironmentResource}
    WHERE ${inArray(schema.computedEnvironmentResource.environmentId, environmentIds)}
    FOR UPDATE NOWAIT
  `);
}

✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#515
File: apps/event-worker/src/workers/compute-systems-release-targets.ts:86-110
Timestamp: 2025-04-28T18:38:21.152Z
Learning: In SQL queries that use `inArray()` with arrays like `deploymentIds` or `environmentIds`, if these arrays are empty, it will generate an invalid `IN ()` clause that PostgreSQL rejects. Adding condition checks (e.g., `if (array.length > 0)`) before executing such queries prevents SQL syntax errors.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch, I should actually be using || for the early return

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 Yes, using || for the early return is actually a more elegant solution! Changing to:

if (deploymentIds.length === 0 || environmentIds.length === 0) return;

This would prevent execution of the transaction entirely if either array is empty, which elegantly avoids the empty IN () clause issue altogether. Much cleaner than adding individual checks before each SQL statement.


🧠 Learnings used
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#515
File: apps/event-worker/src/workers/compute-systems-release-targets.ts:86-110
Timestamp: 2025-04-28T18:38:21.152Z
Learning: In SQL queries that use `inArray()` with arrays like `deploymentIds` or `environmentIds`, if these arrays are empty, it will generate an invalid `IN ()` clause that PostgreSQL rejects. Adding condition checks (e.g., `if (array.length > 0)`) before executing such queries prevents SQL syntax errors.

Comment on lines +23 to +25
await new Promise((resolve) => setTimeout(resolve, 5_000));
});

Copy link
Contributor

@coderabbitai coderabbitai bot Apr 28, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid fixed sleeps – poll the API instead

Using setTimeout for 5 s here makes the suite slow locally and flaky
under load; CI variance may require even longer waits.

Refactor to poll until the worker has finished (example helper):

async function waitForReleaseTarget(api, resourceId: string, timeout = 10_000) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    const r = await api.GET("/v1/resources/{resourceId}/release-targets", {
      params: { path: { resourceId } },
    });
    if (r.data?.length) return r.data[0];
    await new Promise((res) => setTimeout(res, 500));
  }
  throw new Error("release target not created in time");
}

This keeps tests fast yet reliable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

for now we will just keep the sleep intead of polling

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 Understood, we'll keep the current approach with the sleep as is. Thank you for considering the suggestion.

Comment on lines +238 to +261
if (deployment.versions && deployment.versions.length > 0) {
for (const version of deployment.versions) {
const versionResponse = await api.POST("/v1/deployment-versions", {
body: {
...version,
deploymentId: deploymentResponse.data!.id,
},
});

if (versionResponse.response.status !== 201) {
throw new Error(
`Failed to create deployment version: ${JSON.stringify(versionResponse.error)}`,
);
}

const versionData = versionResponse.data!;
versions.push({
id: versionData.id,
name: versionData.name,
tag: versionData.tag,
status: versionData.status ?? "ready",
});
}
}
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

Provide sensible defaults & validation when creating deployment versions

  1. name is optional in the YAML, but the backend may require it. Defaulting name to the tag keeps tests concise and prevents 422 responses.
  2. status defaults to "ready" on the response mapping, but you also need to supply a valid status when the field is omitted in the request body to avoid validation errors.
  3. Consider short-circuiting if no versions are present instead of running an empty loop.
-if (deployment.versions && deployment.versions.length > 0) {
-  for (const version of deployment.versions) {
+for (const version of deployment.versions ?? []) {
+  const {
+    tag,
+    name = tag,                  // default name
+    status = "ready",            // default status
+    ...rest
+  } = version;
+
   const versionResponse = await api.POST("/v1/deployment-versions", {
     body: {
-      ...version,
+      tag,
+      name,
+      status,
+      ...rest,
       deploymentId: deploymentResponse.data!.id,
     },
   });
   /* … */
 }

These guards make the import routine deterministic and reduce test flakiness.

📝 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 (deployment.versions && deployment.versions.length > 0) {
for (const version of deployment.versions) {
const versionResponse = await api.POST("/v1/deployment-versions", {
body: {
...version,
deploymentId: deploymentResponse.data!.id,
},
});
if (versionResponse.response.status !== 201) {
throw new Error(
`Failed to create deployment version: ${JSON.stringify(versionResponse.error)}`,
);
}
const versionData = versionResponse.data!;
versions.push({
id: versionData.id,
name: versionData.name,
tag: versionData.tag,
status: versionData.status ?? "ready",
});
}
}
// Create a deployment version for each entry (no-op if versions is null/undefined or empty)
for (const version of deployment.versions ?? []) {
// Supply sensible defaults up front to avoid 422s
const {
tag,
name = tag, // default name to the tag if omitted
status = "ready", // default status to "ready" if omitted
...rest
} = version;
const versionResponse = await api.POST("/v1/deployment-versions", {
body: {
tag,
name,
status,
...rest,
deploymentId: deploymentResponse.data!.id,
},
});
if (versionResponse.response.status !== 201) {
throw new Error(
`Failed to create deployment version: ${JSON.stringify(
versionResponse.error,
)}`,
);
}
const versionData = versionResponse.data!;
versions.push({
id: versionData.id,
name: versionData.name,
tag: versionData.tag,
status: versionData.status ?? "ready",
});
}

Comment on lines +103 to +108
return {
deployment: release.deployment,
version: release.deployment_version,
variables,
};
});
Copy link
Contributor

@coderabbitai coderabbitai bot Apr 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid leaking internal table columns in the public response

Returning the entire deployment and deployment_version records exposes internal IDs, timestamps, and potentially sensitive metadata.
Define and return a minimal DTO instead, e.g.:

return {
  deployment: { id: release.deployment.id, name: release.deployment.name },
  version:    { id: release.deployment_version.id, tag: release.deployment_version.tag },
  variables,
};

This future-proofs the contract and prevents accidental PII leakage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is nothing in these tables that we want to avoid sending to the public

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 I understand. Thank you for clarifying that all the fields in these tables are intended to be publicly accessible. I'll keep this in mind for future reviews.


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#515
File: apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/route.ts:103-108
Timestamp: 2025-04-28T18:41:58.802Z
Learning: In this project, full records from the `deployment` and `deployment_version` tables are considered safe for public API consumption, and there's no need to create restricted DTOs for them.

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 +20 to +26
authz(({ can, params }) =>
can
.perform(Permission.ReleaseTargetGet)
.on({ type: "releaseTarget", id: params.releaseTargetId ?? "" }),
),
)
.handle<object, { params: Promise<{ releaseTargetId: 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

Add explicit 400 for missing releaseTargetId before RBAC check

params.releaseTargetId ?? "" silently converts an undefined path param to an empty string, which still passes to the RBAC layer and later SQL look-ups.
Returning 404/500 in that situation hides the real client error.

.use(
  authz(({ can, params }) =>
-    can
+    (params.releaseTargetId
+      ? can
         .perform(Permission.ReleaseTargetGet)
         .on({ type: "releaseTarget", id: params.releaseTargetId })
-    ),
+      : false),
  )

Alternatively, short-circuit with a 400 Bad Request before invoking authz.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +94 to +101
const { values } = release.variableSetReleaseSubquery;
const variables = values.map(({ key, value, sensitive }) => {
const strval = String(value);
const resolvedValue = sensitive
? variablesAES256().decrypt(strval)
: strval;
return { key, value: resolvedValue };
});
Copy link
Contributor

@coderabbitai coderabbitai bot Apr 28, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Casting variable values to string loses original types

String(value) erases numeric/boolean/null information and may stringify complex JSON.
Down-stream consumers then need type inference or JSON.parse, which is brittle.

-const strval = String(value);
-const resolvedValue = sensitive
-  ? variablesAES256().decrypt(strval)
-  : strval;
-return { key, value: resolvedValue };
+const raw = sensitive
+  ? variablesAES256().decrypt(String(value))
+  : value;
+return { key, value: raw };

Decrypt only when necessary and keep the original primitive/JSON types intact.

📝 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 { values } = release.variableSetReleaseSubquery;
const variables = values.map(({ key, value, sensitive }) => {
const strval = String(value);
const resolvedValue = sensitive
? variablesAES256().decrypt(strval)
: strval;
return { key, value: resolvedValue };
});
const { values } = release.variableSetReleaseSubquery;
const variables = values.map(({ key, value, sensitive }) => {
const raw = sensitive
? variablesAES256().decrypt(String(value))
: value;
return { key, value: raw };
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is fine

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 Understood! I'll respect your decision to keep the original implementation.

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)
apps/event-worker/src/workers/evaluate-release-target.ts (1)

141-141: ⚠️ Potential issue

Transaction parameter added but DB clients are inconsistent

Similar to the handleVersionRelease function, this function accepts a transaction parameter but still uses the global db client in line 147.

-const varrm = new VariableReleaseManager(db, {
+const varrm = new VariableReleaseManager(tx, {

Using the global db instance defeats the purpose of passing the transaction and reintroduces the transaction isolation issues mentioned in the previous review.

🧹 Nitpick comments (2)
apps/event-worker/src/workers/evaluate-release-target.ts (2)

234-238: Feature flag for job creation

Good practice using a feature flag to control the job creation behavior. This allows for gradual rollout and easier rollback if issues are discovered.

Consider using a constant for the environment variable name and possibly a default value for the check:

-if (process.env.ENABLE_NEW_POLICY_ENGINE === "true" && release != null) {
+const ENABLE_NEW_POLICY_ENGINE = process.env.ENABLE_NEW_POLICY_ENGINE === "true";
+if (ENABLE_NEW_POLICY_ENGINE && release != null) {

240-251: Robust error handling with specific error codes

Excellent error handling with specific PostgreSQL error codes to detect row locking and foreign key issues. The retry mechanism with delay is a good approach for handling temporary conflicts.

Consider extracting the error codes to named constants for better readability and maintainability:

-const isRowLocked = e.code === "55P03";
-const isReleaseTargetNotCommittedYet = e.code === "23503";
+const PG_ERROR_ROW_LOCKED = "55P03";
+const PG_ERROR_FOREIGN_KEY_VIOLATION = "23503";
+
+const isRowLocked = e.code === PG_ERROR_ROW_LOCKED;
+const isReleaseTargetNotCommittedYet = e.code === PG_ERROR_FOREIGN_KEY_VIOLATION;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between b7f4121 and cf22921.

📒 Files selected for processing (5)
  • apps/event-worker/src/workers/compute-systems-release-targets.ts (3 hunks)
  • apps/event-worker/src/workers/evaluate-release-target.ts (3 hunks)
  • apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/openapi.ts (1 hunks)
  • e2e/api/schema.ts (2 hunks)
  • openapi.v1.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/event-worker/src/workers/compute-systems-release-targets.ts
  • e2e/api/schema.ts
  • apps/webservice/src/app/api/v1/release-targets/[releaseTargetId]/releases/openapi.ts
  • openapi.v1.json
🧰 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.

  • apps/event-worker/src/workers/evaluate-release-target.ts
🧬 Code Graph Analysis (1)
apps/event-worker/src/workers/evaluate-release-target.ts (4)
packages/db/src/common.ts (2)
  • Tx (22-22)
  • takeFirst (9-13)
packages/db/src/schema/release.ts (3)
  • releaseTarget (20-42)
  • release (113-124)
  • versionRelease (46-60)
packages/db/src/client.ts (1)
  • db (15-15)
packages/events/src/index.ts (1)
  • getQueue (28-34)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Typecheck
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Lint
🔇 Additional comments (4)
apps/event-worker/src/workers/evaluate-release-target.ts (4)

174-187: Improved transaction handling with row-level locking

Good restructuring to use a database transaction for the entire evaluation process. The code now properly retrieves and locks the release target within the transaction scope.


189-202: Efficient retrieval of existing releases

The code now correctly retrieves the latest version and variable releases within the transaction, ensuring a consistent view of the data before making any changes.


208-215: Smart optimization to avoid redundant releases

Excellent optimization to avoid creating duplicate releases when nothing has changed. The comparison logic ensures that new releases are only created when either the version or variable release has changed.


216-232: Atomic release creation

The release creation is now properly atomic within the transaction. If any part of the process fails, the entire transaction will be rolled back, maintaining data consistency.

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