Skip to content

fix: insert job agents on deployment POST and PUT#922

Merged
adityachoudhari26 merged 1 commit intomainfrom
insert-job-agents
Apr 7, 2026
Merged

fix: insert job agents on deployment POST and PUT#922
adityachoudhari26 merged 1 commit intomainfrom
insert-job-agents

Conversation

@adityachoudhari26
Copy link
Copy Markdown
Member

@adityachoudhari26 adityachoudhari26 commented Apr 7, 2026

Resolves #921

Summary by CodeRabbit

  • New Features

    • Deployments now support persisting multiple job agent configurations during creation and updates.
    • Job agent data is automatically overwritten when deployment configurations are updated.
  • Tests

    • Updated end-to-end test suite to validate job agent deployment functionality and ensure proper resource cleanup.

Copilot AI review requested due to automatic review settings April 7, 2026 17:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

Added persistence of jobAgents to the deployment API. The POST and PUT handlers now insert or update job agent records in the database when jobAgents is provided in the request. E2E tests were updated to use the new jobAgents array format and include deployment cleanup.

Changes

Cohort / File(s) Summary
Deployment API Handler
apps/api/src/routes/v1/workspaces/deployments.ts
Added database persistence for job agents on deployment creation and upsert. Inserts rows into deploymentJobAgent table with deploymentId, jobAgentId, and config. Upsert handler deletes existing job agent records before conditionally inserting new ones.
E2E Deployment Tests
e2e/tests/api/deployments.spec.ts
Updated test cases to use jobAgents array format (with ref, config, selector fields) instead of single-agent fields. Added cleanup logic to delete created deployments after test completion. Updated assertions to validate array-based structure.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Suggested reviewers

  • zacharyblasczyk
  • mleonidas

Poem

🐰 With database hops so spry,
Job agents now persist on high,
No longer lost in transaction's dance,
We've fixed this bug with hoppy chance! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing job agent insertion on deployment POST and PUT operations.
Linked Issues check ✅ Passed The code changes directly address issue #921 by implementing job agent insertion on both deployment creation (POST) and update (PUT) operations as required.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objective: deployment.ts implements the job agent insertion logic, and deployments.spec.ts updates tests to validate the new behavior and cleanup resources.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch insert-job-agents

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes deployments POST/PUT so that provided job agents are persisted (resolving #921), and updates E2E coverage accordingly.

Changes:

  • Insert jobAgents links into deployment_job_agent on deployment creation (POST).
  • Rewrite deployment_job_agent links on deployment upsert (PUT) when jobAgents is provided.
  • Update E2E deployment API tests to send jobAgents and add per-test cleanup deletes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.

File Description
e2e/tests/api/deployments.spec.ts Updates request shape to jobAgents and adds cleanup DELETEs after tests.
apps/api/src/routes/v1/workspaces/deployments.ts Persists jobAgents on POST and rewrites them on PUT via delete+insert.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +243 to +250
if (body.jobAgents != null && body.jobAgents.length > 0)
await db.insert(schema.deploymentJobAgent).values(
body.jobAgents.map((agent) => ({
deploymentId: id,
jobAgentId: agent.ref,
config: agent.config,
})),
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The deployment ↔ job agent link is inserted using the provided agent.ref without verifying that the referenced job agent belongs to the same workspaceId. Because deployment_job_agent only FK’s to job_agent.id, a client could attach a deployment to a job agent from a different workspace (cross-tenant reference). Consider validating all body.jobAgents[*].ref against schema.jobAgent with workspaceId (and return 400/404 on mismatch) before inserting, ideally within the same transaction as the deployment insert.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +250
if (body.jobAgents != null && body.jobAgents.length > 0)
await db.insert(schema.deploymentJobAgent).values(
body.jobAgents.map((agent) => ({
deploymentId: id,
jobAgentId: agent.ref,
config: agent.config,
})),
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

DeploymentJobAgent in the OpenAPI schema includes a required selector, but this handler silently drops it (only jobAgentId + config are persisted). This makes the API behave inconsistently and loses client-provided data. Either persist selector (schema + query/formatting), or explicitly reject/strip it at validation time and update the OpenAPI schema accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines 234 to +250
await db.insert(schema.deployment).values({
id,
name: body.name,
description: body.description ?? "",
resourceSelector: body.resourceSelector ?? "false",
metadata: body.metadata ?? {},
workspaceId,
});

if (body.jobAgents != null && body.jobAgents.length > 0)
await db.insert(schema.deploymentJobAgent).values(
body.jobAgents.map((agent) => ({
deploymentId: id,
jobAgentId: agent.ref,
config: agent.config,
})),
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The deployment row insert and the subsequent deployment_job_agent inserts are not wrapped in a single transaction. If the job agent insert fails (FK violation, duplicate key, etc.), the deployment is still created, leaving partial state. Consider using a transaction encompassing both the deployment insert and the job-agent link inserts so creation is atomic.

Copilot uses AI. Check for mistakes.
Comment on lines +287 to +300
if (body.jobAgents != null)
await db.transaction(async (tx) => {
await tx
.delete(schema.deploymentJobAgent)
.where(eq(schema.deploymentJobAgent.deploymentId, deploymentId));

if (body.jobAgents!.length > 0)
await tx.insert(schema.deploymentJobAgent).values(
body.jobAgents!.map((agent) => ({
deploymentId,
jobAgentId: agent.ref,
config: agent.config,
})),
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Same cross-workspace integrity issue as POST: the code deletes and reinserts links based on agent.ref without checking that each referenced job agent belongs to workspaceId. This allows associating a deployment with another workspace’s job agent if the ID is known. Validate refs against schema.jobAgent scoped to workspaceId before (re)inserting.

Copilot uses AI. Check for mistakes.
Comment on lines 267 to +301
await db
.insert(schema.deployment)
.values({
id: deploymentId,
name: body.name,
description: body.description ?? "",
resourceSelector: body.resourceSelector ?? "false",
metadata: body.metadata ?? {},
workspaceId,
})
.onConflictDoUpdate({
target: schema.deployment.id,
set: {
name: body.name,
description: body.description ?? "",
resourceSelector: body.resourceSelector ?? "false",
metadata: body.metadata ?? {},
},
});

if (body.jobAgents != null)
await db.transaction(async (tx) => {
await tx
.delete(schema.deploymentJobAgent)
.where(eq(schema.deploymentJobAgent.deploymentId, deploymentId));

if (body.jobAgents!.length > 0)
await tx.insert(schema.deploymentJobAgent).values(
body.jobAgents!.map((agent) => ({
deploymentId,
jobAgentId: agent.ref,
config: agent.config,
})),
);
});
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This update path performs the deployment upsert and the job-agent link rewrite in separate statements/transactions. If the link rewrite fails after the deployment upsert succeeds, the request returns 202 but leaves the deployment updated with stale/missing links. Consider a single transaction that includes the deployment upsert and the link delete/insert so the update is atomic when jobAgents is provided.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +89
await api.DELETE(
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
{ params: { path: { workspaceId: workspace.id, deploymentId } } },
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This cleanup DELETE call doesn’t assert the response status. If the delete fails (auth, transient error), the test will still pass and leave leaked deployments that can make later runs flaky. Capture the response and assert status (and optionally confirm the resource is gone).

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +132
await api.DELETE(
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
{ params: { path: { workspaceId: workspace.id, deploymentId } } },
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This cleanup DELETE call doesn’t assert the response status. If the delete fails, the test can still pass while leaking state. Consider asserting the delete status (and optionally use try/finally so cleanup still runs on earlier assertion failures).

Copilot uses AI. Check for mistakes.
Comment on lines +183 to +186
await api.DELETE(
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
{ params: { path: { workspaceId: workspace.id, deploymentId } } },
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This cleanup DELETE call doesn’t assert the response status. If the delete fails, the test can still pass while leaking state. Consider asserting the delete status (and optionally use try/finally so cleanup still runs on earlier assertion failures).

Copilot uses AI. Check for mistakes.
Comment on lines +249 to +252
await api.DELETE(
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}",
{ params: { path: { workspaceId: workspace.id, deploymentId } } },
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This cleanup DELETE call doesn’t assert the response status. If the delete fails, the test can still pass while leaking state. Consider asserting the delete status (and optionally use try/finally so cleanup still runs on earlier assertion failures).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/api/src/routes/v1/workspaces/deployments.ts (2)

267-301: ⚠️ Potential issue | 🔴 Critical

Keep the deployment upsert and agent replacement in the same transaction.

Lines 267-285 commit before the delete/reinsert starts. If the insert here fails, the deployment is updated but the old agent set remains; if two PUTs race, you can also end up with deployment fields from one request and jobAgents from another.

💡 Suggested fix
-  await db
-    .insert(schema.deployment)
-    .values({
-      id: deploymentId,
-      name: body.name,
-      description: body.description ?? "",
-      resourceSelector: body.resourceSelector ?? "false",
-      metadata: body.metadata ?? {},
-      workspaceId,
-    })
-    .onConflictDoUpdate({
-      target: schema.deployment.id,
-      set: {
-        name: body.name,
-        description: body.description ?? "",
-        resourceSelector: body.resourceSelector ?? "false",
-        metadata: body.metadata ?? {},
-      },
-    });
-
-  if (body.jobAgents != null)
-    await db.transaction(async (tx) => {
+  await db.transaction(async (tx) => {
+    await tx
+      .insert(schema.deployment)
+      .values({
+        id: deploymentId,
+        name: body.name,
+        description: body.description ?? "",
+        resourceSelector: body.resourceSelector ?? "false",
+        metadata: body.metadata ?? {},
+        workspaceId,
+      })
+      .onConflictDoUpdate({
+        target: schema.deployment.id,
+        set: {
+          name: body.name,
+          description: body.description ?? "",
+          resourceSelector: body.resourceSelector ?? "false",
+          metadata: body.metadata ?? {},
+        },
+      });
+
+    if (body.jobAgents != null) {
       await tx
         .delete(schema.deploymentJobAgent)
         .where(eq(schema.deploymentJobAgent.deploymentId, deploymentId));
 
-      if (body.jobAgents!.length > 0)
+      if (body.jobAgents.length > 0)
         await tx.insert(schema.deploymentJobAgent).values(
-          body.jobAgents!.map((agent) => ({
+          body.jobAgents.map((agent) => ({
             deploymentId,
             jobAgentId: agent.ref,
             config: agent.config,
           })),
         );
-    });
+    }
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/deployments.ts` around lines 267 - 301,
Wrap the deployment upsert and the jobAgents delete/insert in a single database
transaction so they commit or rollback together: move the await
db.insert(schema.deployment)...onConflictDoUpdate(...) call inside the same
db.transaction(...) that currently performs the delete/insert of
schema.deploymentJobAgent, using the transaction handle (tx) for both the
deployment upsert and the jobAgent delete/insert operations (use tx.insert,
tx.delete and eq(schema.deploymentJobAgent.deploymentId, deploymentId)); ensure
you still check body.jobAgents for null and length and keep deploymentId,
body.name, body.description, body.resourceSelector and body.metadata passed into
the upsert as before so the entire change is atomic.

234-250: ⚠️ Potential issue | 🟠 Major

Make deployment creation atomic with the job-agent inserts.

If the deploymentJobAgent insert fails here, the deployment row from Lines 234-241 is already committed and the client gets an error for a partially created deployment. These writes should succeed or fail together.

💡 Suggested fix
-  await db.insert(schema.deployment).values({
-    id,
-    name: body.name,
-    description: body.description ?? "",
-    resourceSelector: body.resourceSelector ?? "false",
-    metadata: body.metadata ?? {},
-    workspaceId,
-  });
-
-  if (body.jobAgents != null && body.jobAgents.length > 0)
-    await db.insert(schema.deploymentJobAgent).values(
-      body.jobAgents.map((agent) => ({
-        deploymentId: id,
-        jobAgentId: agent.ref,
-        config: agent.config,
-      })),
-    );
+  await db.transaction(async (tx) => {
+    await tx.insert(schema.deployment).values({
+      id,
+      name: body.name,
+      description: body.description ?? "",
+      resourceSelector: body.resourceSelector ?? "false",
+      metadata: body.metadata ?? {},
+      workspaceId,
+    });
+
+    if (body.jobAgents?.length)
+      await tx.insert(schema.deploymentJobAgent).values(
+        body.jobAgents.map((agent) => ({
+          deploymentId: id,
+          jobAgentId: agent.ref,
+          config: agent.config,
+        })),
+      );
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/deployments.ts` around lines 234 - 250,
Wrap the deployment insert and the optional deploymentJobAgent inserts in a
single database transaction so both inserts commit or roll back together: move
the db.insert(schema.deployment).values({...}) and the conditional
db.insert(schema.deploymentJobAgent).values(...) (which uses
body.jobAgents.map(...)) into a single transactional call (e.g., db.transaction
/ db.$transaction depending on the DB client) using the same id and workspaceId
variables, and ensure any error from the job-agent insert causes the transaction
to abort and the error to be returned to the caller.
🧹 Nitpick comments (1)
e2e/tests/api/deployments.spec.ts (1)

135-186: This PUT test still only covers create-via-upsert.

Because deploymentId is new, the test never exercises the new branch that deletes existing deploymentJobAgent rows before re-inserting them. Please add a second PUT against the same deployment and assert the agent set is replaced or cleared.

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

In `@e2e/tests/api/deployments.spec.ts` around lines 135 - 186, The test "should
upsert a deployment with a job agent" currently only verifies create-via-upsert
because it uses a fresh deploymentId; add a second API.PUT against the same
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}" (reusing deploymentId)
that changes the jobAgents payload (e.g., an empty array or a different ref) to
exercise the branch that deletes existing deploymentJobAgent rows before
re-inserting, then GET the deployment and assert deployment.jobAgents reflects
the replacement/clearing (e.g., expect(jobAgents).toHaveLength(0) or contains
the new ref), and finally keep the existing DELETE cleanup. Ensure you reference
the same deploymentId and the same PUT/GET endpoints so the update path inside
the upsert logic is executed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/api/src/routes/v1/workspaces/deployments.ts`:
- Around line 267-301: Wrap the deployment upsert and the jobAgents
delete/insert in a single database transaction so they commit or rollback
together: move the await db.insert(schema.deployment)...onConflictDoUpdate(...)
call inside the same db.transaction(...) that currently performs the
delete/insert of schema.deploymentJobAgent, using the transaction handle (tx)
for both the deployment upsert and the jobAgent delete/insert operations (use
tx.insert, tx.delete and eq(schema.deploymentJobAgent.deploymentId,
deploymentId)); ensure you still check body.jobAgents for null and length and
keep deploymentId, body.name, body.description, body.resourceSelector and
body.metadata passed into the upsert as before so the entire change is atomic.
- Around line 234-250: Wrap the deployment insert and the optional
deploymentJobAgent inserts in a single database transaction so both inserts
commit or roll back together: move the
db.insert(schema.deployment).values({...}) and the conditional
db.insert(schema.deploymentJobAgent).values(...) (which uses
body.jobAgents.map(...)) into a single transactional call (e.g., db.transaction
/ db.$transaction depending on the DB client) using the same id and workspaceId
variables, and ensure any error from the job-agent insert causes the transaction
to abort and the error to be returned to the caller.

---

Nitpick comments:
In `@e2e/tests/api/deployments.spec.ts`:
- Around line 135-186: The test "should upsert a deployment with a job agent"
currently only verifies create-via-upsert because it uses a fresh deploymentId;
add a second API.PUT against the same
"/v1/workspaces/{workspaceId}/deployments/{deploymentId}" (reusing deploymentId)
that changes the jobAgents payload (e.g., an empty array or a different ref) to
exercise the branch that deletes existing deploymentJobAgent rows before
re-inserting, then GET the deployment and assert deployment.jobAgents reflects
the replacement/clearing (e.g., expect(jobAgents).toHaveLength(0) or contains
the new ref), and finally keep the existing DELETE cleanup. Ensure you reference
the same deploymentId and the same PUT/GET endpoints so the update path inside
the upsert logic is executed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ee52de8-87f7-4a56-a090-21c882508e1f

📥 Commits

Reviewing files that changed from the base of the PR and between 52833f1 and 82af17d.

📒 Files selected for processing (2)
  • apps/api/src/routes/v1/workspaces/deployments.ts
  • e2e/tests/api/deployments.spec.ts

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.

bug: deployment POST and PUT do not insert job agents into tables

2 participants