Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@
"id",
"name",
"slug",
"jobAgentSelector",
"jobAgentConfig"
],
"type": "object"
Expand Down
2 changes: 1 addition & 1 deletion apps/api/openapi/schemas/deployments.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ local jobAgentConfig = {

Deployment: {
type: 'object',
required: ['id', 'name', 'slug', 'jobAgentConfig'],
required: ['id', 'name', 'slug', 'jobAgentSelector', 'jobAgentConfig'],
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Making jobAgentSelector required in the Deployment schema appears inconsistent with current API behavior: apps/api/src/routes/v1/workspaces/deployments.ts formats jobAgentSelector as undefined when the stored value is null or the default string "false", which causes the property to be omitted from JSON responses. Either update the API formatting to always return a string value (e.g. include "false"/""), or keep jobAgentSelector optional in the Deployment response schema to match what the server actually returns.

Suggested change
required: ['id', 'name', 'slug', 'jobAgentSelector', 'jobAgentConfig'],
required: ['id', 'name', 'slug', 'jobAgentConfig'],

Copilot uses AI. Check for mistakes.
properties: {
id: { type: 'string' },
name: { type: 'string' },
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1225,7 +1225,7 @@ export interface components {
};
jobAgentId?: string;
/** @description CEL expression to match job agents */
jobAgentSelector?: string;
jobAgentSelector: string;
jobAgents?: components["schemas"]["DeploymentJobAgent"][];
metadata?: {
[key: string]: string;
Expand Down
89 changes: 88 additions & 1 deletion apps/web/app/api/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,26 @@ export interface paths {
patch: operations["requestResourceVariablesUpdate"];
trace?: never;
};
"/v1/workspaces/{workspaceId}/resources/search": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Search resources
* @description Returns a paginated list of resources matching the given filters.
*/
post: operations["searchResources"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/workspaces/{workspaceId}/resources/{resourceIdentifier}/release-targets/deployment/{deploymentId}": {
parameters: {
query?: never;
Expand Down Expand Up @@ -1205,7 +1225,7 @@ export interface components {
};
jobAgentId?: string;
/** @description CEL expression to match job agents */
jobAgentSelector?: string;
jobAgentSelector: string;
jobAgents?: components["schemas"]["DeploymentJobAgent"][];
metadata?: {
[key: string]: string;
Expand Down Expand Up @@ -1529,6 +1549,29 @@ export interface components {
release: components["schemas"]["Release"];
resource?: components["schemas"]["Resource"];
};
ListResourcesFilters: {
identifiers?: string[];
kinds?: string[];
/** @default 500 */
limit: number;
/** @description Exact metadata key/value matches */
metadata?: {
[key: string]: string;
};
/** @default 0 */
offset: number;
/**
* @default asc
* @enum {string}
*/
order: "asc" | "desc";
providerIds?: string[];
/** @description Text search on name or identifier */
query?: string;
/** @enum {string} */
sortBy?: "createdAt" | "updatedAt" | "name" | "kind";
versions?: string[];
};
LiteralValue: components["schemas"]["BooleanValue"] | components["schemas"]["NumberValue"] | components["schemas"]["IntegerValue"] | components["schemas"]["StringValue"] | components["schemas"]["ObjectValue"] | components["schemas"]["NullValue"];
MetricProvider: components["schemas"]["HTTPMetricProvider"] | components["schemas"]["SleepMetricProvider"] | components["schemas"]["DatadogMetricProvider"] | components["schemas"]["PrometheusMetricProvider"] | components["schemas"]["TerraformCloudRunMetricProvider"];
/** @enum {boolean} */
Expand Down Expand Up @@ -5138,6 +5181,50 @@ export interface operations {
};
};
};
searchResources: {
parameters: {
query?: never;
header?: never;
path: {
/** @description ID of the workspace */
workspaceId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ListResourcesFilters"];
};
};
responses: {
/** @description Matching resources */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
items: components["schemas"]["Resource"][];
/** @description Maximum number of items returned */
limit: number;
/** @description Number of items skipped */
offset: number;
/** @description Total number of items available */
total: number;
};
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
getReleaseTargetForResourceInDeployment: {
parameters: {
query?: never;
Expand Down
1 change: 1 addition & 0 deletions apps/workspace-engine/oapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@
"id",
"name",
"slug",
"jobAgentSelector",
"jobAgentConfig",
"metadata"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ local openapi = import '../lib/openapi.libsonnet';
{
Deployment: {
type: 'object',
required: ['id', 'name', 'slug', 'jobAgentConfig', 'metadata'],
required: ['id', 'name', 'slug', 'jobAgentSelector', 'jobAgentConfig', 'metadata'],
properties: {
id: { type: 'string' },
name: { type: 'string' },
Expand Down
12 changes: 5 additions & 7 deletions apps/workspace-engine/pkg/db/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ import (

func ToOapiDeployment(row Deployment) *oapi.Deployment {
d := &oapi.Deployment{
Id: row.ID.String(),
Name: row.Name,
Metadata: row.Metadata,
JobAgentConfig: oapi.JobAgentConfig(row.JobAgentConfig),
Id: row.ID.String(),
Name: row.Name,
Metadata: row.Metadata,
JobAgentConfig: oapi.JobAgentConfig(row.JobAgentConfig),
JobAgentSelector: row.JobAgentSelector,
}
if row.Description != "" {
d.Description = &row.Description
}
if row.JobAgentSelector != "" {
d.JobAgentSelector = &row.JobAgentSelector
}
return d
}

Expand Down
2 changes: 1 addition & 1 deletion apps/workspace-engine/pkg/oapi/oapi.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil
return reconcile.Result{}, fmt.Errorf("get deployment: %w", err)
}

if deployment.JobAgentSelector == nil || *deployment.JobAgentSelector == "" {
if deployment.JobAgentSelector == "" {
if err := c.setter.CompletePlan(ctx, planID); err != nil {
return reconcile.Result{}, fmt.Errorf("mark plan completed: %w", err)
}
Expand All @@ -79,7 +79,7 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil
return reconcile.Result{}, fmt.Errorf("list job agents: %w", err)
}

matchedAgents, err := selector.MatchJobAgents(ctx, *deployment.JobAgentSelector, allAgents)
matchedAgents, err := selector.MatchJobAgents(ctx, deployment.JobAgentSelector, allAgents)
if err != nil {
return reconcile.Result{}, fmt.Errorf("match job agents: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func testDeployment(hasSelector bool) *oapi.Deployment {
}
if hasSelector {
sel := "true"
dep.JobAgentSelector = &sel
dep.JobAgentSelector = sel
}
return dep
}
Expand Down Expand Up @@ -300,8 +300,7 @@ func TestProcess_NoAgents_CompletesPlan(t *testing.T) {

func TestProcess_EmptySelector_CompletesPlan(t *testing.T) {
dep := testDeployment(false)
emptySel := ""
dep.JobAgentSelector = &emptySel
dep.JobAgentSelector = ""

Comment on lines 301 to 304
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

testDeployment(false) already yields an empty JobAgentSelector (zero-value string), so explicitly setting dep.JobAgentSelector = "" here is redundant. This also effectively duplicates the "no selector" behavior already covered by TestProcess_NoAgents_CompletesPlan, and can mask coverage for the distinct case of a non-empty selector with zero matching agents.

Copilot uses AI. Check for mistakes.
getter := &mockGetter{plan: testPlan(), deployment: dep}
setter := &mockSetter{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func getJobAgents(
return nil, err
}

if deployment.JobAgentSelector == nil || *deployment.JobAgentSelector == "" {
if deployment.JobAgentSelector == "" {
return nil, fmt.Errorf("deployment job agent selector is empty")
}

Expand All @@ -74,7 +74,7 @@ func getJobAgents(
return nil, fmt.Errorf("list job agents: %w", err)
}

matched, err := selector.MatchJobAgents(ctx, *deployment.JobAgentSelector, allAgents)
matched, err := selector.MatchJobAgents(ctx, deployment.JobAgentSelector, allAgents)
if err != nil {
return nil, fmt.Errorf("match job agents: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func testDeployment() *oapi.Deployment {
Slug: "test-deployment",
Metadata: map[string]string{},
JobAgentConfig: oapi.JobAgentConfig{},
JobAgentSelector: &sel,
JobAgentSelector: sel,
}
}

Expand Down
15 changes: 5 additions & 10 deletions apps/workspace-engine/svc/controllers/jobeligibility/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,16 +200,11 @@ func (r *reconciler) buildAndDispatchJob(ctx context.Context) error {

span.SetAttributes(attribute.String("deployment.id", deployment.Id))
span.SetAttributes(attribute.String("deployment.name", deployment.Name))
span.SetAttributes(
attribute.String("deployment.job_agent_selector", deployment.JobAgentSelector),
)

selectorIsNil := deployment.JobAgentSelector == nil
span.SetAttributes(attribute.Bool("deployment.job_agent_selector_nil", selectorIsNil))
if !selectorIsNil {
span.SetAttributes(
attribute.String("deployment.job_agent_selector", *deployment.JobAgentSelector),
)
}

if selectorIsNil || *deployment.JobAgentSelector == "" {
if deployment.JobAgentSelector == "" {
msg := fmt.Sprintf("No job agents configured for deployment '%s'", deployment.Name)
span.AddEvent(msg)
return r.createFailureJob(ctx, oapi.JobStatusInvalidJobAgent, msg)
Expand All @@ -221,7 +216,7 @@ func (r *reconciler) buildAndDispatchJob(ctx context.Context) error {
}
span.SetAttributes(attribute.Int("workspace_agents.count", len(allAgents)))

matchedAgents, err := selector.MatchJobAgents(ctx, *deployment.JobAgentSelector, allAgents)
matchedAgents, err := selector.MatchJobAgents(ctx, deployment.JobAgentSelector, allAgents)
if err != nil {
return recordErr(span, "match job agents", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func testDeployment(rt *ReleaseTarget) *oapi.Deployment {
Slug: "test-deployment",
Metadata: map[string]string{},
JobAgentConfig: oapi.JobAgentConfig{},
JobAgentSelector: &sel,
JobAgentSelector: sel,
}
}

Expand Down Expand Up @@ -1160,8 +1160,7 @@ func TestReconcile_NoJobAgentSelector_CreatesFailureJob(t *testing.T) {
rt := testRT()
release := testRelease(rt)
getter, setter := setupHappyPath(rt, release)
emptySel := ""
getter.deployment.JobAgentSelector = &emptySel
getter.deployment.JobAgentSelector = ""

_, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt)
require.NoError(t, err)
Expand All @@ -1174,7 +1173,7 @@ func TestReconcile_NilJobAgentSelector_CreatesFailureJob(t *testing.T) {
rt := testRT()
release := testRelease(rt)
getter, setter := setupHappyPath(rt, release)
getter.deployment.JobAgentSelector = nil
getter.deployment.JobAgentSelector = ""

_, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt)
require.NoError(t, err)
Expand Down Expand Up @@ -1479,7 +1478,7 @@ func TestReconcile_JobAgentConfig_DeepMergesThreeLevels(t *testing.T) {
Name: "test-deployment",
Slug: "test-deployment",
Metadata: map[string]string{},
JobAgentSelector: &sel,
JobAgentSelector: sel,
JobAgentConfig: oapi.JobAgentConfig{
"workflowId": float64(456),
"timeout": float64(60),
Expand Down Expand Up @@ -1563,7 +1562,7 @@ func TestReconcile_SelectorMatchesSpecificAgent(t *testing.T) {

sel := fmt.Sprintf(`jobAgent.id == "%s"`, target.Id)
deployment := testDeployment(rt)
deployment.JobAgentSelector = &sel
deployment.JobAgentSelector = sel

getter := &mockGetter{
rtExists: true,
Expand Down Expand Up @@ -1620,7 +1619,7 @@ func TestReconcile_SelectorByType_MatchesMultiple(t *testing.T) {

sel := `jobAgent.type == "argo-cd"`
deployment := testDeployment(rt)
deployment.JobAgentSelector = &sel
deployment.JobAgentSelector = sel

getter := &mockGetter{
rtExists: true,
Expand Down Expand Up @@ -1670,7 +1669,7 @@ func TestReconcile_SelectorFalse_NoAgentsMatched(t *testing.T) {
agent := testAgent()
sel := "false"
deployment := testDeployment(rt)
deployment.JobAgentSelector = &sel
deployment.JobAgentSelector = sel

getter := &mockGetter{
rtExists: true,
Expand Down Expand Up @@ -1721,7 +1720,7 @@ func TestReconcile_SelectorByName_MatchesSingle(t *testing.T) {

sel := `jobAgent.name == "prod-deployer"`
deployment := testDeployment(rt)
deployment.JobAgentSelector = &sel
deployment.JobAgentSelector = sel

getter := &mockGetter{
rtExists: true,
Expand Down
2 changes: 1 addition & 1 deletion apps/workspace-engine/test/controllers/harness/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func NewTestPipeline(t *testing.T, opts ...PipelineOption) *TestPipeline {
Deployment: &oapi.Deployment{
Id: sc.DeploymentID.String(),
Name: sc.DeploymentName,
JobAgentSelector: &sel,
JobAgentSelector: sel,
JobAgentConfig: oapi.JobAgentConfig{},
},
}
Expand Down
2 changes: 1 addition & 1 deletion packages/workspace-engine-sdk/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export interface components {
jobAgentConfig: components["schemas"]["JobAgentConfig"];
jobAgentId?: string;
/** @description CEL expression to match job agents */
jobAgentSelector?: string;
jobAgentSelector: string;
jobAgents?: components["schemas"]["DeploymentJobAgent"][];
metadata: {
[key: string]: string;
Expand Down
Loading