Skip to content

Conversation

@adityachoudhari26
Copy link
Contributor

@adityachoudhari26 adityachoudhari26 commented Sep 27, 2025

Summary by CodeRabbit

  • New Features

    • Added providers to resolve deployment and resource variables via rules, selectors, and related resources; supports JSON values and decrypting sensitive data.
    • In-memory snapshot store for variable value snapshots.
  • Refactor

    • Variable manager can be constructed with explicit providers/keys for configurable evaluation.
  • Chores

    • Wired new repositories for resource-relationship rules and metadata matching; added secrets dependency.
  • Tests

    • Test adjustments: system prefix matching now case-sensitive.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 27, 2025

Walkthrough

Replaces VariableManager.database(...) with a local getVariableManager(...) that composes ResourceVariableProvider and DeploymentVariableProvider; adds repositories and wiring for resource-relationship rules, an in-memory variable snapshot repo, a VariableManager.create API, and a secrets dependency.

Changes

Cohort / File(s) Summary
Evaluate integration
apps/event-queue/src/release-targets/evaluate/variable-release-manager.ts
Replaces import/use of VariableManager.database(...) with local getVariableManager(workspace, releaseTarget) and standardizes value comparison via a single getStringifiedValue helper.
Variable provider factory & providers
apps/event-queue/src/release-targets/evaluate/variables/variable-manager.ts, apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts, apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts
Adds getVariableManager(workspace, releaseTarget) which builds a VariableManager via VariableManager.create(...) using ResourceVariableProvider and DeploymentVariableProvider. Implements provider logic for resource-scoped variables, deployment variables, reference-based resolution, and sensitive-value decryption.
Relationship rule repositories
apps/event-queue/src/repository/db-resource-relationship-rule-repository.ts, .../db-resource-relationship-rule-target-metadata-equals-repository.ts, .../db-resource-relationship-rule-source-metadata-equals-repository.ts, .../db-resource-relationship-rule-metadata-match-repository.ts
Adds four workspace-scoped DB repository implementations providing CRUD and exists checks for resource relationship rules and their metadata-equals / metadata-match tables.
Repository wiring
apps/event-queue/src/repository/repository.ts, apps/event-queue/src/workspace/workspace.ts
Extends WorkspaceRepositoryOptions and WorkspaceRepository with getters for the new relationship-rule repositories and wires their Db implementations into Workspace construction.
In-memory snapshot repo
apps/event-queue/src/repository/in-memory/variable-value-snapshot.ts
Adds in-memory InMemoryVariableValueSnapshotRepository with CRUD/exists, DB seeding on create, and DB persistence on changes.
Rule-engine API
packages/rule-engine/src/manager/variables/variables.ts
Adds static create(options, providers) to VariableManager to allow manual construction with explicit providers.
DB schema export
packages/db/src/schema/resource-relationship-rule.ts
Exposes new exported type alias ResourceRelationshipRuleTargetMetadataEquals.
Tests
e2e/tests/api/resource-variables.spec.ts
Removes .toLowerCase() when deriving systemPrefix in tests (case-sensitive change).
Dependency
apps/event-queue/package.json
Adds dependency @ctrlplane/secrets: "workspace:*".

Sequence Diagram(s)

sequenceDiagram
  actor Evaluator as ReleaseTarget Evaluator
  participant VRM as variable-release-manager
  participant GVM as getVariableManager()
  participant Repo as Workspace.repository
  participant DB as DB
  participant VM as VariableManager
  participant RVP as ResourceVariableProvider
  participant DVP as DeploymentVariableProvider

  Evaluator->>VRM: evaluate(...)
  VRM->>GVM: getVariableManager(workspace, releaseTarget)
  GVM->>Repo: deploymentVariableRepository.getAll()
  Repo->>DB: query deployment variables (workspace)
  DB-->>Repo: deployment variables
  GVM->>RVP: new ResourceVariableProvider(...)
  GVM->>DVP: new DeploymentVariableProvider(...)
  GVM->>VM: VariableManager.create({keys}, [RVP, DVP])
  VM-->>VRM: VariableManager

  note right of DVP: DVP resolves reference variables via rule repositories,\nresource queries and may decrypt sensitive values

  VRM->>VM: resolve(key)
  VM->>RVP: getVariable(key) (provider 1)
  RVP->>Repo: resourceVariableRepository.getAll()
  Repo->>DB: fetch resource variables
  DB-->>RVP: values
  VM->>DVP: getVariable(key) (provider 2)
  DVP->>Repo: rule/resource/metadata queries
  Repo->>DB: queries
  DB-->>DVP: rule & resources
  DVP-->>VM: resolved value
  VM-->>VRM: final value(s)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • jsbroks

Poem

I nibble code under moonlit beams,
New providers stitched from rabbit dreams.
Rules and secrets, carrots bright,
Variables hop into the night.
Hooray — the manager fetches right! 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive The provided title is too generic and does not clearly summarize the primary change. While it references a “variable manager,” it omits that a new getVariableManager factory was introduced along with resource and deployment variable providers and related repository wiring. It also misuses the term “workspace engine,” which does not align with the project’s components. A more specific title would help reviewers quickly grasp the scope and intent of the changes. Consider renaming the title to explicitly mention the addition of the getVariableManager factory and integration of resource and deployment variable providers, for example “feat(evaluate): add getVariableManager and variable providers for workspace release targets.”
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ws-var-manager

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🧹 Nitpick comments (1)
apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts (1)

12-28: Avoid refetching all resource variables on every lookup

Line 27 triggers getVariables(), which re-queries and filters the entire resource-variable set for each key requested by the VariableManager. During evaluation we iterate over every deployment variable key, so this turns into N identical loads. Cache the filtered list (or memoize it per instance) so we only hit the repository once per evaluation run.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 040b66f and ecb7ef4.

📒 Files selected for processing (12)
  • apps/event-queue/src/release-targets/evaluate/variable-release-manager.ts (2 hunks)
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts (1 hunks)
  • apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts (1 hunks)
  • apps/event-queue/src/release-targets/evaluate/variables/variable-manager.ts (1 hunks)
  • apps/event-queue/src/repository/db-resource-relationship-rule-metadata-match-repository.ts (1 hunks)
  • apps/event-queue/src/repository/db-resource-relationship-rule-repository.ts (1 hunks)
  • apps/event-queue/src/repository/db-resource-relationship-rule-source-metadata-equals-repository.ts (1 hunks)
  • apps/event-queue/src/repository/db-resource-relationship-rule-target-metadata-equals-repository.ts (1 hunks)
  • apps/event-queue/src/repository/repository.ts (2 hunks)
  • apps/event-queue/src/workspace/workspace.ts (2 hunks)
  • packages/db/src/schema/resource-relationship-rule.ts (1 hunks)
  • packages/rule-engine/src/manager/variables/variables.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript with explicit types (prefer interfaces for public APIs)
Import styles: Use named imports, group imports by source (std lib > external > internal)
Consistent type imports: import type { Type } from "module"
Prefer async/await over raw promises
Handle errors explicitly (use try/catch and typed error responses)

Files:

  • apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts
  • packages/db/src/schema/resource-relationship-rule.ts
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
  • packages/rule-engine/src/manager/variables/variables.ts
  • apps/event-queue/src/release-targets/evaluate/variables/variable-manager.ts
  • apps/event-queue/src/release-targets/evaluate/variable-release-manager.ts
  • apps/event-queue/src/repository/repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-target-metadata-equals-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-source-metadata-equals-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-metadata-match-repository.ts
  • apps/event-queue/src/workspace/workspace.ts

⚙️ CodeRabbit configuration file

**/*.{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.

Files:

  • apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts
  • packages/db/src/schema/resource-relationship-rule.ts
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
  • packages/rule-engine/src/manager/variables/variables.ts
  • apps/event-queue/src/release-targets/evaluate/variables/variable-manager.ts
  • apps/event-queue/src/release-targets/evaluate/variable-release-manager.ts
  • apps/event-queue/src/repository/repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-target-metadata-equals-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-source-metadata-equals-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-metadata-match-repository.ts
  • apps/event-queue/src/workspace/workspace.ts
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (CLAUDE.md)

Formatting: Prettier is used with @ctrlplane/prettier-config

Files:

  • apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts
  • packages/db/src/schema/resource-relationship-rule.ts
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
  • packages/rule-engine/src/manager/variables/variables.ts
  • apps/event-queue/src/release-targets/evaluate/variables/variable-manager.ts
  • apps/event-queue/src/release-targets/evaluate/variable-release-manager.ts
  • apps/event-queue/src/repository/repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-target-metadata-equals-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-source-metadata-equals-repository.ts
  • apps/event-queue/src/repository/db-resource-relationship-rule-metadata-match-repository.ts
  • apps/event-queue/src/workspace/workspace.ts
🧬 Code graph analysis (11)
apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts (4)
packages/rule-engine/src/manager/variables/types.ts (2)
  • VariableProvider (11-13)
  • MaybeVariable (9-9)
apps/event-queue/src/workspace/workspace.ts (1)
  • Workspace (165-199)
packages/events/src/kafka/events.ts (1)
  • FullReleaseTarget (66-70)
packages/api/src/router/resource/resource-variables.ts (1)
  • resourceVariables (11-138)
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts (5)
packages/rule-engine/src/manager/variables/types.ts (2)
  • VariableProvider (11-13)
  • MaybeVariable (9-9)
packages/events/src/kafka/events.ts (2)
  • FullReleaseTarget (66-70)
  • FullResource (62-64)
packages/secrets/src/index.ts (1)
  • variablesAES256 (50-51)
packages/db/src/schema/deployment-variables.ts (1)
  • DeploymentVariableValue (174-176)
apps/event-queue/src/selector/in-memory/resource-match.ts (1)
  • resourceMatchesSelector (32-87)
packages/rule-engine/src/manager/variables/variables.ts (1)
packages/rule-engine/src/manager/variables/types.ts (1)
  • VariableProvider (11-13)
apps/event-queue/src/release-targets/evaluate/variables/variable-manager.ts (5)
apps/event-queue/src/workspace/workspace.ts (1)
  • Workspace (165-199)
packages/events/src/kafka/events.ts (1)
  • FullReleaseTarget (66-70)
apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts (1)
  • ResourceVariableProvider (6-37)
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts (1)
  • DeploymentVariableProvider (11-254)
packages/rule-engine/src/manager/variables/variables.ts (1)
  • VariableManager (31-75)
apps/event-queue/src/release-targets/evaluate/variable-release-manager.ts (1)
apps/event-queue/src/release-targets/evaluate/variables/variable-manager.ts (1)
  • getVariableManager (9-32)
apps/event-queue/src/repository/repository.ts (1)
packages/db/src/schema/resource-relationship-rule.ts (4)
  • ResourceRelationshipRule (210-211)
  • ResourceRelationshipRuleTargetMetadataEquals (216-217)
  • ResourceRelationshipRuleSourceMetadataEquals (218-219)
  • ResourceRelationshipRuleMetadataMatch (212-213)
apps/event-queue/src/repository/db-resource-relationship-rule-repository.ts (3)
apps/event-queue/src/repository/repository.ts (1)
  • Repository (8-15)
packages/db/src/schema/resource-relationship-rule.ts (1)
  • ResourceRelationshipRule (210-211)
packages/db/src/common.ts (3)
  • Tx (22-22)
  • takeFirstOrNull (15-20)
  • takeFirst (9-13)
apps/event-queue/src/repository/db-resource-relationship-rule-target-metadata-equals-repository.ts (3)
apps/event-queue/src/repository/repository.ts (1)
  • Repository (8-15)
packages/db/src/schema/resource-relationship-rule.ts (1)
  • ResourceRelationshipRuleTargetMetadataEquals (216-217)
packages/db/src/common.ts (3)
  • Tx (22-22)
  • takeFirstOrNull (15-20)
  • takeFirst (9-13)
apps/event-queue/src/repository/db-resource-relationship-rule-source-metadata-equals-repository.ts (3)
apps/event-queue/src/repository/repository.ts (1)
  • Repository (8-15)
packages/db/src/schema/resource-relationship-rule.ts (1)
  • ResourceRelationshipRuleSourceMetadataEquals (218-219)
packages/db/src/common.ts (3)
  • Tx (22-22)
  • takeFirstOrNull (15-20)
  • takeFirst (9-13)
apps/event-queue/src/repository/db-resource-relationship-rule-metadata-match-repository.ts (3)
apps/event-queue/src/repository/repository.ts (1)
  • Repository (8-15)
packages/db/src/schema/resource-relationship-rule.ts (1)
  • ResourceRelationshipRuleMetadataMatch (212-213)
packages/db/src/common.ts (3)
  • Tx (22-22)
  • takeFirstOrNull (15-20)
  • takeFirst (9-13)
apps/event-queue/src/workspace/workspace.ts (4)
apps/event-queue/src/repository/db-resource-relationship-rule-repository.ts (1)
  • DbResourceRelationshipRuleRepository (9-72)
apps/event-queue/src/repository/db-resource-relationship-rule-target-metadata-equals-repository.ts (1)
  • DbResourceRelationshipRuleTargetMetadataEqualsRepository (9-82)
apps/event-queue/src/repository/db-resource-relationship-rule-source-metadata-equals-repository.ts (1)
  • DbResourceRelationshipRuleSourceMetadataEqualsRepository (9-82)
apps/event-queue/src/repository/db-resource-relationship-rule-metadata-match-repository.ts (1)
  • DbResourceRelationshipRuleMetadataMatchRepository (9-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (1)
apps/event-queue/src/repository/db-resource-relationship-rule-metadata-match-repository.ts (1)

19-77: Enforce workspace scoping on CRUD operations

All of get, update, delete, and exists filter solely by id, so any caller with a UUID can reach across workspaces and read or mutate another tenant’s metadata match row. That breaks our multi-tenant isolation guarantees and is a blocker. Please scope every operation to this.workspaceId, just like getAll already does, e.g. by joining to resourceRelationshipRule (for reads) and constraining updates/deletes with a workspace-bound subquery. A concrete patch would look like:

-import { eq, takeFirst, takeFirstOrNull } from "@ctrlplane/db";
+import { and, eq, inArray, takeFirst, takeFirstOrNull } from "@ctrlplane/db";
@@
-  get(id: string) {
-    return this.db
-      .select()
-      .from(schema.resourceRelationshipRuleMetadataMatch)
-      .where(eq(schema.resourceRelationshipRuleMetadataMatch.id, id))
-      .then(takeFirstOrNull);
-  }
+  get(id: string) {
+    return this.db
+      .select()
+      .from(schema.resourceRelationshipRuleMetadataMatch)
+      .innerJoin(
+        schema.resourceRelationshipRule,
+        eq(
+          schema.resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId,
+          schema.resourceRelationshipRule.id,
+        ),
+      )
+      .where(
+        and(
+          eq(schema.resourceRelationshipRuleMetadataMatch.id, id),
+          eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
+        ),
+      )
+      .then(takeFirstOrNull)
+      .then((row) => row?.resource_relationship_rule_metadata_match ?? null);
+  }
@@
-  update(entity: schema.ResourceRelationshipRuleMetadataMatch) {
+  update(entity: schema.ResourceRelationshipRuleMetadataMatch) {
     return this.db
       .update(schema.resourceRelationshipRuleMetadataMatch)
       .set(entity)
-      .where(eq(schema.resourceRelationshipRuleMetadataMatch.id, entity.id))
+      .where(
+        and(
+          eq(schema.resourceRelationshipRuleMetadataMatch.id, entity.id),
+          inArray(
+            schema.resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId,
+            this.db
+              .select({ id: schema.resourceRelationshipRule.id })
+              .from(schema.resourceRelationshipRule)
+              .where(
+                eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
+              ),
+          ),
+        ),
+      )
       .returning()
       .then(takeFirst);
   }
@@
-  delete(id: string) {
+  delete(id: string) {
     return this.db
       .delete(schema.resourceRelationshipRuleMetadataMatch)
-      .where(eq(schema.resourceRelationshipRuleMetadataMatch.id, id))
+      .where(
+        and(
+          eq(schema.resourceRelationshipRuleMetadataMatch.id, id),
+          inArray(
+            schema.resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId,
+            this.db
+              .select({ id: schema.resourceRelationshipRule.id })
+              .from(schema.resourceRelationshipRule)
+              .where(
+                eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
+              ),
+          ),
+        ),
+      )
       .returning()
       .then(takeFirstOrNull);
   }
@@
-  exists(id: string) {
+  exists(id: string) {
     return this.db
       .select()
-      .from(schema.resourceRelationshipRuleMetadataMatch)
-      .where(eq(schema.resourceRelationshipRuleMetadataMatch.id, id))
+      .from(schema.resourceRelationshipRuleMetadataMatch)
+      .innerJoin(
+        schema.resourceRelationshipRule,
+        eq(
+          schema.resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId,
+          schema.resourceRelationshipRule.id,
+        ),
+      )
+      .where(
+        and(
+          eq(schema.resourceRelationshipRuleMetadataMatch.id, id),
+          eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
+        ),
+      )
       .then(takeFirstOrNull)
       .then((r) => r != null);
   }

Apply the same pattern anywhere else we expose workspace-scoped repositories.

⛔ Skipped due to learnings
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#664
File: apps/event-queue/src/repository/db-resource-repository.ts:42-49
Timestamp: 2025-08-26T20:10:10.062Z
Learning: In the ctrlplane codebase, resource IDs are globally unique across the entire PostgreSQL database, so workspace scoping in WHERE clauses is not necessary when querying by ID.

Comment on lines +28 to +60
private async getRuleByReference(reference: string) {
const allRelationshipRules =
await this.workspace.repository.resourceRelationshipRuleRepository.getAll();
const relationshipRule = allRelationshipRules.find(
(r) => r.reference === reference,
);
if (relationshipRule == null) return null;
const [
allRelationshipRuleSourceMetadataEquals,
allRelationshipRuleTargetMetadataEquals,
allRelationshipRuleMetadataMatch,
] = await Promise.all([
this.workspace.repository.resourceRelationshipRuleSourceMetadataEqualsRepository.getAll(),
this.workspace.repository.resourceRelationshipRuleTargetMetadataEqualsRepository.getAll(),
this.workspace.repository.resourceRelationshipRuleMetadataMatchRepository.getAll(),
]);

const metadataKeysMatch = allRelationshipRuleMetadataMatch.filter(
(r) => r.resourceRelationshipRuleId === relationshipRule.id,
);
const targetMetadataEquals = allRelationshipRuleTargetMetadataEquals.filter(
(r) => r.resourceRelationshipRuleId === relationshipRule.id,
);
const sourceMetadataEquals = allRelationshipRuleSourceMetadataEquals.filter(
(r) => r.resourceRelationshipRuleId === relationshipRule.id,
);
return {
...relationshipRule,
metadataKeysMatch,
targetMetadataEquals,
sourceMetadataEquals,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope DB queries down to rule reference

getRuleByReference pulls entire rule and metadata tables into memory before filtering, which will not scale when the tables grow. We need repository helpers that load a single rule (and its metadata) by reference to keep evaluation performant.

🤖 Prompt for AI Agents
In
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
around lines 28 to 60, the current implementation loads entire relationship rule
and metadata tables then filters in-memory which won't scale; replace these
broad queries with repository helpers that fetch a single relationship rule by
reference and the associated metadata rows by resourceRelationshipRuleId. Add or
use repository methods such as getByReference(reference) on
resourceRelationshipRuleRepository and getByRuleId(ruleId) (or similarly named)
on the three metadata repositories, call them (Promise.all is fine) to load only
the needed rows, and return the same object shape ({ ...relationshipRule,
metadataKeysMatch, targetMetadataEquals, sourceMetadataEquals }). Ensure null
handling remains the same when the rule isn't found.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right now this is not really an issue. if we have issues with memory then we'll fix it then.

Comment on lines +82 to +141
private async getSourceResourceCandidates(
relationshipRule: schema.ResourceRelationshipRule,
) {
const { sourceKind, sourceVersion } = relationshipRule;
const allResources =
await this.workspace.repository.resourceRepository.getAll();
return allResources.filter(
(r) =>
r.kind === sourceKind &&
r.version === sourceVersion &&
r.deletedAt == null,
);
}

private sourceResourceMatchesRule(
relationshipRule: schema.ResourceRelationshipRule,
sourceMetadataEqualsRules: schema.ResourceRelationshipRuleSourceMetadataEquals[],
metadataKeysMatchRules: schema.ResourceRelationshipRuleMetadataMatch[],
candidate: FullResource,
) {
const { resource } = this.releaseTarget;
for (const s of sourceMetadataEqualsRules) {
const sourceMetadata = candidate.metadata[s.key];
if (sourceMetadata == null || sourceMetadata !== s.value) return false;
}

for (const m of metadataKeysMatchRules) {
const sourceMetadata = candidate.metadata[m.sourceKey];
const targetMetadata = resource.metadata[m.targetKey];
if (
sourceMetadata == null ||
targetMetadata == null ||
sourceMetadata !== targetMetadata
)
return false;
}

return true;
}

private async getFullSource(
resource: FullResource,
): Promise<
FullResource & { variables: Record<string, string | number | boolean> }
> {
const allVariables =
await this.workspace.repository.resourceVariableRepository.getAll();
const variables = Object.fromEntries(
allVariables
.filter((v) => v.resourceId === resource.id)
.filter((v) => v.valueType === "direct")
.map((v) => {
const { value, key } = v;
if (v.sensitive)
return [key, variablesAES256().decrypt(String(value))];
if (typeof value === "object") return [key, JSON.stringify(value)];
return [key, value];
}),
);
return { ...resource, variables };
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Narrow resource/variable lookups

Both getSourceResourceCandidates and getFullSource fetch all resources/variables and then filter locally. This is expensive and risks timeouts. Please add targeted repository queries (e.g., listByKindVersion, listByResourceIdAndType) to avoid pulling entire tables for each resolution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

same as above, if performance becomes an actual issue we can change it then

Comment on lines +160 to +171
const sourceResource = sourceResourceCandidates.find((r) =>
this.sourceResourceMatchesRule(
relationshipRule,
sourceMetadataEquals,
metadataKeysMatch,
r,
),
);
if (sourceResource == null) return defaultValue;

const fullSource = await this.getFullSource(sourceResource);
const resolvedPath = _.get(fullSource, path, defaultValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Load full source resource before filtering

sourceResourceCandidates are still partial resources (metadata not populated), so attempting to read candidate.metadata is unsafe and will throw once metadata isn’t eagerly joined. Ensure candidates are hydrated with metadata (e.g., via a repository method that returns FullResource) before evaluating metadata rules.

🤖 Prompt for AI Agents
In
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
around lines 160-171, the code calls sourceResourceMatchesRule against partial
sourceResourceCandidates (whose metadata may be undefined) then only hydrates
the chosen candidate, which will throw when metadata is not eagerly joined; fix
by hydrating candidates before applying the rule: map sourceResourceCandidates
to full resources (await this.getFullSource for each candidate or call a
repository method that returns FullResource), then run sourceResourceMatchesRule
against those fully populated resources (or adapt the match function to
accept/guard missing metadata), and finally select the resolved resource and
read the path from the hydrated full object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the resource repository provides FullResource, which has a metadata map attached to it

Comment on lines +211 to +228
private async getDeploymentVariable(key: string) {
const allDeploymentVariables =
await this.workspace.repository.deploymentVariableRepository.getAll();
const deploymentVariable = allDeploymentVariables.find(
(v) =>
v.deploymentId === this.releaseTarget.deploymentId && v.key === key,
);
if (deploymentVariable == null) return null;
const allDeploymentVariableValues =
await this.workspace.repository.deploymentVariableValueRepository.getAll();
const values = allDeploymentVariableValues.filter(
(v) => v.variableId === deploymentVariable.id,
);
const defaultValue = values.find(
(value) => value.id === deploymentVariable.defaultValueId,
);
return { ...deploymentVariable, values, defaultValue };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid repeated full-table scans in hot path

getDeploymentVariable issues two getAll() calls and filters client-side. Under load this will hammer the database with full-table scans per variable lookup. Please introduce repository methods (e.g., getByDeploymentIdAndKey, getValuesByVariableId) so we fetch only the relevant rows.

🤖 Prompt for AI Agents
In
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
around lines 211-228, the method getDeploymentVariable performs two
repository.getAll() calls and filters in-memory, causing full-table scans;
replace these with targeted repository methods (e.g., add
repository.deploymentVariableRepository.getByDeploymentIdAndKey(deploymentId,
key) to fetch the single variable, and
repository.deploymentVariableValueRepository.getValuesByVariableId(variableId)
to fetch only its values) and update getDeploymentVariable to call those new
methods and compute defaultValue from the returned values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

same as above comments, not worried about performance right now. remember, these are partitioned to workspace. even thousands shoudl not be an issue at all

Comment on lines 26 to 35
async getVariable(key: string): Promise<MaybeVariable> {
const variables = await this.getVariables();
const variable = variables.find((v) => v.key === key) ?? null;
if (variable == null) return null;
return {
id: variable.id,
key,
value: variable.value,
sensitive: variable.sensitive,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Decrypt sensitive resource variable values before emitting them

Line 32 returns variable.value as-is. Resource variables marked sensitive are stored encrypted (see packages/api/src/router/resource/resource-variables.ts), and the old DatabaseResourceVariableProvider decrypted them before handing them to the release flow. With this direct return, every sensitive value will surface as ciphertext, breaking consumers of the release payload. Mirror the decryption logic used on the write path (e.g., variablesAES256().decrypt(...)) before constructing the MaybeVariable.

🤖 Prompt for AI Agents
In
apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts
around lines 26 to 35, the code returns variable.value directly which leaves
sensitive resource variables encrypted; decrypt sensitive values before emitting
them by calling the same decryption used on write (e.g.,
variablesAES256().decrypt(variable.value)) when variable.sensitive is true, and
return the decrypted plaintext in the MaybeVariable.value; ensure you handle
null/undefined values and any decryption errors (bubble or log) consistently
with existing providers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

valid

Comment on lines +28 to +31
return VariableManager.create({ keys }, [
resourceVariableProvider,
deploymentVariableProvider,
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Reintroduce the system variable provider to keep parity with the database flow

Lines 28-31 now instantiate only the resource and deployment providers. The previous wiring via VariableManager.database(...) always included DatabaseSystemVariableSetProvider, so system-scoped variables (e.g. environment/workspace defaults) will now come back as null. Please restore an equivalent provider in this stack so those values continue to resolve.

🤖 Prompt for AI Agents
In apps/event-queue/src/release-targets/evaluate/variables/variable-manager.ts
around lines 28-31, the VariableManager.create call only registers resource and
deployment providers which omits the system-scoped provider and causes system
variables to resolve to null; restore the equivalent
DatabaseSystemVariableSetProvider (or the system variable provider used by
VariableManager.database(...)) by importing/instantiating it and adding it into
the provider array (in the same ordering as the database wiring) so
system-scoped variables (environment/workspace defaults) are resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not using the system variable provider at all, if we need it we will reintroduce it

Comment on lines +56 to +71
delete(id: string) {
return this.db
.delete(schema.resourceRelationshipRule)
.where(eq(schema.resourceRelationshipRule.id, id))
.returning()
.then(takeFirstOrNull);
}

exists(id: string) {
return this.db
.select()
.from(schema.resourceRelationshipRule)
.where(eq(schema.resourceRelationshipRule.id, id))
.then(takeFirstOrNull)
.then((r) => r != null);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Enforce workspace scoping on update/delete/exists

update, delete, and exists don’t filter by workspaceId, so a workspace can mutate or observe rules belonging to another workspace. Add the workspace predicate to each query to maintain tenant isolation.

🤖 Prompt for AI Agents
In apps/event-queue/src/repository/db-resource-relationship-rule-repository.ts
around lines 56 to 71, delete, exists (and the update method) currently only
filter by id and therefore allow cross-workspace access; add the workspace
predicate to each query by Constraining queries with
eq(schema.resourceRelationshipRule.workspaceId, workspaceId) alongside the id
predicate (combined via AND), and ensure each method accepts or uses the
workspaceId parameter from the caller/context so update/delete/exists only
operate on rows matching both id and workspaceId.

Comment on lines +55 to +80
update(entity: schema.ResourceRelationshipRuleSourceMetadataEquals) {
return this.db
.update(schema.resourceRelationshipSourceRuleMetadataEquals)
.set(entity)
.where(
eq(schema.resourceRelationshipSourceRuleMetadataEquals.id, entity.id),
)
.returning()
.then(takeFirst);
}

delete(id: string) {
return this.db
.delete(schema.resourceRelationshipSourceRuleMetadataEquals)
.where(eq(schema.resourceRelationshipSourceRuleMetadataEquals.id, id))
.returning()
.then(takeFirstOrNull);
}

exists(id: string) {
return this.db
.select()
.from(schema.resourceRelationshipSourceRuleMetadataEquals)
.where(eq(schema.resourceRelationshipSourceRuleMetadataEquals.id, id))
.then(takeFirstOrNull)
.then((r) => r != null);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Workspace filter missing on mutating ops

The update, delete, and exists methods lack the workspace constraint; a caller could touch metadata rows for other workspaces. Mirror the join/where logic used in getAll when mutating/checking existence.

🤖 Prompt for AI Agents
In
apps/event-queue/src/repository/db-resource-relationship-rule-source-metadata-equals-repository.ts
around lines 55 to 80, the update, delete, and exists methods do not constrain
operations to a workspace and therefore can affect rows from other workspaces;
modify each method to accept the workspaceId (or use the existing
caller-provided workspaceId) and apply the same join/where logic used in getAll:
join resourceRelationshipSourceRuleMetadataEquals -> resourceRelationshipRule ->
resourceRelationship and add a where clause that filters by
eq(resourceRelationship.workspaceId, workspaceId) (keeping the existing id
equality), then perform the update/delete/select and return the same
returning/takeFirst or takeFirstOrNull results as before.

Comment on lines +19 to +81
get(id: string) {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
.then(takeFirstOrNull);
}

getAll() {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.innerJoin(
schema.resourceRelationshipRule,
eq(
schema.resourceRelationshipTargetRuleMetadataEquals
.resourceRelationshipRuleId,
schema.resourceRelationshipRule.id,
),
)
.where(eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId))
.then((rows) =>
rows.map(
(row) => row.resource_relationship_rule_target_metadata_equals,
),
);
}

create(entity: schema.ResourceRelationshipRuleTargetMetadataEquals) {
return this.db
.insert(schema.resourceRelationshipTargetRuleMetadataEquals)
.values(entity)
.returning()
.then(takeFirst);
}

update(entity: schema.ResourceRelationshipRuleTargetMetadataEquals) {
return this.db
.update(schema.resourceRelationshipTargetRuleMetadataEquals)
.set(entity)
.where(
eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, entity.id),
)
.returning()
.then(takeFirst);
}

delete(id: string) {
return this.db
.delete(schema.resourceRelationshipTargetRuleMetadataEquals)
.where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
.returning()
.then(takeFirstOrNull);
}

exists(id: string) {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
.then(takeFirstOrNull)
.then((r) => r != null);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Lock target-metadata-equals operations to the current workspace

Similar to the metadata-match repository, these CRUD methods look up rows only by id. That lets a caller operate on another workspace’s relationships if they know the UUID, breaking tenant isolation. Please constrain every query/update/delete to this.workspaceId (e.g. via a join against resourceRelationshipRule or a workspace-bounded subquery). One way to address it:

-import { eq, takeFirst, takeFirstOrNull } from "@ctrlplane/db";
+import { and, eq, inArray, takeFirst, takeFirstOrNull } from "@ctrlplane/db";
@@
-  get(id: string) {
-    return this.db
-      .select()
-      .from(schema.resourceRelationshipTargetRuleMetadataEquals)
-      .where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
-      .then(takeFirstOrNull);
-  }
+  get(id: string) {
+    return this.db
+      .select()
+      .from(schema.resourceRelationshipTargetRuleMetadataEquals)
+      .innerJoin(
+        schema.resourceRelationshipRule,
+        eq(
+          schema.resourceRelationshipTargetRuleMetadataEquals
+            .resourceRelationshipRuleId,
+          schema.resourceRelationshipRule.id,
+        ),
+      )
+      .where(
+        and(
+          eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id),
+          eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
+        ),
+      )
+      .then(takeFirstOrNull)
+      .then(
+        (row) => row?.resource_relationship_rule_target_metadata_equals ?? null,
+      );
+  }
@@
-  update(entity: schema.ResourceRelationshipRuleTargetMetadataEquals) {
+  update(entity: schema.ResourceRelationshipRuleTargetMetadataEquals) {
     return this.db
       .update(schema.resourceRelationshipTargetRuleMetadataEquals)
       .set(entity)
-      .where(
-        eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, entity.id),
-      )
+      .where(
+        and(
+          eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, entity.id),
+          inArray(
+            schema.resourceRelationshipTargetRuleMetadataEquals
+              .resourceRelationshipRuleId,
+            this.db
+              .select({ id: schema.resourceRelationshipRule.id })
+              .from(schema.resourceRelationshipRule)
+              .where(
+                eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
+              ),
+          ),
+        ),
+      )
       .returning()
       .then(takeFirst);
   }
@@
-  delete(id: string) {
+  delete(id: string) {
     return this.db
       .delete(schema.resourceRelationshipTargetRuleMetadataEquals)
-      .where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
+      .where(
+        and(
+          eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id),
+          inArray(
+            schema.resourceRelationshipTargetRuleMetadataEquals
+              .resourceRelationshipRuleId,
+            this.db
+              .select({ id: schema.resourceRelationshipRule.id })
+              .from(schema.resourceRelationshipRule)
+              .where(
+                eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
+              ),
+          ),
+        ),
+      )
       .returning()
       .then(takeFirstOrNull);
   }
@@
-  exists(id: string) {
+  exists(id: string) {
     return this.db
       .select()
-      .from(schema.resourceRelationshipTargetRuleMetadataEquals)
-      .where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
+      .from(schema.resourceRelationshipTargetRuleMetadataEquals)
+      .innerJoin(
+        schema.resourceRelationshipRule,
+        eq(
+          schema.resourceRelationshipTargetRuleMetadataEquals
+            .resourceRelationshipRuleId,
+          schema.resourceRelationshipRule.id,
+        ),
+      )
+      .where(
+        and(
+          eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id),
+          eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
+        ),
+      )
       .then(takeFirstOrNull)
       .then((r) => r != null);
   }

Please apply the same workspace guardrails to any companion repositories to keep the isolation invariant 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
get(id: string) {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
.then(takeFirstOrNull);
}
getAll() {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.innerJoin(
schema.resourceRelationshipRule,
eq(
schema.resourceRelationshipTargetRuleMetadataEquals
.resourceRelationshipRuleId,
schema.resourceRelationshipRule.id,
),
)
.where(eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId))
.then((rows) =>
rows.map(
(row) => row.resource_relationship_rule_target_metadata_equals,
),
);
}
create(entity: schema.ResourceRelationshipRuleTargetMetadataEquals) {
return this.db
.insert(schema.resourceRelationshipTargetRuleMetadataEquals)
.values(entity)
.returning()
.then(takeFirst);
}
update(entity: schema.ResourceRelationshipRuleTargetMetadataEquals) {
return this.db
.update(schema.resourceRelationshipTargetRuleMetadataEquals)
.set(entity)
.where(
eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, entity.id),
)
.returning()
.then(takeFirst);
}
delete(id: string) {
return this.db
.delete(schema.resourceRelationshipTargetRuleMetadataEquals)
.where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
.returning()
.then(takeFirstOrNull);
}
exists(id: string) {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.where(eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id))
.then(takeFirstOrNull)
.then((r) => r != null);
}
// at the top of the file
import { and, eq, inArray, takeFirst, takeFirstOrNull } from "@ctrlplane/db";
get(id: string) {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.innerJoin(
schema.resourceRelationshipRule,
eq(
schema.resourceRelationshipTargetRuleMetadataEquals
.resourceRelationshipRuleId,
schema.resourceRelationshipRule.id,
),
)
.where(
and(
eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id),
eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
),
)
.then(takeFirstOrNull)
.then(
(row) => row?.resource_relationship_rule_target_metadata_equals ?? null,
);
}
getAll() {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.innerJoin(
schema.resourceRelationshipRule,
eq(
schema.resourceRelationshipTargetRuleMetadataEquals
.resourceRelationshipRuleId,
schema.resourceRelationshipRule.id,
),
)
.where(eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId))
.then((rows) =>
rows.map(
(row) => row.resource_relationship_rule_target_metadata_equals,
),
);
}
create(entity: schema.ResourceRelationshipRuleTargetMetadataEquals) {
return this.db
.insert(schema.resourceRelationshipTargetRuleMetadataEquals)
.values(entity)
.returning()
.then(takeFirst);
}
update(entity: schema.ResourceRelationshipRuleTargetMetadataEquals) {
return this.db
.update(schema.resourceRelationshipTargetRuleMetadataEquals)
.set(entity)
.where(
and(
eq(
schema.resourceRelationshipTargetRuleMetadataEquals.id,
entity.id,
),
inArray(
schema.resourceRelationshipTargetRuleMetadataEquals
.resourceRelationshipRuleId,
this.db
.select({ id: schema.resourceRelationshipRule.id })
.from(schema.resourceRelationshipRule)
.where(
eq(
schema.resourceRelationshipRule.workspaceId,
this.workspaceId,
),
),
),
),
)
.returning()
.then(takeFirst);
}
delete(id: string) {
return this.db
.delete(schema.resourceRelationshipTargetRuleMetadataEquals)
.where(
and(
eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id),
inArray(
schema.resourceRelationshipTargetRuleMetadataEquals
.resourceRelationshipRuleId,
this.db
.select({ id: schema.resourceRelationshipRule.id })
.from(schema.resourceRelationshipRule)
.where(
eq(
schema.resourceRelationshipRule.workspaceId,
this.workspaceId,
),
),
),
),
)
.returning()
.then(takeFirstOrNull);
}
exists(id: string) {
return this.db
.select()
.from(schema.resourceRelationshipTargetRuleMetadataEquals)
.innerJoin(
schema.resourceRelationshipRule,
eq(
schema.resourceRelationshipTargetRuleMetadataEquals
.resourceRelationshipRuleId,
schema.resourceRelationshipRule.id,
),
)
.where(
and(
eq(schema.resourceRelationshipTargetRuleMetadataEquals.id, id),
eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId),
),
)
.then(takeFirstOrNull)
.then((r) => r != null);
}
🤖 Prompt for AI Agents
In
apps/event-queue/src/repository/db-resource-relationship-rule-target-metadata-equals-repository.ts
lines 19-81: the CRUD methods only filter by id which allows cross-workspace
access; update every read/update/delete/exists query to innerJoin
schema.resourceRelationshipRule and add
.where(eq(schema.resourceRelationshipRule.workspaceId, this.workspaceId)) so the
row is scoped to the current workspace (getAll already does this); for update
and delete ensure the same join+workspace where is used before returning the
row; for create, perform a quick existence check that the referenced
resourceRelationshipRuleId exists and belongs to this.workspaceId (select from
schema.resourceRelationshipRule where id = entity.resourceRelationshipRuleId and
workspaceId = this.workspaceId) and fail/throw if not, then insert; ensure all
returned rows still map to resource_relationship_rule_target_metadata_equals
where appropriate.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ecb7ef4 and 0be630c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • apps/event-queue/package.json (1 hunks)
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts (1 hunks)
  • apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/event-queue/src/release-targets/evaluate/variables/resource-variable-provider.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (CLAUDE.md)

Formatting: Prettier is used with @ctrlplane/prettier-config

Files:

  • apps/event-queue/package.json
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript with explicit types (prefer interfaces for public APIs)
Import styles: Use named imports, group imports by source (std lib > external > internal)
Consistent type imports: import type { Type } from "module"
Prefer async/await over raw promises
Handle errors explicitly (use try/catch and typed error responses)

Files:

  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts

⚙️ CodeRabbit configuration file

**/*.{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.

Files:

  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
🧠 Learnings (2)
📚 Learning: 2025-06-01T19:10:47.122Z
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#579
File: packages/rule-engine/src/rules/concurrency-rule.ts:8-11
Timestamp: 2025-06-01T19:10:47.122Z
Learning: In packages/rule-engine/src/rules/concurrency-rule.ts, the ConcurrencyRule should remain simple without additional validation since database and Zod schemas already handle concurrency validation. The user prefers this rule to be "dumb" and just perform the comparison check rather than duplicating validation logic.

Applied to files:

  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
📚 Learning: 2024-10-29T02:04:50.312Z
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#181
File: packages/api/src/router/deployment.ts:116-131
Timestamp: 2024-10-29T02:04:50.312Z
Learning: In `packages/api/src/router/deployment.ts`, the `list.byDeploymentId` procedure requires multiple database queries due to limitations of the `releaseMatchesCondition` function.

Applied to files:

  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
🧬 Code graph analysis (1)
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts (7)
packages/rule-engine/src/manager/variables/types.ts (2)
  • VariableProvider (11-13)
  • MaybeVariable (9-9)
apps/event-queue/src/workspace/workspace.ts (1)
  • Workspace (165-199)
packages/events/src/kafka/events.ts (2)
  • FullReleaseTarget (66-70)
  • FullResource (62-64)
packages/secrets/src/index.ts (1)
  • variablesAES256 (50-51)
packages/db/src/schema/resource-relationship-rule.ts (4)
  • ResourceRelationshipRule (210-211)
  • ResourceRelationshipRuleTargetMetadataEquals (216-217)
  • ResourceRelationshipRuleSourceMetadataEquals (218-219)
  • ResourceRelationshipRuleMetadataMatch (212-213)
packages/db/src/schema/deployment-variables.ts (1)
  • DeploymentVariableValue (174-176)
apps/event-queue/src/selector/in-memory/resource-match.ts (1)
  • resourceMatchesSelector (32-87)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)

Comment on lines +82 to +93
private async getSourceResourceCandidates(
relationshipRule: schema.ResourceRelationshipRule,
) {
const { sourceKind, sourceVersion } = relationshipRule;
const allResources =
await this.workspace.repository.resourceRepository.getAll();
return allResources.filter(
(r) =>
r.kind === sourceKind &&
r.version === sourceVersion &&
r.deletedAt == null,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix source-kind/version matching logic

Rules that omit sourceKind and/or sourceVersion should act as wildcards, but the current filter requires strict equality, so those rules never match any resource. This breaks reference resolution whenever the rule leaves one of those fields null. Please treat nulls as “match any” before filtering.

-    return allResources.filter(
-      (r) =>
-        r.kind === sourceKind &&
-        r.version === sourceVersion &&
-        r.deletedAt == null,
-    );
+    return allResources.filter((r) => {
+      const kindMatches = sourceKind == null || r.kind === sourceKind;
+      const versionMatches =
+        sourceVersion == null || r.version === sourceVersion;
+      return kindMatches && versionMatches && r.deletedAt == null;
+    });
📝 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
private async getSourceResourceCandidates(
relationshipRule: schema.ResourceRelationshipRule,
) {
const { sourceKind, sourceVersion } = relationshipRule;
const allResources =
await this.workspace.repository.resourceRepository.getAll();
return allResources.filter(
(r) =>
r.kind === sourceKind &&
r.version === sourceVersion &&
r.deletedAt == null,
);
private async getSourceResourceCandidates(
relationshipRule: schema.ResourceRelationshipRule,
) {
const { sourceKind, sourceVersion } = relationshipRule;
const allResources =
await this.workspace.repository.resourceRepository.getAll();
- return allResources.filter(
- (r) =>
- r.kind === sourceKind &&
- r.version === sourceVersion &&
- r.deletedAt == null,
return allResources.filter((r) => {
const kindMatches = sourceKind == null || r.kind === sourceKind;
const versionMatches =
sourceVersion == null || r.version === sourceVersion;
return kindMatches && versionMatches && r.deletedAt == null;
});
}
🤖 Prompt for AI Agents
In
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
around lines 82 to 93, the current filter uses strict equality for sourceKind
and sourceVersion so rules that leave those fields null never match; change the
predicate to treat null/undefined as wildcards by only checking r.kind ===
sourceKind when sourceKind is not null/undefined and only checking r.version ===
sourceVersion when sourceVersion is not null/undefined (e.g. use conditions like
(sourceKind == null || r.kind === sourceKind) and (sourceVersion == null ||
r.version === sourceVersion) while still filtering out deleted resources).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0be630c and a60dd84.

📒 Files selected for processing (4)
  • apps/event-queue/src/release-targets/evaluate/variable-release-manager.ts (4 hunks)
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts (1 hunks)
  • apps/event-queue/src/repository/in-memory/variable-value-snapshot.ts (1 hunks)
  • e2e/tests/api/resource-variables.spec.ts (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/event-queue/src/release-targets/evaluate/variable-release-manager.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript with explicit types (prefer interfaces for public APIs)
Import styles: Use named imports, group imports by source (std lib > external > internal)
Consistent type imports: import type { Type } from "module"
Prefer async/await over raw promises
Handle errors explicitly (use try/catch and typed error responses)

Files:

  • e2e/tests/api/resource-variables.spec.ts
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
  • apps/event-queue/src/repository/in-memory/variable-value-snapshot.ts

⚙️ CodeRabbit configuration file

**/*.{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.

Files:

  • e2e/tests/api/resource-variables.spec.ts
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
  • apps/event-queue/src/repository/in-memory/variable-value-snapshot.ts
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (CLAUDE.md)

Formatting: Prettier is used with @ctrlplane/prettier-config

Files:

  • e2e/tests/api/resource-variables.spec.ts
  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
  • apps/event-queue/src/repository/in-memory/variable-value-snapshot.ts
🧠 Learnings (3)
📚 Learning: 2025-06-01T19:10:47.122Z
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#579
File: packages/rule-engine/src/rules/concurrency-rule.ts:8-11
Timestamp: 2025-06-01T19:10:47.122Z
Learning: In packages/rule-engine/src/rules/concurrency-rule.ts, the ConcurrencyRule should remain simple without additional validation since database and Zod schemas already handle concurrency validation. The user prefers this rule to be "dumb" and just perform the comparison check rather than duplicating validation logic.

Applied to files:

  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
📚 Learning: 2024-10-29T02:04:50.312Z
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#181
File: packages/api/src/router/deployment.ts:116-131
Timestamp: 2024-10-29T02:04:50.312Z
Learning: In `packages/api/src/router/deployment.ts`, the `list.byDeploymentId` procedure requires multiple database queries due to limitations of the `releaseMatchesCondition` function.

Applied to files:

  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
📚 Learning: 2025-06-30T21:19:43.866Z
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#604
File: packages/rule-engine/src/manager/version-manager.ts:124-139
Timestamp: 2025-06-30T21:19:43.866Z
Learning: In packages/rule-engine/src/manager/version-manager.ts, the findDesiredVersion method should use takeFirst (not takeFirstOrNull) because if a desiredVersionId is set but the query fails to find exactly one matching version, it indicates a data integrity or configuration issue that should fail loudly rather than be handled silently.

Applied to files:

  • apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
🧬 Code graph analysis (2)
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts (6)
packages/rule-engine/src/manager/variables/types.ts (2)
  • VariableProvider (11-13)
  • MaybeVariable (9-9)
apps/event-queue/src/workspace/workspace.ts (1)
  • Workspace (165-199)
packages/events/src/kafka/events.ts (2)
  • FullReleaseTarget (66-70)
  • FullResource (62-64)
packages/secrets/src/index.ts (1)
  • variablesAES256 (50-51)
packages/db/src/schema/deployment-variables.ts (1)
  • DeploymentVariableValue (174-176)
apps/event-queue/src/selector/in-memory/resource-match.ts (1)
  • resourceMatchesSelector (32-87)
apps/event-queue/src/repository/in-memory/variable-value-snapshot.ts (1)
packages/db/src/common.ts (1)
  • Tx (22-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • 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)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (2)
e2e/tests/api/resource-variables.spec.ts (1)

175-176: Preserving slug casing looks correct

Variable resolution now compares metadata and selectors using the exact slug segment that upstream systems emit. Dropping the redundant .toLowerCase() keeps the test fixtures aligned with the production casing, avoiding false positives from lowercased keys. Thanks for syncing all occurrences.

Also applies to: 274-275, 438-439, 608-610, 779-781, 910-912, 1097-1099

apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts (1)

88-93: Handle optional source kind/version as wildcards

Rules that leave sourceKind or sourceVersion unset are supposed to match any resource, but the current filter insists on exact equality, so those rules never match and references silently fall back to defaults. Treat the nulls as wildcards while still excluding deleted resources.

-    return allResources.filter(
-      (r) =>
-        r.kind === sourceKind &&
-        r.version === sourceVersion &&
-        r.deletedAt == null,
-    );
+    return allResources.filter((r) => {
+      const kindMatches = sourceKind == null || r.kind === sourceKind;
+      const versionMatches =
+        sourceVersion == null || r.version === sourceVersion;
+      return kindMatches && versionMatches && r.deletedAt == null;
+    });

Comment on lines +125 to +140
FullResource & { variables: Record<string, string | number | boolean> }
> {
const allVariables =
await this.workspace.repository.resourceVariableRepository.getAll();
const variables = Object.fromEntries(
allVariables
.filter((v) => v.resourceId === resource.id)
.filter((v) => v.valueType === "direct")
.map((v) => {
const { value, key } = v;
if (v.sensitive)
return [key, variablesAES256().decrypt(String(value))];
if (typeof value === "object") return [key, JSON.stringify(value)];
return [key, value];
}),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep resource variable objects intact for path resolution

By stringifying non-sensitive JSON values, fullSource.variables loses its structure, so lookups like variables.foo.bar can never succeed and every reference falls back to the default. Return the JSON as-is (and parse decrypted values when possible) so nested paths keep working.

-  ): Promise<
-    FullResource & { variables: Record<string, string | number | boolean> }
-  > {
+  ): Promise<FullResource & { variables: Record<string, unknown> }> {
     const allVariables =
       await this.workspace.repository.resourceVariableRepository.getAll();
     const variables = Object.fromEntries(
       allVariables
         .filter((v) => v.resourceId === resource.id)
         .filter((v) => v.valueType === "direct")
         .map((v) => {
           const { value, key } = v;
-          if (v.sensitive)
-            return [key, variablesAES256().decrypt(String(value))];
-          if (typeof value === "object") return [key, JSON.stringify(value)];
-          return [key, value];
+          if (v.sensitive) {
+            const decrypted = variablesAES256().decrypt(String(value));
+            try {
+              return [key, JSON.parse(decrypted)];
+            } catch {
+              return [key, decrypted];
+            }
+          }
+          return [key, value];
         }),
     );
     return { ...resource, variables };
   }
📝 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
FullResource & { variables: Record<string, string | number | boolean> }
> {
const allVariables =
await this.workspace.repository.resourceVariableRepository.getAll();
const variables = Object.fromEntries(
allVariables
.filter((v) => v.resourceId === resource.id)
.filter((v) => v.valueType === "direct")
.map((v) => {
const { value, key } = v;
if (v.sensitive)
return [key, variablesAES256().decrypt(String(value))];
if (typeof value === "object") return [key, JSON.stringify(value)];
return [key, value];
}),
);
): Promise<FullResource & { variables: Record<string, unknown> }> {
const allVariables =
await this.workspace.repository.resourceVariableRepository.getAll();
const variables = Object.fromEntries(
allVariables
.filter((v) => v.resourceId === resource.id)
.filter((v) => v.valueType === "direct")
.map((v) => {
const { value, key } = v;
if (v.sensitive) {
const decrypted = variablesAES256().decrypt(String(value));
try {
return [key, JSON.parse(decrypted)];
} catch {
return [key, decrypted];
}
}
return [key, value];
}),
);
return { ...resource, variables };
}
🤖 Prompt for AI Agents
In
apps/event-queue/src/release-targets/evaluate/variables/deployment-variable-provider.ts
around lines 125 to 140, the current code stringifies non-sensitive JSON
variable values which destroys nested structure and breaks path lookups;
instead, when v.sensitive decrypt the value and attempt to JSON.parse it (fall
back to the decrypted string if parse fails), and when the value is a
non-sensitive object return it as-is (do not JSON.stringify), so
fullSource.variables preserves nested objects and path resolution works
correctly; update types if needed to allow objects in the resulting variables
map.

@adityachoudhari26 adityachoudhari26 merged commit abfea56 into main Sep 27, 2025
11 checks passed
@adityachoudhari26 adityachoudhari26 deleted the ws-var-manager branch September 27, 2025 03:22
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